chore: Gitea-Releases aus dem Changelog – Skript und CI-Job
CI / pruefen (3.12) (push) Waiting to run
CI / pruefen (3.13) (push) Waiting to run
CI / release (push) Blocked by required conditions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-25 13:27:27 +01:00
co-authored by Claude Opus 5.5
parent fe64f031bd
commit a627044df7
5 changed files with 204 additions and 1 deletions
+138
View File
@@ -0,0 +1,138 @@
#!/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:]))