"""Create or update the Gitea release of a version tag (STANDARDS.md ยง9). The upload to PyPI is twine's job in the workflow; this only writes the release page, with that version's CHANGELOG.md section as its text. It never creates a tag, so it never pushes anything and never starts a CI run. Ported from netOrk's scripts/release.py (#561). """ from __future__ import annotations import json import urllib.error import urllib.request from collections.abc import Callable from typing import Any from napalm_standard.changelog import changelog_section Api = Callable[..., Any] class NotFound(Exception): """The API answered 404.""" def publish(tag: str, package: str, changelog: str, api: Api) -> Any: """Create the release of *tag*, or update it when it exists already.""" version = tag.removeprefix("v") body = changelog_section(changelog, version) if body is None: raise ValueError(f"CHANGELOG.md has no section for [{version}]") payload = { "tag_name": tag, "name": f"{package} {version}", "body": body, "draft": False, "prerelease": False, } try: existing = api("GET", f"/releases/tags/{tag}") except NotFound: return api("POST", "/releases", payload) return api("PATCH", f"/releases/{existing['id']}", payload) def gitea_api(base_url: str, repository: str, token: str) -> Api: # pragma: no cover - I/O """A small JSON client for ``/api/v1/repos/``.""" root = f"{base_url.rstrip('/')}/api/v1/repos/{repository}" def call(method: str, path: str, payload: dict[str, Any] | None = None) -> Any: request = urllib.request.Request( root + path, method=method, data=json.dumps(payload).encode() if payload is not None else None, headers={"Authorization": f"token {token}", "Content-Type": "application/json"}, ) try: with urllib.request.urlopen(request, timeout=30) as response: # noqa: S310 body = response.read() except urllib.error.HTTPError as exc: if exc.code == 404: raise NotFound(path) from exc raise return json.loads(body) if body else {} return call