#!/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" if [ -f CHANGELOG.md ]; then # Alle Abschnitte zwischen dem laufenden und dem gewuenschten Stand. ZIEL_NR="${ZIEL#v}" JETZT_NR="${JETZT#v}" awk -v ziel="$ZIEL_NR" -v jetzt="$JETZT_NR" ' /^## \[/ { nummer = $0 sub(/^## \[/, "", nummer); sub(/\].*/, "", nummer) drucken = (nummer != "Unveröffentlicht" && nummer != jetzt) if (nummer == jetzt) exit } drucken { print " " $0 } ' CHANGELOG.md | head -60 else warnung "Kein CHANGELOG.md 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 || 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 }