Build GUI app on macos/windows/linux runners, one shared release
release / gui-build (macos) (push) Successful in 1m12s
release / gui-build (windows) (push) Successful in 1m28s
release / gui-build (linux) (push) Successful in 1m29s

This commit is contained in:
2026-09-22 03:25:43 +00:00
parent 04a0da2e9a
commit c0c295728f
2 changed files with 67 additions and 15 deletions
+16 -2
View File
@@ -6,8 +6,12 @@ on:
- "v*" - "v*"
jobs: jobs:
macos-gui: gui-build:
runs-on: macos strategy:
fail-fast: false
matrix:
os: [macos, windows, linux]
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -17,7 +21,17 @@ jobs:
- name: Build GUI app zip and dist packages - name: Build GUI app zip and dist packages
run: uv run --extra gui scripts/guibuild.py 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 - name: Create Gitea release and upload assets
if: matrix.os == 'linux'
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: uv run scripts/release.py 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
+51 -13
View File
@@ -11,8 +11,12 @@ Token: GITEA_TOKEN environment variable
Steps: Steps:
1. Find clean-versioned ZIPs in build/ and matching dist/ wheels/sdists 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 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 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 import argparse
@@ -117,6 +121,18 @@ def gitea_headers(token: str) -> dict:
return {"Authorization": f"token {token}", "Accept": "application/json"} 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( def create_release(
client: httpx.Client, client: httpx.Client,
base_url: str, base_url: str,
@@ -125,8 +141,12 @@ def create_release(
version: str, version: str,
notes: str, notes: str,
draft: bool, draft: bool,
) -> int: ) -> tuple[int, set[str]]:
"""Create a Gitea release and return its id.""" """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" url = f"{base_url}/api/v1/repos/{repo}/releases"
payload = { payload = {
"tag_name": tag, "tag_name": tag,
@@ -137,11 +157,17 @@ def create_release(
} }
resp = client.post(url, json=payload) resp = client.post(url, json=payload)
if resp.status_code == 409: 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() resp.raise_for_status()
release_id = resp.json()["id"] release_id = resp.json()["id"]
print(f"Created release id={release_id} (draft={draft})") print(f"Created release id={release_id} (draft={draft})")
return release_id return release_id, set()
def upload_asset( def upload_asset(
@@ -185,6 +211,11 @@ def main() -> None:
parser.add_argument( parser.add_argument(
"--notes", default="", metavar="TEXT", help="Release notes body" "--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() args = parser.parse_args()
try: try:
@@ -202,26 +233,33 @@ def main() -> None:
# Validate all dist files exist before touching Gitea # Validate all dist files exist before touching Gitea
dist_files: dict[str, list[Path]] = {} dist_files: dict[str, list[Path]] = {}
for _, version, _platform_tag in zips: if not args.no_dist:
dist_files[version] = find_dist_files(version) for _, version, _platform_tag in zips:
dist_files[version] = find_dist_files(version)
base_url = cfg["url"].rstrip("/") base_url = cfg["url"].rstrip("/")
repo = cfg["repo"] repo = cfg["repo"]
with httpx.Client(headers=gitea_headers(token)) as client: 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: for zip_path, version, platform_tag in zips:
print(f"\nReleasing {version} ...") print(f"\nReleasing {version} ...")
tag = f"v{version}" tag = f"v{version}"
release_id = release_ids_by_version.get(version) if version not in releases:
if release_id is None: releases[version] = create_release(
release_id = create_release(
client, base_url, repo, tag, version, args.notes, args.draft client, base_url, repo, tag, version, args.notes, args.draft
) )
release_ids_by_version[version] = release_id release_id, uploaded = releases[version]
for path in dist_files[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) 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}") print(f"Uploading platform artifact: {platform_tag}")
upload_asset(client, base_url, repo, release_id, zip_path) upload_asset(client, base_url, repo, release_id, zip_path)
print(f"{tag} published") print(f"{tag} published")