#!/usr/bin/env python3 """Attaches the MSI to a Forgejo release for the current tag (created if missing). Run by ci.yml on tags v*. publish-release.py dist/SQLInsight-Pro-1.2.0-x64.msi Environment (set by Forgejo Actions): GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_REF_NAME, and TOKEN with write access to the repository. Forgejo has no .../releases/latest/download/ URL; installer/get-latest-msi.ps1 finds the newest MSI through the API (GET /repos/{repo}/releases/latest) instead. """ import json import os import sys import urllib.error import urllib.request import uuid SERVER = os.environ["GITHUB_SERVER_URL"].rstrip("/") REPO = os.environ["GITHUB_REPOSITORY"] TAG = os.environ["GITHUB_REF_NAME"] TOKEN = os.environ["TOKEN"] API = f"{SERVER}/api/v1/repos/{REPO}" def call(method, url, body=None, content_type="application/json"): data = json.dumps(body).encode() if isinstance(body, dict) else body request = urllib.request.Request(url, data=data, method=method, headers={ "Authorization": f"token {TOKEN}", "Accept": "application/json", "Content-Type": content_type, }) try: with urllib.request.urlopen(request, timeout=300) as response: text = response.read().decode() return json.loads(text) if text else {} except urllib.error.HTTPError as error: if error.code == 404 and method == "GET": return None sys.exit(f"{method} {url}: {error.code} {error.read().decode()[:300]}") def upload(release, path, name): for asset in release.get("assets") or []: if asset["name"] == name: # a re-run of the same tag replaces the file call("DELETE", f"{API}/releases/{release['id']}/assets/{asset['id']}") 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() call("POST", f"{API}/releases/{release['id']}/assets?name={name}", body, f"multipart/form-data; boundary={boundary}") print(f"vedhæftet {name} ({len(content) // 1024 // 1024} MB)") def main(): if len(sys.argv) != 2: sys.exit(__doc__) msi = sys.argv[1] release = call("GET", f"{API}/releases/tags/{TAG}") if release is None: release = call("POST", f"{API}/releases", { "tag_name": TAG, "name": f"SQLInsight Pro {TAG.removeprefix('v')}", "body": "MSI-installer til Windows (agent og web). Se docs/installation.md.", "draft": False, "prerelease": "-" in TAG, }) print(f"release {TAG} oprettet") upload(release, msi, os.path.basename(msi)) print(f"{SERVER}/{REPO}/releases/tag/{TAG}") if __name__ == "__main__": main()