release.py: Gitea-REST-API statt tea, --attach-only
Das auf dem System vorhandene /usr/bin/tea ist ein Texteditor, nicht die Gitea-CLI. Veroeffentlichung laeuft jetzt direkt ueber die Gitea-REST-API (curl): Release zum Tag anlegen/finden, gleichnamiges Asset ersetzen, ISO hochladen. Token via GITEA_TOKEN. Neuer Modus --attach-only haengt die ISO an das Release der aktuellen Version, ohne bump/build/commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
def3c47634
commit
d5395fc950
+134
-24
@@ -2,7 +2,8 @@
|
||||
"""Release-Automatik fuer PC-Unlocker.
|
||||
|
||||
Zaehlt die Version hoch, pflegt das CHANGELOG, committet + taggt, baut die ISO
|
||||
und veroeffentlicht sie als Gitea-Release (via `tea`) mit der ISO als Anhang.
|
||||
und veroeffentlicht sie als Gitea-Release (ueber die Gitea-REST-API, curl) mit
|
||||
der ISO als Anhang.
|
||||
|
||||
Beispiele:
|
||||
./release.py # Patch-Release (0.1.0 -> 0.1.1), bauen + publizieren
|
||||
@@ -10,19 +11,24 @@ Beispiele:
|
||||
./release.py --major # 0.1.0 -> 1.0.0
|
||||
./release.py --no-build # vorhandene out/pc-unlocker.iso verwenden
|
||||
./release.py --no-publish # nur lokal: bump, commit, tag, (build)
|
||||
./release.py --attach-only # nur die ISO an das Release der AKTUELLEN
|
||||
# Version haengen (kein bump/build/commit)
|
||||
./release.py --dry-run # nur anzeigen, nichts aendern
|
||||
|
||||
Veroeffentlichen pusht den Release-Commit und das Tag nach origin (zwingend,
|
||||
damit Gitea das Release anlegen kann) und ruft dann `tea release create`.
|
||||
Einmalig noetig: `tea login add` fuer git.hacker-net.de.
|
||||
Veroeffentlichen pusht Release-Commit und Tag nach origin und spricht dann die
|
||||
Gitea-API an. Einmalig noetig: ein Gitea-Token in der Umgebungsvariablen
|
||||
GITEA_TOKEN (Gitea -> Einstellungen -> Anwendungen -> Token generieren,
|
||||
Scope write:repository):
|
||||
export GITEA_TOKEN=xxxxxxxxxxxxxxxx
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -46,6 +52,7 @@ def fail(msg: str) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- Versionen
|
||||
def read_version() -> tuple[int, int, int]:
|
||||
m = VERSION_RE.search(INIT.read_text(encoding="utf-8"))
|
||||
if not m:
|
||||
@@ -68,6 +75,7 @@ def write_version(ver: tuple[int, int, int]) -> None:
|
||||
INIT.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
# --------------------------------------------------------------- CHANGELOG
|
||||
def update_changelog(ver_str: str) -> str:
|
||||
"""[Unreleased]-Inhalt in einen datierten Versionsabschnitt ueberfuehren.
|
||||
|
||||
@@ -79,23 +87,113 @@ def update_changelog(ver_str: str) -> str:
|
||||
if idx == -1:
|
||||
fail("'## [Unreleased]' nicht im CHANGELOG gefunden")
|
||||
after = idx + len(marker)
|
||||
# Inhalt bis zur naechsten Versionsueberschrift (## [..]) oder Dateiende
|
||||
nxt = text.find("\n## [", after)
|
||||
end = nxt if nxt != -1 else len(text)
|
||||
body = text[after:end].strip("\n")
|
||||
notes = body if body else "Wartungs-Release."
|
||||
notes = body.strip() if body.strip() else "Wartungs-Release."
|
||||
|
||||
today = dt.date.today().isoformat()
|
||||
new_block = (
|
||||
f"{marker}\n\n"
|
||||
f"## [{ver_str}] - {today}\n\n"
|
||||
f"{body}\n"
|
||||
)
|
||||
new_block = f"{marker}\n\n## [{ver_str}] - {today}\n\n{body}\n"
|
||||
new_text = text[:idx] + new_block + text[end:]
|
||||
CHANGELOG.write_text(new_text, encoding="utf-8")
|
||||
return notes
|
||||
|
||||
|
||||
def changelog_section(ver_str: str) -> str:
|
||||
"""Notizen eines bereits vorhandenen Versionsabschnitts auslesen."""
|
||||
text = CHANGELOG.read_text(encoding="utf-8")
|
||||
m = re.search(rf"^## \[{re.escape(ver_str)}\][^\n]*\n", text, re.M)
|
||||
if not m:
|
||||
return ""
|
||||
start = m.end()
|
||||
nxt = text.find("\n## [", start)
|
||||
end = nxt if nxt != -1 else len(text)
|
||||
return text[start:end].strip()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Gitea
|
||||
def remote_info() -> tuple[str, str, str]:
|
||||
"""(host, owner, repo) aus der origin-URL ableiten."""
|
||||
url = run(["git", "remote", "get-url", "origin"], capture=True).stdout.strip()
|
||||
# https://user@host/owner/repo(.git)
|
||||
m = re.match(r"https?://(?:[^@/]+@)?([^/]+)/([^/]+)/([^/]+?)(?:\.git)?$", url)
|
||||
if not m:
|
||||
# git@host:owner/repo(.git)
|
||||
m = re.match(r"(?:ssh://)?[^@]+@([^:/]+)[:/]([^/]+)/([^/]+?)(?:\.git)?$", url)
|
||||
if not m:
|
||||
fail(f"origin-URL nicht verstanden: {url}")
|
||||
return m.group(1), m.group(2), m.group(3)
|
||||
|
||||
|
||||
def _curl(args: list[str]) -> tuple[int, str]:
|
||||
"""curl aufrufen; (HTTP-Status, Body) zurueckgeben. Token wird nicht geloggt."""
|
||||
res = subprocess.run(["curl", "-sS", "-w", "\n%{http_code}", *args],
|
||||
capture_output=True, text=True)
|
||||
out = res.stdout
|
||||
nl = out.rfind("\n")
|
||||
body = out[:nl] if nl != -1 else out
|
||||
code = out[nl + 1:].strip() if nl != -1 else ""
|
||||
try:
|
||||
status = int(code)
|
||||
except ValueError:
|
||||
status = 0
|
||||
if res.stderr:
|
||||
body = (body + "\n" + res.stderr).strip()
|
||||
return status, body
|
||||
|
||||
|
||||
def gitea_publish(tag: str, notes: str) -> None:
|
||||
token = os.environ.get("GITEA_TOKEN")
|
||||
if not token:
|
||||
fail("Umgebungsvariable GITEA_TOKEN nicht gesetzt.\n"
|
||||
"In Gitea: Einstellungen -> Anwendungen -> Token generieren "
|
||||
"(Scope write:repository), dann z.B.:\n"
|
||||
" export GITEA_TOKEN=xxxxxxxxxxxxxxxx")
|
||||
if not ISO.exists():
|
||||
fail(f"ISO fehlt: {ISO}")
|
||||
|
||||
host, owner, repo = remote_info()
|
||||
api = f"https://{host}/api/v1/repos/{owner}/{repo}"
|
||||
auth = ["-H", f"Authorization: token {token}"]
|
||||
|
||||
# 1) Release zu diesem Tag holen oder anlegen
|
||||
status, body = _curl([*auth, f"{api}/releases/tags/{tag}"])
|
||||
if status == 200:
|
||||
rid = json.loads(body)["id"]
|
||||
print(f" Release existiert bereits (id {rid}).")
|
||||
elif status == 404:
|
||||
payload = json.dumps({"tag_name": tag, "name": tag, "body": notes})
|
||||
status, body = _curl([*auth, "-H", "Content-Type: application/json",
|
||||
"-X", "POST", "-d", payload, f"{api}/releases"])
|
||||
if status not in (200, 201):
|
||||
fail(f"Release anlegen fehlgeschlagen (HTTP {status}):\n{body}")
|
||||
rid = json.loads(body)["id"]
|
||||
print(f" Release angelegt (id {rid}).")
|
||||
else:
|
||||
fail(f"Release-Abfrage fehlgeschlagen (HTTP {status}):\n{body}")
|
||||
|
||||
# 2) Vorhandenes Asset gleichen Namens entfernen (idempotent)
|
||||
name = ISO.name
|
||||
status, body = _curl([*auth, f"{api}/releases/{rid}/assets"])
|
||||
if status == 200:
|
||||
for a in json.loads(body):
|
||||
if a.get("name") == name:
|
||||
_curl([*auth, "-X", "DELETE",
|
||||
f"{api}/releases/{rid}/assets/{a['id']}"])
|
||||
print(f" Altes Asset '{name}' entfernt.")
|
||||
|
||||
# 3) ISO hochladen
|
||||
size_mb = ISO.stat().st_size // (1024 * 1024)
|
||||
print(f" Lade '{name}' hoch ({size_mb} MB) ...")
|
||||
status, body = _curl([*auth, "-X", "POST",
|
||||
"-F", f"attachment=@{ISO};type=application/octet-stream",
|
||||
f"{api}/releases/{rid}/assets?name={name}"])
|
||||
if status not in (200, 201):
|
||||
fail(f"Upload fehlgeschlagen (HTTP {status}):\n{body}")
|
||||
print(f" Upload ok -> https://{host}/{owner}/{repo}/releases/tag/{tag}")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- git
|
||||
def git_clean_or_die() -> None:
|
||||
res = run(["git", "status", "--porcelain"], capture=True)
|
||||
dirty = [l for l in res.stdout.splitlines()
|
||||
@@ -105,6 +203,7 @@ def git_clean_or_die() -> None:
|
||||
+ "\n".join(dirty))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- main
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="PC-Unlocker Release")
|
||||
g = ap.add_mutually_exclusive_group()
|
||||
@@ -114,10 +213,26 @@ def main() -> int:
|
||||
ap.add_argument("--no-build", action="store_true", help="ISO nicht neu bauen")
|
||||
ap.add_argument("--no-publish", action="store_true",
|
||||
help="nur lokal: kein push, kein Gitea-Release")
|
||||
ap.add_argument("--attach-only", action="store_true",
|
||||
help="nur die ISO an das Release der aktuellen Version haengen")
|
||||
ap.add_argument("--dry-run", action="store_true", help="nur anzeigen")
|
||||
args = ap.parse_args()
|
||||
part = args.part or "patch"
|
||||
|
||||
# --- Nur ISO an bestehendes Release der aktuellen Version haengen --------
|
||||
if args.attach_only:
|
||||
cur = ".".join(map(str, read_version()))
|
||||
tag = f"v{cur}"
|
||||
print(f"Attach-only: ISO an Release {tag} haengen")
|
||||
if args.dry_run:
|
||||
print("--dry-run: es wird nichts geaendert.")
|
||||
return 0
|
||||
# Tag muss auf origin liegen, damit das Release darauf zeigen kann
|
||||
run(["git", "push", "origin", tag], check=False)
|
||||
gitea_publish(tag, changelog_section(cur) or tag)
|
||||
print(f"\nFertig: ISO an {tag} angehaengt.")
|
||||
return 0
|
||||
|
||||
part = args.part or "patch"
|
||||
old = read_version()
|
||||
new = bump(old, part)
|
||||
old_s = ".".join(map(str, old))
|
||||
@@ -131,9 +246,9 @@ def main() -> int:
|
||||
|
||||
if not args.no_publish:
|
||||
git_clean_or_die()
|
||||
if shutil.which("tea") is None:
|
||||
fail("`tea` (Gitea CLI) nicht gefunden. Mit --no-publish nur lokal "
|
||||
"arbeiten oder tea installieren.")
|
||||
if not os.environ.get("GITEA_TOKEN"):
|
||||
fail("GITEA_TOKEN nicht gesetzt (fuer Upload noetig). Entweder setzen "
|
||||
"oder mit --no-publish nur lokal arbeiten.")
|
||||
|
||||
# 1) Version + CHANGELOG
|
||||
print("\n==> Version und CHANGELOG aktualisieren")
|
||||
@@ -156,8 +271,7 @@ def main() -> int:
|
||||
if args.no_publish:
|
||||
print(f"\nLokal fertig. Veroeffentlichen spaeter z.B.:\n"
|
||||
f" git push origin HEAD && git push origin {tag}\n"
|
||||
f" tea release create --tag {tag} --title {tag} "
|
||||
f"--asset {ISO}")
|
||||
f" GITEA_TOKEN=... ./release.py --attach-only")
|
||||
return 0
|
||||
|
||||
# 4) Push (noetig, damit Gitea das Release kennt)
|
||||
@@ -166,12 +280,8 @@ def main() -> int:
|
||||
run(["git", "push", "origin", tag])
|
||||
|
||||
# 5) Gitea-Release mit ISO-Anhang
|
||||
print("\n==> Gitea-Release anlegen")
|
||||
run(["tea", "release", "create",
|
||||
"--tag", tag,
|
||||
"--title", tag,
|
||||
"--note", notes,
|
||||
"--asset", str(ISO)])
|
||||
print("\n==> Gitea-Release anlegen und ISO hochladen")
|
||||
gitea_publish(tag, notes)
|
||||
|
||||
print(f"\nFertig: Release {tag} veroeffentlicht, ISO angehaengt.")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user