Add Gitea release script
- Add Windows download link to README
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
Media scanning, indexing, and Netflix-style web streaming for your torrent collection.
|
||||
|
||||
**[Windows portable ZIP download](https://git.zi.fi/LeoVasanko/mediahive/releases)**
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
|
||||
@@ -20,6 +20,9 @@ dependencies = [
|
||||
[project.scripts]
|
||||
mediahive = "mediahive.__main__:main"
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://git.zi.fi/LeoVasanko/mediahive"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling", "hatch-vcs"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""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 or MediaHive-1.2.3.4-win64.zip
|
||||
# Rejects dev/dirty names like MediaHive-1.2.3.dev0+gabcd-win64.zip
|
||||
_CLEAN_ZIP_RE = re.compile(r"^MediaHive-(\d+(?:\.\d+)*)-win64\.zip$")
|
||||
|
||||
|
||||
def find_releasable_zips() -> list[tuple[Path, str]]:
|
||||
"""Return (path, version) pairs for clean-versioned ZIPs in build/."""
|
||||
build_dir = REPO_ROOT / "build"
|
||||
results = []
|
||||
for p in sorted(build_dir.glob("MediaHive-*-win64.zip")):
|
||||
m = _CLEAN_ZIP_RE.match(p.name)
|
||||
if m:
|
||||
results.append((p, m.group(1)))
|
||||
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 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:
|
||||
for zip_path, version in zips:
|
||||
print(f"\nReleasing {version} ...")
|
||||
tag = f"v{version}"
|
||||
release_id = create_release(
|
||||
client, base_url, repo, tag, version, args.notes, args.draft
|
||||
)
|
||||
for path in [zip_path, *dist_files[version]]:
|
||||
upload_asset(client, base_url, repo, release_id, 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)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Release failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user