Files
betreuungs-cockpit/scripts/update.sh
T
Florian Wilde 38f4d8d91f fix: Skripte lagen ohne Ausfuehrungsrecht im Repository
Die Anleitung sagt "./scripts/update.sh" - auf Linux kam dort
"Permission denied". Die drei .sh-Dateien lagen mit Modus 644 statt 755
im Index.

Ursache ist der Entwicklungsrechner: Unter Windows steht
core.filemode=false, git nimmt ein fehlendes Ausfuehrungsrecht dort gar
nicht wahr. Gemeldet hat es der Betrieb, nicht der Bau.

Wer von 1.1.0 oder aelter kommt, braucht einmalig "bash
scripts/update.sh" - das alte Skript ist ja noch das nicht ausfuehrbare.
Steht jetzt in README und Installationsanleitung.

Zusaetzlich setzt update.sh nach dem Umschalten selbst ein chmod +x auf
scripts/*.sh. Das faengt den Fall ab, dass jemand Dateien von Hand
kopiert und das Recht dabei verliert - genau so kam man bisher an dieses
Skript, bevor es in der laufenden Fassung existierte.

Neu: tests/skripte.test.ts prueft nicht Verhalten, sondern Verpackung -
Ausfuehrungsrecht im Index, Shebang, scripts/ im Docker-Abbild, die
Sicherungsskripte in der Compose-Datei. Es ist der zweite Fehler dieser
Art (vorher fehlte scripts/ ganz im Abbild, 1.0.1); beide Male war der
Code richtig und kein Test sah hin, wie die Dateien beim Server
ankommen. Gegenprobe gemacht: mit zurueckgesetztem Recht schlaegt er
fehl und nennt die Datei.

Nebenbei aufgefallen: scripts/version.mjs datierte in UTC. Diese Fassung
waere um 00:08 deutscher Zeit auf den Vortag datiert worden. Rechnet
jetzt in Europe/Berlin.
2026-08-23 00:13:05 +02:00

393 lines
14 KiB
Bash
Executable File

#!/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)"
# 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
}