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.
This commit is contained in:
@@ -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
|
||||||
@@ -19,3 +19,4 @@ package-lock.json
|
|||||||
# Dotfiles
|
# Dotfiles
|
||||||
.*
|
.*
|
||||||
!.gitignore
|
!.gitignore
|
||||||
|
!.gitea/
|
||||||
|
|||||||
+51
-13
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user