Push-Skript auf einen Commit pro Lauf umgestellt

This commit is contained in:
2026-09-20 12:52:26 +02:00
parent 47587b4884
commit c067be362b

View File

@@ -1,72 +1,103 @@
# -*- coding: utf-8 -*-
"""Laedt Dateien ueber die Gitea-Contents-API hoch (create oder update)."""
"""Laedt alle Projektdateien in EINEM Commit nach Gitea (ChangeFiles-API).
Aufruf: python3 build/push_gitea.py "Commit-Nachricht"
Uebersprungen werden die Pfade, die die Workflows selbst pflegen. Sonst wuerde
ein lokaler Altstand die Ergebnisse eines Laufs ueberschreiben.
"""
import base64, json, os, sys, urllib.request, urllib.error
def env():
# Von den Workflows gepflegt, nie von hier ueberschreiben:
NICHT_UEBERSCHREIBEN = {'artikel-index.json', 'status-importe.json'}
NICHT_UEBERSCHREIBEN_ORDNER = ('briefe/', 'nachbessern/', 'artikel/')
AUS_ORDNER = {'.git', 'node_modules', '__pycache__'}
AUS_DATEIEN = {'.env'}
def env(pfad='.env'):
d = {}
for line in open('.env', encoding='utf-8'):
line = line.strip()
if not line or line.startswith('#') or '=' not in line: continue
k, v = line.split('=', 1)
for zeile in open(pfad, encoding='utf-8'):
zeile = zeile.strip()
if not zeile or zeile.startswith('#') or '=' not in zeile:
continue
k, v = zeile.split('=', 1)
v = v.strip()
if v.startswith('"') and v.endswith('"'): v = v[1:-1].replace('\\$', '$').replace('\\"', '"')
if v.startswith('"') and v.endswith('"'):
v = v[1:-1].replace('\\$', '$').replace('\\"', '"')
d[k] = v
return d
E = env()
BASE = E['GITEA_BASE_URL'].rstrip('/')
REPO = f"{BASE}/api/v1/repos/{E['GITEA_OWNER']}/{E['GITEA_REPO']}/contents"
REPO = f"{BASE}/api/v1/repos/{E['GITEA_OWNER']}/{E['GITEA_REPO']}"
KOPF = {'Authorization': 'token ' + E['GITEA_TOKEN'], 'Content-Type': 'application/json'}
def ruf(methode, url, daten=None):
r = urllib.request.Request(url, method=methode, headers=KOPF,
data=json.dumps(daten).encode() if daten else None)
try:
with urllib.request.urlopen(r, timeout=60) as a:
return a.status, json.loads(a.read().decode())
with urllib.request.urlopen(r, timeout=90) as a:
roh = a.read().decode()
return a.status, (json.loads(roh) if roh else {})
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read().decode() or '{}')
roh = e.read().decode()
return e.code, (json.loads(roh) if roh.startswith(('{', '[')) else {'text': roh[:300]})
def hoch(pfad, nachricht):
with open(pfad, 'rb') as f:
inhalt = base64.b64encode(f.read()).decode()
ziel = pfad.replace(os.sep, '/')
code, _ = ruf('GET', f"{REPO}/{ziel}?ref={E.get('branch','main')}")
sha = None
def sammle():
raus = []
for wurzel, ordner, namen in os.walk('.'):
ordner[:] = [o for o in ordner if o not in AUS_ORDNER]
for n in namen:
p = os.path.relpath(os.path.join(wurzel, n), '.').replace(os.sep, '/')
if p in AUS_DATEIEN or p in NICHT_UEBERSCHREIBEN:
continue
if p.startswith(NICHT_UEBERSCHREIBEN_ORDNER):
continue
raus.append(p)
return sorted(raus)
def main():
nachricht = sys.argv[1] if len(sys.argv) > 1 else 'Update'
dateien = sammle()
# Vorhandene Dateien brauchen operation "update" plus sha, neue "create".
code, vorhanden = ruf('GET', f"{REPO}/git/trees/main?recursive=1")
bekannt = {}
if code == 200:
code_g, body = ruf('GET', f"{REPO}/{ziel}?ref=main")
sha = body.get('sha')
daten = {'content': inhalt, 'message': nachricht, 'branch': 'main'}
if sha:
daten['sha'] = sha
c, b = ruf('PUT', f"{REPO}/{ziel}", daten)
for e in (vorhanden.get('tree') or []):
if e.get('type') == 'blob':
bekannt[e['path']] = e['sha']
eintraege = []
for p in dateien:
with open(p, 'rb') as f:
inhalt = base64.b64encode(f.read()).decode()
e = {'path': p, 'content': inhalt}
if p in bekannt:
e['operation'] = 'update'
e['sha'] = bekannt[p]
else:
e['operation'] = 'create'
eintraege.append(e)
code, antwort = ruf('POST', f"{REPO}/contents",
{'files': eintraege, 'message': nachricht, 'branch': 'main'})
if code in (200, 201):
sha = ((antwort.get('commit') or {}).get('sha') or '')[:8]
neu = sum(1 for e in eintraege if e['operation'] == 'create')
print(f"Ein Commit {sha}: {len(eintraege)} Dateien ({neu} neu, {len(eintraege)-neu} aktualisiert)")
print("Uebersprungen (von den Workflows gepflegt): "
+ ', '.join(sorted(NICHT_UEBERSCHREIBEN) + list(NICHT_UEBERSCHREIBEN_ORDNER)))
else:
c, b = ruf('POST', f"{REPO}/{ziel}", daten)
ok = c in (200, 201)
print(f" {'ok ' if ok else 'FEHLER'} {ziel}" + ('' if ok else f" -> {c} {str(b)[:160]}"))
return ok
print(f"FEHLER {code}: {json.dumps(antwort)[:400]}")
sys.exit(1)
AUS = {'.git', '.env', 'node_modules', '__pycache__'}
# Diese Pfade schreiben die Workflows selbst. Wuerde das Skript sie hochladen,
# ueberschreibt ein lokaler Altstand die Ergebnisse eines Laufs.
NICHT_UEBERSCHREIBEN = {'artikel-index.json', 'status-importe.json'}
NICHT_UEBERSCHREIBEN_ORDNER = ('briefe' + os.sep, 'nachbessern' + os.sep, 'artikel' + os.sep)
dateien = []
for wurzel, ordner, namen in os.walk('.'):
ordner[:] = [o for o in ordner if o not in AUS]
for n in namen:
p = os.path.relpath(os.path.join(wurzel, n), '.')
if p in AUS or p.startswith('.env') and p != '.env.example': continue
if p == '.env': continue
if p in NICHT_UEBERSCHREIBEN or p.startswith(NICHT_UEBERSCHREIBEN_ORDNER):
continue
dateien.append(p)
nachricht = sys.argv[1] if len(sys.argv) > 1 else 'Update'
print("Uebersprungen (von den Workflows gepflegt): "
+ ', '.join(sorted(NICHT_UEBERSCHREIBEN)) + ', ' + ', '.join(NICHT_UEBERSCHREIBEN_ORDNER))
fehler = 0
for p in sorted(dateien):
if not hoch(p, nachricht): fehler += 1
print(f"\n{len(dateien)} Dateien, {fehler} Fehler")
if __name__ == '__main__':
main()