Files
timemaster/update.sh
T
patrick 3dfcff30e7 update.sh: switch backend sync to git pull instead of rsync
Servers now have git repos initialized tracking origin/main.
Step 3 does git pull --ff-only instead of rsync. Frontend dist
is still rsynced since it's gitignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:31:11 +02:00

555 lines
20 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 Deployment & Update Script
# Lokal ausführen verbindet sich per SSH auf Produktivserver
#
# Usage:
# ./update.sh [OPTIONS]
#
# Options:
# --no-tests Tests überspringen
# --no-frontend Frontend-Build und -Sync überspringen
# --server 137 Nur Server 192.168.1.137
# --server 164 Nur Server 192.168.1.164
# --server both Beide Server (Standard)
# --dry-run Alle Befehle ausgeben, aber NICHT ausführen
# --help Diese Hilfe anzeigen
#
# Schritte (Reihenfolge ist kritisch):
# 1. Tests auf Server 137 ausführen (via SSH)
# 2. Frontend bauen (lokal, npm run build)
# 3. git pull origin main auf Server(n)
# 4. Alembic-Migration auf Server ausführen
# 5. Service neustarten
# 6. Health-Check (3 Versuche)
# 7. Frontend-Dist synchronisieren
# 8. Abschlussbericht
# =============================================================================
set -euo pipefail
# =============================================================================
# KONFIGURATION
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$SCRIPT_DIR/backend"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
SERVER_137="root@192.168.1.137"
SERVER_164="root@192.168.1.164"
REMOTE_PATH="/opt/timemaster"
REMOTE_VENV="$REMOTE_PATH/backend/venv"
HEALTH_RETRIES=3
HEALTH_WAIT=3
# =============================================================================
# FARBEN & AUSGABE
# =============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
step() { echo -e "\n${BLUE}${BOLD}$*${RESET}"; }
ok() { echo -e " ${GREEN}$*${RESET}"; }
fail() { echo -e " ${RED}$*${RESET}"; }
warn() { echo -e " ${YELLOW}$*${RESET}"; }
info() { echo -e " ${RESET}$*${RESET}"; }
hr() { echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"; }
die() {
echo ""
fail "$1"
echo ""
hr
echo -e "${RED}${BOLD} ABBRUCH: $1${RESET}"
hr
print_summary
exit 1
}
# =============================================================================
# ARGUMENT-PARSING
# =============================================================================
OPT_TESTS=true
OPT_FRONTEND=true
OPT_SERVER="both"
OPT_DRYRUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
--no-tests)
OPT_TESTS=false
shift ;;
--no-frontend)
OPT_FRONTEND=false
shift ;;
--server)
if [[ -z "${2:-}" ]]; then
die "--server benötigt einen Wert: 137, 164 oder both"
fi
OPT_SERVER="$2"
if [[ "$OPT_SERVER" != "137" && "$OPT_SERVER" != "164" && "$OPT_SERVER" != "both" ]]; then
die "--server muss 137, 164 oder both sein (nicht: $OPT_SERVER)"
fi
shift 2 ;;
--dry-run)
OPT_DRYRUN=true
shift ;;
--help|-h)
# Zeige nur den Header-Kommentarblock (bis zur ersten Leerzeile im Kommentar-Block)
awk '/^#!/{next} /^#/{sub(/^# ?/,""); print; next} /^[^#]/{exit}' "$0"
exit 0 ;;
*)
die "Unbekanntes Argument: $1 (--help für Hilfe)"
;;
esac
done
# Server-Liste aufbauen
SERVERS=()
case "$OPT_SERVER" in
137) SERVERS=("$SERVER_137") ;;
164) SERVERS=("$SERVER_164") ;;
both) SERVERS=("$SERVER_137" "$SERVER_164") ;;
esac
# =============================================================================
# ZUSAMMENFASSUNGS-TRACKING
# =============================================================================
START_TIME=$(date +%s)
declare -A STEP_STATUS # name → "OK" | "FAIL" | "SKIP" | "WARN"
declare -a STEP_ORDER=()
step_record() {
local name="$1" status="$2"
STEP_STATUS["$name"]="$status"
STEP_ORDER+=("$name")
}
print_summary() {
local end_time elapsed
end_time=$(date +%s)
elapsed=$(( end_time - START_TIME ))
echo ""
hr
echo -e "${BOLD} Abschlussbericht (Dauer: ${elapsed}s)${RESET}"
hr
for name in "${STEP_ORDER[@]}"; do
local status="${STEP_STATUS[$name]}"
case "$status" in
OK) echo -e " ${GREEN}$name${RESET}" ;;
FAIL) echo -e " ${RED}$name${RESET}" ;;
SKIP) echo -e " ${YELLOW} $name (übersprungen)${RESET}" ;;
WARN) echo -e " ${YELLOW}$name${RESET}" ;;
esac
done
hr
}
# =============================================================================
# HILFSFUNKTIONEN
# =============================================================================
# Führt einen Befehl aus oder gibt ihn im Dry-Run nur aus
run_cmd() {
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} $*"
return 0
fi
"$@"
}
# SSH-Befehl mit Fehlerbehandlung
run_ssh() {
local server="$1"
shift
local cmd="$*"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server \"$cmd\""
return 0
fi
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" "$cmd"; then
return 1
fi
}
# rsync mit Fehlerbehandlung
run_rsync() {
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} rsync $*"
return 0
fi
rsync "$@"
}
# SSH-Erreichbarkeit prüfen
check_ssh() {
local server="$1"
if $OPT_DRYRUN; then
info "Dry-Run: SSH-Check für $server übersprungen"
return 0
fi
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" "true" 2>/dev/null; then
return 1
fi
}
# =============================================================================
# HEADER
# =============================================================================
hr
echo -e "${BOLD} TimeMaster Deployment Script${RESET}"
echo -e " Datum: $(date '+%Y-%m-%d %H:%M:%S')"
echo -e " Server: ${SERVERS[*]}"
echo -e " Tests: $(if $OPT_TESTS; then echo "ja"; else echo "nein (--no-tests)"; fi)"
echo -e " Frontend: $(if $OPT_FRONTEND; then echo "ja"; else echo "nein (--no-frontend)"; fi)"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}${BOLD}Modus: DRY-RUN (keine Änderungen werden durchgeführt)${RESET}"
fi
hr
# =============================================================================
# PRE-FLIGHT: Lokale Verzeichnisse prüfen
# =============================================================================
step "Pre-Flight: Lokale Verzeichnisse prüfen"
if [[ ! -d "$BACKEND_DIR" ]]; then
die "Backend-Verzeichnis nicht gefunden: $BACKEND_DIR"
fi
ok "Backend-Verzeichnis vorhanden: $BACKEND_DIR"
if $OPT_FRONTEND; then
if [[ ! -d "$FRONTEND_DIR" ]]; then
die "Frontend-Verzeichnis nicht gefunden: $FRONTEND_DIR"
fi
if [[ ! -f "$FRONTEND_DIR/package.json" ]]; then
die "package.json nicht gefunden: $FRONTEND_DIR/package.json"
fi
ok "Frontend-Verzeichnis vorhanden: $FRONTEND_DIR"
fi
# SSH-Erreichbarkeit aller Zielserver prüfen
for server in "${SERVERS[@]}"; do
step "Pre-Flight: SSH-Verbindung zu $server prüfen"
if check_ssh "$server"; then
ok "SSH-Verbindung zu $server erfolgreich"
else
die "SSH-Verbindung zu $server fehlgeschlagen.\n Prüfe: ssh $server\n SSH-Key konfiguriert?"
fi
done
step_record "Pre-Flight" "OK"
# =============================================================================
# SCHRITT 1: TESTS (auf Server 137)
# =============================================================================
if ! $OPT_TESTS; then
step "Schritt 1: Tests (übersprungen via --no-tests)"
step_record "1. Tests" "SKIP"
else
step "Schritt 1: Tests auf $SERVER_137 ausführen"
info "Befehl: cd /opt/timemaster/backend && source venv/bin/activate && python -m pytest -x -q"
TEST_CMD="cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && python -m pytest -x -q 2>&1"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $SERVER_137 \"$TEST_CMD\""
step_record "1. Tests" "SKIP"
else
TEST_OUTPUT=$(ssh -o ConnectTimeout=30 -o BatchMode=yes "$SERVER_137" "$TEST_CMD" 2>&1) || true
TEST_RC=${PIPESTATUS[0]:-$?}
# Ausgabe der Tests anzeigen
echo "$TEST_OUTPUT" | while IFS= read -r line; do
info "$line"
done
# Prüfen ob Tests fehlgeschlagen (pytest gibt RC != 0 bei Fehler)
if echo "$TEST_OUTPUT" | grep -qE "^(FAILED|ERROR|[0-9]+ failed)"; then
fail "Tests FEHLGESCHLAGEN"
echo ""
echo -e " ${RED}Fehlerdetails:${RESET}"
echo "$TEST_OUTPUT" | grep -A 5 -E "^(FAILED|ERROR)" | while IFS= read -r line; do
echo -e " $line"
done
step_record "1. Tests" "FAIL"
die "Tests fehlgeschlagen Deploy abgebrochen. Keine Dateien wurden auf den Server übertragen."
fi
# Erfolgsmeldung aus pytest-Ausgabe extrahieren
PASSED_LINE=$(echo "$TEST_OUTPUT" | grep -E "passed" | tail -1 || echo "")
if [[ -n "$PASSED_LINE" ]]; then
ok "Tests bestanden: $PASSED_LINE"
else
ok "Tests abgeschlossen ohne Fehler"
fi
step_record "1. Tests" "OK"
fi
fi
# =============================================================================
# SCHRITT 2: FRONTEND BAUEN
# =============================================================================
if ! $OPT_FRONTEND; then
step "Schritt 2: Frontend-Build (übersprungen via --no-frontend)"
step_record "2. Frontend-Build" "SKIP"
else
step "Schritt 2: Frontend bauen (npm run build)"
info "Verzeichnis: $FRONTEND_DIR"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} cd $FRONTEND_DIR && npm run build"
step_record "2. Frontend-Build" "SKIP"
else
BUILD_OUTPUT=$(cd "$FRONTEND_DIR" && npm run build 2>&1) || BUILD_RC=$?
BUILD_RC=${BUILD_RC:-0}
if [[ $BUILD_RC -ne 0 ]]; then
fail "Frontend-Build FEHLGESCHLAGEN (Exit-Code: $BUILD_RC)"
echo ""
echo -e " ${RED}Build-Ausgabe:${RESET}"
echo "$BUILD_OUTPUT" | tail -30 | while IFS= read -r line; do
echo -e " $line"
done
step_record "2. Frontend-Build" "FAIL"
die "Frontend-Build fehlgeschlagen Deploy abgebrochen."
fi
if [[ ! -d "$FRONTEND_DIR/dist" ]]; then
step_record "2. Frontend-Build" "FAIL"
die "Frontend-Build scheinbar erfolgreich, aber dist/-Verzeichnis fehlt: $FRONTEND_DIR/dist"
fi
DIST_SIZE=$(du -sh "$FRONTEND_DIR/dist" | cut -f1)
ok "Frontend gebaut: $FRONTEND_DIR/dist ($DIST_SIZE)"
step_record "2. Frontend-Build" "OK"
fi
fi
# =============================================================================
# SCHRITT 3, 4, 5, 6, 7: PRO SERVER AUSFÜHREN
# =============================================================================
SERVER_RESULTS=()
ALL_SERVERS_OK=true
for server in "${SERVERS[@]}"; do
SERVER_SHORT="${server##*@}" # "192.168.1.137" aus "root@192.168.1.137"
echo ""
hr
echo -e "${BOLD} Server: $server${RESET}"
hr
SERVER_OK=true
# -------------------------------------------------------------------------
# SCHRITT 3: git pull vom Gitea
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 3: git pull (origin/main)"
GIT_PULL_CMD="git config --global --add safe.directory $REMOTE_PATH 2>/dev/null; cd $REMOTE_PATH && git pull --ff-only origin main 2>&1"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server \"$GIT_PULL_CMD\""
step_record "3. git pull [$SERVER_SHORT]" "SKIP"
else
GIT_OUTPUT=$(ssh -o ConnectTimeout=30 -o BatchMode=yes "$server" "$GIT_PULL_CMD" 2>&1)
GIT_RC=$?
echo "$GIT_OUTPUT" | while IFS= read -r line; do info "$line"; done
if [ $GIT_RC -eq 0 ]; then
ok "git pull erfolgreich"
step_record "3. git pull [$SERVER_SHORT]" "OK"
else
fail "git pull FEHLGESCHLAGEN (Exit-Code: $GIT_RC)"
step_record "3. git pull [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
warn "Server $server wird übersprungen (git pull fehlgeschlagen)"
continue
fi
fi
# -------------------------------------------------------------------------
# SCHRITT 4: Alembic-Migration
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 4: Alembic-Migration"
MIGRATE_CMD="cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic upgrade head 2>&1"
info "Befehl: $MIGRATE_CMD"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server \"$MIGRATE_CMD\""
step_record "4. Migration [$SERVER_SHORT]" "SKIP"
else
MIGRATE_OUTPUT=$(ssh -o ConnectTimeout=30 -o BatchMode=yes "$server" "$MIGRATE_CMD" 2>&1) || MIGRATE_RC=$?
MIGRATE_RC=${MIGRATE_RC:-0}
echo "$MIGRATE_OUTPUT" | while IFS= read -r line; do
info "$line"
done
if [[ $MIGRATE_RC -ne 0 ]]; then
fail "Alembic-Migration FEHLGESCHLAGEN (Exit-Code: $MIGRATE_RC)"
echo ""
warn "Service wird NICHT neugestartet (DB-Schema könnte inkonsistent sein)"
echo ""
echo -e " ${RED}Letzte 20 Server-Log-Zeilen:${RESET}"
ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" \
"journalctl -u timemaster -n 20 --no-pager 2>/dev/null || echo '(keine Logs verfügbar)'" \
| while IFS= read -r line; do echo -e " $line"; done
echo ""
echo -e " ${YELLOW}Diagnose-Befehle:${RESET}"
echo -e " ssh $server 'cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic current'"
echo -e " ssh $server 'cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic history --verbose'"
step_record "4. Migration [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
warn "Server $server wird übersprungen (Migration fehlgeschlagen, Service NICHT neugestartet)"
continue
fi
# Prüfe ob Migration "up to date" ist (kein Fehler, aber auch keine Änderung)
if echo "$MIGRATE_OUTPUT" | grep -qi "already up to date\|no migrations"; then
ok "Migration: bereits aktuell (keine neuen Migrationen)"
else
ok "Migration erfolgreich"
fi
step_record "4. Migration [$SERVER_SHORT]" "OK"
fi
# -------------------------------------------------------------------------
# SCHRITT 5: Service neustarten
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 5: Service neustarten"
if run_ssh "$server" "systemctl restart timemaster"; then
ok "Service neugestartet"
step_record "5. Service-Restart [$SERVER_SHORT]" "OK"
else
RESTART_RC=$?
fail "systemctl restart timemaster FEHLGESCHLAGEN (Exit-Code: $RESTART_RC)"
echo ""
echo -e " ${RED}Letzte 30 Journal-Zeilen:${RESET}"
ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" \
"journalctl -u timemaster -n 30 --no-pager 2>/dev/null || echo '(keine Logs verfügbar)'" \
| while IFS= read -r line; do echo -e " $line"; done
echo ""
echo -e " ${YELLOW}Diagnose-Befehle:${RESET}"
echo -e " ssh $server 'systemctl status timemaster'"
echo -e " ssh $server 'journalctl -u timemaster -n 100 --no-pager'"
step_record "5. Service-Restart [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
warn "Server $server: Service-Start fehlgeschlagen"
continue
fi
# -------------------------------------------------------------------------
# SCHRITT 6: Health-Check
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 6: Health-Check (${HEALTH_RETRIES} Versuche, ${HEALTH_WAIT}s Pause)"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server 'curl -sf http://localhost:8000/health'"
step_record "6. Health-Check [$SERVER_SHORT]" "SKIP"
else
HEALTH_OK=false
for attempt in $(seq 1 $HEALTH_RETRIES); do
info "Versuch $attempt/$HEALTH_RETRIES..."
HEALTH_OUTPUT=$(ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" \
"curl -sf --connect-timeout 5 --max-time 10 http://localhost:8000/health 2>&1") \
&& HEALTH_RC=0 || HEALTH_RC=$?
if [[ $HEALTH_RC -eq 0 ]]; then
ok "Health-Check erfolgreich (Versuch $attempt): $HEALTH_OUTPUT"
HEALTH_OK=true
break
else
warn "Versuch $attempt fehlgeschlagen"
if [[ $attempt -lt $HEALTH_RETRIES ]]; then
info "Warte ${HEALTH_WAIT}s..."
sleep "$HEALTH_WAIT"
fi
fi
done
if ! $HEALTH_OK; then
fail "Health-Check nach $HEALTH_RETRIES Versuchen FEHLGESCHLAGEN"
echo ""
warn "Service läuft möglicherweise nicht korrekt. Log-Befehle:"
echo -e " ${YELLOW}ssh $server 'journalctl -u timemaster -n 50 --no-pager'${RESET}"
echo -e " ${YELLOW}ssh $server 'systemctl status timemaster'${RESET}"
echo -e " ${YELLOW}ssh $server 'curl -v http://localhost:8000/health'${RESET}"
step_record "6. Health-Check [$SERVER_SHORT]" "WARN"
SERVER_OK=false
ALL_SERVERS_OK=false
# KEIN Abbruch hier nur Warnung, Frontend noch synchronisieren
else
step_record "6. Health-Check [$SERVER_SHORT]" "OK"
fi
fi
# -------------------------------------------------------------------------
# SCHRITT 7: Frontend-Dist synchronisieren
# -------------------------------------------------------------------------
if ! $OPT_FRONTEND; then
step "[$SERVER_SHORT] Schritt 7: Frontend-Sync (übersprungen via --no-frontend)"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "SKIP"
else
step "[$SERVER_SHORT] Schritt 7: Frontend-Dist synchronisieren"
info "rsync: $FRONTEND_DIR/dist/ → $server:$REMOTE_PATH/frontend/dist/"
if [[ ! -d "$FRONTEND_DIR/dist" ]] && ! $OPT_DRYRUN; then
warn "dist/-Verzeichnis nicht gefunden: $FRONTEND_DIR/dist"
warn "Frontend-Build wurde eventuell nicht ausgeführt (--no-frontend?)"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "WARN"
else
if run_rsync -avz --delete \
"$FRONTEND_DIR/dist/" \
"$server:$REMOTE_PATH/frontend/dist/" ; then
ok "Frontend-Dist synchronisiert"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "OK"
else
RSYNC_FE_RC=$?
fail "Frontend-rsync FEHLGESCHLAGEN (Exit-Code: $RSYNC_FE_RC)"
fail "Befehl war: rsync -avz --delete $FRONTEND_DIR/dist/ $server:$REMOTE_PATH/frontend/dist/"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
fi
fi
fi
if $SERVER_OK; then
SERVER_RESULTS+=("${GREEN}$server${RESET}")
else
SERVER_RESULTS+=("${YELLOW}$server (mit Warnungen/Fehlern)${RESET}")
fi
done # Ende Server-Schleife
# =============================================================================
# ABSCHLUSSBERICHT
# =============================================================================
echo ""
hr
echo -e "${BOLD} Server-Ergebnisse:${RESET}"
for result in "${SERVER_RESULTS[@]}"; do
echo -e " $result"
done
print_summary
if $ALL_SERVERS_OK; then
echo -e "${GREEN}${BOLD} DEPLOYMENT ERFOLGREICH${RESET}"
else
echo -e "${YELLOW}${BOLD} DEPLOYMENT MIT WARNUNGEN ABGESCHLOSSEN${RESET}"
echo -e "${YELLOW} Bitte die Fehlermeldungen oben prüfen.${RESET}"
fi
hr
exit 0