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:
Executable
+344
@@ -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
|
||||
Reference in New Issue
Block a user