From 909f9e3ca35ca73795a32781209d0d4350c945b3 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Mon, 13 Apr 2026 16:01:00 +0200 Subject: [PATCH] Feat: update.sh Script + Doku aktualisiert --- docs/update-backup.md | 83 +++++----- update.sh | 352 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 387 insertions(+), 48 deletions(-) create mode 100755 update.sh diff --git a/docs/update-backup.md b/docs/update-backup.md index 3d7078a..6a500ca 100644 --- a/docs/update-backup.md +++ b/docs/update-backup.md @@ -36,37 +36,34 @@ Beide Pfade liegen außerhalb des Containers und bleiben bei `docker compose dow ## Update -Datenbankmigrationen werden **automatisch** beim nächsten Container-Start eingespielt. Das Script `scripts/migrate.php` prüft beim Start, welche Migrations-Dateien noch nicht angewendet wurden, und führt sie der Reihe nach aus. - -### Vollständige Update-Anleitung - -#### Schritt 1 – Datenbank sichern +Für ein vollständiges Update steht `update.sh` im Projektwurzelverzeichnis bereit. Es führt alle notwendigen Schritte automatisch aus. ```bash -bash scripts/backup.sh +./update.sh ``` -#### Schritt 2 – Aktuellen Code holen +Das Script erledigt in dieser Reihenfolge: -```bash -git pull -``` +1. **Voraussetzungen** – Docker, Docker Compose und Git werden geprüft +2. **Backup** – `scripts/backup.sh` wird vor jeder Änderung aufgerufen +3. **git pull** – Code wird aktualisiert; geänderte Dateien werden erkannt +4. **Image-Rebuild** – Das Docker-Image wird neu gebaut, wenn `Dockerfile`, `composer.json`, `composer.lock`, `php/`, `apache/` oder `docker-entrypoint.sh` geändert wurden +5. **Container-Neustart** – `docker compose down --remove-orphans` + `up -d` +6. **Healthcheck** – Wartet bis die Datenbank `healthy` meldet (max. 120 s) +7. **Migrationen** – `scripts/migrate.php` wird explizit ausgeführt (idempotent) +8. **Tests** – Die Test-Suite wird ausgeführt und das Ergebnis angezeigt -#### Schritt 3 – Container neu bauen und starten +### Optionen -```bash -docker compose up -d --build -``` +| Option | Bedeutung | +|---|---| +| `--no-backup` | Backup überspringen (z. B. wenn gerade erst gesichert) | +| `--no-tests` | Tests nicht ausführen (schnellerer Ablauf) | +| `--force-rebuild` | Docker-Image immer neu bauen, unabhängig von Dateiänderungen | -Dieser Befehl baut das PHP-Image neu (falls sich `Dockerfile` oder `composer.json` geändert haben) und startet alle Container. Der Web-Container führt beim Start automatisch `scripts/migrate.php` aus. +### Migrationen -#### Schritt 4 – Migrationen prüfen - -```bash -docker compose logs web | grep migrate -``` - -Erwartete Ausgabe: +Datenbankmigrationen werden von `scripts/migrate.php` verwaltet. Das Script prüft, welche Migrations-Dateien noch nicht angewendet wurden, und führt sie der Reihe nach aus. Es wird sowohl vom Container-Entrypoint als auch von `update.sh` aufgerufen. ``` [migrate] Starte Migrations-Prüfung... @@ -74,33 +71,23 @@ Erwartete Ausgabe: [migrate] N Migration(en) erfolgreich angewendet. ``` -Falls ein Fehler auftritt, das Backup aus Schritt 1 einspielen: +Migrationen können auch manuell ausgelöst werden: + +```bash +docker compose exec web php scripts/migrate.php +``` + +### Diagnose bei Fehlern + +```bash +docker compose ps # Container-Status +docker compose logs web # Web-Container-Log (PHP-Fehler, Migrationen) +docker compose logs db # Datenbank-Log +``` + +Falls ein Update fehlschlägt und ein Rollback nötig ist, das letzte Backup einspielen: ```bash docker compose exec -T db mariadb -u steuerkram -psteuerkram steuerkram \ - < ~/steuerkram_backup_YYYYMMDD_HHMM.sql -``` - -#### Schritt 5 – Anwendung prüfen - -```bash -docker compose ps # alle Container müssen "running" sein -docker compose logs web # keine PHP-Fehler oder Exceptions -``` - ---- - -### Kurzform ohne Systemd - -```bash -bash scripts/backup.sh && git pull && docker compose up -d --build -docker compose logs web | grep migrate -``` - -### Kurzform mit Systemd - -```bash -bash scripts/backup.sh && git pull && docker compose build -sudo systemctl restart steuerkram -journalctl -u steuerkram | grep migrate + < backups/steuerkram_db_YYYYMMDD_HHMM.sql ``` diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..ad693db --- /dev/null +++ b/update.sh @@ -0,0 +1,352 @@ +#!/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="$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 +fi + +# ── 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 Datenbank-Migrationen" + +# 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..." + TEST_OUTPUT=$(docker compose exec -T web php /var/www/html/tests/run.php 2>&1) || true + TEST_EXIT=${PIPESTATUS[0]:-0} + + echo "$TEST_OUTPUT" | while read -r line; do info "$line"; done + + if echo "$TEST_OUTPUT" | grep -q "FEHLER\|FAILED\|fehlgeschlagen\|failed"; 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