#!/usr/bin/env bash # ════════════════════════════════════════════════════════════════════════════ # Steuerkram – Update-Script # # Spielt alle Arten von Änderungen vollständig ein: # • git pull # • Backup (vor jeder Änderung) # • Composer-Abhängigkeiten # • Docker-Image-Rebuild (bei Änderungen an Dockerfile, composer.json, etc.) # • Datenbank-Migrationen # • Berechtigungen und Upload-Verzeichnisse # • Optionale Testausführung # # Usage: # ./update.sh [--no-backup] [--no-tests] [--force-rebuild] # ════════════════════════════════════════════════════════════════════════════ set -euo pipefail # ── Optionen ───────────────────────────────────────────────────────────────── OPT_NO_BACKUP=false OPT_NO_TESTS=false OPT_FORCE_REBUILD=false for arg in "$@"; do case "$arg" in --no-backup) OPT_NO_BACKUP=true ;; --no-tests) OPT_NO_TESTS=true ;; --force-rebuild) OPT_FORCE_REBUILD=true ;; --help|-h) echo "Usage: $0 [--no-backup] [--no-tests] [--force-rebuild]" exit 0 ;; *) echo "Unbekannte Option: $arg" >&2 echo "Usage: $0 [--no-backup] [--no-tests] [--force-rebuild]" >&2 exit 1 ;; esac done # ── Pfade ──────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" cd "$PROJECT_DIR" # ── Farben ─────────────────────────────────────────────────────────────────── if [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' else RED='' GREEN='' YELLOW='' CYAN='' BOLD='' NC='' fi step() { echo -e "\n${BOLD}${CYAN}══ $* ${NC}"; } ok() { echo -e " ${GREEN}✓${NC} $*"; } warn() { echo -e " ${YELLOW}⚠${NC} $*"; } fail() { echo -e " ${RED}✗${NC} $*" >&2; } info() { echo -e " $*"; } # ── Fehlerbehandlung ───────────────────────────────────────────────────────── UPDATE_FAILED=false on_error() { local exit_code=$? local line=$1 fail "Fehler in Zeile $line (Exit-Code: $exit_code)" UPDATE_FAILED=true } trap 'on_error $LINENO' ERR # ── Startzeit ──────────────────────────────────────────────────────────────── START_TIME=$(date +%s) echo "" echo -e "${BOLD}════════════════════════════════════════════════════════${NC}" echo -e "${BOLD} Steuerkram Update – $(date '+%Y-%m-%d %H:%M:%S')${NC}" echo -e "${BOLD}════════════════════════════════════════════════════════${NC}" # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 1: Voraussetzungen prüfen # ════════════════════════════════════════════════════════════════════════════ step "1/8 Voraussetzungen prüfen" if ! command -v docker &>/dev/null; then fail "Docker ist nicht installiert oder nicht im PATH." exit 1 fi ok "Docker gefunden: $(docker --version | head -1)" if ! docker info &>/dev/null 2>&1; then fail "Docker-Daemon läuft nicht. Bitte starten: sudo systemctl start docker" exit 1 fi ok "Docker-Daemon erreichbar" if ! docker compose version &>/dev/null 2>&1; then fail "Docker Compose (Plugin) nicht verfügbar." exit 1 fi ok "Docker Compose gefunden: $(docker compose version)" if ! git -C "$PROJECT_DIR" rev-parse --git-dir &>/dev/null; then fail "Kein Git-Repository in $PROJECT_DIR" exit 1 fi ok "Git-Repository gefunden" # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 2: Backup erstellen # ════════════════════════════════════════════════════════════════════════════ step "2/8 Backup erstellen" if [[ "$OPT_NO_BACKUP" == "true" ]]; then warn "Backup übersprungen (--no-backup)" else # Prüfen ob Container läuft (Backup braucht laufende DB) if docker compose ps --status running db 2>/dev/null | grep -q "db"; then info "Erstelle Backup vor dem Update..." if bash "$PROJECT_DIR/scripts/backup.sh"; then ok "Backup erfolgreich erstellt" else warn "Backup fehlgeschlagen – Update wird trotzdem fortgesetzt" warn "Prüfe den Backup-Fehler manuell: ./scripts/backup.sh" fi else warn "Datenbank-Container nicht aktiv – Backup übersprungen" warn "Beim ersten Start ist das normal." fi fi # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 3: git pull + Änderungen erkennen # ════════════════════════════════════════════════════════════════════════════ step "3/8 Code aktualisieren (git pull)" BEFORE_COMMIT=$(git rev-parse HEAD) BEFORE_SHORT=$(git rev-parse --short HEAD) # Lokale Änderungen prüfen if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then warn "Ungespeicherte lokale Änderungen vorhanden:" git status --short | head -20 | while read -r line; do info "$line"; done warn "Diese Änderungen bleiben erhalten (kein stash)." fi git pull --ff-only 2>&1 | while read -r line; do info "$line"; done AFTER_COMMIT=$(git rev-parse HEAD) AFTER_SHORT=$(git rev-parse --short HEAD) if [[ "$BEFORE_COMMIT" == "$AFTER_COMMIT" ]]; then ok "Bereits auf dem neuesten Stand ($AFTER_SHORT)" CHANGED_FILES=() else ok "Aktualisiert: $BEFORE_SHORT → $AFTER_SHORT" mapfile -t CHANGED_FILES < <(git diff --name-only "$BEFORE_COMMIT" "$AFTER_COMMIT" 2>/dev/null || true) info "Geänderte Dateien (${#CHANGED_FILES[@]}):" for f in "${CHANGED_FILES[@]}"; do info " $f"; done # Skript hat sich selbst geändert → neu starten damit die neue Logik läuft if printf '%s\n' "${CHANGED_FILES[@]}" | grep -q "^scripts/update\.sh$"; then warn "update.sh wurde aktualisiert – starte Skript neu..." exec "$SCRIPT_DIR/update.sh" --no-backup "$@" fi fi # Asset-Version aktualisieren (Cache-Buster für CSS/JS – immer, vor Docker-Restart) git rev-parse --short HEAD > "$PROJECT_DIR/public/static/.asset-version" ok "Asset-Version gesetzt: $(cat "$PROJECT_DIR/public/static/.asset-version")" # ── Rebuild-Bedarf ermitteln ───────────────────────────────────────────────── NEEDS_REBUILD=false if [[ "$OPT_FORCE_REBUILD" == "true" ]]; then NEEDS_REBUILD=true info "Rebuild erzwungen (--force-rebuild)" fi # Dateien, die einen Rebuild des Docker-Images erfordern REBUILD_TRIGGERS=( "Dockerfile" "composer.json" "composer.lock" "php/" "apache/" "docker-entrypoint.sh" ) if [[ "$BEFORE_COMMIT" != "$AFTER_COMMIT" ]]; then for trigger in "${REBUILD_TRIGGERS[@]}"; do for changed in "${CHANGED_FILES[@]}"; do if [[ "$changed" == "$trigger" || "$changed" == ${trigger}* ]]; then NEEDS_REBUILD=true info "Rebuild ausgelöst durch: $changed" break 2 fi done done fi # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 4: Docker-Image bauen (wenn nötig) # ════════════════════════════════════════════════════════════════════════════ step "4/8 Docker-Image" if [[ "$NEEDS_REBUILD" == "true" ]]; then info "Baue Docker-Image neu..." docker compose build --no-cache web 2>&1 | while read -r line; do # Fortschrittszeilen komprimieren [[ "$line" =~ ^#[0-9]+ ]] && echo -n "." || info "$line" done echo "" ok "Image erfolgreich gebaut" else ok "Kein Rebuild nötig (keine relevanten Änderungen)" fi # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 5: Container neu starten # ════════════════════════════════════════════════════════════════════════════ step "5/8 Container neu starten" info "Stoppe laufende Container..." docker compose down --remove-orphans 2>&1 | while read -r line; do info "$line"; done info "Starte Container..." docker compose up -d 2>&1 | while read -r line; do info "$line"; done ok "Container gestartet" # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 6: Auf Betriebsbereitschaft warten # ════════════════════════════════════════════════════════════════════════════ step "6/8 Auf Betriebsbereitschaft warten" TIMEOUT=120 ELAPSED=0 INTERVAL=3 info "Warte auf Datenbank-Healthcheck (max. ${TIMEOUT}s)..." while true; do DB_STATUS=$(docker compose ps --format json db 2>/dev/null \ | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('Health',''))" 2>/dev/null \ || docker compose ps db 2>/dev/null | grep -oP '(healthy|starting|unhealthy)' | head -1 \ || echo "unknown") if [[ "$DB_STATUS" == "healthy" ]]; then ok "Datenbank ist bereit (${ELAPSED}s)" break fi if [[ $ELAPSED -ge $TIMEOUT ]]; then fail "Timeout: Datenbank nicht bereit nach ${TIMEOUT}s" fail "Status: $DB_STATUS" info "Logs: docker compose logs db" UPDATE_FAILED=true break fi echo -n "." sleep $INTERVAL ELAPSED=$((ELAPSED + INTERVAL)) done echo "" # Web-Container kurz warten (Entrypoint läuft noch) info "Warte auf Web-Container..." sleep 5 WEB_STATUS=$(docker compose ps --format json web 2>/dev/null \ | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('State',''))" 2>/dev/null \ || docker compose ps web 2>/dev/null | grep -oP '(running|exited|restarting)' | head -1 \ || echo "unknown") if [[ "$WEB_STATUS" == "running" ]]; then ok "Web-Container läuft" else warn "Web-Container Status: $WEB_STATUS" info "Logs der letzten 20 Zeilen:" docker compose logs --tail=20 web 2>/dev/null | while read -r line; do info "$line"; done fi # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 7: Migrationen explizit ausführen # ════════════════════════════════════════════════════════════════════════════ step "7/8 Autoloader + Datenbank-Migrationen" # Autoloader neu generieren: vendor/ liegt im Image (nicht gemountet), # neue Klassen in src/ wären sonst nach einem Pull ohne Rebuild unbekannt. info "Generiere Composer-Autoloader neu..." AUTOLOAD_OUTPUT=$(docker compose exec -T web composer dump-autoload --optimize --no-interaction 2>&1) || { warn "composer dump-autoload fehlgeschlagen – Autoloader möglicherweise veraltet" echo "$AUTOLOAD_OUTPUT" | while read -r line; do info "$line"; done } ok "Autoloader aktualisiert" # Der Entrypoint führt Migrationen bereits aus, aber wir führen sie # nochmals explizit aus um sicherzustellen, dass alle eingespielt sind # und um den Status im Log zu sehen. info "Führe Migrationen aus (idempotent)..." MIGRATE_OUTPUT=$(docker compose exec -T web php /var/www/html/scripts/migrate.php 2>&1) || { fail "Migrationen fehlgeschlagen" echo "$MIGRATE_OUTPUT" | while read -r line; do info "$line"; done UPDATE_FAILED=true } echo "$MIGRATE_OUTPUT" | while read -r line; do info "$line"; done if [[ "$UPDATE_FAILED" == "false" ]]; then ok "Migrationen erfolgreich" fi # ════════════════════════════════════════════════════════════════════════════ # SCHRITT 8: Tests ausführen # ════════════════════════════════════════════════════════════════════════════ step "8/8 Tests" if [[ "$OPT_NO_TESTS" == "true" ]]; then warn "Tests übersprungen (--no-tests)" else info "Führe Test-Suite aus..." if TEST_OUTPUT=$(docker compose exec -T web php /var/www/html/tests/run.php 2>&1); then TEST_EXIT=0 else TEST_EXIT=$? fi echo "$TEST_OUTPUT" | while read -r line; do info "$line"; done if [[ "$TEST_EXIT" -ne 0 ]]; then warn "Einige Tests sind fehlgeschlagen – bitte prüfen" warn "Vollständiger Testlauf: docker compose exec web php tests/run.php" else ok "Alle Tests bestanden" fi fi # ════════════════════════════════════════════════════════════════════════════ # ZUSAMMENFASSUNG # ════════════════════════════════════════════════════════════════════════════ END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) echo "" echo -e "${BOLD}════════════════════════════════════════════════════════${NC}" if [[ "$UPDATE_FAILED" == "true" ]]; then echo -e "${RED}${BOLD} Update mit Fehlern abgeschlossen (${DURATION}s)${NC}" echo -e "${BOLD}════════════════════════════════════════════════════════${NC}" echo "" echo " Diagnose:" echo " docker compose logs web" echo " docker compose logs db" echo " docker compose ps" echo "" exit 1 else echo -e "${GREEN}${BOLD} Update erfolgreich (${DURATION}s)${NC}" echo -e "${BOLD}════════════════════════════════════════════════════════${NC}" echo "" if [[ "$BEFORE_COMMIT" != "$AFTER_COMMIT" ]]; then echo -e " ${CYAN}$BEFORE_SHORT${NC} → ${GREEN}$AFTER_SHORT${NC}" else echo -e " Commit: ${GREEN}$AFTER_SHORT${NC} (keine neuen Commits)" fi echo "" echo " Anwendung: http://localhost:8080" echo "" fi