From 5f8fc453a154e8651800ccb09204f72de893b4bf Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 22 Oct 2026 13:19:53 +0200 Subject: [PATCH] release.py: Anmeldung per Benutzer/Passwort oder Token Auth-Reihenfolge: GITEA_TOKEN, dann GITEA_USER/GITEA_PASSWORD, dann interaktive Abfrage (Passwort ohne Echo via getpass). Zugangsdaten werden curl ueber eine Config auf stdin (-K -) uebergeben, damit Token/Passwort nicht in der Prozessliste auftauchen. 401 weist bei aktiver 2FA auf Token-Nutzung hin. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 10 +++++ release.py | 106 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 344102e..62ee514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Versionierung nach [SemVer](https://semver.org/lang/de/). ## [Unreleased] +### Changed +- `release.py`: Veröffentlichung über die Gitea-REST-API (curl) statt `tea`. + Anmeldung per `GITEA_TOKEN`, `GITEA_USER`/`GITEA_PASSWORD` oder interaktiver + Abfrage von Benutzername/Passwort; Zugangsdaten gehen über eine curl-Config + auf stdin (nicht in der Prozessliste sichtbar). + +### Added +- `release.py --attach-only`: ISO an das Release der aktuellen Version hängen, + ohne Versionssprung/Build/Commit. + ## [0.1.2] - 2026-10-22 ### Added diff --git a/release.py b/release.py index fca2cae..97be191 100755 --- a/release.py +++ b/release.py @@ -16,16 +16,19 @@ Beispiele: ./release.py --dry-run # nur anzeigen, nichts aendern 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 +Gitea-API an. Anmeldung in dieser Reihenfolge: + 1. Token aus GITEA_TOKEN (export GITEA_TOKEN=xxxx) + 2. GITEA_USER / GITEA_PASSWORD (aus der Umgebung) + 3. interaktive Abfrage von Benutzername + Passwort (ohne Echo) +Bei aktivierter 2FA funktioniert Passwort-Login nicht -- dann einen Token +verwenden (Gitea -> Einstellungen -> Anwendungen -> Token, Scope write:repository). """ from __future__ import annotations import argparse import datetime as dt +import getpass import json import os import re @@ -125,10 +128,49 @@ def remote_info() -> tuple[str, str, str]: 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) +class Auth: + """Gitea-Authentifizierung: Token (bevorzugt) oder Benutzername/Passwort. + + Reihenfolge: GITEA_TOKEN -> GITEA_USER/GITEA_PASSWORD -> interaktive Abfrage. + Die Zugangsdaten werden curl ueber eine Config auf stdin (-K -) uebergeben, + damit weder Token noch Passwort in der Prozessliste (argv) auftauchen. + """ + + def __init__(self) -> None: + self.token = os.environ.get("GITEA_TOKEN") or "" + self.user = os.environ.get("GITEA_USER") or "" + self.password = os.environ.get("GITEA_PASSWORD") or "" + + def ensure(self) -> None: + if self.token or (self.user and self.password): + return + if not sys.stdin.isatty(): + fail("Keine Zugangsdaten: GITEA_TOKEN oder GITEA_USER/GITEA_PASSWORD " + "setzen (keine interaktive Eingabe moeglich).") + print("Gitea-Anmeldung (leer lassen beim Benutzernamen, um einen Token " + "einzugeben):") + self.user = input(" Benutzername: ").strip() + if self.user: + self.password = getpass.getpass(" Passwort: ") + if not self.password: + fail("Kein Passwort eingegeben.") + else: + self.token = getpass.getpass(" Token: ").strip() + if not self.token: + fail("Weder Benutzername noch Token eingegeben.") + + def curl_config(self) -> str: + if self.token: + return f'header = "Authorization: token {self.token}"\n' + return f'user = "{self.user}:{self.password}"\n' + + +def _curl(auth: "Auth", args: list[str]) -> tuple[int, str]: + """curl aufrufen; (HTTP-Status, Body). Auth kommt per stdin-Config (-K -).""" + res = subprocess.run( + ["curl", "-sS", "-K", "-", "-w", "\n%{http_code}", *args], + input=auth.curl_config(), capture_output=True, text=True, + ) out = res.stdout nl = out.rfind("\n") body = out[:nl] if nl != -1 else out @@ -142,52 +184,49 @@ def _curl(args: list[str]) -> tuple[int, str]: 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") +def gitea_publish(auth: "Auth", tag: str, notes: str) -> None: 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}"]) + 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"]) + 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}).") + elif status == 401: + fail("Anmeldung abgelehnt (HTTP 401). Falsche Zugangsdaten -- oder 2FA " + "aktiv: dann einen Token statt Passwort verwenden.") 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"]) + 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']}"]) + _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}"]) + 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}") @@ -226,9 +265,11 @@ def main() -> int: if args.dry_run: print("--dry-run: es wird nichts geaendert.") return 0 + auth = Auth() + auth.ensure() # 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) + gitea_publish(auth, tag, changelog_section(cur) or tag) print(f"\nFertig: ISO an {tag} angehaengt.") return 0 @@ -244,11 +285,13 @@ def main() -> int: print("--dry-run: es wird nichts geaendert.") return 0 + auth: Auth | None = None if not args.no_publish: git_clean_or_die() - if not os.environ.get("GITEA_TOKEN"): - fail("GITEA_TOKEN nicht gesetzt (fuer Upload noetig). Entweder setzen " - "oder mit --no-publish nur lokal arbeiten.") + # Zugangsdaten frueh klaeren (ggf. abfragen), damit der Build nicht + # umsonst laeuft, falls keine Anmeldung moeglich ist. + auth = Auth() + auth.ensure() # 1) Version + CHANGELOG print("\n==> Version und CHANGELOG aktualisieren") @@ -271,7 +314,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" GITEA_TOKEN=... ./release.py --attach-only") + f" ./release.py --attach-only (fragt Benutzer/Passwort ab)") return 0 # 4) Push (noetig, damit Gitea das Release kennt) @@ -281,7 +324,8 @@ def main() -> int: # 5) Gitea-Release mit ISO-Anhang print("\n==> Gitea-Release anlegen und ISO hochladen") - gitea_publish(tag, notes) + assert auth is not None + gitea_publish(auth, tag, notes) print(f"\nFertig: Release {tag} veroeffentlicht, ISO angehaengt.") return 0