#!/usr/bin/env bash # Auftragsbuch: einen Vorschlags-Branch ANNEHMEN — der letzte Meter des propose-only-Kreislaufs. # Werkstatt/Orchestrator liefern Branches (wartung/*, orchestrator/*), der Commander klickt in # der Zentrale „Annehmen" → dieses Skript macht die bisherige Git-Handarbeit: Merge im # isolierten Worktree (Lehre: NIE im Live-Checkout arbeiten) → Push nach main → Deploy → # Health-Check → bei Rot automatischer Revert + Redeploy + Alarm. # # WICHTIG: läuft als EIGENE systemd-Unit (systemd-run, startet routers/auftragsbuch.py), # NICHT als Kind des Backends — deploy.sh startet mission-control-2 neu und würde sonst # den eigenen Eltern-Prozess mitten im Lauf töten. Außerdem wird es vom Backend als # /tmp-KOPIE gestartet (deploy.sh macht git reset --hard → die Datei unter den Füßen # eines laufenden bash zu tauschen korrumpiert das Skript, bekannte Falle). # # Nutzung: auftrag-annehmen.sh (z. B. wartung/saubere-zusammenfassung) set -uo pipefail BRANCH="${1:?Nutzung: auftrag-annehmen.sh }" SRC="${MC2_SRC:-$HOME/mission-control-v2}" STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}" API="${MC_API:-http://127.0.0.1:9001}" SLUG="$(echo "$BRANCH" | tr '/' '-')" WT="/tmp/annahme-$SLUG" LOG="/tmp/annahme-$SLUG.log" notify(){ bash "$SRC/deploy/notify.sh" -s "[Auftragsbuch]" "$1" || true; } # Status-Fortschritt für die UI (atomar via tmp+replace; die API liest die Datei nur). status(){ # $1=state $2=detail python3 - "$STATUS" "$BRANCH" "$1" "$2" <<'PY' import json, os, sys, time p, branch, state, detail = sys.argv[1:5] try: d = json.load(open(p, encoding="utf-8")) except Exception: d = {} d.setdefault("branches", {})[branch] = {"state": state, "detail": detail, "ts": time.time()} tmp = p + ".tmp" json.dump(d, open(tmp, "w", encoding="utf-8"), ensure_ascii=False) os.replace(tmp, p) PY } cleanup_wt(){ git -C "$SRC" worktree remove --force "$WT" 2>/dev/null || true; } fail(){ status "fehlgeschlagen" "$1" notify "Vorschlag '$BRANCH' konnte NICHT eingespielt werden: $1 — nichts wurde verändert, der Branch bleibt liegen." cleanup_wt exit 1 } status "laeuft" "Merge wird vorbereitet" cd "$SRC" || fail "Live-Checkout $SRC fehlt" git fetch -q origin || fail "git fetch (Gitea) fehlgeschlagen" git rev-parse --verify -q "refs/remotes/origin/$BRANCH" >/dev/null \ || fail "Branch origin/$BRANCH existiert nicht (schon gemergt/gelöscht?)" PRE="$(git rev-parse origin/main)" # Kaputt aufgesetzte Branches (Worker machte git init/Shallow statt zu klonen) haben KEINEN # gemeinsamen Vorfahren — git verweigert den Merge immer. Ehrlich sagen statt „Konflikt". git merge-base origin/main "origin/$BRANCH" >/dev/null 2>&1 \ || fail "Branch hat keinen gemeinsamen Ursprung mit main (kaputt aufgesetzt, z. B. git init statt Klonen) — bitte ablehnen (mit Grund) und die Idee neu in die Queue geben" # Isolierter Worktree — der Live-Checkout bleibt bis zum Deploy unberührt. cleanup_wt git worktree add --detach "$WT" origin/main >/dev/null 2>&1 || fail "Worktree konnte nicht angelegt werden" MERGE_MSG="Auftragsbuch: '$BRANCH' angenommen (Ein-Klick-Gate)" if ! git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ merge --no-ff "origin/$BRANCH" -m "$MERGE_MSG" >>"$LOG" 2>&1; then git -C "$WT" merge --abort 2>/dev/null || true # Selbstheilung: veralteten Branch mechanisch auf main rebasen (zweiter Worktree am # Branch-Kopf). Klappt das sauber, wird der rebasede Stand gemergt — kein PC nötig. status "laeuft" "Merge-Konflikt — Auto-Rebase wird versucht" WT2="/tmp/annahme-rebase-$SLUG" git worktree remove --force "$WT2" 2>/dev/null || true REBASED="" if git worktree add --detach "$WT2" "origin/$BRANCH" >/dev/null 2>&1 \ && git -C "$WT2" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ rebase origin/main >>"$LOG" 2>&1; then REBASED="$(git -C "$WT2" rev-parse HEAD)" else git -C "$WT2" rebase --abort 2>/dev/null || true fi git worktree remove --force "$WT2" 2>/dev/null || true [ -n "$REBASED" ] || fail "Merge-Konflikt mit main, Auto-Rebase scheiterte ebenfalls — echter Inhaltskonflikt; am einfachsten ablehnen und die Idee neu in die Queue geben (frischer Branch von aktuellem main)" git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ merge --no-ff "$REBASED" -m "$MERGE_MSG — auto-rebased (Branch war veraltet)" >>"$LOG" 2>&1 \ || { git -C "$WT" merge --abort 2>/dev/null || true; fail "Merge nach Auto-Rebase fehlgeschlagen (unerwartet) — Log: $LOG"; } fi # py_compile-Gate über die durch den Merge geänderten Python-Dateien (AGENTS.md-Regel). PYS="$(git -C "$WT" diff --name-only "$PRE"..HEAD -- '*.py' 2>/dev/null | tr '\n' ' ')" if [ -n "${PYS// /}" ]; then PYBIN="$SRC/backend/.venv/bin/python"; [ -x "$PYBIN" ] || PYBIN="python3" # shellcheck disable=SC2086 ( cd "$WT" && "$PYBIN" -m py_compile $PYS ) >>"$LOG" 2>&1 || fail "py_compile-Gate rot — der Patch enthält kaputtes Python" fi status "laeuft" "Push nach main läuft" PUSHED=0 for _ in 1 2 3; do git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1 && { PUSHED=1; break; } sleep 5 done [ "$PUSHED" = 1 ] || fail "Push nach main fehlgeschlagen (Gitea nicht erreichbar / Token?)" cleanup_wt status "laeuft" "Deploy läuft — die Zentrale startet gleich kurz neu" notify "Vorschlag '$BRANCH' angenommen: Merge auf main ist durch, Deploy läuft. Die Zentrale ist gleich kurz weg." # deploy.sh als Kopie ausführen (Selbst-Reset-Falle: es resettet das Repo, in dem es liegt). cp "$SRC/deploy/deploy.sh" "/tmp/annahme-deploy-$SLUG.sh" DEPLOY_OK=1 bash "/tmp/annahme-deploy-$SLUG.sh" >>"$LOG" 2>&1 || DEPLOY_OK=0 # Health-Check mit Geduld (Dienst-Neustart + Warmup brauchen einen Moment). HEALTH=0 if [ "$DEPLOY_OK" = 1 ]; then for _ in $(seq 1 18); do curl -sf -m 5 "$API/api/health" >/dev/null 2>&1 && { HEALTH=1; break; } sleep 5 done fi if [ "$HEALTH" = 1 ]; then # Gemergt + live + grün → der Remote-Branch hat seinen Zweck erfüllt. git -C "$SRC" push origin --delete "$BRANCH" >>"$LOG" 2>&1 || true git -C "$SRC" fetch -q --prune origin 2>/dev/null || true status "eingespielt" "Deploy grün, Health-Check bestanden" notify "Vorschlag '$BRANCH' ist LIVE — Deploy grün, Health-Check bestanden. ✅" exit 0 fi # ── Rollback: Merge revertieren, main zurückschieben, neu deployen, Alarm ── status "rollback" "Health rot — automatischer Revert läuft" git fetch -q origin || true cleanup_wt if git worktree add --detach "$WT" origin/main >/dev/null 2>&1 \ && git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ revert -m 1 --no-edit HEAD >>"$LOG" 2>&1 \ && git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1; then cleanup_wt cp "$SRC/deploy/deploy.sh" "/tmp/annahme-rollback-$SLUG.sh" bash "/tmp/annahme-rollback-$SLUG.sh" >>"$LOG" 2>&1 || true R=0 for _ in $(seq 1 12); do curl -sf -m 5 "$API/api/health" >/dev/null 2>&1 && { R=1; break; } sleep 5 done if [ "$R" = 1 ]; then status "zurueckgerollt" "Health blieb rot — Merge automatisch revertiert, Box läuft wieder" notify "Vorschlag '$BRANCH' hat den Health-Check GERISSEN. Automatisch zurückgerollt — die Box läuft wieder auf dem alten Stand. Der Branch bleibt zur Analyse liegen. Log: $LOG" exit 1 fi fi cleanup_wt status "kritisch" "Rollback fehlgeschlagen — Box braucht Hilfe" curl -sf -m 5 -X POST "$API/api/alarm" -H 'Content-Type: application/json' \ --data "{\"subject\":\"[Auftragsbuch]\",\"text\":\"KRITISCH: Annehmen von $BRANCH UND Rollback fehlgeschlagen — Zentrale prüfen. Log: $LOG\"}" >/dev/null 2>&1 \ || notify "KRITISCH: Annehmen von '$BRANCH' UND Rollback fehlgeschlagen — bitte Box prüfen (restore.sh liegt bereit). Log: $LOG" exit 2