Files
FireAdministration/update.sh
T
florian.wilde 3b20abcb23
Pruefung / pruefung (push) Canceled after 0s
Pruefung / oberflaeche (push) Canceled after 0s
Version 1.0.0 — Freigabe für den Echtbetrieb (#17)
2026-09-06 20:08:16 +00: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