#!/usr/bin/env python3 """Attach files to the Gitea release of the current tag; create the release when missing. TOKEN= python3 .gitea/scripts/release-attach.py dist/*.zip dist/*.tar.gz Reads GITHUB_SERVER_URL, GITHUB_REPOSITORY and GITHUB_REF_NAME (the tag), as Gitea Actions sets them. A new release gets the tag's CHANGELOG.md section as its text. An asset of the same name from an earlier run of the tag is replaced. Used by .gitea/workflows/release.yml. """ import json import os import re import sys import urllib.error import urllib.parse import urllib.request import uuid from pathlib import Path ROOT = Path(__file__).resolve().parents[2] def changelog(tag): path = ROOT / "CHANGELOG.md" if not path.exists(): return "" text = path.read_text(encoding="utf-8") m = re.search(rf"^## {re.escape(tag)}\b.*?$(.*?)(?=^## |\Z)", text, re.M | re.S) return m.group(1).strip() if m else "" class Gitea: def __init__(self, server, repo, token): self.api = f"{server.rstrip('/')}/api/v1/repos/{repo}" self.auth = {"Authorization": f"token {token}"} def call(self, method, path, body=None, ctype="application/json"): req = urllib.request.Request( self.api + path, data=body, method=method, headers={**self.auth, "Content-Type": ctype, "Accept": "application/json"}, ) with urllib.request.urlopen(req, timeout=300) as r: data = r.read() return json.loads(data) if data.strip() else None def release(self, tag): try: return self.call("GET", f"/releases/tags/{urllib.parse.quote(tag, safe='')}"), False except urllib.error.HTTPError as e: if e.code != 404: raise body = json.dumps({"tag_name": tag, "name": f"NFC tag writer {tag}", "body": changelog(tag)}) return self.call("POST", "/releases", body.encode()), True def upload(self, rel, name, data): b = uuid.uuid4().hex head = ( f'--{b}\r\nContent-Disposition: form-data; name="attachment"; filename="{name}"\r\n' "Content-Type: application/octet-stream\r\n\r\n" ).encode() body = head + data + f"\r\n--{b}--\r\n".encode() self.call( "POST", f"/releases/{rel['id']}/assets?name={urllib.parse.quote(name)}", body, f"multipart/form-data; boundary={b}", ) def main(): paths = [Path(p) for p in sys.argv[1:]] if not paths: sys.exit(__doc__) env = {k: os.environ.get(k, "") for k in ("GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_REF_NAME", "TOKEN")} missing = [k for k, v in env.items() if not v] if missing: sys.exit(f"missing environment: {', '.join(missing)}") g = Gitea(env["GITHUB_SERVER_URL"], env["GITHUB_REPOSITORY"], env["TOKEN"]) rel, created = g.release(env["GITHUB_REF_NAME"]) print(("created" if created else "found") + f" release {env['GITHUB_REF_NAME']} (id {rel['id']})") old = {a["name"]: a["id"] for a in rel.get("assets") or []} for p in paths: if p.name in old: g.call("DELETE", f"/releases/{rel['id']}/assets/{old[p.name]}") data = p.read_bytes() g.upload(rel, p.name, data) print(f" {'replaced' if p.name in old else 'attached'} {p.name} ({len(data):,} bytes)") if __name__ == "__main__": main()