Files
betreuungs-cockpit/scripts/update.sh
T
Florian Wilde 42eb242767 feat: Update-Skript, das die vergessenen Schritte mitnimmt
Bisher war Aktualisieren eine Folge von Befehlen aus der Anleitung, und
zwei davon vergisst man zuverlaessig: die Sicherung vorher und die beiden
SQL-Skripte hinterher. Genau die hat es hier schon gekostet.

./scripts/update.sh macht das in einem Durchgang - mit zwei
Eigenschaften, auf die es ankommt:

Bis zur Rueckfrage wird nichts veraendert. Der erste Entwurf hat die
eigenen Aenderungen beiseite gelegt und danach am Serverzugriff
abgebrochen - Halbzustand, aus dem man erst wieder herausfinden muss.
Jetzt laufen Pruefung, Abgleich und Changelog-Anzeige vollstaendig
durch, bevor irgendetwas angefasst wird.

Eigene Aenderungen an versionierten Dateien blockieren nicht. Im Betrieb
passt man die docker-compose.yml an, bis etwas laeuft; genau diese
Anpassungen sind spaeter oft schon eingearbeitet. Das Skript legt sie mit
git stash beiseite - rueckholbar, nicht weggeworfen - und sagt, wie man
sie zurueckbekommt. Wer sie wirklich loswerden will, nimmt --verwerfen.
Die .env bleibt immer unangetastet.

Schlaegt die Sicherung fehl, wird nicht aktualisiert. Schlaegt der Bau
fehl, nennt das Skript den Weg zurueck.

Nebenbei behoben: Der Port der MinIO-Konsole stand fest auf 9001. Lief
dort schon etwas, startete der ganze Stapel nicht. Jetzt ueber
MINIO_KONSOLE_PORT einstellbar - dieselbe Moeglichkeit, die APP_PORT
schon hatte. Aufgefallen beim Aufsetzen einer Testumgebung.

Etappe: 8
Phase: Haertung
2026-08-22 15:09:01 +02:00

357 lines
12 KiB
Bash

#!/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/tty || antwort=""
case "$antwort" in
[jJ]|[jJ][aA]) ;;
*) printf '\n'; warnung "Abgebrochen, nichts geaendert."; exit 0 ;;
esac
fi
# --- 5. Sicherung ------------------------------------------------------------
if [ "$OHNE_SICHERUNG" = "1" ]; then
schritt "Sicherung wird uebersprungen"
warnung "Auf eigene Gefahr - eine Migration laesst sich nicht zurueckrollen."
else
schritt "Sicherung"
if docker compose ps --status running --services 2>/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/tty || weiter=""
case "$weiter" in [jJ]|[jJ][aA]) ;; *) abbruch "Abgebrochen." ;; esac
fi
fi
fi
# --- 6. Umschalten und bauen -------------------------------------------------
# Aenderungen an versionierten Dateien blockieren das Umschalten.
#
# Im Betrieb ist das der Normalfall: Man passt die docker-compose.yml an,
# bis etwas laeuft. Genau diese Anpassungen sind spaeter oft schon
# eingearbeitet - das Skript soll sie also nicht zum Hindernis machen.
#
# Verworfen wird trotzdem nichts stillschweigend. Standard ist "beiseite
# legen": git stash macht es rueckholbar. Wer sie wirklich weghaben will,
# nimmt --verwerfen.
#
# Die .env ist nie betroffen - sie ist nicht versioniert.
schritt "Eigene Aenderungen"
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
printf '\n Diese versionierten Dateien sind veraendert:\n\n'
git status --short --untracked-files=no | sed 's/^/ /'
printf '\n'
if [ "$VERWERFEN" = "1" ]; then
ENTSCHEIDUNG="verwerfen"
elif [ "$OHNE_RUECKFRAGE" = "1" ]; then
# Ohne jemanden zum Fragen: die sichere Variante.
ENTSCHEIDUNG="beiseite"
else
printf ' %s[b]%s eiseite legen (git stash, spaeter rueckholbar)\n' "$FETT" "$AUS"
printf ' %s[v]%s erwerfen (endgueltig weg)\n' "$FETT" "$AUS"
printf ' %s[a]%s bbrechen\n\n' "$FETT" "$AUS"
printf ' Was soll damit geschehen? [b/v/a] '
read -r wahl </dev/tty || wahl=""
case "$wahl" in
[bB]|'') ENTSCHEIDUNG="beiseite" ;;
[vV]) ENTSCHEIDUNG="verwerfen" ;;
*) abbruch "Abgebrochen, nichts geaendert." ;;
esac
fi
if [ "$ENTSCHEIDUNG" = "verwerfen" ]; then
git checkout -- .
warnung "Aenderungen verworfen."
else
STASH_NAME="vor Update $(date +%Y-%m-%d_%H%M)"
git stash push --quiet --message "$STASH_NAME" -- . \
|| abbruch "Konnte die Aenderungen nicht beiseite legen."
gut "Beiseite gelegt als: $STASH_NAME"
printf ' Ansehen: git stash list\n'
printf ' Zurueck: git stash pop\n'
printf ' Wegwerfen: git stash drop\n'
fi
else
gut "Arbeitskopie ist sauber"
fi
schritt "Umschalten auf $ZIEL"
git checkout --quiet "$ZIEL"
gut "$(git describe --tags --always)"
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
}