feat(betrieb): Abbild, Compose-Stapel und Betriebsskripte
Dreistufiges Dockerfile mit standalone-Ausgabe; im Laufzeitabbild liegen weder Quelltext noch Bauwerkzeuge, und es läuft unter einem eigenen Benutzer. Die Migrationen erledigt ein eigener Einmaldienst aus der Bau-Stufe. Der Versuch, nur die Prisma-CLI ins Laufzeitabbild zu kopieren, scheiterte an ihren transitiven Modulen; sie alle mitzunehmen hätte das Abbild aufgebläht. Der Dienst läuft vor der App und muss sauber durchlaufen. Zwei Stellen mussten faul werden, weil Next beim Bauen Seitendaten sammelt und dabei Module lädt, die es zur Bauzeit noch ohne Umgebungsvariablen gibt: - Der Prisma-Client entsteht erst beim ersten Zugriff, über einen Proxy. - Die Auth.js-Konfiguration wird als Funktion übergeben. Ebenso liest prisma.config.ts die URL über process.env statt über env(), das beim Bauen ohne Datenbank abbricht. Dazu backup.sh, update.sh mit Rückrollpfad und scripts/release.sh. Der Dump wurde nachweislich in eine Wegwerf-Datenbank zurückgespielt und stimmt zeilengenau mit dem Original überein. Alle Skripte sind shellcheck-sauber. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
126ad93301
commit
f91d015cc4
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env bash
|
||||
# Aktualisiert den Planer auf eine neue Version.
|
||||
# Für Linux mit Docker Compose. Läuft im Projektverzeichnis.
|
||||
set -euo pipefail
|
||||
|
||||
WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$WURZEL"
|
||||
|
||||
ZIELVERSION=""
|
||||
OHNE_RUECKFRAGE=0
|
||||
OHNE_BACKUP=0
|
||||
NUR_PRUEFEN=0
|
||||
WARTEZEIT=180
|
||||
|
||||
LOGVERZEICHNIS="$WURZEL/logs"
|
||||
LOGDATEI="$LOGVERZEICHNIS/update-$(date '+%Y-%m-%d_%H-%M-%S').log"
|
||||
|
||||
hilfe() {
|
||||
cat <<'ENDE'
|
||||
Verwendung: update.sh [Optionen]
|
||||
|
||||
--version vX.Y.Z Auf diese Version aktualisieren statt auf die neueste
|
||||
--ja Ohne Rückfrage durchlaufen
|
||||
--ohne-backup Keine Sicherung anlegen (nicht empfohlen)
|
||||
--nur-pruefen Nur zeigen, was passieren würde
|
||||
--wartezeit <s> Sekunden, die auf einen gesunden Start gewartet wird
|
||||
(Standard: 180)
|
||||
--hilfe Diese Übersicht
|
||||
|
||||
Ablauf: Voraussetzungen prüfen, Versionen ermitteln, Änderungen zeigen,
|
||||
sichern, Ziel-Tag auschecken, bauen, starten, auf /api/health warten.
|
||||
Schlägt etwas fehl, wird auf die vorherige Version zurückgerollt.
|
||||
ENDE
|
||||
}
|
||||
|
||||
meldung() { printf '%s %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI"; }
|
||||
fehler() {
|
||||
printf '%s Fehler: %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$LOGDATEI" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
ZIELVERSION="${2:-}"
|
||||
[ -n "$ZIELVERSION" ] || fehler '--version braucht eine Angabe wie v0.1.0.'
|
||||
shift 2
|
||||
;;
|
||||
--ja) OHNE_RUECKFRAGE=1; shift ;;
|
||||
--ohne-backup) OHNE_BACKUP=1; shift ;;
|
||||
--nur-pruefen) NUR_PRUEFEN=1; shift ;;
|
||||
--wartezeit)
|
||||
WARTEZEIT="${2:-}"
|
||||
case "$WARTEZEIT" in
|
||||
''|*[!0-9]*) fehler '--wartezeit braucht eine ganze Zahl.' ;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--hilfe|-h) hilfe; exit 0 ;;
|
||||
*) fehler "Unbekannte Option '$1'. 'update.sh --hilfe' zeigt die Übersicht." ;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "$LOGVERZEICHNIS"
|
||||
|
||||
# --- 1. Voraussetzungen ----------------------------------------------------
|
||||
meldung 'Voraussetzungen prüfen.'
|
||||
for werkzeug in git docker curl jq; do
|
||||
command -v "$werkzeug" >/dev/null 2>&1 || fehler "$werkzeug fehlt."
|
||||
done
|
||||
docker compose version >/dev/null 2>&1 || fehler 'docker compose fehlt.'
|
||||
[ -d "$WURZEL/.git" ] || fehler 'Kein Git-Arbeitsbaum – im Projektverzeichnis aufrufen.'
|
||||
[ -f "$WURZEL/.env" ] || fehler '.env fehlt.'
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
fehler 'Der Arbeitsbaum ist nicht sauber. Erst aufräumen oder committen.'
|
||||
fi
|
||||
|
||||
APP_PORT="$(grep -E '^APP_PORT=' .env | tail -1 | cut -d= -f2- || true)"
|
||||
APP_PORT="${APP_PORT:-3000}"
|
||||
HEALTH_URL="http://127.0.0.1:${APP_PORT}/api/health"
|
||||
|
||||
# --- 2. Versionen ermitteln ------------------------------------------------
|
||||
meldung 'Versionen ermitteln.'
|
||||
INSTALLIERT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null | jq -r '.version // empty' || true)"
|
||||
if [ -z "$INSTALLIERT" ]; then
|
||||
INSTALLIERT="$(jq -r '.version' package.json)"
|
||||
meldung "Die laufende Instanz war nicht erreichbar; nehme package.json: $INSTALLIERT"
|
||||
else
|
||||
meldung "Installiert: $INSTALLIERT"
|
||||
fi
|
||||
|
||||
VORHER_REF="$(git rev-parse --abbrev-ref HEAD)"
|
||||
[ "$VORHER_REF" != 'HEAD' ] || VORHER_REF="$(git rev-parse HEAD)"
|
||||
|
||||
git fetch --tags --quiet origin || fehler 'git fetch ist fehlgeschlagen.'
|
||||
|
||||
if [ -z "$ZIELVERSION" ]; then
|
||||
ZIELVERSION="$(git tag --list 'v*' --sort=-v:refname | head -1)"
|
||||
[ -n "$ZIELVERSION" ] || fehler 'Es gibt keinen Versions-Tag auf origin.'
|
||||
fi
|
||||
git rev-parse -q --verify "refs/tags/${ZIELVERSION}" >/dev/null \
|
||||
|| fehler "Den Tag $ZIELVERSION gibt es nicht."
|
||||
meldung "Ziel: $ZIELVERSION"
|
||||
|
||||
if [ "v$INSTALLIERT" = "$ZIELVERSION" ]; then
|
||||
meldung 'Die Zielversion läuft bereits. Nichts zu tun.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 3. Änderungen zeigen --------------------------------------------------
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
meldung "Änderungen bis $ZIELVERSION laut CHANGELOG.md:"
|
||||
# Vom Abschnitt der Zielversion bis zum Abschnitt der installierten Version.
|
||||
awk -v ziel="${ZIELVERSION#v}" -v alt="$INSTALLIERT" '
|
||||
$0 ~ "^## \\[" ziel "\\]" { zeigen = 1 }
|
||||
zeigen && $0 ~ "^## \\[" alt "\\]" { exit }
|
||||
zeigen { print " " $0 }
|
||||
' CHANGELOG.md | tee -a "$LOGDATEI"
|
||||
fi
|
||||
|
||||
if [ "$NUR_PRUEFEN" -eq 1 ]; then
|
||||
meldung "Nur geprüft: $INSTALLIERT würde auf ${ZIELVERSION#v} aktualisiert."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$OHNE_RUECKFRAGE" -eq 0 ]; then
|
||||
printf 'Jetzt auf %s aktualisieren? [j/N] ' "$ZIELVERSION"
|
||||
read -r antwort
|
||||
case "$antwort" in
|
||||
[jJ]|[jJ][aA]) ;;
|
||||
*) meldung 'Abgebrochen.'; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- 4. Sicherung ----------------------------------------------------------
|
||||
SICHERUNG=''
|
||||
if [ "$OHNE_BACKUP" -eq 0 ]; then
|
||||
meldung 'Sicherung anlegen.'
|
||||
./backup.sh --tage 30 | tee -a "$LOGDATEI"
|
||||
SICHERUNG="$(find "$WURZEL/sicherungen" -maxdepth 1 -name 'planer-db-*.dump' -print0 2>/dev/null \
|
||||
| xargs -0 ls -t 2>/dev/null | head -1 || true)"
|
||||
else
|
||||
meldung 'Sicherung übersprungen (--ohne-backup).'
|
||||
fi
|
||||
|
||||
zurueckrollen() {
|
||||
meldung "Zurückrollen auf $VORHER_REF."
|
||||
git checkout --quiet "$VORHER_REF" || meldung 'Das Auschecken der alten Version schlug fehl.'
|
||||
docker compose build >>"$LOGDATEI" 2>&1 || meldung 'Der Neubau der alten Version schlug fehl.'
|
||||
docker compose up -d >>"$LOGDATEI" 2>&1 || meldung 'Der Start der alten Version schlug fehl.'
|
||||
|
||||
if [ -n "$SICHERUNG" ]; then
|
||||
meldung "Die Datenbank wurde NICHT zurückgespielt. Sicherung: $SICHERUNG"
|
||||
meldung 'Wiederherstellen mit:'
|
||||
meldung " docker compose exec -T db pg_restore -U planer -d planer --clean --if-exists < \"$SICHERUNG\""
|
||||
fi
|
||||
meldung "Vollständiges Protokoll: $LOGDATEI"
|
||||
}
|
||||
|
||||
# --- 5. Auschecken, bauen, starten ----------------------------------------
|
||||
meldung "Auf $ZIELVERSION wechseln."
|
||||
if ! git checkout --quiet "$ZIELVERSION"; then
|
||||
fehler "Der Wechsel auf $ZIELVERSION schlug fehl."
|
||||
fi
|
||||
|
||||
meldung 'Abbild bauen.'
|
||||
if ! docker compose build >>"$LOGDATEI" 2>&1; then
|
||||
meldung 'Der Bau ist fehlgeschlagen.'
|
||||
zurueckrollen
|
||||
exit 1
|
||||
fi
|
||||
|
||||
meldung 'Dienste starten.'
|
||||
if ! docker compose up -d >>"$LOGDATEI" 2>&1; then
|
||||
meldung 'Der Start ist fehlgeschlagen.'
|
||||
zurueckrollen
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 6. Auf Gesundheit warten ---------------------------------------------
|
||||
meldung "Auf $HEALTH_URL warten (bis zu ${WARTEZEIT}s)."
|
||||
ERWARTET="${ZIELVERSION#v}"
|
||||
VERGANGEN=0
|
||||
GESUND=0
|
||||
while [ "$VERGANGEN" -lt "$WARTEZEIT" ]; do
|
||||
ANTWORT="$(curl -fsS --max-time 5 "$HEALTH_URL" 2>/dev/null || true)"
|
||||
if [ -n "$ANTWORT" ]; then
|
||||
STATUS="$(printf '%s' "$ANTWORT" | jq -r '.status // empty')"
|
||||
LAEUFT="$(printf '%s' "$ANTWORT" | jq -r '.version // empty')"
|
||||
if [ "$STATUS" = 'ok' ] && [ "$LAEUFT" = "$ERWARTET" ]; then
|
||||
GESUND=1
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 5
|
||||
VERGANGEN=$((VERGANGEN + 5))
|
||||
done
|
||||
|
||||
# --- 7. Ergebnis -----------------------------------------------------------
|
||||
if [ "$GESUND" -eq 0 ]; then
|
||||
meldung "Die neue Version meldet sich nicht gesund mit Version $ERWARTET."
|
||||
docker compose logs --tail 50 app >>"$LOGDATEI" 2>&1 || true
|
||||
zurueckrollen
|
||||
exit 1
|
||||
fi
|
||||
|
||||
meldung "Fertig. Der Planer läuft in Version $ERWARTET."
|
||||
meldung "Protokoll: $LOGDATEI"
|
||||
Reference in New Issue
Block a user