Files
timemaster/scripts/check_migrations.sh
T
patrick 8e5e76d062 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>
2026-05-23 21:16:24 +02:00

345 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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