From ff10c86e840c5d22a51ccdd352c206b545e576b0 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Tue, 22 Sep 2026 03:54:05 +0000 Subject: [PATCH] Add Gitea Actions cross-platform release workflow Build the GUI app on macos/windows/linux host runners on v* tag pushes. All platform jobs converge on one Gitea release; release.py now reuses an existing release, skips duplicate assets, and supports --no-dist so only the linux job uploads the wheel/sdist. --- .gitea/workflows/release.yaml | 37 ++++++++++++++++++++ .gitignore | 1 + scripts/release.py | 64 ++++++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 .gitea/workflows/release.yaml diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..51eb48b --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,37 @@ +name: release + +on: + push: + tags: + - "v*" + +jobs: + gui-build: + strategy: + fail-fast: false + matrix: + os: [macos, windows, linux] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + # setuptools_scm needs full history/tags to compute the version + fetch-depth: 0 + + - name: Build GUI app zip and dist packages + run: uv run --extra gui scripts/guibuild.py + + # Every platform converges on the one release for the tag; release.py + # reuses an existing release and skips already-uploaded assets. + # Only the linux job publishes the wheel/sdist (identical across platforms). + - name: Create Gitea release and upload assets + if: matrix.os == 'linux' + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: uv run scripts/release.py + + - name: Attach platform zip to the Gitea release + if: matrix.os != 'linux' + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: uv run scripts/release.py --no-dist diff --git a/.gitignore b/.gitignore index 74241ef..0dfef6f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ package-lock.json # Dotfiles .* !.gitignore +!.gitea/ diff --git a/scripts/release.py b/scripts/release.py index 053a712..d10b6ce 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -11,8 +11,12 @@ Token: GITEA_TOKEN environment variable Steps: 1. Find clean-versioned ZIPs in build/ and matching dist/ wheels/sdists 2. Abort if any dist files are missing for a found ZIP version - 3. Create a Gitea release for each version and upload all assets + 3. Create a Gitea release for each version (or reuse the existing one + for the tag, skipping already-uploaded assets) and upload all assets 4. Remind the user to run: uv publish + +Parallel CI platform builds converge on one release per tag; pass --no-dist +on all but one platform so only it uploads the wheel/sdist. """ import argparse @@ -117,6 +121,18 @@ def gitea_headers(token: str) -> dict: return {"Authorization": f"token {token}", "Accept": "application/json"} +def get_release_by_tag( + client: httpx.Client, base_url: str, repo: str, tag: str +) -> dict | None: + """Return the existing release for a tag, or None.""" + url = f"{base_url}/api/v1/repos/{repo}/releases/tags/{tag}" + resp = client.get(url) + if resp.status_code == 404: + return None + resp.raise_for_status() + return resp.json() + + def create_release( client: httpx.Client, base_url: str, @@ -125,8 +141,12 @@ def create_release( version: str, notes: str, draft: bool, -) -> int: - """Create a Gitea release and return its id.""" +) -> tuple[int, set[str]]: + """Create a Gitea release, or reuse the existing one for the tag. + + Returns (release_id, names of assets already attached), so parallel + platform builds can converge on one release without conflicts. + """ url = f"{base_url}/api/v1/repos/{repo}/releases" payload = { "tag_name": tag, @@ -137,11 +157,17 @@ def create_release( } resp = client.post(url, json=payload) if resp.status_code == 409: - raise RuntimeError(f"A release for tag '{tag}' already exists on Gitea.") + existing = get_release_by_tag(client, base_url, repo, tag) + if existing is None: + raise RuntimeError(f"Release for tag '{tag}' conflicts but cannot be read.") + release_id = existing["id"] + assets = {a["name"] for a in existing.get("assets", [])} + print(f"Release for tag '{tag}' already exists (id={release_id}), reusing it.") + return release_id, assets resp.raise_for_status() release_id = resp.json()["id"] print(f"Created release id={release_id} (draft={draft})") - return release_id + return release_id, set() def upload_asset( @@ -185,6 +211,11 @@ def main() -> None: parser.add_argument( "--notes", default="", metavar="TEXT", help="Release notes body" ) + parser.add_argument( + "--no-dist", + action="store_true", + help="Skip wheel/sdist upload (for parallel platform builds; one job uploads them)", + ) args = parser.parse_args() try: @@ -202,26 +233,33 @@ def main() -> None: # Validate all dist files exist before touching Gitea dist_files: dict[str, list[Path]] = {} - for _, version, _platform_tag in zips: - dist_files[version] = find_dist_files(version) + if not args.no_dist: + for _, version, _platform_tag in zips: + dist_files[version] = find_dist_files(version) base_url = cfg["url"].rstrip("/") repo = cfg["repo"] with httpx.Client(headers=gitea_headers(token)) as client: - release_ids_by_version: dict[str, int] = {} + releases: dict[str, tuple[int, set[str]]] = {} for zip_path, version, platform_tag in zips: print(f"\nReleasing {version} ...") tag = f"v{version}" - release_id = release_ids_by_version.get(version) - if release_id is None: - release_id = create_release( + if version not in releases: + releases[version] = create_release( client, base_url, repo, tag, version, args.notes, args.draft ) - release_ids_by_version[version] = release_id - for path in dist_files[version]: + release_id, uploaded = releases[version] + for path in dist_files.get(version, []): + if path.name in uploaded: + print(f"Skipping {path.name}, already on the release.") + continue upload_asset(client, base_url, repo, release_id, path) + release_id, uploaded = releases[version] + if zip_path.name in uploaded: + print(f"Skipping {zip_path.name}, already on the release.") + continue print(f"Uploading platform artifact: {platform_tag}") upload_asset(client, base_url, repo, release_id, zip_path) print(f" ✓ {tag} published")