#!/usr/bin/env python3 # Creates or updates the Gitea release for a tag and attaches files to it. Its # notes are that version's section of CHANGELOG.md. Run again for the same tag, # it updates the notes and replaces files of the same name. # # gitea-release.py ... # # Needs GITEA_URL (https://gitea.example.com), GITEA_REPO (owner/name) and # GITEA_TOKEN. Used by .gitea/workflows/release.yaml. import json import os import sys import urllib.error import urllib.parse import urllib.request import uuid def api(method, path, body=None, content_type="application/json"): url = f"{os.environ['GITEA_URL'].rstrip('/')}/api/v1/repos/{os.environ['GITEA_REPO']}{path}" if isinstance(body, dict): body = json.dumps(body).encode() headers = {"Authorization": f"token {os.environ['GITEA_TOKEN']}", "Accept": "application/json"} if body is not None: headers["Content-Type"] = content_type request = urllib.request.Request(url, data=body, method=method, headers=headers) try: with urllib.request.urlopen(request, timeout=300) as response: data = response.read() return json.loads(data) if data else None except urllib.error.HTTPError as e: if e.code == 404 and method == "GET": return None sys.exit(f"{method} {url}: HTTP {e.code}: {e.read().decode(errors='replace')}") def changelog_section(version): """The lines under '## ' in CHANGELOG.md, up to the next '## '.""" lines, found = [], False with open("CHANGELOG.md") as f: for line in f: if line.startswith("## "): if found: break found = line[3:].split()[0] == version continue if found: lines.append(line) if not found: sys.exit(f"CHANGELOG.md has no '## {version}' section") return "".join(lines).strip() + "\n" def upload(release_id, path): name = os.path.basename(path) boundary = uuid.uuid4().hex with open(path, "rb") as f: content = f.read() body = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{name}\"\r\n" f"Content-Type: application/octet-stream\r\n\r\n").encode() + content + f"\r\n--{boundary}--\r\n".encode() api("POST", f"/releases/{release_id}/assets?name={urllib.parse.quote(name)}", body, f"multipart/form-data; boundary={boundary}") print(f"attached {name}") def main(tag, files): notes = changelog_section(tag[1:] if tag.startswith("v") else tag) release = api("GET", f"/releases/tags/{urllib.parse.quote(tag)}") if release is None: release = api("POST", "/releases", {"tag_name": tag, "name": tag, "body": notes}) print(f"created release {tag}") else: release = api("PATCH", f"/releases/{release['id']}", {"body": notes}) print(f"updated release {tag}") names = {os.path.basename(p) for p in files} for asset in release.get("assets") or []: if asset["name"] in names: api("DELETE", f"/releases/{release['id']}/assets/{asset['id']}") for path in files: upload(release["id"], path) print(release.get("html_url", "")) if __name__ == "__main__": if len(sys.argv) < 3: sys.exit("usage: gitea-release.py ...") for var in ("GITEA_URL", "GITEA_REPO", "GITEA_TOKEN"): if not os.environ.get(var): sys.exit(f"{var} is not set") main(sys.argv[1], sys.argv[2:])