#!/usr/bin/env bash # # Betreuungs-Cockpit aktualisieren. # # ./scripts/update.sh auf die neueste veroeffentlichte Fassung # ./scripts/update.sh v1.2.0 auf eine bestimmte Fassung # ./scripts/update.sh --ja ohne Rueckfrage (fuer cron) # ./scripts/update.sh --pruefen nur nachsehen, nichts aendern # ./scripts/update.sh --verwerfen eigene Aenderungen an versionierten # Dateien wegwerfen statt beiseitelegen # # Was das Skript tut, in dieser Reihenfolge: # # 1. Voraussetzungen pruefen (Verzeichnis, git, docker, .env) # 2. Fassungen holen und vergleichen # 3. Zeigen, was sich aendert (Changelog-Abschnitte dazwischen) # 4. Nachfragen # 5. SICHERUNG anstossen - und abbrechen, wenn sie nicht klappt # 6. Eigene Aenderungen beiseitelegen, umschalten, neu bauen, starten # 7. Die beiden SQL-Skripte ausfuehren (sie sind idempotent) # 8. Pruefen, ob die neue Fassung wirklich laeuft # # Die Reihenfolge ist Absicht: Bis einschliesslich Schritt 4 wird nichts # veraendert. Wer vorher abbricht - oder wobei etwas schiefgeht -, findet # alles so vor wie zuvor. Erst danach wird angefasst. # # Schritt 5 ist der wichtigste: Eine Datenbank-Migration laesst sich nicht # zurueckrollen. Ohne Sicherung wird hier nichts aktualisiert. # # Schritt 7 ebenfalls: Die SQL-Skripte laufen NICHT mit den Migrationen # mit. Wer sie vergisst, hat einen veralteten Trigger und merkt es erst, # wenn etwas nicht geht. # # Die .env wird nie angefasst - sie ist nicht versioniert. # Die geschweiften Klammern sorgen dafuer, dass bash die ganze Datei liest, # bevor sie irgendetwas ausfuehrt. Ohne sie koennte der git-checkout weiter # unten das Skript unter den eigenen Fuessen austauschen. { set -euo pipefail # --- Ausgabe ----------------------------------------------------------------- if [ -t 1 ]; then ROT=$'\033[31m'; GRUEN=$'\033[32m'; GELB=$'\033[33m'; FETT=$'\033[1m'; AUS=$'\033[0m' else ROT=''; GRUEN=''; GELB=''; FETT=''; AUS='' fi schritt() { printf '\n%s==> %s%s\n' "$FETT" "$1" "$AUS"; } gut() { printf ' %s%s%s\n' "$GRUEN" "$1" "$AUS"; } warnung() { printf ' %s%s%s\n' "$GELB" "$1" "$AUS"; } abbruch() { printf '\n%sAbbruch: %s%s\n\n' "$ROT" "$1" "$AUS" >&2; exit 1; } # --- Aufrufparameter --------------------------------------------------------- ZIEL="" OHNE_RUECKFRAGE=0 NUR_PRUEFEN=0 OHNE_SICHERUNG=0 VERWERFEN=0 ENTSCHEIDUNG="" for arg in "$@"; do case "$arg" in --ja|-j) OHNE_RUECKFRAGE=1 ;; --pruefen|-p) NUR_PRUEFEN=1 ;; --ohne-sicherung) OHNE_SICHERUNG=1 ;; --verwerfen) VERWERFEN=1 ;; --hilfe|-h) sed -n '2,34p' "$0" | sed 's/^#\s\?//' exit 0 ;; -*) abbruch "Unbekannte Angabe: $arg (--hilfe zeigt die Moeglichkeiten)" ;; *) ZIEL="$arg" ;; esac done # --- 1. Voraussetzungen ------------------------------------------------------ schritt "Voraussetzungen" cd "$(dirname "$0")/.." || abbruch "Das Projektverzeichnis ist nicht erreichbar." PROJEKT="$(pwd)" [ -f docker-compose.yml ] || abbruch "Hier liegt keine docker-compose.yml: $PROJEKT" [ -f .env ] || abbruch "Es gibt keine .env. Ohne sie startet die Anwendung nicht." command -v git >/dev/null || abbruch "git ist nicht installiert." docker compose version >/dev/null 2>&1 || abbruch "docker compose (v2) ist nicht verfuegbar." gut "Verzeichnis: $PROJEKT" # Nur nachsehen - angefasst wird die Arbeitskopie erst kurz vor dem # Umschalten, wenn Sicherung und Rueckfrage durch sind. Ein Abbruch # dazwischen darf keinen Halbzustand hinterlassen. SCHMUTZIG="" if [ -n "$(git status --porcelain --untracked-files=no)" ]; then SCHMUTZIG="ja" warnung "Es gibt eigene Aenderungen an versionierten Dateien." git status --short --untracked-files=no | sed 's/^/ /' printf ' Sie werden vor dem Umschalten beiseite gelegt.\n' else gut "Arbeitskopie ist sauber" fi # --- 2. Was ist da, was kommt ------------------------------------------------ schritt "Fassungen" if ! git fetch --tags --quiet origin; then abbruch "Konnte die Fassungen nicht vom Server holen. Erreichst du ihn? Stimmen die Zugangsdaten? Es wurde nichts geaendert." fi JETZT="$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)" NEUESTE="$(git tag --list 'v*' --sort=-v:refname | head -1)" [ -n "$NEUESTE" ] || abbruch "Es gibt keine veroeffentlichte Fassung (kein Tag)." if [ -z "$ZIEL" ]; then ZIEL="$NEUESTE" fi git rev-parse --verify --quiet "$ZIEL^{commit}" >/dev/null \ || abbruch "Die Fassung '$ZIEL' gibt es nicht. Vorhanden: $(git tag --list 'v*' --sort=-v:refname | head -5 | sed 's/^/ /')" printf ' Jetzt: %s\n' "$JETZT" printf ' Ziel: %s%s%s\n' "$FETT" "$ZIEL" "$AUS" if [ "$(git rev-parse HEAD)" = "$(git rev-parse "$ZIEL^{commit}")" ]; then gut "Bereits auf dem gewuenschten Stand - nichts zu tun." exit 0 fi # --- 3. Was aendert sich ----------------------------------------------------- schritt "Was sich aendert" # Der Changelog muss aus der ZIELfassung kommen: Der laufende Stand kennt # die neuen Abschnitte ja noch gar nicht. CHANGELOG="$(git show "$ZIEL:CHANGELOG.md" 2>/dev/null || true)" if [ -n "$CHANGELOG" ]; then # Alle Abschnitte zwischen dem laufenden und dem gewuenschten Stand. JETZT_NR="${JETZT#v}" printf '%s ' "$CHANGELOG" | awk -v jetzt="$JETZT_NR" ' /^## \[/ { nummer = $0 sub(/^## \[/, "", nummer); sub(/\].*/, "", nummer) if (nummer == jetzt) exit drucken = (nummer != "Unveröffentlicht") } drucken { print " " $0 } ' | head -60 else warnung "Kein CHANGELOG.md in $ZIEL gefunden." fi printf '\n' git log --oneline --no-decorate "HEAD..$ZIEL" | head -15 | sed 's/^/ /' if [ "$NUR_PRUEFEN" = "1" ]; then printf '\n' gut "Nur geprueft - es wurde nichts geaendert." [ -n "$SCHMUTZIG" ] && warnung "Beim echten Lauf wuerden deine Aenderungen beiseite gelegt." exit 0 fi # --- 4. Nachfragen ----------------------------------------------------------- if [ "$OHNE_RUECKFRAGE" != "1" ]; then printf '\n%sJetzt aktualisieren auf %s? [j/N] %s' "$FETT" "$ZIEL" "$AUS" read -r antwort /dev/null | grep -qx sicherung; then if docker compose exec -T sicherung /bin/sh /sicherung.sh; then gut "Sicherung angelegt und selbst geprueft." else abbruch "Die Sicherung ist fehlgeschlagen. Es wird nichts aktualisiert. Mit --ohne-sicherung laesst sich das uebergehen - aber ueberleg dir das zweimal." fi else warnung "Der Dienst 'sicherung' laeuft nicht - es wurde nichts gesichert." if [ "$OHNE_RUECKFRAGE" != "1" ]; then printf '\n %sTrotzdem weitermachen? [j/N] %s' "$FETT" "$AUS" read -r weiter /dev/null; then IM_WEG="$(git checkout "$ZIEL" 2>&1 | sed -n '/would be overwritten/,/^Aborting/p' | grep -E '^\s' | tr -d ' ' | grep -v '^$' || true)" if [ -n "$IM_WEG" ]; then ZEIT="$(date +%Y%m%d-%H%M%S)" printf ' Diese unversionierten Dateien stehen im Weg: ' for datei in $IM_WEG; do [ -e "$datei" ] || continue mv "$datei" "${datei}.vor-update-${ZEIT}" printf ' %s -> %s.vor-update-%s ' "$datei" "$datei" "$ZEIT" done git checkout --quiet "$ZIEL" || abbruch "Das Umschalten auf $ZIEL ist fehlgeschlagen. Deine Aenderungen liegen im Stash: git stash list" else abbruch "Das Umschalten auf $ZIEL ist fehlgeschlagen. $([ -n "$SCHMUTZIG" ] && printf ' Deine Aenderungen liegen im Stash: git stash list')" fi fi gut "$(git describe --tags --always)" # Das Ausfuehrbarkeitsbit kommt normalerweise aus dem Repository mit. Wer # Dateien von Hand kopiert hat - etwa um an dieses Skript zu kommen, bevor # es in der laufenden Fassung existierte -, hat es verloren. Dann waere der # naechste Aufruf ein "Permission denied", und man sucht an der falschen # Stelle. chmod +x scripts/*.sh 2>/dev/null || true schritt "Bauen und starten" printf ' Das dauert beim ersten Mal einige Minuten.\n\n' if ! GIT_COMMIT="$(git rev-parse --short HEAD)" \ BUILD_ZEIT="$(date -Iseconds)" \ docker compose up -d --build; then printf '\n' abbruch "Bauen oder Starten ist fehlgeschlagen. Die Quellen stehen jetzt auf $ZIEL, die Anwendung laeuft aber noch mit dem alten Abbild - oder gar nicht. Zurueck auf den vorherigen Stand: git checkout $JETZT && docker compose up -d --build $([ -n "$SCHMUTZIG" ] && printf '\n Deine Aenderungen liegen im Stash: git stash list')" fi # --- 7. Warten, bis die Anwendung antwortet ---------------------------------- schritt "Warten auf die Anwendung" # Adresse und Port stehen in der .env - Standard ist die Loopback-Adresse. ADRESSE="$(grep -E '^APP_BIND=' .env | tail -1 | cut -d= -f2- | tr -d '"' || true)" PORT="$(grep -E '^APP_PORT=' .env | tail -1 | cut -d= -f2- | tr -d '"' || true)" [ -n "$ADRESSE" ] || ADRESSE="127.0.0.1" [ -n "$PORT" ] || PORT="3000" # Auf 0.0.0.0 kann man nicht zugreifen - dann ueber die Loopback-Adresse. [ "$ADRESSE" = "0.0.0.0" ] && ADRESSE="127.0.0.1" GESUND="http://${ADRESSE}:${PORT}/api/gesundheit" ANTWORT="" for _ in $(seq 1 60); do ANTWORT="$(curl -fsS --max-time 5 "$GESUND" 2>/dev/null || true)" case "$ANTWORT" in *'"bereit"'*) break ;; esac printf '.' sleep 3 done printf '\n' case "$ANTWORT" in *'"bereit"'*) gut "Die Anwendung antwortet." ;; *) printf '\n' docker compose logs app --tail 40 | sed 's/^/ /' abbruch "Die Anwendung antwortet nicht unter $GESUND. Die letzten Zeilen aus dem Log stehen oben. Zurueck geht es mit: git checkout $JETZT && docker compose up -d --build" ;; esac # --- 8. SQL-Skripte ---------------------------------------------------------- schritt "Volltextsuche und Audit-Schutz" printf ' Diese beiden laufen nicht mit den Migrationen mit.\n' printf ' Sie sind idempotent - mehrfaches Ausfuehren schadet nicht.\n\n' PRISMA="node node_modules/prisma/build/index.js" for skript in volltextsuche audit-schutz; do if docker compose exec -T app $PRISMA db execute \ --schema prisma/schema.prisma --file "prisma/sql/${skript}.sql" >/dev/null 2>&1; then gut "${skript}.sql ausgefuehrt" else warnung "${skript}.sql ist fehlgeschlagen - bitte von Hand nachholen:" printf ' docker compose exec app %s db execute \\\n' "$PRISMA" printf ' --schema prisma/schema.prisma --file prisma/sql/%s.sql\n' "$skript" fi done # --- 9. Nachschau ------------------------------------------------------------ schritt "Ergebnis" LAEUFT="$(printf '%s' "$ANTWORT" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')" ERWARTET="${ZIEL#v}" printf ' Erwartet: %s\n' "$ERWARTET" printf ' Laeuft: %s\n' "${LAEUFT:-unbekannt}" if [ "$LAEUFT" = "$ERWARTET" ]; then printf '\n' gut "Fertig. Version $ERWARTET laeuft." else printf '\n' warnung "Die laufende Fassung stimmt nicht mit der erwarteten ueberein." warnung "Vermutlich hat der Bau den alten Behaelter stehen lassen:" printf ' docker compose up -d --build --force-recreate app\n' fi printf '\n Zustand: docker compose ps\n' printf ' Log: docker compose logs app --tail 50\n' printf ' Zurueck: git checkout %s && docker compose up -d --build\n\n' "$JETZT" exit 0 }