Files
grm/scripts/publish.py
T

141 lines
3.7 KiB
Python

#!/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=<token> [PYPI_TOKEN=<token>] python3 scripts/publish.py <tag> <repo>
"""
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(
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()