#!/usr/bin/env python3 import json import mimetypes import os import sys import urllib.error import urllib.parse import urllib.request from pathlib import Path def api_request(method: str, url: str, token: str, data: bytes | None = None, headers: dict | None = None): req_headers = { "Authorization": f"token {token}", "Accept": "application/json", "User-Agent": "curl/8.10.1", **(headers or {}), } req = urllib.request.Request(url, data=data, headers=req_headers, method=method) try: with urllib.request.urlopen(req, timeout=60) as resp: body = resp.read() if not body: return None return json.loads(body) except urllib.error.HTTPError as exc: body = exc.read().decode("utf-8", "ignore") raise RuntimeError(f"HTTP {exc.code} for {method} {url}: {body}") from exc def get_or_create_release(api_base: str, owner: str, repo: str, tag: str, title: str, body: str, token: str, target: str): tag_url = f"{api_base}/repos/{owner}/{repo}/releases/tags/{urllib.parse.quote(tag, safe='')}" try: release = api_request("GET", tag_url, token) if release is not None: return release except RuntimeError as exc: if "HTTP 404" not in str(exc): raise create_url = f"{api_base}/repos/{owner}/{repo}/releases" payload = { "tag_name": tag, "name": title, "body": body, "draft": False, "prerelease": False, "target_commitish": target, } return api_request( "POST", create_url, token, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, ) def replace_release_assets(api_base: str, owner: str, repo: str, release: dict, files: list[Path], token: str): release_id = release["id"] existing_assets = {asset["name"]: asset for asset in release.get("assets", [])} for path in files: name = path.name if name in existing_assets: asset_id = existing_assets[name]["id"] delete_url = f"{api_base}/repos/{owner}/{repo}/releases/{release_id}/assets/{asset_id}" api_request("DELETE", delete_url, token) upload_url = f"{api_base}/repos/{owner}/{repo}/releases/{release_id}/assets?name={urllib.parse.quote(name, safe='')}" content_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream" data = path.read_bytes() api_request( "POST", upload_url, token, data=data, headers={"Content-Type": content_type}, ) def main() -> int: token = os.environ["GITEA_TOKEN"] api_base = os.environ["GITEA_API_URL"].rstrip("/") owner = os.environ["REPO_OWNER"] repo = os.environ["REPO_NAME"] version = os.environ["VERSION"] target = os.environ.get("TARGET_SHA", "main") files = [Path(p) for p in sys.argv[1:]] if not files: raise SystemExit("usage: publish_release.py [file ...]") missing = [str(p) for p in files if not p.exists()] if missing: raise SystemExit(f"missing files: {', '.join(missing)}") tool = os.environ.get("TOOL", "iperf3") tag = f"{tool}-v{version}" title = f"{tool} {version}" body = ( f"Static musl build for {tool} {version}.\\n\\n" f"Repository: {owner}/{repo}\\n" f"Commit: {target}" ) release = get_or_create_release(api_base, owner, repo, tag, title, body, token, target) replace_release_assets(api_base, owner, repo, release, files, token) print(f"published_tag={tag}") for path in files: print(f"asset={path.name}") return 0 if __name__ == "__main__": raise SystemExit(main())