Files
FireAdministration/update.sh
T
Florian WildeandClaude Opus 5 30faa6cb02
Pruefung / pruefung (pull_request) Canceled after 0s
Pruefung / oberflaeche (pull_request) Canceled after 0s
Version 1.0.0 — Freigabe fuer den Echtbetrieb
Kein neues Modul. Zwoelf Phasen sind fertig, und die Wehr kann ab jetzt
echte Daten hineingeben, ohne befuerchten zu muessen, sie beim naechsten
Update zu verlieren.

Neu dazu, weil beides zum Betrieb gehoert:

update.sh aktualisiert auf einem Linux-Server in einem Befehl. Der Ablauf
darf an jeder Stelle abbrechen, ohne eine halb aktualisierte Anlage zu
hinterlassen: erst nachsehen ob es etwas Neues gibt, dann sichern, dann
holen und bauen (schlaegt das fehl, laeuft die alte Fassung unveraendert
weiter), dann umschalten, dann pruefen ob die Anwendung antwortet. Tut sie
es nicht, stehen die Protokollzeilen da und die Befehle fuer den Rueckweg.

docs/KURZANLEITUNG.md ist der Einstieg fuer die Wehr - nach Rollen
sortiert, aufgabenorientiert statt nach Modulen, und bei jedem heiklen
Punkt steht das Warum dabei. Die vollstaendige Anleitung hat 63 Kapitel;
die liest niemand, bevor er das erste Geraet pruefen will.

Die Roadmap haelt den Stand JEDER Freigabevoraussetzung fest, auch der
offenen: Datenuebernahme entfaellt (die Wehr pflegt von Hand ein),
Sicherungskonzept erledigt, Loeschkonzept technisch vollstaendig aber
nicht abgenommen, Schulung steht aus, Atemschutzhinweis bewusst stehen
gelassen. Eine Freigabe, die abhakt was nicht geprueft wurde, ist keine.

Nebenbei die Ursache der Testflackerei gefunden, die sich durch mehrere
Phasen zog: zwei Datenbanktests schalteten beim Aufraeumen ihre Trigger
mit ALTER TABLE ... DISABLE TRIGGER ab - eine Schemaaenderung, die fuer
die ganze Datenbank gilt. Vitest faehrt die Dateien parallel, also pruefte
eine Datei gerade dass ein Trigger greift, waehrend eine andere ihn
abgeraeumt hatte. Jetzt sitzungslokal ueber session_replication_role.

Erstmals in diesem Projekt: fuenf Unit-Testlaeufe hintereinander 601 gruen
und ein E2E-Lauf mit 268 gruen ohne Fehlschlag und ohne Flackern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 18:09:26 +02:00

235 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# FLORIAN aktualisieren.
#
# ./update.sh neue Fassung holen und einspielen
# ./update.sh --pruefen nur nachsehen, ob es etwas Neues gibt
# ./update.sh --ohne-sicherung für Testinstallationen
#
# Der Ablauf ist so gebaut, dass er an jeder Stelle abbrechen darf, ohne die
# Wehr mit einer halb aktualisierten Anlage zurückzulassen:
#
# 1. Nachsehen, ob überhaupt etwas Neues da ist.
# 2. SICHERN — Datenbank und Dateien. Vor allem anderen.
# 3. Neue Fassung holen und Abbilder bauen.
# 4. Umschalten. Der App-Container spielt dabei die Migrationen ein.
# 5. Nachsehen, ob die Anwendung wieder antwortet.
#
# Geht Schritt 3 schief, läuft die alte Fassung weiter — es wurde nichts
# angefasst. Geht Schritt 4 oder 5 schief, steht in der Ausgabe, mit welchem
# Befehl sich der vorherige Stand zurückholen lässt.
#
# Voraussetzungen: Linux, Docker mit Compose-Erweiterung, git, ein .env im
# selben Verzeichnis.
# =============================================================================
set -Eeuo pipefail
VERZEICHNIS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$VERZEICHNIS"
COMPOSE=(docker compose -f docker-compose.yml)
NUR_PRUEFEN=false
OHNE_SICHERUNG=false
for argument in "$@"; do
case "$argument" in
--pruefen) NUR_PRUEFEN=true ;;
--ohne-sicherung) OHNE_SICHERUNG=true ;;
-h|--help)
sed -n '3,20p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "Unbekannte Angabe: $argument" >&2
echo "Bekannt sind: --pruefen, --ohne-sicherung, --help" >&2
exit 2
;;
esac
done
# --- Ausgabe ----------------------------------------------------------------
# Farben nur, wenn wirklich ein Terminal dranhängt. In einem Cron-Protokoll
# sind Steuerzeichen nur Müll.
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() { echo; echo "${FETT}==> $*${AUS}"; }
hinweis() { echo " $*"; }
warnung() { echo "${GELB} $*${AUS}"; }
fehler() { echo "${ROT}FEHLER: $*${AUS}" >&2; }
# Wo wir standen, bevor etwas verändert wurde — für die Rückkehr.
STAND_VORHER=""
SICHERUNG=""
abbruch() {
local zeile=$1
echo
fehler "Abbruch in Zeile $zeile."
echo
if [ -n "$STAND_VORHER" ]; then
echo "Der vorherige Stand war: $STAND_VORHER"
echo "Zurueck damit:"
echo " git checkout $STAND_VORHER"
echo " ${COMPOSE[*]} up -d --build"
fi
if [ -n "$SICHERUNG" ]; then
echo
echo "Die Sicherung von vorhin liegt in: $SICHERUNG"
echo "Zurueckspielen (ueberschreibt den Bestand):"
echo " npm run wiederherstellen -- --quelle $SICHERUNG --wirklich"
fi
exit 1
}
trap 'abbruch $LINENO' ERR
# --- Voraussetzungen --------------------------------------------------------
schritt "Voraussetzungen"
for werkzeug in git docker; do
if ! command -v "$werkzeug" >/dev/null 2>&1; then
fehler "$werkzeug ist nicht installiert."
exit 1
fi
done
if ! docker compose version >/dev/null 2>&1; then
fehler "Docker Compose fehlt (die Erweiterung 'docker compose', nicht docker-compose)."
exit 1
fi
if [ ! -f .env ]; then
fehler "Es gibt keine .env in $VERZEICHNIS."
hinweis "Fuer eine Erstinstallation: cp .env.example .env und ausfuellen."
exit 1
fi
# Ein Update mit unverbuchten Aenderungen wuerde sie beim Holen ueberschreiben
# oder in einem Konflikt enden. Beides mitten in einem Update ist unschoen.
if [ -n "$(git status --porcelain)" ]; then
fehler "Im Verzeichnis liegen Aenderungen, die nicht eingecheckt sind."
git status --short | head -20
echo
hinweis "Bitte erst klaeren. Die .env gehoert nicht ins Git und stoert hier nicht."
exit 1
fi
hinweis "Docker, git und .env sind da."
# --- Gibt es etwas Neues? ---------------------------------------------------
schritt "Nach einer neuen Fassung sehen"
ZWEIG="$(git rev-parse --abbrev-ref HEAD)"
STAND_VORHER="$(git rev-parse HEAD)"
git fetch --quiet origin "$ZWEIG"
STAND_NEU="$(git rev-parse "origin/$ZWEIG")"
if [ "$STAND_VORHER" = "$STAND_NEU" ]; then
hinweis "Alles aktuell — nichts zu tun."
hinweis "Stand: $(git log -1 --format='%h %s')"
exit 0
fi
ANZAHL="$(git rev-list --count "$STAND_VORHER..$STAND_NEU")"
hinweis "$ANZAHL neue Aenderung(en):"
git log --oneline --no-decorate "$STAND_VORHER..$STAND_NEU" | head -20 | sed 's/^/ /'
if [ "$NUR_PRUEFEN" = true ]; then
echo
hinweis "Nur nachgesehen — es wurde nichts geaendert."
hinweis "Zum Einspielen: ./update.sh"
exit 0
fi
# --- Sichern ----------------------------------------------------------------
# Vor allem anderen, und ohne Umgehung im Regelfall. Ein Update ist der
# haeufigste Anlass, eine Sicherung zu brauchen.
if [ "$OHNE_SICHERUNG" = true ]; then
schritt "Sicherung uebersprungen"
warnung "--ohne-sicherung ist gesetzt. Bei einem Fehler gibt es kein Zurueck."
else
schritt "Sichern"
if ! command -v npm >/dev/null 2>&1; then
fehler "npm fehlt — ohne das laeuft das Sicherungsskript nicht."
hinweis "Entweder Node installieren oder bewusst mit --ohne-sicherung fahren."
exit 1
fi
npm run --silent sicherung
SICHERUNG="$(find sicherungen -maxdepth 1 -mindepth 1 -type d | sort | tail -1)"
hinweis "Liegt in: $SICHERUNG"
fi
# --- Holen und bauen --------------------------------------------------------
# Bis hierher ist nichts umgeschaltet: schlaegt der Bau fehl, laeuft die alte
# Fassung unveraendert weiter.
schritt "Neue Fassung holen"
git merge --ff-only "origin/$ZWEIG"
hinweis "Jetzt auf: $(git log -1 --format='%h %s')"
schritt "Abbilder bauen"
hinweis "Das dauert ein paar Minuten."
"${COMPOSE[@]}" build
# --- Umschalten -------------------------------------------------------------
# Der App-Container spielt beim Start die ausstehenden Migrationen ein; der
# Worker tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig
# migrieren.
schritt "Umschalten"
"${COMPOSE[@]}" up -d --remove-orphans
# --- Nachsehen --------------------------------------------------------------
schritt "Nachsehen, ob die Anwendung antwortet"
ADRESSE="http://localhost:3000/api/health"
if grep -q '^APP_URL=' .env; then
ADRESSE="$(grep '^APP_URL=' .env | head -1 | cut -d= -f2- | tr -d '"'"'"' ')/api/health"
fi
hinweis "Frage $ADRESSE ..."
GESUND=false
for versuch in $(seq 1 30); do
if curl -fsS --max-time 5 "$ADRESSE" >/dev/null 2>&1; then
GESUND=true
break
fi
sleep 5
[ $((versuch % 6)) -eq 0 ] && hinweis " noch nicht da, warte weiter ($((versuch * 5)) s) ..."
done
if [ "$GESUND" != true ]; then
fehler "Die Anwendung antwortet nach zweieinhalb Minuten nicht."
echo
echo "Die letzten Zeilen aus dem Protokoll:"
"${COMPOSE[@]}" logs --tail 40 app || true
echo
echo "Zurueck auf den vorherigen Stand:"
echo " git checkout $STAND_VORHER"
echo " ${COMPOSE[*]} up -d --build"
[ -n "$SICHERUNG" ] && echo " npm run wiederherstellen -- --quelle $SICHERUNG --wirklich"
exit 1
fi
# Ab hier ist nichts mehr rueckgaengig zu machen, was das Skript angerichtet
# haette — deshalb keine Fehlerfalle mehr, die zum Rueckbau raet.
trap - ERR
schritt "Fertig"
echo "${GRUEN} FLORIAN laeuft auf dem Stand $(git log -1 --format='%h %s')${AUS}"
echo
hinweis "Zustand der Dienste:"
"${COMPOSE[@]}" ps --format ' {{.Service}}\t{{.Status}}' 2>/dev/null || "${COMPOSE[@]}" ps
if [ -n "$SICHERUNG" ]; then
echo
hinweis "Die Sicherung von vorhin liegt in $SICHERUNG."
hinweis "Sie kann weg, sobald ein paar Tage nichts aufgefallen ist."
fi