add deployment updater scripts

- update.sh: full deploy pipeline (tests → build → rsync → migrate → restart → health-check)
  supports --no-tests, --no-frontend, --server 137|164|both, --dry-run
  aborts before service restart if migration fails (DB protection)
- scripts/check_migrations.sh: compare local Alembic head vs. server

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 21:16:24 +02:00
parent fbc04bc2c0
commit 8e5e76d062
2 changed files with 876 additions and 599 deletions
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env bash
# =============================================================================
# TimeMaster Migrations-Sync-Check
#
# Prüft ob der lokale Alembic-Head mit dem Stand auf dem Server übereinstimmt.
# Gibt aus: lokaler Head, Server-Head, ob Sync nötig ist.
#
# Usage:
# ./scripts/check_migrations.sh [137|164|both]
#
# Argumente:
# 137 Nur Server 192.168.1.137 prüfen (Standard)
# 164 Nur Server 192.168.1.164 prüfen
# both Beide Server prüfen
#
# Exit-Codes:
# 0 Alles synchron (oder Dry-Run)
# 1 Mindestens ein Server ist nicht synchron oder Fehler
# =============================================================================
set -euo pipefail
# =============================================================================
# KONFIGURATION
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BACKEND_DIR="$PROJECT_DIR/backend"
SERVER_137="root@192.168.1.137"
SERVER_164="root@192.168.1.164"
REMOTE_PATH="/opt/timemaster"
REMOTE_VENV="$REMOTE_PATH/backend/venv"
# =============================================================================
# FARBEN & AUSGABE
# =============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
ok() { echo -e " ${GREEN}$*${RESET}"; }
fail() { echo -e " ${RED}$*${RESET}"; }
warn() { echo -e " ${YELLOW}$*${RESET}"; }
info() { echo -e " $*"; }
step() { echo -e "\n${BLUE}${BOLD}$*${RESET}"; }
hr() { echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"; }
die() {
echo ""
fail "$1"
exit 1
}
# =============================================================================
# ARGUMENT-PARSING
# =============================================================================
TARGET="${1:-137}"
case "$TARGET" in
137) SERVERS=("$SERVER_137") ;;
164) SERVERS=("$SERVER_164") ;;
both) SERVERS=("$SERVER_137" "$SERVER_164") ;;
--help|-h)
awk '/^#!/{next} /^#/{sub(/^# ?/,""); print; next} /^[^#]/{exit}' "$0"
exit 0
;;
*)
die "Unbekanntes Argument: $TARGET (Erwartet: 137, 164 oder both)"
;;
esac
# =============================================================================
# PRE-FLIGHT: Lokales Alembic verfügbar?
# =============================================================================
hr
echo -e "${BOLD} TimeMaster Migrations-Sync-Check${RESET}"
echo -e " Datum: $(date '+%Y-%m-%d %H:%M:%S')"
echo -e " Ziel-Server: ${SERVERS[*]}"
hr
# Prüfe ob alembic.ini vorhanden
if [[ ! -f "$BACKEND_DIR/alembic.ini" ]]; then
die "alembic.ini nicht gefunden: $BACKEND_DIR/alembic.ini"
fi
# Prüfe ob migrations/versions/ vorhanden
if [[ ! -d "$BACKEND_DIR/migrations/versions" ]]; then
die "Migrations-Verzeichnis nicht gefunden: $BACKEND_DIR/migrations/versions"
fi
# =============================================================================
# LOKALEN ALEMBIC-HEAD ERMITTELN
# =============================================================================
step "Lokalen Alembic-Head ermitteln"
# Lokaler Head aus den Migrations-Dateien extrahieren (ohne echte DB-Verbindung)
# Strategie: letzte Migration via "down_revision" Kette auflösen
#
# Schnellere Methode: Migrations-Datei mit der höchsten Revisions-ID finden,
# die keine "down_revision" von einer anderen Datei ist (also Head-Node).
# Wir suchen alle revision-IDs und down_revision-IDs und finden die,
# die keine down_revision einer anderen ist.
LOCAL_HEAD=""
LOCAL_HEAD_FILE=""
# Alle .py-Dateien in migrations/versions/ einlesen
declare -A REVISION_MAP # revision_id → dateiname
declare -A DOWN_REVISION # revision_id → down_revision
while IFS= read -r migration_file; do
# Revision-ID extrahieren (Zeile: revision = 'XXXX')
rev=$(grep -oP "^revision\s*=\s*['\"]?\K[a-f0-9]+" "$migration_file" 2>/dev/null | head -1 || true)
down=$(grep -oP "^down_revision\s*=\s*['\"]?\K[a-f0-9]+" "$migration_file" 2>/dev/null | head -1 || true)
if [[ -n "$rev" ]]; then
REVISION_MAP["$rev"]="$(basename "$migration_file")"
DOWN_REVISION["$rev"]="${down:-}"
fi
done < <(find "$BACKEND_DIR/migrations/versions" -name "*.py" -not -name "__pycache__" | sort)
MIGRATION_COUNT=${#REVISION_MAP[@]}
info "Gefundene Migrations-Dateien: $MIGRATION_COUNT"
if [[ $MIGRATION_COUNT -eq 0 ]]; then
warn "Keine Migrations-Dateien gefunden in $BACKEND_DIR/migrations/versions/"
else
# Head-Revision finden: diejenige, die von keiner anderen als down_revision referenziert wird
for rev in "${!REVISION_MAP[@]}"; do
IS_HEAD=true
for other_rev in "${!DOWN_REVISION[@]}"; do
if [[ "${DOWN_REVISION[$other_rev]}" == "$rev" ]]; then
IS_HEAD=false
break
fi
done
if $IS_HEAD; then
LOCAL_HEAD="$rev"
LOCAL_HEAD_FILE="${REVISION_MAP[$rev]}"
break
fi
done
if [[ -n "$LOCAL_HEAD" ]]; then
ok "Lokaler Head: $LOCAL_HEAD"
info "Datei: $LOCAL_HEAD_FILE"
else
warn "Lokaler Head konnte nicht ermittelt werden (zyklische Abhängigkeiten oder keine Dateien?)"
info "Tipp: Prüfe manuell mit: cd $BACKEND_DIR && alembic heads"
fi
fi
# Fallback: Neueste Datei nach Namenskonvention (Präfix wie 0022_)
if [[ -z "$LOCAL_HEAD" ]]; then
LATEST_FILE=$(ls -1 "$BACKEND_DIR/migrations/versions/"*.py 2>/dev/null | \
grep -v '__' | sort -t'_' -k1,1 -V | tail -1 || true)
if [[ -n "$LATEST_FILE" ]]; then
LATEST_REV=$(grep -oP "^revision\s*=\s*['\"]?\K[a-f0-9]+" "$LATEST_FILE" 2>/dev/null | head -1 || true)
if [[ -n "$LATEST_REV" ]]; then
LOCAL_HEAD="$LATEST_REV"
LOCAL_HEAD_FILE="$(basename "$LATEST_FILE")"
warn "Fallback: Letzte Datei: $LOCAL_HEAD_FILE$LOCAL_HEAD"
fi
fi
fi
# =============================================================================
# ALLE VERFÜGBAREN MIGRATIONEN ANZEIGEN
# =============================================================================
step "Lokale Migrations-Kette"
# Kette von unten nach oben aufbauen
if [[ ${#REVISION_MAP[@]} -gt 0 ]]; then
# Startnode finden (down_revision ist leer oder None)
CHAIN_CURRENT=""
for rev in "${!DOWN_REVISION[@]}"; do
if [[ -z "${DOWN_REVISION[$rev]}" ]]; then
CHAIN_CURRENT="$rev"
break
fi
done
# Kette traversieren
CHAIN_COUNT=0
while [[ -n "$CHAIN_CURRENT" ]]; do
CHAIN_FILE="${REVISION_MAP[$CHAIN_CURRENT]:-unbekannt}"
MARKER=""
[[ "$CHAIN_CURRENT" == "$LOCAL_HEAD" ]] && MARKER=" ${GREEN}← HEAD${RESET}"
info "$CHAIN_FILE ($CHAIN_CURRENT)$MARKER"
(( CHAIN_COUNT++ ))
# Nächsten Node finden (der diesen als down_revision hat)
NEXT_REV=""
for rev in "${!DOWN_REVISION[@]}"; do
if [[ "${DOWN_REVISION[$rev]}" == "$CHAIN_CURRENT" ]]; then
NEXT_REV="$rev"
break
fi
done
CHAIN_CURRENT="$NEXT_REV"
done
if [[ $CHAIN_COUNT -eq 0 ]]; then
info "(keine lineare Kette gefunden)"
fi
fi
# =============================================================================
# SERVER-HEADS PRÜFEN
# =============================================================================
EXIT_CODE=0
declare -A SERVER_HEADS
for server in "${SERVERS[@]}"; do
SERVER_SHORT="${server##*@}"
step "Server $server: aktuellen Migrations-Stand abrufen"
# SSH-Erreichbarkeit prüfen
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" "true" 2>/dev/null; then
fail "SSH-Verbindung zu $server fehlgeschlagen"
warn "Prüfe: ssh $server"
SERVER_HEADS["$SERVER_SHORT"]="FEHLER (SSH)"
EXIT_CODE=1
continue
fi
# alembic current auf dem Server ausführen
ALEMBIC_CMD="cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic current 2>&1"
ALEMBIC_OUTPUT=$(ssh -o ConnectTimeout=20 -o BatchMode=yes "$server" "$ALEMBIC_CMD" 2>&1) \
&& ALEMBIC_RC=0 || ALEMBIC_RC=$?
if [[ $ALEMBIC_RC -ne 0 ]]; then
fail "alembic current FEHLGESCHLAGEN auf $server (Exit-Code: $ALEMBIC_RC)"
echo "$ALEMBIC_OUTPUT" | while IFS= read -r line; do info "$line"; done
warn "Mögliche Ursachen:"
info " - venv nicht vorhanden: $REMOTE_VENV"
info " - alembic.ini nicht auf dem Server: $REMOTE_PATH/backend/alembic.ini"
info " - DB-Verbindung auf dem Server fehlgeschlagen"
info " - Diagnose: ssh $server 'cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic current'"
SERVER_HEADS["$SERVER_SHORT"]="FEHLER (alembic)"
EXIT_CODE=1
continue
fi
# Revision aus alembic-Ausgabe extrahieren
# alembic current gibt aus: "abcd1234 (head)" oder "abcd1234"
SERVER_REV=$(echo "$ALEMBIC_OUTPUT" | grep -oP "^[a-f0-9]{4,}" | head -1 || true)
SERVER_IS_HEAD=$(echo "$ALEMBIC_OUTPUT" | grep -c "(head)" || true)
if [[ -z "$SERVER_REV" ]]; then
# Möglicherweise leere Datenbank (keine Migrationen gelaufen)
if echo "$ALEMBIC_OUTPUT" | grep -qi "no current"; then
SERVER_REV="(keine Migration gelaufen)"
warn "Server $server: Noch keine Migration ausgeführt"
else
SERVER_REV="(unbekannt)"
warn "Server $server: Revision konnte nicht aus alembic-Ausgabe extrahiert werden"
info "alembic-Ausgabe:"
echo "$ALEMBIC_OUTPUT" | while IFS= read -r line; do info " $line"; done
fi
SERVER_HEADS["$SERVER_SHORT"]="$SERVER_REV"
EXIT_CODE=1
continue
fi
SERVER_HEADS["$SERVER_SHORT"]="$SERVER_REV"
info "alembic-Ausgabe: $ALEMBIC_OUTPUT"
# Vergleich: lokaler Head vs. Server
if [[ -n "$LOCAL_HEAD" ]]; then
if [[ "$SERVER_REV" == "$LOCAL_HEAD" ]]; then
ok "Server $server ist SYNCHRON"
info " Lokaler Head : $LOCAL_HEAD ($LOCAL_HEAD_FILE)"
info " Server-Stand : $SERVER_REV"
if [[ "$SERVER_IS_HEAD" -gt 0 ]]; then
info " Status : (head)"
fi
else
fail "Server $server ist NICHT synchron Migration nötig!"
info " Lokaler Head : $LOCAL_HEAD ($LOCAL_HEAD_FILE)"
info " Server-Stand : $SERVER_REV"
echo ""
echo -e " ${YELLOW}Zum Synchronisieren:${RESET}"
echo -e " ${YELLOW}ssh $server 'cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic upgrade head'${RESET}"
echo -e " ${YELLOW}# oder: ./update.sh --no-tests --no-frontend --server ${SERVER_SHORT}${RESET}"
EXIT_CODE=1
fi
else
warn "Kein lokaler Head bekannt kein Vergleich möglich"
info " Server-Stand : $SERVER_REV"
fi
done
# =============================================================================
# ZUSAMMENFASSUNG
# =============================================================================
echo ""
hr
echo -e "${BOLD} Zusammenfassung${RESET}"
hr
echo ""
if [[ -n "$LOCAL_HEAD" ]]; then
echo -e " ${BOLD}Lokaler Head:${RESET} $LOCAL_HEAD"
echo -e " ($LOCAL_HEAD_FILE)"
else
echo -e " ${BOLD}Lokaler Head:${RESET} ${YELLOW}(konnte nicht ermittelt werden)${RESET}"
fi
echo ""
for server in "${SERVERS[@]}"; do
SERVER_SHORT="${server##*@}"
server_rev="${SERVER_HEADS[$SERVER_SHORT]:-unbekannt}"
if [[ -n "$LOCAL_HEAD" && "$server_rev" == "$LOCAL_HEAD" ]]; then
echo -e " ${GREEN}${RESET} ${BOLD}$server${RESET}$server_rev ${GREEN}(synchron)${RESET}"
elif [[ "$server_rev" == FEHLER* ]]; then
echo -e " ${RED}${RESET} ${BOLD}$server${RESET}${RED}$server_rev${RESET}"
else
echo -e " ${RED}${RESET} ${BOLD}$server${RESET}$server_rev ${YELLOW}(Migration nötig)${RESET}"
fi
done
echo ""
if [[ $EXIT_CODE -eq 0 ]]; then
echo -e " ${GREEN}${BOLD}Alle geprüften Server sind synchron.${RESET}"
else
echo -e " ${YELLOW}${BOLD}Mindestens ein Server benötigt eine Migration oder hat Fehler.${RESET}"
echo ""
echo -e " ${YELLOW}Schnell-Sync (nur Migration + Restart):${RESET}"
case "$TARGET" in
137) echo -e " ./update.sh --no-tests --no-frontend --server 137" ;;
164) echo -e " ./update.sh --no-tests --no-frontend --server 164" ;;
both) echo -e " ./update.sh --no-tests --no-frontend --server both" ;;
esac
fi
hr
exit $EXIT_CODE
Regular → Executable
+532 -599
View File
File diff suppressed because it is too large Load Diff