#!/usr/bin/env python3 """Build package, optionally publish to PyPI, and create Gitea release. Uses git-cliff to generate the release notes from conventional commits. Usage: REPO_TOKEN= [PYPI_TOKEN=] python3 scripts/publish.py """ import os import shutil import subprocess # nosec B404 import sys import click from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] from gitea_runner_manager.api_clients import GiteaClient from gitea_runner_manager.config import GITEA_API_URL from gitea_runner_manager.exceptions import APIError from gitea_runner_manager.i18n import _ load_dotenv(override=True) CLIFF_CONFIG = "cliff.toml" def generate_release_notes(tag: str) -> str: """Generate release notes for the given tag using git-cliff. Falls back to a generic message if git-cliff is not available. """ cliff_bin = shutil.which("git-cliff") if not cliff_bin: return f"Release {tag}\n\nSee CHANGELOG.md for details." try: result = subprocess.run( # nosec B603 [cliff_bin, "--config", CLIFF_CONFIG, "--latest", "--strip", "header"], capture_output=True, text=True, check=False, ) if result.returncode == 0 and result.stdout.strip(): return result.stdout.strip() except FileNotFoundError: pass return f"Release {tag}\n\nSee CHANGELOG.md for details." def build_package() -> None: """Build the Python package using python -m build.""" result = subprocess.run( # nosec B603 [sys.executable, "-m", "build"], capture_output=True, text=True, check=False, ) if result.returncode != 0: raise click.ClickException( _( "Oops! Package build failed:\n{stderr}", stderr=result.stderr.strip(), ) ) def publish_to_pypi(token: str) -> None: """Publish built packages to PyPI using twine.""" result = subprocess.run( # nosec B603 [ sys.executable, "-m", "twine", "upload", "dist/*", "-u", "__token__", "-p", token, ], capture_output=True, text=True, check=False, ) if result.returncode != 0: raise click.ClickException( _( "Oops! PyPI publish failed:\n{stderr}", stderr=result.stderr.strip(), ) ) click.echo(_("Published to PyPI.")) @click.command() @click.argument("tag") @click.argument("repo") def main(tag: str, repo: str) -> None: gitea_token = os.environ.get("REPO_TOKEN", "") if not gitea_token: raise click.ClickException(_("ERROR: REPO_TOKEN is not set.")) pypi_token = os.environ.get("PYPI_TOKEN", "") build_package() if pypi_token: publish_to_pypi(pypi_token) else: click.echo(_("PYPI_TOKEN not set — skipping PyPI publish. No worries, we'll just create the Gitea release.")) owner, repo_name = repo.split("/") client = GiteaClient(GITEA_API_URL, gitea_token, owner, repo_name) release_body = generate_release_notes(tag) try: client.create_release_idempotent( tag=tag, body=release_body, ) except APIError as e: raise click.ClickException( _( "Release creation failed with HTTP {status}: {message}", status=e.status, message=e.message, ) ) from None click.echo( _( "Nice! Gitea release {tag} created.", tag=tag, ) ) if __name__ == "__main__": # pragma: no cover main()