Files
netmover/scripts/gitea_release.py
T
Florian WildeandClaude Opus 5.5 a627044df7
CI / pruefen (3.12) (push) Waiting to run
CI / pruefen (3.13) (push) Waiting to run
CI / release (push) Blocked by required conditions
chore: Gitea-Releases aus dem Changelog – Skript und CI-Job
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:27:27 +01:00

139 lines
4.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Gitea-Release zu einem Tag anlegen oder aktualisieren – Text aus CHANGELOG.md.
scripts/gitea_release.py v0.11.0 # ein Tag
scripts/gitea_release.py --all # alle Tags (bestehende Releases werden aktualisiert)
Token (Gitea → Einstellungen → Anwendungen, Berechtigung „repository: Lesen und
Schreiben“) aus ``GITEA_TOKEN`` oder ``~/.config/netmover/gitea-token`` (Modus 0600).
Server und Repository aus ``GITEA_URL``/``GITEA_REPO``, sonst aus ``git remote origin``.
In Gitea Actions setzt der Workflow beides selbst (siehe ``.gitea/workflows/ci.yml``).
Nur Standardbibliothek – läuft ohne venv.
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TOKEN_FILE = Path.home() / ".config" / "netmover" / "gitea-token"
TAG_RE = re.compile(r"^v\d+\.\d+\.\d+$")
def changelog_section(text: str, version: str) -> str:
"""Abschnitt ``## [version] – datum`` bis zum nächsten ``## [``, ohne Überschrift."""
m = re.search(
rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|^\[[^\]]+\]: |\Z)",
text,
re.M | re.S,
)
if m is None:
raise SystemExit(f"CHANGELOG.md enthält keinen Abschnitt [{version}]")
return m.group(1).strip()
def _git(*args: str) -> str:
return subprocess.run(
["git", *args], # noqa: S607
cwd=ROOT,
check=True,
capture_output=True,
text=True,
).stdout.strip()
def release_title(tag: str) -> str:
"""Betreff der Tag-Annotation, z. B. „NetMover 0.11.0 – mehrere Ziele je Regel“."""
return _git("tag", "-l", "--format=%(contents:subject)", tag) or f"NetMover {tag[1:]}"
def remote() -> tuple[str, str]:
url, repo = os.environ.get("GITEA_URL"), os.environ.get("GITEA_REPO")
if url and repo:
return url.rstrip("/"), repo
origin = _git("config", "--get", "remote.origin.url")
m = re.match(r"^(https?://[^/]+)/(.+?)(\.git)?$", origin)
if m is None:
raise SystemExit(f"Kann Server/Repository nicht aus {origin!r} ableiten")
return m.group(1), m.group(2)
def token() -> str:
value = os.environ.get("GITEA_TOKEN", "").strip()
if not value and TOKEN_FILE.is_file():
if TOKEN_FILE.stat().st_mode & 0o077:
raise SystemExit(f"{TOKEN_FILE} ist für andere lesbar – chmod 600")
value = TOKEN_FILE.read_text().strip()
if not value:
raise SystemExit(f"Kein Token: GITEA_TOKEN setzen oder {TOKEN_FILE} anlegen")
return value
def _api(method: str, url: str, tok: str, body: dict[str, object] | None = None) -> object:
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method) # noqa: S310 – https-URL
req.add_header("Authorization", f"token {tok}")
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "application/json")
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
return json.loads(resp.read() or b"null")
def publish(tag: str, base: str, repo: str, tok: str, changelog: str) -> str:
payload: dict[str, object] = {
"tag_name": tag,
"name": release_title(tag),
"body": changelog_section(changelog, tag[1:]),
"draft": False,
"prerelease": False,
}
api = f"{base}/api/v1/repos/{repo}/releases"
try:
existing = _api("GET", f"{api}/tags/{tag}", tok)
except urllib.error.HTTPError as exc:
if exc.code != 404:
raise
_api("POST", api, tok, payload)
return "angelegt"
if not isinstance(existing, dict):
raise SystemExit(f"Unerwartete Antwort für {tag}: {existing!r}")
_api("PATCH", f"{api}/{existing['id']}", tok, payload)
return "aktualisiert"
def main(argv: list[str]) -> int:
if not argv:
print(__doc__)
return 2
tags = (
[t for t in _git("tag", "-l", "--sort=v:refname").splitlines() if TAG_RE.match(t)]
if argv == ["--all"]
else argv
)
changelog = (ROOT / "CHANGELOG.md").read_text()
for tag in tags: # zuerst alles prüfen, dann veröffentlichen
if not TAG_RE.match(tag):
raise SystemExit(f"Kein Versions-Tag: {tag}")
changelog_section(changelog, tag[1:])
base, repo = remote()
tok = token()
for tag in tags:
try:
what = publish(tag, base, repo, tok, changelog)
except urllib.error.HTTPError as exc:
print(f"{tag}: Fehler {exc.code} {exc.reason}", file=sys.stderr)
return 1
print(f"{tag}: Release {what} – {base}/{repo}/releases/tag/{tag}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))