Merge pull request 'Add the release workflow (MIL-004 task 5)' (#80) from mil-004-release-workflow into main
This commit was merged in pull request #80.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
name: Release
|
||||
|
||||
# Releases this repository (ADR-0006, ADR-0007, MIL-004 task 5). It is started
|
||||
# by hand with the version to release; it creates the version tag, the release
|
||||
# page from the changelog entry and moves the major tag, and only when the
|
||||
# commit is the tip of main, its validation run is green and the changelog has
|
||||
# an entry for the version. dry_run defaults to true: it checks and prints and
|
||||
# writes nothing, so a release is a deliberate second run with dry_run false.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to release, MAJOR.MINOR.PATCH without a leading v"
|
||||
required: true
|
||||
dry_run:
|
||||
description: "true checks only; false creates the tag, the release page and the major tag"
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check and publish the release
|
||||
env:
|
||||
GITEA_API_URL: ${{ gitea.api_url }}
|
||||
SOURCE_REPOSITORY: ${{ gitea.repository }}
|
||||
RELEASE_CREDENTIALS: ${{ secrets.TOKEN_FOR_GITEA_RELEASE || github.token }}
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
RELEASE_DRY_RUN: ${{ inputs.dry_run }}
|
||||
RELEASE_SHA: ${{ gitea.sha }}
|
||||
run: python3 tools/release.py
|
||||
@@ -25,6 +25,10 @@ tagged the workflows are delivered from `main` and are not pinned.
|
||||
|
||||
### Added
|
||||
|
||||
- Workflow `release.yml`: started by hand with a version, it releases only the tip of
|
||||
`main` whose validation run is green and whose changelog entry exists, creates
|
||||
the version tag, the release page and the major tag, and does nothing but
|
||||
check until `dry_run` is set to `false`.
|
||||
- Scoped workflow `sync-github-metadata.yml`: copies the description and topics
|
||||
of a Gitea repository to its GitHub push mirror on every push to `main`, by
|
||||
hand and daily at 03:17 UTC. A repository without a GitHub mirror is skipped
|
||||
|
||||
@@ -68,7 +68,7 @@ Rows that say Implemented or Guides written exist today. The rest is planned and
|
||||
| Onboard a repository, manage runners, diagnose failures | Guides written ([onboarding](guides/onboarding.md), [troubleshooting](guides/troubleshooting.md)); scoped delivery to other repositories not yet verified; brief use cases open (MIL-002) |
|
||||
| Publish a repository wiki from `wiki/` (opt-in, delivered to every organization) | Implemented in `.gitea/scoped_workflows/publish-wiki.yml`, with offline tests; the pilot in a second repository is pending (MIL-006) |
|
||||
| Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adoption in RepoFoundry is in review ([guide](guides/quality-checks.md)) |
|
||||
| Versioned release and publishing | Release process, changelog and upgrade guide defined; the first release `v1.0.0` and the release workflow are pending (MIL-004) ([guide](guides/upgrading.md)) |
|
||||
| Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists; the first release `v1.0.0` is pending (MIL-004) ([guide](guides/upgrading.md)) |
|
||||
|
||||
### 🔄 Sync GitHub metadata
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ is validated, versioned and announced, and how consumers pin a version. The
|
||||
decisions are in ADR-0006; this guide is the checklist. It is not published
|
||||
on the wiki.
|
||||
|
||||
Until the release workflow exists (MIL-004 task 5) the steps are done by hand.
|
||||
Steps 5 and 6 are done by the **Release** workflow (`.gitea/workflows/release.yml`,
|
||||
`tools/release.py`); the manual commands below are the fallback.
|
||||
|
||||
## What a version is
|
||||
|
||||
@@ -34,17 +35,26 @@ Until the release workflow exists (MIL-004 task 5) the steps are done by hand.
|
||||
new major version, and a new major version needs a Breaking entry.
|
||||
4. **Get the approval** of S01 for the release, in the pull request that adds
|
||||
the changelog entry.
|
||||
5. **Tag.** Create `vMAJOR.MINOR.PATCH` on the approved commit, then move
|
||||
`vMAJOR` to it:
|
||||
5. **Run the Release workflow in dry-run mode.** In the Actions tab of this
|
||||
repository start **Release** on `main` with the version and `dry_run`
|
||||
`true`. It checks that the commit is the tip of `main`, that its validation
|
||||
run is green, that the changelog has an entry for the version and that the
|
||||
tag does not exist, and writes nothing.
|
||||
6. **Release.** Start **Release** again with `dry_run` `false`. It creates the
|
||||
tag `vMAJOR.MINOR.PATCH`, the release page with the text of
|
||||
`python tools/changelog.py notes x.y.z`, and moves `vMAJOR` to the commit
|
||||
when this is the highest version of its major. The page on Gitea is the
|
||||
only destination (ADR-0007); nothing is attached to it. The workflow uses
|
||||
the token of its own run; if Gitea refuses it, store `TOKEN_FOR_GITEA_RELEASE`
|
||||
as a secret with write access to this repository only.
|
||||
|
||||
By hand, the same steps are:
|
||||
```bash
|
||||
git tag -a v1.2.0 -m "v1.2.0" <commit>
|
||||
git push origin v1.2.0
|
||||
git tag -f v1 <commit> && git push -f origin v1
|
||||
```
|
||||
6. **Announce.** Create the release page for the tag with the text of
|
||||
`python tools/changelog.py notes x.y.z`, and say whether consumers must act.
|
||||
The release page of this repository on Gitea is the only destination
|
||||
(ADR-0007); nothing is attached to it.
|
||||
and create the release page with the notes of the changelog entry.
|
||||
7. **Verify.** One repository pinned to the previous exact tag still runs the
|
||||
same code; one repository on the major tag receives the release.
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Offline tests for tools/release.py and .gitea/workflows/release.yml."""
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from workflow_source import REPO_ROOT, load_script
|
||||
|
||||
release = load_script(REPO_ROOT / "tools" / "release.py", "release")
|
||||
WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "release.yml"
|
||||
|
||||
SHA = "a" * 40
|
||||
CONTEXT = "Validate workflows / validate (push)"
|
||||
|
||||
CHANGELOG = """# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Something later.
|
||||
|
||||
## [1.1.0] - 2026-11-01
|
||||
|
||||
### Added
|
||||
|
||||
- A workflow.
|
||||
|
||||
## [1.0.0] - 2026-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- First release.
|
||||
"""
|
||||
|
||||
|
||||
class FakeGitea:
|
||||
"""Answers the calls of the release script and records the writes."""
|
||||
|
||||
def __init__(self, tip=SHA, status="success", tags=(), context=CONTEXT):
|
||||
self.tip = tip
|
||||
self.status = status
|
||||
self.tags = list(tags)
|
||||
self.context = context
|
||||
self.writes = []
|
||||
|
||||
def __call__(self, url, method="GET", headers=None, body=None):
|
||||
path = url.split("?")[0]
|
||||
if method != "GET":
|
||||
self.writes.append((method, path.rsplit("/repos/o/r", 1)[-1], body))
|
||||
return None
|
||||
if path.endswith("/branches/main"):
|
||||
return {"commit": {"id": self.tip}}
|
||||
if path.endswith(f"/commits/{SHA}/status"):
|
||||
if self.status is None:
|
||||
return {"statuses": []}
|
||||
return {"statuses": [{"context": self.context, "status": self.status}]}
|
||||
if path.endswith("/tags"):
|
||||
return [{"name": name} for name in self.tags] if "page=1" in url else []
|
||||
raise AssertionError(f"unexpected request {method} {url}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env(tmp_path, monkeypatch):
|
||||
changelog = tmp_path / "CHANGELOG.md"
|
||||
changelog.write_text(CHANGELOG, encoding="utf-8")
|
||||
values = {
|
||||
"GITEA_API_URL": "https://git.example/api/v1",
|
||||
"SOURCE_REPOSITORY": "o/r",
|
||||
"RELEASE_CREDENTIALS": "t" * 40,
|
||||
"RELEASE_VERSION": "1.1.0",
|
||||
"RELEASE_SHA": SHA,
|
||||
"RELEASE_DRY_RUN": "false",
|
||||
"CHANGELOG_FILE": str(changelog),
|
||||
}
|
||||
for key, value in values.items():
|
||||
monkeypatch.setenv(key, value)
|
||||
monkeypatch.delenv("RELEASE_BRANCH", raising=False)
|
||||
monkeypatch.delenv("VALIDATION_CONTEXT", raising=False)
|
||||
return monkeypatch
|
||||
|
||||
|
||||
def run(gitea):
|
||||
return release.main(request=gitea)
|
||||
|
||||
|
||||
def test_green_commit_with_entry_creates_release_and_major_tag(env):
|
||||
gitea = FakeGitea(tags=["v1.0.0", "v1"])
|
||||
assert run(gitea) == 0
|
||||
methods = [(m, p) for m, p, _ in gitea.writes]
|
||||
assert methods == [("POST", "/releases"), ("DELETE", "/tags/v1"), ("POST", "/tags")]
|
||||
release_body = gitea.writes[0][2]
|
||||
assert release_body["tag_name"] == "v1.1.0"
|
||||
assert release_body["target_commitish"] == SHA
|
||||
assert "### Added" in release_body["body"] and "A workflow." in release_body["body"]
|
||||
assert gitea.writes[2][2]["tag_name"] == "v1" and gitea.writes[2][2]["target"] == SHA
|
||||
|
||||
|
||||
def test_first_release_creates_the_major_tag_without_deleting(env):
|
||||
env.setenv("RELEASE_VERSION", "1.0.0")
|
||||
gitea = FakeGitea(tags=[])
|
||||
assert run(gitea) == 0
|
||||
assert [(m, p) for m, p, _ in gitea.writes] == [("POST", "/releases"), ("POST", "/tags")]
|
||||
|
||||
|
||||
def test_failed_validation_run_creates_no_tag(env):
|
||||
gitea = FakeGitea(status="failure")
|
||||
with pytest.raises(release.WorkflowError) as error:
|
||||
run(gitea)
|
||||
assert error.value.category == "validation"
|
||||
assert gitea.writes == []
|
||||
|
||||
|
||||
def test_missing_validation_run_creates_no_tag(env):
|
||||
gitea = FakeGitea(status=None)
|
||||
with pytest.raises(release.WorkflowError) as error:
|
||||
run(gitea)
|
||||
assert "No validation run" in str(error.value)
|
||||
assert gitea.writes == []
|
||||
|
||||
|
||||
def test_commit_that_is_not_the_tip_is_refused(env):
|
||||
gitea = FakeGitea(tip="b" * 40)
|
||||
with pytest.raises(release.WorkflowError) as error:
|
||||
run(gitea)
|
||||
assert "not the tip" in str(error.value)
|
||||
assert gitea.writes == []
|
||||
|
||||
|
||||
def test_version_without_changelog_entry_is_refused(env):
|
||||
env.setenv("RELEASE_VERSION", "1.2.0")
|
||||
gitea = FakeGitea()
|
||||
with pytest.raises(release.WorkflowError) as error:
|
||||
run(gitea)
|
||||
assert error.value.category == "validation"
|
||||
assert gitea.writes == []
|
||||
|
||||
|
||||
def test_existing_tag_is_never_moved(env):
|
||||
gitea = FakeGitea(tags=["v1.1.0", "v1.0.0", "v1"])
|
||||
with pytest.raises(release.WorkflowError) as error:
|
||||
run(gitea)
|
||||
assert "already exists" in str(error.value)
|
||||
assert gitea.writes == []
|
||||
|
||||
|
||||
def test_older_patch_does_not_move_the_major_tag(env):
|
||||
env.setenv("RELEASE_VERSION", "1.0.0")
|
||||
gitea = FakeGitea(tags=["v1.1.0", "v1"])
|
||||
assert run(gitea) == 0
|
||||
assert [(m, p) for m, p, _ in gitea.writes] == [("POST", "/releases")]
|
||||
|
||||
|
||||
def test_dry_run_is_the_default_and_writes_nothing(env, capsys):
|
||||
env.delenv("RELEASE_DRY_RUN")
|
||||
gitea = FakeGitea(tags=["v1.0.0", "v1"])
|
||||
assert run(gitea) == 0
|
||||
assert gitea.writes == []
|
||||
assert "Dry run" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["v1.0.0", "1.0", "1.0.0-rc1", ""])
|
||||
def test_malformed_version_is_a_configuration_error(env, version):
|
||||
env.setenv("RELEASE_VERSION", version)
|
||||
with pytest.raises(release.WorkflowError) as error:
|
||||
run(FakeGitea())
|
||||
assert error.value.category == "configuration"
|
||||
|
||||
|
||||
def test_missing_token_is_a_configuration_error(env):
|
||||
env.setenv("RELEASE_CREDENTIALS", " ")
|
||||
with pytest.raises(release.WorkflowError) as error:
|
||||
run(FakeGitea())
|
||||
assert error.value.category == "configuration"
|
||||
|
||||
|
||||
def test_workflow_runs_the_file_and_defaults_to_a_dry_run():
|
||||
data = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
|
||||
triggers = data.get("on", data.get(True))
|
||||
inputs = triggers["workflow_dispatch"]["inputs"]
|
||||
assert inputs["dry_run"]["default"] == "true"
|
||||
job = data["jobs"]["release"]
|
||||
assert job["permissions"] == {"contents": "write"}
|
||||
runs = [step.get("run", "") for step in job["steps"]]
|
||||
assert "python3 tools/release.py" in runs
|
||||
@@ -0,0 +1,271 @@
|
||||
"""Create a release of this repository: version tag, release page, major tag.
|
||||
|
||||
Run by .gitea/workflows/release.yml (MIL-004 task 5, ADR-0006, ADR-0007). It
|
||||
publishes a release only when all of these hold, and stops before any write
|
||||
when one does not:
|
||||
|
||||
- the version is MAJOR.MINOR.PATCH and CHANGELOG.md has an entry for it;
|
||||
- the commit is the tip of the default branch;
|
||||
- the validation run of that commit is green;
|
||||
- the tag vMAJOR.MINOR.PATCH does not exist yet.
|
||||
|
||||
It then creates the release page (which creates the tag) from the changelog
|
||||
entry and moves the major tag vMAJOR to the commit when the version is the
|
||||
highest of its major version. It reads its settings from the environment:
|
||||
|
||||
GITEA_API_URL Gitea API base, for example https://host/api/v1
|
||||
SOURCE_REPOSITORY owner/repo that is released
|
||||
RELEASE_CREDENTIALS the token of the run, as clear text
|
||||
RELEASE_VERSION the version, for example 1.0.0 (no leading v)
|
||||
RELEASE_SHA the commit to release
|
||||
RELEASE_DRY_RUN "true" (default) checks and prints, writes nothing
|
||||
RELEASE_BRANCH the default branch (default: main)
|
||||
VALIDATION_CONTEXT the status context that must be green
|
||||
(default: Validate workflows / validate (push))
|
||||
CHANGELOG_FILE default: CHANGELOG.md
|
||||
|
||||
Every failure is printed as ``ERROR: [category] message`` with the categories
|
||||
configuration, validation, credentials, permissions and api; no secret value
|
||||
is printed.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REQUEST_TIMEOUT_SECONDS = 30
|
||||
DEFAULT_CONTEXT = "Validate workflows / validate (push)"
|
||||
_VERSION = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
||||
_TAG = re.compile(r"^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
||||
|
||||
|
||||
class WorkflowError(RuntimeError):
|
||||
"""A failure with a category.
|
||||
|
||||
configuration: a setting or the changelog is missing or malformed.
|
||||
validation: the release is not allowed (commit, run, tag or changelog).
|
||||
credentials: Gitea rejected the token (HTTP 401).
|
||||
permissions: the token is valid but not allowed (HTTP 403).
|
||||
api: any other API or network failure.
|
||||
"""
|
||||
|
||||
def __init__(self, category, message):
|
||||
super().__init__(f"[{category}] {message}")
|
||||
self.category = category
|
||||
self.detail = message
|
||||
|
||||
|
||||
def load_changelog_module():
|
||||
"""Import tools/changelog.py, which sits next to this file."""
|
||||
path = Path(__file__).resolve().parent / "changelog.py"
|
||||
spec = importlib.util.spec_from_file_location("changelog", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def request_json(url, method="GET", headers=None, body=None):
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(body).encode("utf-8") if body is not None else None,
|
||||
headers=headers or {},
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
|
||||
content = response.read()
|
||||
return json.loads(content) if content else None
|
||||
except urllib.error.HTTPError as error:
|
||||
target = url.split("?")[0]
|
||||
detail = f"Gitea API request failed with HTTP {error.code} ({error.reason}) for {method} {target}"
|
||||
if error.code == 401:
|
||||
raise WorkflowError("credentials", f"{detail}. The token was rejected.") from None
|
||||
if error.code == 403:
|
||||
raise WorkflowError(
|
||||
"permissions",
|
||||
f"{detail}. The token lacks write access to the contents of the repository.",
|
||||
) from None
|
||||
raise WorkflowError("api", detail) from None
|
||||
except (urllib.error.URLError, TimeoutError) as error:
|
||||
raise WorkflowError("api", f"Gitea API is unreachable ({error}).") from None
|
||||
|
||||
|
||||
def parse_version(raw):
|
||||
"""Return the version as a tuple, or raise a configuration error."""
|
||||
text = (raw or "").strip()
|
||||
match = _VERSION.match(text)
|
||||
if not match:
|
||||
raise WorkflowError(
|
||||
"configuration",
|
||||
f"'{text}' is not a version MAJOR.MINOR.PATCH (write 1.0.0, without a leading v).",
|
||||
)
|
||||
return tuple(int(part) for part in match.groups())
|
||||
|
||||
|
||||
def split_repository(full_name):
|
||||
owner, separator, repo = (full_name or "").partition("/")
|
||||
if not separator or not owner or not repo:
|
||||
raise WorkflowError("configuration", "Could not determine the Gitea repository.")
|
||||
return owner, repo
|
||||
|
||||
|
||||
def release_notes(changelog_text, version, changelog):
|
||||
"""Return the release page text for a version, or fail as a validation error."""
|
||||
problems = changelog.validate(changelog_text)
|
||||
if problems:
|
||||
raise WorkflowError("validation", "CHANGELOG.md breaks its convention: " + "; ".join(problems))
|
||||
try:
|
||||
return changelog.notes(changelog_text, version)
|
||||
except LookupError as error:
|
||||
raise WorkflowError("validation", str(error)) from None
|
||||
|
||||
|
||||
def list_tag_names(api, headers, request):
|
||||
"""Return every tag name of the repository."""
|
||||
names = []
|
||||
page = 1
|
||||
while True:
|
||||
batch = request(f"{api}/tags?limit=50&page={page}", headers=headers)
|
||||
if not batch:
|
||||
return names
|
||||
names += [item["name"] for item in batch]
|
||||
if len(batch) < 50:
|
||||
return names
|
||||
page += 1
|
||||
|
||||
|
||||
def check_commit(api, headers, request, sha, branch, context):
|
||||
"""The commit is the tip of the branch and its validation run is green."""
|
||||
tip = request(f"{api}/branches/{urllib.parse.quote(branch, safe='')}", headers=headers)
|
||||
tip_sha = (tip or {}).get("commit", {}).get("id", "")
|
||||
if tip_sha != sha:
|
||||
raise WorkflowError(
|
||||
"validation",
|
||||
f"{sha[:7]} is not the tip of {branch} ({tip_sha[:7] or 'unknown'}); "
|
||||
"release from the latest commit of the default branch.",
|
||||
)
|
||||
status = request(f"{api}/commits/{sha}/status", headers=headers) or {}
|
||||
for item in status.get("statuses", []):
|
||||
if item.get("context") == context:
|
||||
if item.get("status") != "success":
|
||||
raise WorkflowError(
|
||||
"validation",
|
||||
f"The validation run '{context}' of {sha[:7]} is {item.get('status')}, not success.",
|
||||
)
|
||||
return
|
||||
raise WorkflowError(
|
||||
"validation",
|
||||
f"No validation run '{context}' was found for {sha[:7]}; wait for it or run it first.",
|
||||
)
|
||||
|
||||
|
||||
def plan_tags(tags, version):
|
||||
"""Return (exact tag, major tag, whether the major tag moves)."""
|
||||
exact = "v{}.{}.{}".format(*version)
|
||||
major = f"v{version[0]}"
|
||||
if exact in tags:
|
||||
raise WorkflowError("validation", f"The tag {exact} already exists; tags are never moved.")
|
||||
same_major = [
|
||||
tuple(int(p) for p in m.groups())
|
||||
for m in (_TAG.match(name) for name in tags)
|
||||
if m and int(m.group(1)) == version[0]
|
||||
]
|
||||
moves = all(version > other for other in same_major)
|
||||
return exact, major, moves
|
||||
|
||||
|
||||
def publish(api, headers, request, sha, exact, major, moves, notes, major_exists):
|
||||
"""Create the release page (and with it the tag), then move the major tag."""
|
||||
request(
|
||||
f"{api}/releases",
|
||||
method="POST",
|
||||
headers=headers,
|
||||
body={
|
||||
"tag_name": exact,
|
||||
"target_commitish": sha,
|
||||
"name": exact,
|
||||
"body": notes,
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
},
|
||||
)
|
||||
if not moves:
|
||||
return
|
||||
if major_exists:
|
||||
request(f"{api}/tags/{major}", method="DELETE", headers=headers)
|
||||
request(
|
||||
f"{api}/tags",
|
||||
method="POST",
|
||||
headers=headers,
|
||||
body={"tag_name": major, "target": sha, "message": f"{major} points to {exact}"},
|
||||
)
|
||||
|
||||
|
||||
def main(request=request_json):
|
||||
version = parse_version(os.environ.get("RELEASE_VERSION"))
|
||||
owner, repo = split_repository(os.environ.get("SOURCE_REPOSITORY"))
|
||||
sha = (os.environ.get("RELEASE_SHA") or "").strip()
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", sha):
|
||||
raise WorkflowError("configuration", "RELEASE_SHA must be the 40-character commit to release.")
|
||||
token = (os.environ.get("RELEASE_CREDENTIALS") or "").strip()
|
||||
if not token:
|
||||
raise WorkflowError(
|
||||
"configuration",
|
||||
"No token for the release: the run has none and TOKEN_FOR_GITEA_RELEASE is not set.",
|
||||
)
|
||||
dry_run = (os.environ.get("RELEASE_DRY_RUN") or "true").strip().lower() != "false"
|
||||
branch = os.environ.get("RELEASE_BRANCH") or "main"
|
||||
context = os.environ.get("VALIDATION_CONTEXT") or DEFAULT_CONTEXT
|
||||
changelog_path = Path(os.environ.get("CHANGELOG_FILE") or "CHANGELOG.md")
|
||||
|
||||
changelog = load_changelog_module()
|
||||
name = "{}.{}.{}".format(*version)
|
||||
try:
|
||||
text = changelog_path.read_text(encoding="utf-8")
|
||||
except OSError as error:
|
||||
raise WorkflowError("configuration", f"Cannot read {changelog_path}: {error}") from None
|
||||
notes = release_notes(text, name, changelog)
|
||||
|
||||
api_url = os.environ["GITEA_API_URL"].rstrip("/")
|
||||
api = f"{api_url}/repos/{urllib.parse.quote(owner, safe='')}/{urllib.parse.quote(repo, safe='')}"
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
check_commit(api, headers, request, sha, branch, context)
|
||||
tags = list_tag_names(api, headers, request)
|
||||
exact, major, moves = plan_tags(tags, version)
|
||||
print(f"Checks passed for {exact} on {sha[:7]}: changelog entry, tip of {branch}, green run, new tag.")
|
||||
|
||||
if dry_run:
|
||||
print(
|
||||
f"Dry run: would create the release page {exact}"
|
||||
+ (f" and point {major} to it." if moves else f" and leave {major} where it is.")
|
||||
)
|
||||
return 0
|
||||
|
||||
publish(api, headers, request, sha, exact, major, moves, notes, major in tags)
|
||||
print(f"Released {exact} at {sha[:7]}.")
|
||||
print(
|
||||
f"The major tag {major} now points to {exact}."
|
||||
if moves
|
||||
else f"The major tag {major} was not moved: a higher {version[0]}.x release exists."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except WorkflowError as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- translation of Capabilities.md, source sha256 c1d5b1f173ef -->
|
||||
<!-- translation of Capabilities.md, source sha256 94af1c3e6949 -->
|
||||
|
||||
> Dette er en oversættelse af [`Capabilities`](Capabilities). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres.
|
||||
|
||||
@@ -15,7 +15,7 @@ Rækker, der siger Implementeret eller Vejledninger er skrevet, findes i dag. Re
|
||||
| Tilslutning af et repository, administration af runners, fejlfinding | Vejledninger er skrevet ([tilslutning](Onboarding.da), [fejlfinding](Troubleshooting.da)); scoped levering til andre repositories er ikke verificeret endnu; korte use cases er åbne (MIL-002) |
|
||||
| Publicering af et repositorys wiki fra `wiki/` (tilmelding, leveres til alle organisationer) | Implementeret i `.gitea/scoped_workflows/publish-wiki.yml`, med offline-tests; pilotforsøget i et andet repository afventer (MIL-006) |
|
||||
| Fælles kvalitetskontrol (Python og shell) | Implementeret som det genbrugelige workflow `.gitea/workflows/quality-checks.yml`, med offline-tests og et forsøg i en anden organisation; tilslutningen i RepoFoundry er til gennemgang ([vejledning](QualityChecks.da)) |
|
||||
| Versioneret udgivelse og publicering | Udgivelsesproces, changelog og opgraderingsvejledning er defineret; den første udgivelse `v1.0.0` og udgivelsesworkflowet mangler (MIL-004) ([vejledning](Upgrading.da)) |
|
||||
| Versioneret udgivelse og publicering | Udgivelsesproces, changelog og opgraderingsvejledning er defineret; udgivelsesworkflowet findes; den første udgivelse `v1.0.0` mangler (MIL-004) ([vejledning](Upgrading.da)) |
|
||||
|
||||
### Synkronisering af GitHub-metadata
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Rows that say Implemented or Guides written exist today. The rest is planned and
|
||||
| Onboard a repository, manage runners, diagnose failures | Guides written ([onboarding](Onboarding), [troubleshooting](Troubleshooting)); scoped delivery to other repositories not yet verified; brief use cases open (MIL-002) |
|
||||
| Publish a repository wiki from `wiki/` (opt-in, delivered to every organization) | Implemented in `.gitea/scoped_workflows/publish-wiki.yml`, with offline tests; the pilot in a second repository is pending (MIL-006) |
|
||||
| Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adoption in RepoFoundry is in review ([guide](QualityChecks)) |
|
||||
| Versioned release and publishing | Release process, changelog and upgrade guide defined; the first release `v1.0.0` and the release workflow are pending (MIL-004) ([guide](Upgrading)) |
|
||||
| Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists; the first release `v1.0.0` is pending (MIL-004) ([guide](Upgrading)) |
|
||||
|
||||
### Sync GitHub metadata
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- translation of Capabilities.md, source sha256 c1d5b1f173ef -->
|
||||
<!-- translation of Capabilities.md, source sha256 94af1c3e6949 -->
|
||||
|
||||
> Ini ialah terjemahan [`Capabilities`](Capabilities). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya.
|
||||
|
||||
@@ -15,7 +15,7 @@ Baris yang menyatakan Dilaksanakan atau Panduan telah ditulis wujud hari ini. Se
|
||||
| Menyertakan repositori, mengurus runner, mendiagnosis kegagalan | Panduan telah ditulis ([menyertakan repositori](Onboarding.ms), [diagnosis](Troubleshooting.ms)); penghantaran scoped ke repositori lain belum disahkan; use case ringkas masih terbuka (MIL-002) |
|
||||
| Menerbitkan wiki repositori daripada `wiki/` (pilih menyertai, dihantar ke setiap organisasi) | Dilaksanakan dalam `.gitea/scoped_workflows/publish-wiki.yml`, dengan ujian luar talian; juruterbang dalam repositori kedua belum dibuat (MIL-006) |
|
||||
| Semakan kualiti bersama (Python dan shell) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks.yml`, dengan ujian luar talian dan percubaan dalam organisasi kedua; penerimaan dalam RepoFoundry sedang disemak ([panduan](QualityChecks.ms)) |
|
||||
| Keluaran berversi dan penerbitan | Proses keluaran, changelog dan panduan naik taraf telah ditakrifkan; keluaran pertama `v1.0.0` dan aliran kerja keluaran belum ada (MIL-004) ([panduan](Upgrading.ms)) |
|
||||
| Keluaran berversi dan penerbitan | Proses keluaran, changelog dan panduan naik taraf telah ditakrifkan; aliran kerja keluaran sudah ada; keluaran pertama `v1.0.0` belum ada (MIL-004) ([panduan](Upgrading.ms)) |
|
||||
|
||||
### Menyegerakkan metadata GitHub
|
||||
|
||||
|
||||
Reference in New Issue
Block a user