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