Files
mediahive/scripts/release.py
T

233 lines
7.4 KiB
Python

"""Publish a MediaHive release to Gitea.
Usage:
uv run scripts/release.py [--draft] [--notes "Release notes"]
Reads from [project.urls] Repository in pyproject.toml.
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
4. Remind the user to run: uv publish
"""
import argparse
import os
import re
import sys
import tomllib
from pathlib import Path
from urllib.parse import urlparse
import httpx
REPO_ROOT = Path(__file__).parent.parent
# ---------------------------------------------------------------------------
# Config / token helpers
# ---------------------------------------------------------------------------
def load_gitea_config() -> dict:
pyproject = REPO_ROOT / "pyproject.toml"
with open(pyproject, "rb") as f:
data = tomllib.load(f)
repo_url = data.get("project", {}).get("urls", {}).get("Repository")
if not repo_url:
raise RuntimeError("[project.urls] Repository missing from pyproject.toml")
parsed = urlparse(repo_url.rstrip("/"))
parts = parsed.path.lstrip("/").split("/", 1)
if len(parts) != 2:
raise RuntimeError(
"[project.urls] Repository must include owner and repo, e.g. https://git.example.com/owner/repo"
)
return {
"url": f"{parsed.scheme}://{parsed.netloc}",
"repo": f"{parts[0]}/{parts[1]}",
}
def load_token() -> str:
token = os.environ.get("GITEA_TOKEN", "").strip()
if not token:
raise RuntimeError("GITEA_TOKEN environment variable is not set")
return token
# ---------------------------------------------------------------------------
# ZIP + dist helpers
# ---------------------------------------------------------------------------
# Matches MediaHive-1.2.3-win64.zip, MediaHive-1.2.3-macos-arm64.zip, etc.
# Rejects dev/dirty versions like MediaHive-1.2.3.dev0+gabcd-win64.zip
_CLEAN_ZIP_RE = re.compile(r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.zip$")
def find_releasable_zips() -> list[tuple[Path, str, str]]:
"""Return (path, version, platform_tag) for clean-versioned ZIPs in build/."""
build_dir = REPO_ROOT / "build"
results = []
for p in sorted(build_dir.glob("MediaHive-*.zip")):
m = _CLEAN_ZIP_RE.match(p.name)
if m:
results.append((p, m.group(1), m.group(2)))
return results
def find_dist_files(version: str) -> list[Path]:
"""Return wheel and sdist paths in dist/ for the given version.
Raises FileNotFoundError listing every missing file if any are absent.
"""
dist_dir = REPO_ROOT / "dist"
ver = re.escape(version)
wheel = next((p for p in dist_dir.glob(f"mediahive-{version}-*.whl")), None)
sdist = next(
(
p
for p in dist_dir.glob(f"mediahive-{version}.*")
if p.suffix in (".gz", ".zip") and p.name != f"mediahive-{version}.zip"
),
None,
)
missing = []
if wheel is None:
missing.append(f" dist/mediahive-{version}-*.whl")
if sdist is None:
missing.append(f" dist/mediahive-{version}.tar.gz (or .zip)")
if missing:
raise FileNotFoundError(
f"Missing dist files for version {version}:\n"
+ "\n".join(missing)
+ "\nRun: uv build"
)
return [wheel, sdist]
# ---------------------------------------------------------------------------
# Gitea API helpers
# ---------------------------------------------------------------------------
def gitea_headers(token: str) -> dict:
return {"Authorization": f"token {token}", "Accept": "application/json"}
def create_release(
client: httpx.Client,
base_url: str,
repo: str,
tag: str,
version: str,
notes: str,
draft: bool,
) -> int:
"""Create a Gitea release and return its id."""
url = f"{base_url}/api/v1/repos/{repo}/releases"
payload = {
"tag_name": tag,
"name": f"MediaHive {version}",
"body": notes,
"draft": draft,
"prerelease": False,
}
resp = client.post(url, json=payload)
if resp.status_code == 409:
raise RuntimeError(f"A release for tag '{tag}' already exists on Gitea.")
resp.raise_for_status()
release_id = resp.json()["id"]
print(f"Created release id={release_id} (draft={draft})")
return release_id
def upload_asset(
client: httpx.Client,
base_url: str,
repo: str,
release_id: int,
path: Path,
) -> str:
"""Upload a file to the release and return the download URL."""
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
size_mb = path.stat().st_size / (1024 * 1024)
mime = "application/zip" if path.suffix == ".zip" else "application/octet-stream"
print(f"Uploading {path.name} ({size_mb:.1f} MB) ...")
with open(path, "rb") as fh:
resp = client.post(
url,
files={"attachment": (path.name, fh, mime)},
timeout=300,
)
resp.raise_for_status()
download_url = resp.json()["browser_download_url"]
print(f" -> {download_url}")
return download_url
# ---------------------------------------------------------------------------
# Entrypoint
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Publish a MediaHive release to Gitea")
parser.add_argument(
"--draft", action="store_true", help="Create as a draft release"
)
parser.add_argument(
"--notes", default="", metavar="TEXT", help="Release notes body"
)
args = parser.parse_args()
try:
cfg = load_gitea_config()
token = load_token()
zips = find_releasable_zips()
if not zips:
raise FileNotFoundError(
"No clean-versioned ZIPs found in build/.\n"
"Run scripts/winbuild.py first."
)
# 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)
base_url = cfg["url"].rstrip("/")
repo = cfg["repo"]
with httpx.Client(headers=gitea_headers(token)) as client:
release_ids_by_version: dict[str, int] = {}
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(
client, base_url, repo, tag, version, args.notes, args.draft
)
release_ids_by_version[version] = release_id
for path in dist_files[version]:
upload_asset(client, base_url, repo, release_id, path)
print(f"Uploading platform artifact: {platform_tag}")
upload_asset(client, base_url, repo, release_id, zip_path)
print(f" ✓ {tag} published")
print("\nDone. To publish to PyPI, run:")
print(" uv publish")
except Exception as e:
print(f"✗ Release failed: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()