Auto-Push: Werkzeuge täglich nach claude-werkzeuge spiegeln
taeglich_push.py ruft nach dem Vault-Push werkzeuge_spiegel.py auf: committeter Stand von .vault-tools/ und .claude/ ohne Trading-Werkzeuge und watermarks-remover, README aus werkzeuge-README.md. Fallkatalog 14 von 14, drei Mutanten erkannt. veroeffentlichung.md und Stamm nachgezogen; Gitea seit heute anonym lesbar. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6f9659ad06
commit
fe20d21d6e
7 files changed
+1243
-33
No files matched your search
@@ -0,0 +1,172 @@
|
||||
"""Spiegelt die Werkzeuge des Vaults in das öffentliche Repo claude-werkzeuge.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-10-14
|
||||
|
||||
WAS GESPIEGELT WIRD
|
||||
Der committete Stand (HEAD) des Vaults unter .vault-tools/ und .claude/,
|
||||
ohne die Ordner in AUSSCHLUESSE, dazu werkzeuge-README.md als README.md.
|
||||
Ungesicherte Änderungen im Arbeitsbaum gehen nicht mit. Was die
|
||||
.gitignore des Vaults draußen hält (settings.local.json, __pycache__,
|
||||
lauf.log), ist deshalb auch hier draußen.
|
||||
|
||||
AUSSCHLUESSE ist eine Negativliste: Ein neuer Werkzeugordner geht beim
|
||||
nächsten Lauf mit hinaus. So angeordnet (Pascal, 2026-10-14: künftig alle
|
||||
Werkzeuge außer denen des Trading-Projekts). Ein neues Werkzeug des
|
||||
Trading-Projekts gehört deshalb in AUSSCHLUESSE, bevor es committet wird.
|
||||
|
||||
WOHIN
|
||||
In einen Klon außerhalb des Vaults (KLON). Der Lauf holt den Remote-Stand,
|
||||
leert den Klon bis auf .git, packt den Stand neu aus und committet und
|
||||
pusht nur, wenn sich etwas geändert hat. Die Remote-Adresse entsteht aus
|
||||
der origin-Adresse des Vaults, letzter Pfadteil ersetzt; Benutzername und
|
||||
Host stehen so nirgends im Quelltext. Das Token kommt wie beim Vault-Push
|
||||
über askpass.sh aus GITEA_TOKEN.
|
||||
|
||||
SCHUTZ
|
||||
Geleert wird nur ein Klon, dessen origin genau auf die erwartete Adresse
|
||||
zeigt. Ein Klon im Vault, ein fremder Klon und ein nicht leerer Ordner
|
||||
ohne .git sind Befunde, nichts wird gelöscht.
|
||||
|
||||
BENANNTE LÜCKEN
|
||||
* Kein Geheimnis-Scan. Was im Vault unter .vault-tools/ oder .claude/
|
||||
committet ist, geht hinaus, wie beim Vault-Push.
|
||||
* Nur reguläre Dateien. Symlinks im Vault werden übersprungen.
|
||||
* Eine Änderung direkt im Repo claude-werkzeuge überschreibt der nächste
|
||||
Lauf.
|
||||
* Ein gescheiterter Push lässt den Commit im Klon stehen; der nächste Lauf
|
||||
setzt den Klon auf den Remote-Stand zurück und committet neu.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
KLON = Path(r"D:\ClaudeData\claude-werkzeuge")
|
||||
REPO_NAME = "claude-werkzeuge.git"
|
||||
README_QUELLE = ".vault-tools/auto-push/werkzeuge-README.md"
|
||||
ASKPASS_REL = Path(".vault-tools") / "auto-push" / "askpass.sh"
|
||||
|
||||
QUELLEN = (".vault-tools/", ".claude/")
|
||||
# Verankert ab der Vault-Wurzel, nie als bloße Ordnernamen.
|
||||
AUSSCHLUESSE = (
|
||||
".vault-tools/trading-plattform-pruefung/",
|
||||
".vault-tools/watermarks-remover/",
|
||||
)
|
||||
|
||||
TIMEOUT_GIT_S = 30
|
||||
TIMEOUT_NETZ_S = 90
|
||||
|
||||
|
||||
class SpiegelFehler(Exception):
|
||||
"""Ein Schritt ist gescheitert; die Meldung ist der Befund."""
|
||||
|
||||
|
||||
def git(wo: Path, wurzel: Path, *argumente: str, timeout: int = TIMEOUT_GIT_S) -> bytes:
|
||||
umgebung = dict(
|
||||
os.environ,
|
||||
GIT_TERMINAL_PROMPT="0",
|
||||
GIT_ASKPASS=str(wurzel / ASKPASS_REL),
|
||||
)
|
||||
try:
|
||||
lauf = subprocess.run(
|
||||
["git", "-C", str(wo), *argumente],
|
||||
env=umgebung,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as abgelaufen:
|
||||
raise SpiegelFehler(f"git {argumente[0]} nach {timeout} s abgebrochen") from abgelaufen
|
||||
except OSError as fehler:
|
||||
raise SpiegelFehler(f"git nicht startbar: {type(fehler).__name__}: {fehler}") from fehler
|
||||
if lauf.returncode != 0:
|
||||
text = (lauf.stdout + b"\n" + lauf.stderr).decode("utf-8", errors="replace")
|
||||
raise SpiegelFehler(f"git {argumente[0]}: Exit {lauf.returncode}: {' '.join(text.split())[:300]}")
|
||||
return lauf.stdout
|
||||
|
||||
|
||||
def gehoert_dazu(pfad: str) -> bool:
|
||||
return pfad.startswith(QUELLEN) and not pfad.startswith(AUSSCHLUESSE)
|
||||
|
||||
|
||||
def ziel_adresse(wurzel: Path) -> str:
|
||||
adresse = git(wurzel, wurzel, "remote", "get-url", "origin").decode("utf-8").strip()
|
||||
if "/" not in adresse:
|
||||
raise SpiegelFehler(f"origin des Vaults ohne Pfad: {adresse}")
|
||||
return adresse.rsplit("/", 1)[0] + "/" + REPO_NAME
|
||||
|
||||
|
||||
def raeume(pfad: Path) -> None:
|
||||
"""Löscht Datei oder Ordner, auch schreibgeschützte Dateien."""
|
||||
|
||||
def erzwinge(funktion, ziel, _fehler):
|
||||
os.chmod(ziel, stat.S_IWRITE)
|
||||
funktion(ziel)
|
||||
|
||||
if pfad.is_dir() and not pfad.is_symlink():
|
||||
shutil.rmtree(pfad, onexc=erzwinge)
|
||||
else:
|
||||
pfad.unlink()
|
||||
|
||||
|
||||
def bereite_klon(wurzel: Path, klon: Path, adresse: str) -> None:
|
||||
"""Klont beim ersten Lauf, setzt danach auf den Remote-Stand zurück."""
|
||||
basis = wurzel.resolve()
|
||||
ziel = klon.resolve()
|
||||
if ziel == basis or basis in ziel.parents:
|
||||
raise SpiegelFehler(f"Klon {klon} liegt im Vault; abgebrochen, nichts geleert")
|
||||
if not (klon / ".git").is_dir():
|
||||
if klon.exists() and any(klon.iterdir()):
|
||||
raise SpiegelFehler(f"{klon} ist kein Git-Klon und nicht leer; abgebrochen, nichts geleert")
|
||||
klon.parent.mkdir(parents=True, exist_ok=True)
|
||||
git(klon.parent, wurzel, "clone", "-q", "-c", "core.autocrlf=false", adresse, str(klon),
|
||||
timeout=TIMEOUT_NETZ_S)
|
||||
return
|
||||
vorhanden = git(klon, wurzel, "remote", "get-url", "origin").decode("utf-8").strip()
|
||||
if vorhanden != adresse:
|
||||
raise SpiegelFehler(
|
||||
f"origin des Klons zeigt auf {vorhanden}, erwartet {adresse}; abgebrochen, nichts geleert"
|
||||
)
|
||||
git(klon, wurzel, "fetch", "-q", "origin", timeout=TIMEOUT_NETZ_S)
|
||||
git(klon, wurzel, "reset", "-q", "--hard", "origin/main")
|
||||
|
||||
|
||||
def spiegele(wurzel: Path, klon: Path = KLON) -> tuple[str, str]:
|
||||
"""Gibt ("gespiegelt" oder "unveraendert", Text) zurück. Wirft SpiegelFehler."""
|
||||
adresse = ziel_adresse(wurzel)
|
||||
stand = git(wurzel, wurzel, "rev-parse", "--short", "HEAD").decode("utf-8").strip()
|
||||
# Ganzer HEAD statt Pfadangaben: git archive bricht mit "pathspec did not match"
|
||||
# ab, sobald eine Quelle leer ist. Gefiltert wird danach mit gehoert_dazu.
|
||||
archiv = git(wurzel, wurzel, "archive", "--format=tar", "HEAD")
|
||||
|
||||
with tarfile.open(fileobj=io.BytesIO(archiv)) as tar:
|
||||
mitglieder = [m for m in tar.getmembers() if m.isfile() and gehoert_dazu(m.name)]
|
||||
if README_QUELLE not in {m.name for m in mitglieder}:
|
||||
raise SpiegelFehler(f"README-Quelle {README_QUELLE} fehlt im Vault-Stand {stand}")
|
||||
|
||||
bereite_klon(wurzel, klon, adresse)
|
||||
for eintrag in klon.iterdir():
|
||||
if eintrag.name != ".git":
|
||||
raeume(eintrag)
|
||||
for mitglied in mitglieder:
|
||||
tar.extract(mitglied, klon, filter="data")
|
||||
readme = tar.extractfile(README_QUELLE)
|
||||
(klon / "README.md").write_bytes(readme.read())
|
||||
|
||||
git(klon, wurzel, "add", "-A")
|
||||
roh = git(klon, wurzel, "diff", "--cached", "--name-only", "-z").decode("utf-8")
|
||||
geaendert = [z for z in roh.split("\0") if z]
|
||||
if not geaendert:
|
||||
return "unveraendert", f"Vault-Stand {stand}"
|
||||
git(klon, wurzel, "commit", "-q", "-m",
|
||||
f"Abgleich mit dem Vault-Stand {stand}\n\n{len(geaendert)} Datei(en) geändert.\n"
|
||||
"Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.")
|
||||
git(klon, wurzel, "push", "-q", "origin", "HEAD:main", timeout=TIMEOUT_NETZ_S)
|
||||
return "gespiegelt", f"{len(geaendert)} Datei(en), Vault-Stand {stand}"
|
||||
Reference in new issue
Block a user