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.
393 lines
14 KiB
Bash
Executable File
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
|
|
}
|