#!/usr/bin/env python3 import argparse import json import os import sys import urllib.parse import urllib.request def _api_base() -> str: api = os.environ.get("GITHUB_API_URL") if not api: raise RuntimeError("GITHUB_API_URL is not set") return api.rstrip("/") def _repo() -> str: repo = os.environ.get("GITHUB_REPOSITORY") if not repo: raise RuntimeError("GITHUB_REPOSITORY is not set") return repo def _token() -> str: token = os.environ.get("GITEA_TOKEN") if not token: raise RuntimeError("GITEA_TOKEN is not set") return token def _request(path: str, method: str = "GET", data=None, headers=None): url = f"{_api_base()}{path}" req_headers = { "Accept": "application/json", "Authorization": f"token {_token()}", } if headers: req_headers.update(headers) body = None if data is not None: if isinstance(data, (dict, list)): body = json.dumps(data).encode("utf-8") req_headers["Content-Type"] = "application/json" elif isinstance(data, bytes): body = data else: body = str(data).encode("utf-8") req = urllib.request.Request(url, data=body, headers=req_headers, method=method) try: with urllib.request.urlopen(req) as response: raw = response.read() if not raw: return None return json.loads(raw.decode("utf-8")) except urllib.error.HTTPError as exc: payload = exc.read().decode("utf-8", errors="replace") raise RuntimeError(f"{method} {url} failed: {exc.code} {payload}") from exc def _release_path(tag: str) -> str: return f"/repos/{_repo()}/releases/tags/{urllib.parse.quote(tag, safe='')}" def ensure_release(args) -> int: try: release = _request(_release_path(args.tag)) except RuntimeError as exc: if " 404 " not in str(exc): raise release = None if release is None: payload = { "tag_name": args.tag, "target_commitish": args.target, "name": args.name or args.tag, "body": args.notes or "", "draft": args.draft, "prerelease": args.prerelease, } release = _request(f"/repos/{_repo()}/releases", method="POST", data=payload) output = os.environ.get("GITHUB_OUTPUT") if output: with open(output, "a", encoding="utf-8") as fh: fh.write(f"release_id={release['id']}\n") fh.write(f"release_tag={release['tag_name']}\n") print(json.dumps({"id": release["id"], "tag": release["tag_name"]})) return 0 def _list_assets(release_id: int): release = _request(f"/repos/{_repo()}/releases/{release_id}") return release.get("assets") or release.get("attachments") or [] def upload_asset(args) -> int: filename = args.name or os.path.basename(args.file) for asset in _list_assets(args.release_id): if asset.get("name") == filename and asset.get("id") is not None: _request( f"/repos/{_repo()}/releases/{args.release_id}/assets/{asset['id']}", method="DELETE", ) with open(args.file, "rb") as fh: payload = fh.read() path = ( f"/repos/{_repo()}/releases/{args.release_id}/assets" f"?name={urllib.parse.quote(filename, safe='')}" ) response = _request( path, method="POST", data=payload, headers={"Content-Type": "application/octet-stream"}, ) print(json.dumps({"id": response.get("id"), "name": response.get("name", filename)})) return 0 def main() -> int: parser = argparse.ArgumentParser() sub = parser.add_subparsers(dest="cmd", required=True) ensure = sub.add_parser("ensure-release") ensure.add_argument("--tag", required=True) ensure.add_argument("--target", required=True) ensure.add_argument("--name", default="") ensure.add_argument("--notes", default="") ensure.add_argument("--draft", action="store_true") ensure.add_argument("--prerelease", action="store_true") upload = sub.add_parser("upload-asset") upload.add_argument("--release-id", required=True, type=int) upload.add_argument("--file", required=True) upload.add_argument("--name", default="") args = parser.parse_args() if args.cmd == "ensure-release": return ensure_release(args) if args.cmd == "upload-asset": return upload_asset(args) parser.print_help() return 1 if __name__ == "__main__": try: raise SystemExit(main()) except Exception as exc: print(f"gitea_release.py failed: {exc}", file=sys.stderr) raise