Files
driver-standard/napalm_standard/release.py
T
christianmanivong db6b2e4ea7
CI / test (3.11) (push) Successful in 34s
CI / test (3.12) (push) Successful in 30s
CI / test (3.13) (push) Successful in 37s
feat: standards, shared CI and release tooling for NAPALM drivers
STANDARDS.md binds every repository in the NAPALM org: one default branch,
Conventional Commits, the device-types contract, test-driven development,
clean-code limits, a quality ratchet for existing code, packaging rules and
releases by tag to PyPI.

napalm-standard holds what a tool can check: the conformance check (with
release rules on a tag), the quality ratchet (ported from netOrk's
quality_ratchet.py), and the Gitea release from the CHANGELOG section.
.gitea/workflows/driver.yml is the CI every driver calls; template/ is a new
driver's starting point and passes the check as it is.
2026-10-22 19:01:14 +02:00

68 lines
2.2 KiB
Python

"""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/<repository>``."""
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