#!/usr/bin/env bash # Aktualisiert den Planer auf eine neue Version. # Für Linux mit Docker Compose. Läuft im Projektverzeichnis. set -euo pipefail WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$WURZEL" ZIELVERSION="" OHNE_RUECKFRAGE=0 OHNE_BACKUP=0 NUR_PRUEFEN=0 WARTEZEIT=180 LOGVERZEICHNIS="$WURZEL/logs" LOGDATEI="$LOGVERZEICHNIS/update-$(date '+%Y-%m-%d_%H-%M-%S').log" hilfe() { cat <<'ENDE' Verwendung: update.sh [Optionen] --version vX.Y.Z Auf diese Version aktualisieren statt auf die neueste --ja Ohne Rückfrage durchlaufen --ohne-backup Keine Sicherung anlegen (nicht empfohlen) --nur-pruefen Nur zeigen, was passieren würde --wartezeit Sekunden, die auf einen gesunden Start gewartet wird (Standard: 180) --hilfe Diese Übersicht Ablauf: Voraussetzungen prüfen, Versionen ermitteln, Änderungen zeigen, sichern, Ziel-Tag auschecken, bauen, starten, auf /api/health warten. Schlägt etwas fehl, wird auf die vorherige Version zurückgerollt. ENDE } meldung() { printf '%s %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI"; } fehler() { printf '%s Fehler: %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI" >&2 exit 1 } while [ $# -gt 0 ]; do case "$1" in --version) ZIELVERSION="${2:-}" [ -n "$ZIELVERSION" ] || fehler '--version braucht eine Angabe wie v0.1.0.' shift 2 ;; --ja) OHNE_RUECKFRAGE=1; shift ;; --ohne-backup) OHNE_BACKUP=1; shift ;; --nur-pruefen) NUR_PRUEFEN=1; shift ;; --wartezeit) WARTEZEIT="${2:-}" case "$WARTEZEIT" in ''|*[!0-9]*) fehler '--wartezeit braucht eine ganze Zahl.' ;; esac shift 2 ;; --hilfe|-h) hilfe; exit 0 ;; *) fehler "Unbekannte Option '$1'. 'update.sh --hilfe' zeigt die Übersicht." ;; esac done mkdir -p "$LOGVERZEICHNIS" # --- 1. Voraussetzungen ---------------------------------------------------- meldung 'Voraussetzungen prüfen.' for werkzeug in git docker curl jq; do command -v "$werkzeug" >/dev/null 2>&1 || fehler "$werkzeug fehlt." done docker compose version >/dev/null 2>&1 || fehler 'docker compose fehlt.' [ -d "$WURZEL/.git" ] || fehler 'Kein Git-Arbeitsbaum – im Projektverzeichnis aufrufen.' [ -f "$WURZEL/.env" ] || fehler '.env fehlt.' if [ -n "$(git status --porcelain)" ]; then fehler 'Der Arbeitsbaum ist nicht sauber. Erst aufräumen oder committen.' fi APP_PORT="$(grep -E '^APP_PORT=' .env | tail -1 | cut -d= -f2- || true)" APP_PORT="${APP_PORT:-3000}" HEALTH_URL="http://127.0.0.1:${APP_PORT}/api/health" # --- 2. Versionen ermitteln ------------------------------------------------ meldung 'Versionen ermitteln.' INSTALLIERT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null | jq -r '.version // empty' || true)" if [ -z "$INSTALLIERT" ]; then INSTALLIERT="$(jq -r '.version' package.json)" meldung "Die laufende Instanz war nicht erreichbar; nehme package.json: $INSTALLIERT" else meldung "Installiert: $INSTALLIERT" fi VORHER_REF="$(git rev-parse --abbrev-ref HEAD)" [ "$VORHER_REF" != 'HEAD' ] || VORHER_REF="$(git rev-parse HEAD)" git fetch --tags --quiet origin || fehler 'git fetch ist fehlgeschlagen.' if [ -z "$ZIELVERSION" ]; then ZIELVERSION="$(git tag --list 'v*' --sort=-v:refname | head -1)" [ -n "$ZIELVERSION" ] || fehler 'Es gibt keinen Versions-Tag auf origin.' fi git rev-parse -q --verify "refs/tags/${ZIELVERSION}" >/dev/null \ || fehler "Den Tag $ZIELVERSION gibt es nicht." meldung "Ziel: $ZIELVERSION" if [ "v$INSTALLIERT" = "$ZIELVERSION" ]; then meldung 'Die Zielversion läuft bereits. Nichts zu tun.' exit 0 fi # --- 3. Änderungen zeigen -------------------------------------------------- if [ -f CHANGELOG.md ]; then meldung "Änderungen bis $ZIELVERSION laut CHANGELOG.md:" # Vom Abschnitt der Zielversion bis zum Abschnitt der installierten Version. awk -v ziel="${ZIELVERSION#v}" -v alt="$INSTALLIERT" ' $0 ~ "^## \\[" ziel "\\]" { zeigen = 1 } zeigen && $0 ~ "^## \\[" alt "\\]" { exit } zeigen { print " " $0 } ' CHANGELOG.md | tee -a "$LOGDATEI" fi if [ "$NUR_PRUEFEN" -eq 1 ]; then meldung "Nur geprüft: $INSTALLIERT würde auf ${ZIELVERSION#v} aktualisiert." exit 0 fi if [ "$OHNE_RUECKFRAGE" -eq 0 ]; then printf 'Jetzt auf %s aktualisieren? [j/N] ' "$ZIELVERSION" read -r antwort case "$antwort" in [jJ]|[jJ][aA]) ;; *) meldung 'Abgebrochen.'; exit 0 ;; esac fi # --- 4. Sicherung ---------------------------------------------------------- SICHERUNG='' if [ "$OHNE_BACKUP" -eq 0 ]; then meldung 'Sicherung anlegen.' ./backup.sh --tage 30 | tee -a "$LOGDATEI" SICHERUNG="$(find "$WURZEL/sicherungen" -maxdepth 1 -name 'planer-db-*.dump' -print0 2>/dev/null \ | xargs -0 ls -t 2>/dev/null | head -1 || true)" else meldung 'Sicherung übersprungen (--ohne-backup).' fi zurueckrollen() { meldung "Zurückrollen auf $VORHER_REF." git checkout --quiet "$VORHER_REF" || meldung 'Das Auschecken der alten Version schlug fehl.' docker compose build >>"$LOGDATEI" 2>&1 || meldung 'Der Neubau der alten Version schlug fehl.' docker compose up -d >>"$LOGDATEI" 2>&1 || meldung 'Der Start der alten Version schlug fehl.' if [ -n "$SICHERUNG" ]; then meldung "Die Datenbank wurde NICHT zurückgespielt. Sicherung: $SICHERUNG" meldung 'Wiederherstellen mit:' meldung " docker compose exec -T db pg_restore -U planer -d planer --clean --if-exists < \"$SICHERUNG\"" fi meldung "Vollständiges Protokoll: $LOGDATEI" } # --- 5. Auschecken, bauen, starten ---------------------------------------- meldung "Auf $ZIELVERSION wechseln." if ! git checkout --quiet "$ZIELVERSION"; then fehler "Der Wechsel auf $ZIELVERSION schlug fehl." fi meldung 'Abbild bauen.' if ! docker compose build >>"$LOGDATEI" 2>&1; then meldung 'Der Bau ist fehlgeschlagen.' zurueckrollen exit 1 fi meldung 'Dienste starten.' if ! docker compose up -d >>"$LOGDATEI" 2>&1; then meldung 'Der Start ist fehlgeschlagen.' zurueckrollen exit 1 fi # --- 6. Auf Gesundheit warten --------------------------------------------- meldung "Auf $HEALTH_URL warten (bis zu ${WARTEZEIT}s)." ERWARTET="${ZIELVERSION#v}" VERGANGEN=0 GESUND=0 while [ "$VERGANGEN" -lt "$WARTEZEIT" ]; do ANTWORT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null || true)" if [ -n "$ANTWORT" ]; then STATUS="$(printf '%s' "$ANTWORT" | jq -r '.status // empty')" LAEUFT="$(printf '%s' "$ANTWORT" | jq -r '.version // empty')" if [ "$STATUS" = 'ok' ] && [ "$LAEUFT" = "$ERWARTET" ]; then GESUND=1 break fi fi sleep 5 VERGANGEN=$((VERGANGEN + 5)) done # --- 7. Ergebnis ----------------------------------------------------------- if [ "$GESUND" -eq 0 ]; then meldung "Die neue Version meldet sich nicht gesund mit Version $ERWARTET." docker compose logs --tail 50 app >>"$LOGDATEI" 2>&1 || true zurueckrollen exit 1 fi meldung "Fertig. Der Planer läuft in Version $ERWARTET." meldung "Protokoll: $LOGDATEI"