Files
betreuungs-cockpit/scripts/update.sh
T
Florian Wilde 9603917a86 fix: Changelog aus der Zielfassung, blockierende Dateien beiseite
Zwei Fehler beim ersten Probelauf des Update-Skripts.

Der Changelog blieb leer. Das Skript las CHANGELOG.md aus der Arbeitskopie
- also aus dem LAUFENDEN Stand, der die neuen Abschnitte naturgemaess noch
nicht kennt. Jetzt kommt er ueber git show aus der Zielfassung.

Das Umschalten scheiterte an einer unversionierten Datei. Genau das
passiert beim Sprung auf die Fassung, die dieses Skript eingefuehrt hat:
Man holt es sich von Hand, und in der Zielfassung gehoert es dann dazu -
git weigert sich, eine unversionierte Datei zu ueberschreiben. Solche
Dateien werden jetzt nicht weggeworfen, sondern nach .vor-update-<zeit>
beiseite geschoben, danach laeuft das Umschalten durch.

Etappe: 8
Phase: Haertung
2026-08-22 15:10:40 +02:00

386 lines
14 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"
# 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/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"
# Eine unversionierte Datei kann im Weg stehen, wenn die Zielfassung sie
# fuehrt. Das passiert beim Update auf die Fassung, die dieses Skript
# eingefuehrt hat: Man hat es sich von Hand geholt, jetzt gehoert es dazu.
# Solche Dateien werden nicht weggeworfen, sondern beiseite geschoben.
if ! git checkout --quiet "$ZIEL" 2>/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)"
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
}