CI / pruefen (pull_request) Canceled after 0s
Das Update-Skript prüfte allein http://127.0.0.1:APP_PORT/api/health. Hinter einem Reverse Proxy ist der App-Port oft gar nicht auf dem Host veröffentlicht — dann meldete das Skript nach zwei Minuten einen Fehlschlag, obwohl das Update sauber durchgelaufen war. Schlägt die Abfrage vom Host fehl, zählt jetzt der HEALTHCHECK des Containers. Den bringt das Image schon mit; er fragt /api/health von innen ab und ist damit unabhängig von der Portveröffentlichung. Im Fehlerfall nennt das Skript zusätzlich den Container-Zustand und ordnet ihn ein: kein Container (Start fehlgeschlagen), startet noch (Migrationen dauern), läuft aber ungesund (Fehler in der Anwendung), oder ein älteres Image ohne Healthcheck. Ohne diese Auskunft sah ein nicht veröffentlichter Port genauso aus wie eine abgestürzte Anwendung. container_zustand() gibt bewusst immer 0 zurück — unter set -e dürfte die Diagnose nicht selbst abbrechen, bevor der Weg zurück ausgegeben ist. Dazu drei Merkposten in CLAUDE.md, die diese Sitzung Zeit gekostet haben: Klickwege müssen gegen einen Produktionsbau geprüft werden (im Entwicklungsmodus hydratisiert unter headless Chromium nichts, weil der HMR-Socket scheitert — ohne jede Fehlermeldung), vor npm start den Port prüfen, und dass scripts/shot.mjs sich nicht anmeldet.
308 lines
10 KiB
Bash
Executable File
308 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Flimmer aktualisieren.
|
|
#
|
|
# ./update.sh sichern, holen, bauen, neu starten
|
|
# ./update.sh --ohne-backup ohne Datenbanksicherung
|
|
# ./update.sh --ohne-pull nur neu bauen und starten (kein git pull)
|
|
# ./update.sh --hilfe
|
|
#
|
|
# Grundhaltung: Erst sichern, dann anfassen. Bricht etwas ab, steht am Ende,
|
|
# wie der Weg zurück aussieht — mit dem konkreten Befehl und der konkreten
|
|
# Sicherungsdatei, nicht bloß dem Rat, ein Backup zu haben.
|
|
|
|
set -euo pipefail
|
|
|
|
# --- Ausgabe -----------------------------------------------------------------
|
|
|
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
AKZENT=$'\033[38;5;214m'; GUT=$'\033[32m'; WARN=$'\033[33m'
|
|
FEHLER=$'\033[31m'; LEISE=$'\033[2m'; AUS=$'\033[0m'
|
|
else
|
|
AKZENT=""; GUT=""; WARN=""; FEHLER=""; LEISE=""; AUS=""
|
|
fi
|
|
|
|
schritt() { printf '\n%s▸ %s%s\n' "$AKZENT" "$1" "$AUS"; }
|
|
info() { printf ' %s\n' "$1"; }
|
|
leise() { printf ' %s%s%s\n' "$LEISE" "$1" "$AUS"; }
|
|
gut() { printf ' %s✓ %s%s\n' "$GUT" "$1" "$AUS"; }
|
|
warnung() { printf ' %s! %s%s\n' "$WARN" "$1" "$AUS"; }
|
|
abbruch() { printf '\n%s✗ %s%s\n' "$FEHLER" "$1" "$AUS" >&2; exit 1; }
|
|
|
|
hilfe() {
|
|
cat <<'ENDE'
|
|
Flimmer aktualisieren
|
|
|
|
./update.sh [OPTIONEN]
|
|
|
|
Ablauf: Datenbank sichern → Änderungen holen → Image bauen → neu starten →
|
|
warten, bis die Instanz wieder antwortet. Die Datenbankmigrationen spielt der
|
|
Container beim Start selbst ein.
|
|
|
|
Geprüft wird zuerst http://127.0.0.1:APP_PORT/api/health. Ist der Port gar
|
|
nicht auf dem Host veröffentlicht — üblich hinter einem Reverse Proxy —,
|
|
zählt stattdessen der Healthcheck des Containers.
|
|
|
|
Optionen:
|
|
--ohne-backup Keine Sicherung anlegen (nicht empfohlen)
|
|
--ohne-pull Nichts aus dem Repository holen, nur neu bauen und starten
|
|
--behalte N Wie viele Sicherungen aufgehoben werden (Standard: 10)
|
|
--hilfe, -h Diese Übersicht
|
|
|
|
Die Sicherungen liegen unter backups/ und sind gzip-komprimierte pg_dumps.
|
|
ENDE
|
|
}
|
|
|
|
# --- Optionen ----------------------------------------------------------------
|
|
|
|
MIT_BACKUP=1
|
|
MIT_PULL=1
|
|
BEHALTE=10
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--ohne-backup) MIT_BACKUP=0 ;;
|
|
--ohne-pull) MIT_PULL=0 ;;
|
|
--behalte) shift; BEHALTE="${1:-10}" ;;
|
|
--hilfe|-h) hilfe; exit 0 ;;
|
|
*) abbruch "Unbekannte Option: $1 (./update.sh --hilfe)" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# --- Voraussetzungen ---------------------------------------------------------
|
|
|
|
cd "$(dirname "$0")"
|
|
|
|
[ -f docker-compose.yml ] || abbruch "Hier liegt keine docker-compose.yml. Das Skript gehört ins Flimmer-Verzeichnis."
|
|
[ -f .env ] || abbruch "Es fehlt die .env. Lege sie an: cp .env.example .env"
|
|
|
|
command -v docker >/dev/null 2>&1 || abbruch "docker ist nicht installiert oder nicht im Pfad."
|
|
|
|
if docker compose version >/dev/null 2>&1; then
|
|
COMPOSE="docker compose"
|
|
elif command -v docker-compose >/dev/null 2>&1; then
|
|
COMPOSE="docker-compose"
|
|
else
|
|
abbruch "Weder 'docker compose' noch 'docker-compose' gefunden."
|
|
fi
|
|
|
|
docker info >/dev/null 2>&1 || abbruch "Der Docker-Dienst antwortet nicht. Läuft er?"
|
|
|
|
# Werte aus der .env lesen, ohne sie auszuführen.
|
|
env_wert() {
|
|
local schluessel="$1" standard="${2:-}"
|
|
local zeile
|
|
zeile=$(grep -E "^[[:space:]]*${schluessel}=" .env 2>/dev/null | tail -1 || true)
|
|
if [ -z "$zeile" ]; then printf '%s' "$standard"; return; fi
|
|
zeile="${zeile#*=}"
|
|
zeile="${zeile%\"}"; zeile="${zeile#\"}"
|
|
zeile="${zeile%\'}"; zeile="${zeile#\'}"
|
|
printf '%s' "$zeile"
|
|
}
|
|
|
|
DB_USER=$(env_wert POSTGRES_USER flimmer)
|
|
DB_NAME=$(env_wert POSTGRES_DB flimmer)
|
|
PORT=$(env_wert APP_PORT 3000)
|
|
|
|
printf '%s\n' "Flimmer aktualisieren"
|
|
leise "Verzeichnis: $(pwd)"
|
|
leise "Compose: $COMPOSE"
|
|
|
|
# --- Stand festhalten --------------------------------------------------------
|
|
|
|
IST_GIT=0
|
|
VORHER_REF=""
|
|
if [ -d .git ] && command -v git >/dev/null 2>&1; then
|
|
IST_GIT=1
|
|
VORHER_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unbekannt")
|
|
fi
|
|
|
|
VORHER_VERSION=$(grep -m1 '"version"' package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/' || echo "?")
|
|
leise "Aktuell: Version $VORHER_VERSION${VORHER_REF:+ (${VORHER_REF})}"
|
|
|
|
# --- Sicherung ---------------------------------------------------------------
|
|
|
|
SICHERUNG=""
|
|
if [ "$MIT_BACKUP" -eq 1 ]; then
|
|
schritt "Datenbank sichern"
|
|
|
|
# -q gibt es in beiden Compose-Fassungen; --status erst ab v2.
|
|
if [ -z "$($COMPOSE ps -q db 2>/dev/null)" ]; then
|
|
warnung "Die Datenbank läuft nicht — es gibt also auch nichts zu sichern."
|
|
else
|
|
mkdir -p backups
|
|
SICHERUNG="backups/flimmer-$(date +%Y-%m-%d-%H%M%S).sql.gz"
|
|
|
|
if $COMPOSE exec -T db pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists \
|
|
| gzip > "$SICHERUNG" 2>/dev/null && [ -s "$SICHERUNG" ]; then
|
|
gut "$SICHERUNG ($(du -h "$SICHERUNG" | cut -f1))"
|
|
else
|
|
rm -f "$SICHERUNG"
|
|
SICHERUNG=""
|
|
abbruch "Die Sicherung ist fehlgeschlagen. Abbruch — ohne Sicherung wird hier nichts aktualisiert.
|
|
Wenn du es trotzdem willst: ./update.sh --ohne-backup"
|
|
fi
|
|
|
|
# Ältere Sicherungen aufräumen, die jüngsten bleiben.
|
|
if [ "$BEHALTE" -gt 0 ]; then
|
|
ANZAHL=$(ls -1 backups/flimmer-*.sql.gz 2>/dev/null | wc -l)
|
|
if [ "$ANZAHL" -gt "$BEHALTE" ]; then
|
|
ls -1t backups/flimmer-*.sql.gz | tail -n +$((BEHALTE + 1)) | xargs -r rm --
|
|
leise "Ältere Sicherungen entfernt, die letzten $BEHALTE bleiben."
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
warnung "Ohne Sicherung — auf eigenes Risiko."
|
|
fi
|
|
|
|
# --- Änderungen holen --------------------------------------------------------
|
|
|
|
if [ "$MIT_PULL" -eq 1 ]; then
|
|
schritt "Änderungen holen"
|
|
|
|
if [ "$IST_GIT" -eq 0 ]; then
|
|
warnung "Kein Git-Repository — übersprungen."
|
|
elif [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
warnung "Es gibt lokale Änderungen; git pull wird übersprungen."
|
|
leise "Ansehen mit: git status"
|
|
else
|
|
ZWEIG=$(git rev-parse --abbrev-ref HEAD)
|
|
if git pull --ff-only 2>&1 | sed 's/^/ /'; then
|
|
NACHHER_REF=$(git rev-parse --short HEAD)
|
|
if [ "$NACHHER_REF" = "$VORHER_REF" ]; then
|
|
gut "Schon auf dem neuesten Stand ($ZWEIG)."
|
|
else
|
|
gut "$VORHER_REF → $NACHHER_REF"
|
|
fi
|
|
else
|
|
abbruch "git pull ist fehlgeschlagen. Vermutlich sind Zweig und Gegenstelle auseinandergelaufen."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
NEUE_VERSION=$(grep -m1 '"version"' package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/' || echo "?")
|
|
|
|
# --- Bauen und starten -------------------------------------------------------
|
|
|
|
schritt "Image bauen"
|
|
info "Das dauert beim ersten Mal einige Minuten."
|
|
$COMPOSE build 2>&1 | sed 's/^/ /' || abbruch "Der Build ist fehlgeschlagen. Die laufende Instanz ist unangetastet."
|
|
gut "Gebaut."
|
|
|
|
schritt "Neu starten"
|
|
$COMPOSE up -d 2>&1 | sed 's/^/ /' || abbruch "Der Start ist fehlgeschlagen. Log ansehen: $COMPOSE logs -f app"
|
|
gut "Container laufen."
|
|
|
|
# --- Warten, bis die Instanz antwortet ---------------------------------------
|
|
|
|
schritt "Auf die Instanz warten"
|
|
info "Die Datenbankmigrationen spielt der Container beim Start selbst ein."
|
|
|
|
# Der Container meldet seinen Zustand selbst — der HEALTHCHECK im Image fragt
|
|
# /api/health von innen ab. Das ist der verlässlichere Weg: hinter einem
|
|
# Reverse Proxy ist der App-Port oft gar nicht auf dem Host veröffentlicht,
|
|
# und dann sagt ein fehlschlagendes curl nichts über die Instanz aus.
|
|
# Gibt immer 0 zurück: die Funktion läuft auch im Fehlerpfad, und unter
|
|
# `set -e` dürfte eine Auskunft über den Zustand nicht selbst das Skript
|
|
# abbrechen, bevor der Weg zurück ausgegeben wurde.
|
|
container_zustand() {
|
|
local id
|
|
id=$($COMPOSE ps -q app 2>/dev/null || true)
|
|
if [ -z "$id" ]; then
|
|
printf 'kein-container'
|
|
return 0
|
|
fi
|
|
# Ohne HEALTHCHECK im Image ist .State.Health leer — dann bleibt der
|
|
# Laufzustand (running/exited) als grobe Auskunft.
|
|
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}ohne-healthcheck:{{.State.Status}}{{end}}' \
|
|
"$id" 2>/dev/null || printf 'unbekannt'
|
|
return 0
|
|
}
|
|
|
|
GESUND=0
|
|
UEBER_CONTAINER=0
|
|
for _ in $(seq 1 60); do
|
|
if curl -fsS "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then
|
|
GESUND=1
|
|
break
|
|
fi
|
|
if [ "$(container_zustand)" = "healthy" ]; then
|
|
GESUND=1
|
|
UEBER_CONTAINER=1
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
if [ "$GESUND" -eq 1 ]; then
|
|
if [ "$UEBER_CONTAINER" -eq 1 ]; then
|
|
gut "Der Container meldet sich gesund."
|
|
leise "Auf Port ${PORT} war nichts zu erreichen — das ist normal, wenn die App"
|
|
leise "nur über einen Reverse Proxy erreichbar ist und den Port nicht veröffentlicht."
|
|
else
|
|
ANTWORT=$(curl -fsS "http://127.0.0.1:${PORT}/api/health" 2>/dev/null || true)
|
|
gut "Flimmer antwortet auf Port ${PORT}."
|
|
leise "$ANTWORT"
|
|
fi
|
|
|
|
printf '\n%s✓ Fertig — Version %s%s\n' "$GUT" "$NEUE_VERSION" "$AUS"
|
|
if [ "$VORHER_VERSION" != "$NEUE_VERSION" ]; then
|
|
info "Was sich geändert hat, steht in CHANGELOG.md."
|
|
fi
|
|
if [ -n "$SICHERUNG" ]; then
|
|
leise "Sicherung: $SICHERUNG"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# --- Fehlschlag: konkreter Weg zurück ----------------------------------------
|
|
|
|
printf '\n%s✗ Die Instanz antwortet nach zwei Minuten nicht.%s\n\n' "$FEHLER" "$AUS" >&2
|
|
|
|
# Der Container-Zustand grenzt ein, wo es klemmt — ohne ihn sähe ein nicht
|
|
# veröffentlichter Port genauso aus wie eine abgestürzte App.
|
|
ZUSTAND=$(container_zustand)
|
|
case "$ZUSTAND" in
|
|
kein-container)
|
|
info "Es läuft gar kein app-Container. Der Start ist offenbar fehlgeschlagen."
|
|
;;
|
|
starting)
|
|
info "Der Container startet noch. Möglicherweise dauern die Migrationen länger —"
|
|
info "sieh im Log nach und gib ihm etwas Zeit."
|
|
;;
|
|
unhealthy)
|
|
info "Der Container läuft, meldet sich aber als nicht gesund. Der Fehler liegt"
|
|
info "also in der Anwendung, nicht am Port."
|
|
;;
|
|
ohne-healthcheck:running)
|
|
info "Der Container läuft, das Image bringt aber keinen Healthcheck mit (ältere"
|
|
info "Fassung). Ob die App wirklich antwortet, verrät nur das Log."
|
|
;;
|
|
ohne-healthcheck:*)
|
|
info "Der Container ist nicht gelaufen: ${ZUSTAND#ohne-healthcheck:}"
|
|
;;
|
|
*)
|
|
info "Container-Zustand: ${ZUSTAND}"
|
|
;;
|
|
esac
|
|
printf '\n'
|
|
|
|
info "Log ansehen:"
|
|
printf ' %s logs --tail 50 app\n\n' "$COMPOSE"
|
|
|
|
if [ -n "$SICHERUNG" ] || [ -n "$VORHER_REF" ]; then
|
|
info "Weg zurück:"
|
|
if [ "$IST_GIT" -eq 1 ] && [ -n "$VORHER_REF" ]; then
|
|
printf ' git checkout %s && %s up -d --build\n' "$VORHER_REF" "$COMPOSE"
|
|
fi
|
|
if [ -n "$SICHERUNG" ]; then
|
|
printf ' gunzip -c %s | %s exec -T db psql -U %s -d %s\n' \
|
|
"$SICHERUNG" "$COMPOSE" "$DB_USER" "$DB_NAME"
|
|
fi
|
|
printf '\n'
|
|
fi
|
|
|
|
exit 1
|