chore(deploy): entkoppelte Self-Update-Skripte pro Server

Jeder Server aktualisiert sich eigenstaendig aus dem Repo – kein
Cross-Server-SSH, kein "both". 137 und 164 komplett getrennt.

- server-update.sh: laeuft AUF dem Server (git pull --ff-only → alembic
  upgrade head → restart → health). Guard: bricht ab wenn venv fehlt.
- push-frontend.sh: lokal, baut Frontend + rsync dist/ an EINEN Server
  (Node laeuft nicht auf den Servern, dist ist gitignored). Guard gegen
  Ausfuehrung auf einem Zielserver.
- deploy.sh + update.sh stillgelegt (Abbruch-Stubs): deploy.sh rsyncte den
  Arbeitsbaum in den Server-git-Baum und verschmutzte ihn (git pull brach);
  update.sh koppelte beide Server ("both").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 16:29:43 +02:00
co-authored by Claude Opus 4.8
parent d13350b38b
commit 8ae792fd90
4 changed files with 142 additions and 656 deletions
+14 -107
View File
@@ -1,107 +1,14 @@
#!/bin/bash #!/usr/bin/env bash
# ============================================================ # =============================================================================
# deploy.sh Lokal ausführen, deployt auf 192.168.1.137 # STILLGELEGT nicht mehr verwenden.
# Usage: #
# ./deploy.sh → nur sync + restart # Dieses Skript rsyncte das lokale Arbeitsverzeichnis in den git-Baum des
# ./deploy.sh --migrate → sync + alembic upgrade + restart # Servers und verschmutzte ihn dadurch (spätere 'git pull' schlugen fehl).
# ./deploy.sh --setup → erstes Setup auf dem Server #
# ============================================================ # Neuer, entkoppelter Ablauf (jeder Server macht sich selbst):
set -e # 1. Lokal: git add -A && git commit && git push origin main
# 2. Server: ssh root@<host> 'cd /opt/timemaster && ./server-update.sh'
SERVER="root@192.168.1.137" # 3. Frontend (falls geändert, lokal): ./push-frontend.sh <137|164>
REMOTE="/opt/timemaster" # =============================================================================
LOCAL="$(cd "$(dirname "$0")" && pwd)" echo "deploy.sh ist stillgelegt. Nutze: git push → server-update.sh (auf dem Server) → push-frontend.sh (lokal)." >&2
exit 1
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}==>${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
# ── Erstes Setup ──────────────────────────────────────────────────────────────
if [[ "$1" == "--setup" ]]; then
log "Erstes Server-Setup wird ausgeführt..."
ssh "$SERVER" "apt-get update -q && apt-get install -y \
python3.12 python3.12-venv python3.12-dev \
postgresql postgresql-contrib \
redis-server nginx \
git build-essential libpq-dev curl"
ssh "$SERVER" "mkdir -p $REMOTE/backend $REMOTE/frontend/dist"
log "Dateien übertragen..."
rsync -avz --exclude='__pycache__' --exclude='*.pyc' --exclude='.env' \
--exclude='venv/' --exclude='.git/' \
"$LOCAL/" "$SERVER:$REMOTE/"
ssh "$SERVER" bash << 'REMOTE_SETUP'
set -e
cd /opt/timemaster/backend
# venv
python3.12 -m venv venv
source venv/bin/activate
pip install --upgrade pip -q
pip install -r requirements.txt -q
# PostgreSQL
systemctl enable --now postgresql
sudo -u postgres psql -tc "SELECT 1 FROM pg_roles WHERE rolname='timemaster'" | grep -q 1 || \
sudo -u postgres psql -c "CREATE ROLE timemaster LOGIN PASSWORD 'timemaster_secret_CHANGE_ME';"
sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='timemaster_db'" | grep -q 1 || \
sudo -u postgres psql -c "CREATE DATABASE timemaster_db OWNER timemaster;"
# Redis
systemctl enable --now redis-server
# .env
if [ ! -f .env ]; then
cp .env.example .env
echo ""
echo "⚠️ WICHTIG: .env auf dem Server anpassen!"
echo " ssh root@192.168.1.137 'nano /opt/timemaster/backend/.env'"
fi
# systemd service
cp /opt/timemaster/timemaster.service /etc/systemd/system/
systemctl daemon-reload
echo "✓ Setup abgeschlossen"
echo "Nächster Schritt: .env befüllen, dann: ./deploy.sh --migrate"
REMOTE_SETUP
exit 0
fi
# ── Normaler Deploy ───────────────────────────────────────────────────────────
log "Sync → $SERVER:$REMOTE/backend"
rsync -avz --progress \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='.env' \
--exclude='venv/' \
--exclude='.git/' \
--exclude='tests/' \
"$LOCAL/backend/" "$SERVER:$REMOTE/backend/"
# Frontend build + sync (falls vorhanden)
if [ -d "$LOCAL/frontend/dist" ]; then
log "Frontend sync..."
rsync -avz "$LOCAL/frontend/dist/" "$SERVER:$REMOTE/frontend/dist/"
fi
# ── Migration (optional) ──────────────────────────────────────────────────────
if [[ "$1" == "--migrate" ]]; then
log "Alembic upgrade head..."
ssh "$SERVER" "cd $REMOTE/backend && source venv/bin/activate && alembic upgrade head"
fi
# ── Service neu starten ───────────────────────────────────────────────────────
log "Service restart..."
ssh "$SERVER" "systemctl restart timemaster && sleep 2 && systemctl is-active timemaster"
log "Logs (letzte 20 Zeilen):"
ssh "$SERVER" "journalctl -u timemaster -n 20 --no-pager"
log "✓ Deploy abgeschlossen → http://192.168.1.137:8000/health"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# =============================================================================
# push-frontend.sh LOKAL auf dem Dev-Rechner ausführen
#
# Baut das Frontend und liefert die dist/ an GENAU EINEN Server.
# Nötig, weil auf den Servern kein Node läuft und frontend/dist gitignored ist
# (server-update.sh kann das Frontend also nicht selbst bauen).
#
# Usage:
# ./push-frontend.sh 137 # → root@192.168.1.137
# ./push-frontend.sh 164 # → root@192.168.1.164
# ./push-frontend.sh 137 --no-build # bestehendes dist/ nur syncen
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
REMOTE_PATH="/opt/timemaster"
GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
log() { echo -e "${GREEN}==>${NC} $*"; }
die() { echo -e "${RED}[FEHLER]${NC} $*" >&2; exit 1; }
TARGET="${1:-}"
case "$TARGET" in
137) SERVER="root@192.168.1.137" ;;
164) SERVER="root@192.168.1.164" ;;
*) die "Server angeben: 137 oder 164 (z.B. ./push-frontend.sh 137)" ;;
esac
# Schutz: nicht versehentlich auf dem Zielserver ausführen
for ip in 192.168.1.137 192.168.1.164; do
[[ " $(hostname -I 2>/dev/null) " == *" $ip "* ]] && die "Läuft auf einem Zielserver nur lokal auf dem Dev-Rechner ausführen."
done
DO_BUILD=true
[[ "${2:-}" == "--no-build" ]] && DO_BUILD=false
if $DO_BUILD; then
log "npm run build"
( cd "$FRONTEND_DIR" && npm run build ) || die "Frontend-Build fehlgeschlagen."
fi
[[ -f "$FRONTEND_DIR/dist/index.html" ]] || die "dist/index.html fehlt erst bauen."
log "rsync dist/ → $SERVER:$REMOTE_PATH/frontend/dist/"
rsync -az --delete "$FRONTEND_DIR/dist/" "$SERVER:$REMOTE_PATH/frontend/dist/" \
|| die "rsync fehlgeschlagen (SSH-Key/Host-Key prüfen)."
log "✓ Frontend auf $TARGET aktualisiert"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# =============================================================================
# server-update.sh AUF dem jeweiligen Server ausführen (nicht lokal!)
#
# Aktualisiert GENAU DIESEN Server eigenständig aus dem Gitea-Repo:
# git pull → alembic upgrade head → service restart → health-check
#
# Jeder Server macht sich selbst kein Cross-Server-SSH, kein "both".
# 137 und 164 sind komplett entkoppelt.
#
# Frontend: wird NICHT hier gebaut (kein Node auf den Servern, dist ist
# gitignored). Frontend liefert der Dev-Rechner separat per push-frontend.sh.
#
# Usage (auf dem Server):
# cd /opt/timemaster && ./server-update.sh
# ./server-update.sh --no-migrate # nur pull + restart
# =============================================================================
set -euo pipefail
REMOTE="/opt/timemaster"
SERVICE="timemaster"
HEALTH_URL="http://localhost:8000/health"
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'
log() { echo -e "${GREEN}==>${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
die() { echo -e "${RED}[FEHLER]${NC} $*" >&2; exit 1; }
DO_MIGRATE=true
[[ "${1:-}" == "--no-migrate" ]] && DO_MIGRATE=false
# ── Sicherstellen, dass wir wirklich auf einem Server sind ────────────────────
[[ -d "$REMOTE/backend/venv" ]] || die "venv fehlt ($REMOTE/backend/venv) falscher Host? Dieses Skript läuft AUF dem Server."
cd "$REMOTE"
git config --global --add safe.directory "$REMOTE" 2>/dev/null || true
# ── 1. Repo aktualisieren ─────────────────────────────────────────────────────
log "git pull --ff-only origin main"
if ! git pull --ff-only origin main; then
die "git pull fehlgeschlagen. Arbeitsbaum sauber? ('git status' prüfen, ggf. 'git stash')."
fi
log "Jetzt auf: $(git log --oneline -1)"
# ── 2. Migration ──────────────────────────────────────────────────────────────
if $DO_MIGRATE; then
log "alembic upgrade head"
( cd "$REMOTE/backend" && source venv/bin/activate && alembic upgrade head ) \
|| die "Migration fehlgeschlagen Service wird NICHT neugestartet (Schema evtl. inkonsistent)."
else
warn "Migration übersprungen (--no-migrate)"
fi
# ── 3. Service neustarten ─────────────────────────────────────────────────────
log "systemctl restart $SERVICE"
systemctl restart "$SERVICE"
sleep 2
state="$(systemctl is-active "$SERVICE" || true)"
[[ "$state" == "active" ]] || { journalctl -u "$SERVICE" -n 30 --no-pager; die "Service nicht aktiv ($state)."; }
log "Service aktiv"
# ── 4. Health-Check ───────────────────────────────────────────────────────────
if curl -sf --max-time 10 "$HEALTH_URL" >/dev/null; then
log "Health OK ($HEALTH_URL)"
else
warn "Health-Check fehlgeschlagen Logs prüfen: journalctl -u $SERVICE -n 50"
fi
log "✓ Update abgeschlossen auf $(hostname)"
+11 -549
View File
@@ -1,554 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ============================================================================= # =============================================================================
# TimeMaster Deployment & Update Script # STILLGELEGT nicht mehr verwenden.
# Lokal ausführen verbindet sich per SSH auf Produktivserver
# #
# Usage: # Alte Variante: lokal starten, SSHt in BEIDE Server ("both") und deployt sie
# ./update.sh [OPTIONS] # gekoppelt. Nicht gewünscht 137 und 164 sollen entkoppelt sein, jeder Server
# aktualisiert sich selbst.
# #
# Options: # Neuer, entkoppelter Ablauf:
# --no-tests Tests überspringen # 1. Lokal: git add -A && git commit && git push origin main
# --no-frontend Frontend-Build und -Sync überspringen # 2. Server: ssh root@<host> 'cd /opt/timemaster && ./server-update.sh'
# --server 137 Nur Server 192.168.1.137 # (jeder Server einzeln, unabhängig kein Cross-Server-SSH)
# --server 164 Nur Server 192.168.1.164 # 3. Frontend (falls geändert, lokal): ./push-frontend.sh <137|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
# ============================================================================= # =============================================================================
echo "update.sh ist stillgelegt. Nutze: git push → server-update.sh (auf dem Server) → push-frontend.sh (lokal)." >&2
set -euo pipefail exit 1
# =============================================================================
# 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