From c91908adce58c147dc1c0772c87a71e15d2cf502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Eggert=20Lundsteen=20Olsen?= Date: Thu, 1 Oct 2026 07:31:43 +0200 Subject: [PATCH] CI: publish the MSI as a Forgejo release on tags On a v* tag the msi job creates the release (if missing) and attaches the MSI under its own name and as SQLInsight-Pro-x64.msi, so the newest can be fetched without login from releases/latest/download/. The artifact upload stays for ordinary pushes. Uses the Actions token, or RELEASE_TOKEN if set. Co-Authored-By: Claude Opus 5.5 (1M context) --- .forgejo/workflows/ci.yml | 8 ++++ installer/publish-release.py | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100755 installer/publish-release.py diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index af3f8b6..95b7ee0 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -41,6 +41,14 @@ jobs: with: name: SQLInsight-Pro-msi path: dist/*.msi + - name: Udgiv som release + # Kun tags (v1.2.0): MSI'en vedhæftes releasen og kan hentes uden login fra + # /releases/latest/download/SQLInsight-Pro-x64.msi. RELEASE_TOKEN (valgfri secret) bruges, + # hvis Actions' automatiske token ikke må oprette releases. + if: startsWith(github.ref, 'refs/tags/v') + env: + TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + run: python3 installer/publish-release.py dist/*.msi integration: name: Integration (${{ matrix.image }}) diff --git a/installer/publish-release.py b/installer/publish-release.py new file mode 100755 index 0000000..6209500 --- /dev/null +++ b/installer/publish-release.py @@ -0,0 +1,72 @@ +#!/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. The MSI is attached twice: under its own name and as SQLInsight-Pro-x64.msi, so the +newest one can always be fetched from .../releases/latest/download/SQLInsight-Pro-x64.msi. +""" +import json +import os +import sys +import urllib.error +import urllib.request +import uuid + +LATEST_NAME = "SQLInsight-Pro-x64.msi" + +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)) + upload(call("GET", f"{API}/releases/{release['id']}"), msi, LATEST_NAME) + print(f"{SERVER}/{REPO}/releases/tag/{TAG}") + + +if __name__ == "__main__": + main()