ci: separate manual release publishing
This commit is contained in:
+50
-15
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate and attach one tested Signage release bundle to an existing Gitea release."""
|
||||
"""Validate and publish one tested Signage release bundle through the Gitea API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -124,7 +124,8 @@ class Gitea:
|
||||
raise Refused("GITHUB_SERVER_URL, GITHUB_REPOSITORY, and GITHUB_TOKEN are required")
|
||||
|
||||
def request(self, method: str, path: str, body: bytes | None = None,
|
||||
headers: dict[str, str] | None = None, *, authenticated: bool = True) -> bytes:
|
||||
headers: dict[str, str] | None = None, *, authenticated: bool = True,
|
||||
missing_ok: bool = False) -> bytes | None:
|
||||
target = path if path.startswith("http") else f"{self.server}/api/v1/repos/{self.repository}{path}"
|
||||
request_headers = dict(headers or {})
|
||||
if authenticated:
|
||||
@@ -134,29 +135,61 @@ class Gitea:
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
return response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
if missing_ok and error.code == 404:
|
||||
return None
|
||||
detail = error.read()[:300].decode("utf-8", "replace")
|
||||
raise Refused(f"{method} {path} answered {error.code}: {detail}") from error
|
||||
|
||||
def json(self, method: str, path: str) -> object:
|
||||
data = self.request(method, path)
|
||||
return json.loads(data) if data else None
|
||||
def json(self, method: str, path: str, body: object | None = None,
|
||||
*, missing_ok: bool = False) -> object:
|
||||
encoded = json.dumps(body).encode() if body is not None else None
|
||||
headers = {"Content-Type": "application/json"} if encoded is not None else None
|
||||
data = self.request(method, path, encoded, headers, missing_ok=missing_ok)
|
||||
return json.loads(data) if data is not None else None
|
||||
|
||||
def public_download(self, url: str) -> bytes:
|
||||
return self.request("GET", url, authenticated=False)
|
||||
|
||||
|
||||
def upload(directory: Path, tag: str, revision: str) -> None:
|
||||
def publish(directory: Path, tag: str, revision: str) -> None:
|
||||
# This validates every local byte before the first API request, and therefore before creating or
|
||||
# mutating a release. Corresponding-source policy verification happens in the workflow first.
|
||||
files = expected_paths(directory, tag, revision)
|
||||
gitea = Gitea()
|
||||
quoted_tag = urllib.parse.quote(tag, safe="")
|
||||
release = gitea.json("GET", f"/releases/tags/{quoted_tag}")
|
||||
tagged = gitea.json("GET", f"/tags/{quoted_tag}")
|
||||
if not isinstance(release, dict):
|
||||
raise Refused(f"release {tag} does not exist")
|
||||
if not isinstance(tagged, dict) or tagged.get("commit", {}).get("sha") != revision:
|
||||
raise Refused(f"tag {tag} does not name {revision}")
|
||||
release = gitea.json("GET", f"/releases/tags/{quoted_tag}", missing_ok=True)
|
||||
tagged = gitea.json("GET", f"/tags/{quoted_tag}", missing_ok=True)
|
||||
if tagged is not None and (
|
||||
not isinstance(tagged, dict) or tagged.get("commit", {}).get("sha") != revision
|
||||
):
|
||||
raise Refused(f"existing tag {tag} does not name {revision}")
|
||||
if release is not None and tagged is None:
|
||||
raise Refused(f"release {tag} exists without a readable tag")
|
||||
if release is None:
|
||||
release = gitea.json("POST", "/releases", {
|
||||
"tag_name": tag,
|
||||
# A full immutable commit, never a moving branch, is the release's tag target.
|
||||
"target_commitish": revision,
|
||||
"name": f"Ekklesia Signage {tag}",
|
||||
"body": (
|
||||
"Published by the manual Publish Signage Release workflow after package, "
|
||||
"external integration, and corresponding-source verification."
|
||||
),
|
||||
# Existing Signage 0.x releases use Gitea's prerelease convention.
|
||||
"draft": False,
|
||||
"prerelease": True,
|
||||
})
|
||||
if not isinstance(release, dict):
|
||||
raise Refused(f"Gitea did not return the created release {tag}")
|
||||
tagged = gitea.json("GET", f"/tags/{quoted_tag}")
|
||||
if not isinstance(tagged, dict) or tagged.get("commit", {}).get("sha") != revision:
|
||||
raise Refused(f"created tag {tag} does not name {revision}")
|
||||
elif not isinstance(release, dict):
|
||||
raise Refused(f"cannot read release {tag}")
|
||||
|
||||
release_id = release.get("id")
|
||||
if not isinstance(release_id, int):
|
||||
raise Refused(f"release {tag} has no numeric id")
|
||||
listed = gitea.json("GET", f"/releases/{release_id}/assets")
|
||||
present = {asset["name"]: asset for asset in listed or []}
|
||||
unexpected = sorted(set(present) - {path.name for path in files})
|
||||
@@ -207,8 +240,10 @@ def upload(directory: Path, tag: str, revision: str) -> None:
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
try:
|
||||
if len(argv) != 4 or argv[0] not in {"verify-package", "verify", "upload"}:
|
||||
raise Refused("usage: release-assets.py (verify-package|verify|upload) DIRECTORY TAG REVISION")
|
||||
if len(argv) != 4 or argv[0] not in {"verify-package", "verify", "publish"}:
|
||||
raise Refused(
|
||||
"usage: release-assets.py (verify-package|verify|publish) DIRECTORY TAG REVISION"
|
||||
)
|
||||
command, directory, tag, revision = argv
|
||||
selected = (package_pair(Path(directory), tag, revision)
|
||||
if command == "verify-package"
|
||||
@@ -217,7 +252,7 @@ def main(argv: list[str]) -> int:
|
||||
for path in selected:
|
||||
print(path)
|
||||
else:
|
||||
upload(Path(directory), tag, revision)
|
||||
publish(Path(directory), tag, revision)
|
||||
return 0
|
||||
except (OSError, json.JSONDecodeError, Refused) as error:
|
||||
print(f"release-assets: REFUSED: {error}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user