#!/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