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
386 lines
14 KiB
Bash
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
|
|
}
|