CI / pruefen (push) Has been cancelled
DEPLOYMENT.md fuehrt Schritt fuer Schritt vom leeren Server zur Instanz hinter TLS: Docker, Konfiguration, Reverse Proxy mit Caddy oder nginx, Firewall, Sicherungen per Cron. Dazu, was an Haertung schon eingebaut ist - und was der Betreiber selbst beisteuern muss. scripts/update.sh macht die Schritte in der richtigen Reihenfolge: pruefen, holen, sichern, bauen, starten, Gesundheit pruefen. Erst bauen und dann umschalten, damit ein gescheiterter Bau die laufende Fassung nicht mitnimmt; die Sicherung liegt vor jeder Schemaaenderung. Antwortet /api/health nach einer Minute nicht, zeigt es die letzten Logzeilen und nennt den Weg zurueck. Es weigert sich zu laufen, solange APP_SECRET leer oder noch der Entwicklungswert ist - mit diesem Wert liegen die Git-Tokens faktisch offen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
164 lines
7.2 KiB
Bash
164 lines
7.2 KiB
Bash
#!/usr/bin/env bash
|
||
# ─────────────────────────────────────────────────────────────
|
||
# Nebula auf einem Server aktualisieren.
|
||
#
|
||
# Holt den neuen Stand, sichert die Datenbank, baut das Abbild, spielt die
|
||
# Migrationen ein und startet neu. Bricht ab, sobald ein Schritt scheitert –
|
||
# und sagt dann, wie zurueckzukommen ist.
|
||
#
|
||
# Aufruf: ./scripts/update.sh
|
||
# ./scripts/update.sh --no-pull (nur neu bauen und starten)
|
||
# ./scripts/update.sh --ref v1.4.0 (auf einen Stand festlegen)
|
||
# ./scripts/update.sh --yes (ohne Rueckfrage)
|
||
#
|
||
# Rueckgabe 0 heisst: die neue Fassung laeuft und antwortet gesund.
|
||
# ─────────────────────────────────────────────────────────────
|
||
set -euo pipefail
|
||
|
||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||
cd "$root"
|
||
|
||
pull=1
|
||
ref=""
|
||
zusage=0
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--no-pull) pull=0 ;;
|
||
--ref) ref="${2:-}"; shift ;;
|
||
--yes|-y) zusage=1 ;;
|
||
-h|--help) sed -n '3,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||
*) echo "Unbekannte Option: $1" >&2; exit 2 ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
# ── Farben, aber nur wenn ein Terminal zusieht ───────────────
|
||
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() { printf '\n%s▸ %s%s\n' "$fett" "$1" "$aus"; }
|
||
ok() { printf ' %s✓%s %s\n' "$gruen" "$aus" "$1"; }
|
||
warnung() { printf ' %s!%s %s\n' "$gelb" "$aus" "$1"; }
|
||
abbruch() { printf '\n%s✗ %s%s\n' "$rot" "$1" "$aus" >&2; exit 1; }
|
||
|
||
# ── Voraussetzungen ──────────────────────────────────────────
|
||
schritt "Voraussetzungen"
|
||
|
||
command -v docker >/dev/null || abbruch "docker nicht gefunden."
|
||
docker compose version >/dev/null 2>&1 || abbruch "docker compose (v2) nicht gefunden."
|
||
[ -f "$root/.env" ] || abbruch "Keine .env in $root. Zuerst 'node scripts/setup.mjs' ausfuehren."
|
||
[ -f "$root/docker-compose.yml" ] || abbruch "Kein docker-compose.yml in $root."
|
||
ok "docker, compose und .env vorhanden"
|
||
|
||
# APP_SECRET muss gesetzt sein – sonst liegen die Git-Tokens faktisch offen.
|
||
if grep -qE '^\s*APP_SECRET\s*=\s*("?)(nebula-dev-secret[^"]*|)\1\s*$' "$root/.env"; then
|
||
abbruch "APP_SECRET ist leer oder noch der Entwicklungswert. Bitte in der .env setzen."
|
||
fi
|
||
ok "APP_SECRET gesetzt"
|
||
|
||
vorher="$(git rev-parse --short HEAD 2>/dev/null || echo "unbekannt")"
|
||
version_vorher="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")"
|
||
printf ' Aktuell: v%s (%s)\n' "$version_vorher" "$vorher"
|
||
|
||
# ── Neuen Stand holen ────────────────────────────────────────
|
||
if [ "$pull" -eq 1 ]; then
|
||
schritt "Neuen Stand holen"
|
||
|
||
if [ -n "$(git status --porcelain)" ]; then
|
||
abbruch "Das Arbeitsverzeichnis ist nicht sauber. Bitte erst committen oder verwerfen."
|
||
fi
|
||
|
||
git fetch --all --tags --quiet
|
||
if [ -n "$ref" ]; then
|
||
git checkout --quiet "$ref"
|
||
ok "Auf $ref gewechselt"
|
||
else
|
||
zweig="$(git rev-parse --abbrev-ref HEAD)"
|
||
git pull --ff-only --quiet origin "$zweig"
|
||
ok "$zweig aktualisiert"
|
||
fi
|
||
|
||
nachher="$(git rev-parse --short HEAD)"
|
||
version_nachher="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")"
|
||
|
||
if [ "$vorher" = "$nachher" ]; then
|
||
warnung "Kein neuer Stand – es wird trotzdem neu gebaut."
|
||
else
|
||
printf ' Neu: v%s (%s)\n' "$version_nachher" "$nachher"
|
||
printf '\n Was dazukam:\n'
|
||
git --no-pager log --oneline --no-decorate "$vorher..$nachher" 2>/dev/null | sed 's/^/ /' || true
|
||
fi
|
||
else
|
||
warnung "Ohne --pull: der vorhandene Stand wird neu gebaut."
|
||
fi
|
||
|
||
# ── Rueckfrage ───────────────────────────────────────────────
|
||
if [ "$zusage" -eq 0 ] && [ -t 0 ]; then
|
||
printf '\n%sWeiter? Die Datenbank wird vorher gesichert. [j/N] %s' "$fett" "$aus"
|
||
read -r antwort
|
||
case "$antwort" in [jJyY]*) ;; *) abbruch "Abgebrochen." ;; esac
|
||
fi
|
||
|
||
# ── Datenbank sichern ────────────────────────────────────────
|
||
schritt "Datenbank sichern"
|
||
|
||
# Die Datenbank muss laufen, sonst gibt es nichts zu sichern.
|
||
docker compose up -d db >/dev/null
|
||
sicherung=""
|
||
if [ -x "$root/scripts/backup.sh" ]; then
|
||
ausgabe="$("$root/scripts/backup.sh" 2>&1)" || abbruch "Sicherung fehlgeschlagen:\n$ausgabe"
|
||
sicherung="$(printf '%s' "$ausgabe" | grep -oE '[^ ]*backups/[^ ]*\.sql[^ ]*' | tail -n 1 || true)"
|
||
ok "Gesichert${sicherung:+: $sicherung}"
|
||
else
|
||
warnung "scripts/backup.sh fehlt – es wird ohne Sicherung fortgefahren."
|
||
fi
|
||
|
||
# ── Bauen ────────────────────────────────────────────────────
|
||
schritt "Abbild bauen"
|
||
# Erst bauen, dann umschalten: ein Fehlschlag laesst die alte Fassung laufen.
|
||
docker compose --profile full build app || abbruch "Der Bau ist gescheitert. Die laufende Fassung ist unveraendert."
|
||
ok "Abbild gebaut"
|
||
|
||
# ── Starten ──────────────────────────────────────────────────
|
||
schritt "Starten"
|
||
# Die Migrationen laufen im entrypoint.sh, bevor der Server hochkommt.
|
||
docker compose --profile full up -d || abbruch "Der Start ist gescheitert."
|
||
ok "Container gestartet"
|
||
|
||
# ── Gesundheitspruefung ──────────────────────────────────────
|
||
schritt "Gesundheitspruefung"
|
||
|
||
port="$(sed -n 's/^[[:space:]]*APP_PORT[[:space:]]*=[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' "$root/.env" | tail -n 1)"
|
||
port="${port:-3000}"
|
||
|
||
gesund=0
|
||
for versuch in $(seq 1 30); do
|
||
if curl -fsS "http://127.0.0.1:${port}/api/health" >/dev/null 2>&1; then
|
||
gesund=1
|
||
break
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
if [ "$gesund" -ne 1 ]; then
|
||
printf '\n%sLetzte Zeilen aus dem Log:%s\n' "$fett" "$aus"
|
||
docker compose --profile full logs --tail 40 app || true
|
||
printf '\n%sZurueck zur vorigen Fassung:%s\n' "$fett" "$aus"
|
||
printf ' git checkout %s && ./scripts/update.sh --no-pull --yes\n' "$vorher"
|
||
[ -n "$sicherung" ] && printf ' Datenbank notfalls: ./scripts/restore.sh %s\n' "$sicherung"
|
||
abbruch "Die Anwendung antwortet nach 60 Sekunden nicht gesund."
|
||
fi
|
||
|
||
antwort_json="$(curl -fsS "http://127.0.0.1:${port}/api/health")"
|
||
ok "Gesund: $antwort_json"
|
||
|
||
# ── Fertig ───────────────────────────────────────────────────
|
||
version_jetzt="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")"
|
||
printf '\n%s%s✓ Nebula v%s laeuft auf Port %s%s\n' "$gruen" "$fett" "$version_jetzt" "$port" "$aus"
|
||
[ -n "$sicherung" ] && printf ' Sicherung vor dem Update: %s\n' "$sicherung"
|
||
printf ' Log mitlesen: docker compose --profile full logs -f app\n\n'
|