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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d5395fc950
commit
5f8fc453a1
@@ -6,6 +6,16 @@ Versionierung nach [SemVer](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.1.2] - 2026-10-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+75
-31
@@ -16,16 +16,19 @@ Beispiele:
|
|||||||
./release.py --dry-run # nur anzeigen, nichts aendern
|
./release.py --dry-run # nur anzeigen, nichts aendern
|
||||||
|
|
||||||
Veroeffentlichen pusht Release-Commit und Tag nach origin und spricht dann die
|
Veroeffentlichen pusht Release-Commit und Tag nach origin und spricht dann die
|
||||||
Gitea-API an. Einmalig noetig: ein Gitea-Token in der Umgebungsvariablen
|
Gitea-API an. Anmeldung in dieser Reihenfolge:
|
||||||
GITEA_TOKEN (Gitea -> Einstellungen -> Anwendungen -> Token generieren,
|
1. Token aus GITEA_TOKEN (export GITEA_TOKEN=xxxx)
|
||||||
Scope write:repository):
|
2. GITEA_USER / GITEA_PASSWORD (aus der Umgebung)
|
||||||
export GITEA_TOKEN=xxxxxxxxxxxxxxxx
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import getpass
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -125,10 +128,49 @@ def remote_info() -> tuple[str, str, str]:
|
|||||||
return m.group(1), m.group(2), m.group(3)
|
return m.group(1), m.group(2), m.group(3)
|
||||||
|
|
||||||
|
|
||||||
def _curl(args: list[str]) -> tuple[int, str]:
|
class Auth:
|
||||||
"""curl aufrufen; (HTTP-Status, Body) zurueckgeben. Token wird nicht geloggt."""
|
"""Gitea-Authentifizierung: Token (bevorzugt) oder Benutzername/Passwort.
|
||||||
res = subprocess.run(["curl", "-sS", "-w", "\n%{http_code}", *args],
|
|
||||||
capture_output=True, text=True)
|
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
|
out = res.stdout
|
||||||
nl = out.rfind("\n")
|
nl = out.rfind("\n")
|
||||||
body = out[:nl] if nl != -1 else out
|
body = out[:nl] if nl != -1 else out
|
||||||
@@ -142,52 +184,49 @@ def _curl(args: list[str]) -> tuple[int, str]:
|
|||||||
return status, body
|
return status, body
|
||||||
|
|
||||||
|
|
||||||
def gitea_publish(tag: str, notes: str) -> None:
|
def gitea_publish(auth: "Auth", 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():
|
if not ISO.exists():
|
||||||
fail(f"ISO fehlt: {ISO}")
|
fail(f"ISO fehlt: {ISO}")
|
||||||
|
|
||||||
host, owner, repo = remote_info()
|
host, owner, repo = remote_info()
|
||||||
api = f"https://{host}/api/v1/repos/{owner}/{repo}"
|
api = f"https://{host}/api/v1/repos/{owner}/{repo}"
|
||||||
auth = ["-H", f"Authorization: token {token}"]
|
|
||||||
|
|
||||||
# 1) Release zu diesem Tag holen oder anlegen
|
# 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:
|
if status == 200:
|
||||||
rid = json.loads(body)["id"]
|
rid = json.loads(body)["id"]
|
||||||
print(f" Release existiert bereits (id {rid}).")
|
print(f" Release existiert bereits (id {rid}).")
|
||||||
elif status == 404:
|
elif status == 404:
|
||||||
payload = json.dumps({"tag_name": tag, "name": tag, "body": notes})
|
payload = json.dumps({"tag_name": tag, "name": tag, "body": notes})
|
||||||
status, body = _curl([*auth, "-H", "Content-Type: application/json",
|
status, body = _curl(auth, ["-H", "Content-Type: application/json",
|
||||||
"-X", "POST", "-d", payload, f"{api}/releases"])
|
"-X", "POST", "-d", payload,
|
||||||
|
f"{api}/releases"])
|
||||||
if status not in (200, 201):
|
if status not in (200, 201):
|
||||||
fail(f"Release anlegen fehlgeschlagen (HTTP {status}):\n{body}")
|
fail(f"Release anlegen fehlgeschlagen (HTTP {status}):\n{body}")
|
||||||
rid = json.loads(body)["id"]
|
rid = json.loads(body)["id"]
|
||||||
print(f" Release angelegt (id {rid}).")
|
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:
|
else:
|
||||||
fail(f"Release-Abfrage fehlgeschlagen (HTTP {status}):\n{body}")
|
fail(f"Release-Abfrage fehlgeschlagen (HTTP {status}):\n{body}")
|
||||||
|
|
||||||
# 2) Vorhandenes Asset gleichen Namens entfernen (idempotent)
|
# 2) Vorhandenes Asset gleichen Namens entfernen (idempotent)
|
||||||
name = ISO.name
|
name = ISO.name
|
||||||
status, body = _curl([*auth, f"{api}/releases/{rid}/assets"])
|
status, body = _curl(auth, [f"{api}/releases/{rid}/assets"])
|
||||||
if status == 200:
|
if status == 200:
|
||||||
for a in json.loads(body):
|
for a in json.loads(body):
|
||||||
if a.get("name") == name:
|
if a.get("name") == name:
|
||||||
_curl([*auth, "-X", "DELETE",
|
_curl(auth, ["-X", "DELETE",
|
||||||
f"{api}/releases/{rid}/assets/{a['id']}"])
|
f"{api}/releases/{rid}/assets/{a['id']}"])
|
||||||
print(f" Altes Asset '{name}' entfernt.")
|
print(f" Altes Asset '{name}' entfernt.")
|
||||||
|
|
||||||
# 3) ISO hochladen
|
# 3) ISO hochladen
|
||||||
size_mb = ISO.stat().st_size // (1024 * 1024)
|
size_mb = ISO.stat().st_size // (1024 * 1024)
|
||||||
print(f" Lade '{name}' hoch ({size_mb} MB) ...")
|
print(f" Lade '{name}' hoch ({size_mb} MB) ...")
|
||||||
status, body = _curl([*auth, "-X", "POST",
|
status, body = _curl(auth, ["-X", "POST",
|
||||||
"-F", f"attachment=@{ISO};type=application/octet-stream",
|
"-F", f"attachment=@{ISO};type=application/octet-stream",
|
||||||
f"{api}/releases/{rid}/assets?name={name}"])
|
f"{api}/releases/{rid}/assets?name={name}"])
|
||||||
if status not in (200, 201):
|
if status not in (200, 201):
|
||||||
fail(f"Upload fehlgeschlagen (HTTP {status}):\n{body}")
|
fail(f"Upload fehlgeschlagen (HTTP {status}):\n{body}")
|
||||||
print(f" Upload ok -> https://{host}/{owner}/{repo}/releases/tag/{tag}")
|
print(f" Upload ok -> https://{host}/{owner}/{repo}/releases/tag/{tag}")
|
||||||
@@ -226,9 +265,11 @@ def main() -> int:
|
|||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print("--dry-run: es wird nichts geaendert.")
|
print("--dry-run: es wird nichts geaendert.")
|
||||||
return 0
|
return 0
|
||||||
|
auth = Auth()
|
||||||
|
auth.ensure()
|
||||||
# Tag muss auf origin liegen, damit das Release darauf zeigen kann
|
# Tag muss auf origin liegen, damit das Release darauf zeigen kann
|
||||||
run(["git", "push", "origin", tag], check=False)
|
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.")
|
print(f"\nFertig: ISO an {tag} angehaengt.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -244,11 +285,13 @@ def main() -> int:
|
|||||||
print("--dry-run: es wird nichts geaendert.")
|
print("--dry-run: es wird nichts geaendert.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
auth: Auth | None = None
|
||||||
if not args.no_publish:
|
if not args.no_publish:
|
||||||
git_clean_or_die()
|
git_clean_or_die()
|
||||||
if not os.environ.get("GITEA_TOKEN"):
|
# Zugangsdaten frueh klaeren (ggf. abfragen), damit der Build nicht
|
||||||
fail("GITEA_TOKEN nicht gesetzt (fuer Upload noetig). Entweder setzen "
|
# umsonst laeuft, falls keine Anmeldung moeglich ist.
|
||||||
"oder mit --no-publish nur lokal arbeiten.")
|
auth = Auth()
|
||||||
|
auth.ensure()
|
||||||
|
|
||||||
# 1) Version + CHANGELOG
|
# 1) Version + CHANGELOG
|
||||||
print("\n==> Version und CHANGELOG aktualisieren")
|
print("\n==> Version und CHANGELOG aktualisieren")
|
||||||
@@ -271,7 +314,7 @@ def main() -> int:
|
|||||||
if args.no_publish:
|
if args.no_publish:
|
||||||
print(f"\nLokal fertig. Veroeffentlichen spaeter z.B.:\n"
|
print(f"\nLokal fertig. Veroeffentlichen spaeter z.B.:\n"
|
||||||
f" git push origin HEAD && git push origin {tag}\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
|
return 0
|
||||||
|
|
||||||
# 4) Push (noetig, damit Gitea das Release kennt)
|
# 4) Push (noetig, damit Gitea das Release kennt)
|
||||||
@@ -281,7 +324,8 @@ def main() -> int:
|
|||||||
|
|
||||||
# 5) Gitea-Release mit ISO-Anhang
|
# 5) Gitea-Release mit ISO-Anhang
|
||||||
print("\n==> Gitea-Release anlegen und ISO hochladen")
|
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.")
|
print(f"\nFertig: Release {tag} veroeffentlicht, ISO angehaengt.")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user