Steuerkram/scripts/update.sh

409 lines
19 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
# ════════════════════════════════════════════════════════════════════════════
# 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/9 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: Update-Skripte vorab aktualisieren
# ════════════════════════════════════════════════════════════════════════════
# Alle folgenden Schritte werden von Skripten in scripts/ erledigt (allen voran
# backup.sh). Liefe hier noch die alte Fassung, würde ein Update mit den Mitteln
# des Vorgänger-Stands abgesichert – genau so entstand das leere Datei-Backup
# nach dem Umzug der Belege aus dem Web-Root.
# Deshalb: Ändert sich irgendetwas unter scripts/, wird zuerst gepullt und das
# Skript in der neuen Fassung neu gestartet – noch vor dem Backup.
step "2/9 Update-Skripte prüfen"
if [[ "${STEUERKRAM_SELF_UPDATED:-0}" == "1" ]]; then
ok "Skripte sind auf dem neuesten Stand (Neustart bereits erfolgt)"
else
if git fetch --quiet 2>/dev/null && git rev-parse --abbrev-ref '@{u}' &>/dev/null; then
if git diff --quiet HEAD '@{u}' -- scripts/ 2>/dev/null; then
ok "Keine Änderungen an den Update-Skripten"
else
warn "Update-Skripte haben sich geändert – ziehe sie vorab und starte neu"
git diff --name-only HEAD '@{u}' -- scripts/ | while read -r line; do info " $line"; done
# Ausgangs-Commit für die spätere Rebuild-Erkennung sichern, sonst
# sähe der Neustart keine Änderungen mehr und würde den Rebuild
# überspringen.
export STEUERKRAM_BEFORE_COMMIT="$(git rev-parse HEAD)"
git pull --ff-only 2>&1 | while read -r line; do info "$line"; done
export STEUERKRAM_SELF_UPDATED=1
info "Starte update.sh in der aktualisierten Fassung neu..."
echo ""
exec "$SCRIPT_DIR/update.sh" "$@"
fi
else
warn "Kein Upstream-Branch erreichbar – überspringe Skript-Vorabprüfung"
fi
fi
# ════════════════════════════════════════════════════════════════════════════
# SCHRITT 3: Backup erstellen
# ════════════════════════════════════════════════════════════════════════════
step "3/9 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 4: git pull + Änderungen erkennen
# ════════════════════════════════════════════════════════════════════════════
step "4/9 Code aktualisieren (git pull)"
# Wurde in Schritt 2 bereits gepullt, gilt der dort gesicherte Ausgangsstand –
# sonst bliebe die Rebuild-Erkennung unten blind.
BEFORE_COMMIT="${STEUERKRAM_BEFORE_COMMIT:-$(git rev-parse HEAD)}"
BEFORE_SHORT=$(git rev-parse --short "$BEFORE_COMMIT")
# 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
# Ein Neustart wegen geänderter Skripte ist hier nicht mehr nötig –
# Schritt 2 hat scripts/ bereits vor dem Backup aktualisiert.
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 5: Docker-Image bauen (wenn nötig)
# ════════════════════════════════════════════════════════════════════════════
step "5/9 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 6: Container neu starten
# ════════════════════════════════════════════════════════════════════════════
step "6/9 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 7: Auf Betriebsbereitschaft warten
# ════════════════════════════════════════════════════════════════════════════
step "7/9 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 8: Migrationen explizit ausführen
# ════════════════════════════════════════════════════════════════════════════
step "8/9 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 9: Tests ausführen
# ════════════════════════════════════════════════════════════════════════════
step "9/9 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