fix(sync): Guard gegen Datenverlust — reset --hard nur bei sauberem Baum

sync_state.py bricht den git-Sync ab + alarmiert per Telegram, wenn der
Live-Arbeitsbaum uncommittete Aenderungen oder ungepushte Commits hat,
statt sie per reset --hard FETCH_HEAD zu vernichten. Watchdog laeuft
unabhaengig weiter. Behebt die Ursache von Issue #123.
This commit is contained in:
Homelab Cursor 2026-06-22 20:38:35 +02:00
parent 38b91136c2
commit 4f61172468

View file

@ -387,12 +387,45 @@ def git_sync(cfg: config.HomelabConfig):
["git", "-C", str(REPO), "fetch", forgejo_url, "main", "--quiet"], ["git", "-C", str(REPO), "fetch", forgejo_url, "main", "--quiet"],
capture_output=True, timeout=30, capture_output=True, timeout=30,
) )
# -- Schutz gegen Datenverlust (Ref Issue #123) --------------------
# Kein blindes 'reset --hard', wenn der Live-Arbeitsbaum nicht-committete
# Aenderungen oder lokale, noch nicht gepushte Commits hat -- sonst wuerde
# echte Arbeit vernichtet (das Beinahe-Desaster vom Juni 2026).
# Stattdessen: git-Sync ueberspringen + Telegram-Alarm; Mensch loest auf.
dirty = subprocess.run(
["git", "-C", str(REPO), "status", "--porcelain", "--untracked-files=no"],
capture_output=True, text=True, timeout=15,
).stdout.strip()
ahead = subprocess.run(
["git", "-C", str(REPO), "rev-list", "--count", "FETCH_HEAD..HEAD"],
capture_output=True, text=True, timeout=15,
).stdout.strip()
if dirty or (ahead.isdigit() and int(ahead) > 0):
n_dirty = len(dirty.splitlines()) if dirty else 0
n_ahead = ahead if ahead.isdigit() else "?"
log("GUARD: git-Sync abgebrochen -- Arbeitsbaum nicht sauber "
"(%s geaenderte Datei(en), %s ungepushte Commit(s)). "
"reset --hard uebersprungen (Schutz vor Datenverlust, Ref #123)."
% (n_dirty, n_ahead))
tg_alert(
cfg, "sync_guard",
"*Homelab git-Sync ABGEBROCHEN*%0A%0A"
"Live-Repo (CT116) ist nicht sauber:%0A"
"- " + str(n_dirty) + " uncommittete Datei(en)%0A"
"- " + str(n_ahead) + " ungepushte Commit(s)%0A%0A"
"reset --hard FETCH_HEAD wurde UEBERSPRUNGEN, um Arbeit nicht zu "
"vernichten (Ref Issue #123).%0ABitte manuell aufloesen: committen + "
"pushen oder gezielt verwerfen. Watchdog laeuft normal weiter."
)
return forgejo_url, False
subprocess.run( subprocess.run(
["git", "-C", str(REPO), "reset", "--hard", "FETCH_HEAD"], ["git", "-C", str(REPO), "reset", "--hard", "FETCH_HEAD"],
capture_output=True, timeout=15, capture_output=True, timeout=15,
) )
return forgejo_url return forgejo_url, True
def git_commit_and_push(cfg: config.HomelabConfig, forgejo_url: str): def git_commit_and_push(cfg: config.HomelabConfig, forgejo_url: str):
@ -436,16 +469,16 @@ def main():
log("Sync startet...") log("Sync startet...")
cfg = config.parse_config(REPO / "homelab.conf") cfg = config.parse_config(REPO / "homelab.conf")
forgejo_url = git_sync(cfg) forgejo_url, repo_clean = git_sync(cfg)
cfg = config.parse_config(REPO / "homelab.conf") cfg = config.parse_config(REPO / "homelab.conf")
# Watchdog # Watchdog (laeuft immer, unabhaengig vom git-Zustand)
log("Watchdog läuft...") log("Watchdog laeuft...")
check_service(cfg, 109, "rss-manager", "RSS Manager") check_service(cfg, 109, "rss-manager", "RSS Manager")
check_docker(cfg, 101, "wordpress-app", "WordPress Docker") check_docker(cfg, 101, "wordpress-app", "WordPress Docker")
# STATE.md Dateien generieren if repo_clean:
states = { states = {
"arakava-news/STATE.md": generate_arakava_state(cfg), "arakava-news/STATE.md": generate_arakava_state(cfg),
"infrastructure/STATE.md": generate_infra_state(cfg), "infrastructure/STATE.md": generate_infra_state(cfg),
@ -457,12 +490,14 @@ def main():
full_path.parent.mkdir(parents=True, exist_ok=True) full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.write_text(content) full_path.write_text(content)
CHANGED = True CHANGED = True
log(f"{path} aktualisiert") log("%s aktualisiert" % path)
if CHANGED: if CHANGED:
git_commit_and_push(cfg, forgejo_url) git_commit_and_push(cfg, forgejo_url)
else: else:
log("Keine Änderungen") log("Keine Aenderungen")
else:
log("git-Sync uebersprungen (Guard aktiv) -- STATE-Regen + Push ausgelassen")
log("Sync abgeschlossen") log("Sync abgeschlossen")