b2in/dev/betrieb/deploy.sh
Kevin Adametz b5ed756d67 Git-Deploy per Tag, Betriebsüberwachung und Release-Seeder für den Livegang
- dev/betrieb/deploy.sh: Deploy nur per Tag, mit --seed (Seeder im Wartungsmodus)
  und --initial (Vorschau/Live einmalig an Ort und Stelle auf Git umstellen)
- dev/betrieb/vorschau-auffrischen.sh: Live-DB und Live-Medien in die Vorschau (Generalprobe)
- InteriorReleaseSeeder: alle Inhaltsschritte des Relaunchs in einem Lauf, idempotent;
  Displays, Playlists und Mediathek bleiben unberührt
- DisplayInteriorBrandingSeeder überschreibt keinen inzwischen gepflegten Claim mehr
- Monitoring: GlitchTip (Server + Browser), /health mit Token, Scheduler-Herzschlag,
  Alarme per Mail und ntfy, Version in about und /health
- public/build versioniert; .gitignore: macOS-Muster Icon traf resources/views/flux/icon/
- Runbook: dev/betrieb/README.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:56:23 +00:00

294 lines
11 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# Deploy eines Git-Tags auf Vorschau oder Live (B2in). Grundlage: Baukasten
# dev/git-deploy-monitoring, Ablauf für B2in: dev/betrieb/README.md.
#
# Aufruf im App-Verzeichnis auf dem Server:
#
# bash dev/betrieb/deploy.sh v2.0.0 # ohne neue Migrationen
# bash dev/betrieb/deploy.sh v2.0.0 --migrations # mit Migrationen, DB-Backup vorher!
# bash dev/betrieb/deploy.sh v2.0.0 --migrations --seed=InteriorReleaseSeeder
#
# Optionen:
# --migrations Neue Migrationen ausführen. Ohne diese Option bricht das
# Skript ab, sobald der Tag neue Migrationen mitbringt.
# --clear-schedule php artisan schedule:clear-cache (alte withoutOverlapping-Sperren).
# --seed=<Klasse> Seeder im Wartungsmodus ausführen (nach den Migrationen,
# vor den Caches) – für Inhaltsschritte, die die Release-Notiz nennt.
# --initial Einmalig: ein Verzeichnis ohne Git (bisher SFTP/Archiv) an Ort und
# Stelle zum Checkout des Tags machen. Sichert vorher den Code nach
# ~/<verzeichnis>-vor-git-<zeit>.tgz, zeigt Dateien, die es nur auf dem
# Server gibt, und verschiebt sie nach Rückfrage nach ~/<verzeichnis>-vor-git-<zeit>/.
# Verlangt --migrations (vorher DB-Backup).
#
# Umgebung (optional):
# DEPLOY_FPM_RELOAD Befehl zum Neuladen von PHP-FPM (Standard siehe unten).
# Leer setzen, wenn FPM von Hand im Server-Panel neu geladen
# wird — dann wartet das Skript auf Enter. Nie „Restart“:
# der entfernt kurz den Socket, Nginx liefert 502.
# DEPLOY_PHP_FPM_SERVICE systemd-Name des FPM-Dienstes (Standard php8.4-fpm).
# DEPLOY_REPOSITORY Repository für --initial (Standard: Host-Alias forge-b2in aus ~/.ssh/config).
#
# Exit-Codes: 0 fertig, 1 abgebrochen (siehe Ausgabe), 2 falscher Aufruf,
# 3 live, aber kein Queue-Worker hat den Test-Job abgeholt.
#
# Rollback: dasselbe Skript mit dem vorherigen Tag. Nach --initial: Sicherung
# zurückspielen (Pfad steht in der Ausgabe) und DB-Backup einspielen.
set -euo pipefail
# ---- Projekt-Einstellungen --------------------------------------------------
DEPLOY_SCRIPT="dev/betrieb/deploy.sh" # Pfad dieses Skripts im Repository
PHP_FPM_SERVICE="${DEPLOY_PHP_FPM_SERVICE:-php8.4-fpm}" # systemd-Name des FPM-Dienstes
ABOUT_SECTION="b2in" # AboutCommand::add('B2in', …) im AppServiceProvider
QUEUE_PROBE=0 # B2in stellt keine Jobs in die Queue (Mails gehen sofort raus)
REPOSITORY="${DEPLOY_REPOSITORY:-forge-b2in:kadmin/b2in.git}"
# -----------------------------------------------------------------------------
# Git ersetzt diese Datei beim Checkout. Bash liest Skripte stückweise, deshalb
# läuft der Deploy aus einer Kopie.
if [[ -z "${DEPLOY_COPY:-}" ]]; then
copy="$(mktemp)"
cp "$0" "$copy"
DEPLOY_COPY="$copy" exec bash "$copy" "$@"
fi
trap 'rm -f "$DEPLOY_COPY"' EXIT
tag="${1:-}"
shift || true
run_migrations=0
clear_schedule=0
initial=0
seeders=()
for option in "$@"; do
case "$option" in
--migrations) run_migrations=1 ;;
--clear-schedule) clear_schedule=1 ;;
--initial) initial=1 ;;
--seed=*)
seeder="${option#--seed=}"
if [[ ! "$seeder" =~ ^[A-Za-z][A-Za-z0-9]*$ ]]; then
echo "Ungültiger Seeder: $seeder" >&2; exit 2
fi
seeders+=("$seeder") ;;
*) echo "Unbekannte Option: $option" >&2; exit 2 ;;
esac
done
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Aufruf: bash $DEPLOY_SCRIPT vX.Y.Z [--migrations] [--seed=<Klasse>] [--clear-schedule] [--initial]" >&2
exit 2
fi
if [[ ! -f artisan ]]; then
echo "Im App-Verzeichnis aufrufen (artisan fehlt hier)." >&2
exit 2
fi
step() { printf '\n==> %s\n' "$*"; }
if [[ "$initial" -eq 1 ]]; then
if [[ -d .git ]]; then
echo "--initial nur für ein Verzeichnis ohne Git – hier gibt es .git schon." >&2
exit 2
fi
if [[ "$run_migrations" -ne 1 ]]; then
echo "--initial verlangt --migrations (vorher DB-Backup ziehen)." >&2
exit 2
fi
stamp="$(date +%Y-%m-%d-%H%M)"
site="$(basename "$PWD")"
backup="$HOME/$site-vor-git-$stamp.tgz"
parked="$HOME/$site-vor-git-$stamp"
step "Sicherung des Codes nach $backup (ohne storage/, node_modules/)"
# Exit 1 = „Datei hat sich beim Lesen geändert“ (Logs o. Ä.) – die Sicherung ist trotzdem brauchbar.
tar czf "$backup" --exclude=./storage --exclude=./node_modules --exclude=./public/storage . || [[ $? -eq 1 ]]
ls -lh "$backup"
step "Git einrichten, Tag $tag holen"
git init -q
git remote add origin "$REPOSITORY"
if ! git fetch --quiet --tags origin || ! git rev-parse --quiet --verify "refs/tags/$tag^{commit}" > /dev/null; then
rm -rf .git
echo "Tag $tag nicht abrufbar (Deploy-Key? Tag gepusht?). .git wieder entfernt, nichts verändert." >&2
exit 1
fi
git update-ref --no-deref HEAD "$(git rev-parse "$tag^{commit}")"
git read-tree HEAD
# Ignorier-Regeln des Tags gelten schon jetzt (die .gitignore im Verzeichnis ist noch die alte).
git show "$tag:.gitignore" > .git/info/exclude
changed="$(git status --porcelain --untracked-files=no | wc -l | tr -d ' ')"
server_only="$(git -c core.quotePath=false ls-files --others --exclude-standard)"
echo "$changed Dateien weichen vom Tag ab oder fehlen – der Checkout ersetzt sie."
if [[ -n "$server_only" ]]; then
echo
echo "Diese $(echo "$server_only" | wc -l | tr -d ' ') Dateien gibt es nur auf dem Server (nicht im Tag, nicht ignoriert):"
echo "$server_only" | sed 's/^/ /' | head -200
echo
echo "Sie werden nach $parked/ verschoben (Pfade bleiben erhalten). Wird eine davon gebraucht,"
echo "jetzt abbrechen und sie ins Repository oder in die .gitignore aufnehmen (neuer Tag)."
fi
read -r -p "Weiter mit dem Deploy von $tag? [ja/nein] " answer
if [[ "$answer" != "ja" ]]; then
rm -rf .git
echo "Abgebrochen. .git wieder entfernt, nichts verändert (Sicherung bleibt liegen)." >&2
exit 1
fi
previous="vor-git (Sicherung: $backup)"
new_migrations="(Umstellung: alle offenen Migrationen)"
else
if [[ ! -d .git ]]; then
echo "Hier gibt es kein .git – für die einmalige Umstellung --initial verwenden." >&2
exit 2
fi
step "Arbeitsverzeichnis prüfen"
drift="$(git status --porcelain)"
if [[ -n "$drift" ]]; then
echo "Das Arbeitsverzeichnis weicht vom ausgecheckten Stand ab:" >&2
echo "$drift" >&2
echo "Erst klären (git diff), dann deployen. Nichts wurde verändert." >&2
exit 1
fi
step "Tags von origin holen"
git fetch --quiet --tags origin
if ! git rev-parse --quiet --verify "refs/tags/$tag^{commit}" > /dev/null; then
echo "Tag $tag gibt es auf origin nicht." >&2
exit 1
fi
previous="$(cat RELEASE 2>/dev/null || git describe --tags --always)"
if [[ "$(git rev-parse HEAD)" == "$(git rev-parse "$tag^{commit}")" ]]; then
echo "$tag ist bereits ausgecheckt. Nichts zu tun."
exit 0
fi
fi
echo "Bisher: $previous → Ziel: $tag"
for build in b2in b2a stileigentum style2own portal; do
if ! git cat-file -e "$tag:public/build/$build/manifest.json" 2> /dev/null; then
echo "public/build/$build/manifest.json fehlt im Tag $tag – Assets lokal bauen und committen. Nichts wurde verändert." >&2
exit 1
fi
done
if [[ "$initial" -ne 1 ]]; then
new_migrations="$(git diff --name-only --diff-filter=A HEAD "$tag" -- database/migrations)"
fi
if [[ -n "$new_migrations" ]]; then
echo "Neue Migrationen:"
echo "$new_migrations" | sed 's/^/ /'
if [[ "$run_migrations" -ne 1 ]]; then
echo "Abbruch: DB-Backup ziehen und mit --migrations erneut aufrufen. Nichts wurde verändert." >&2
exit 1
fi
fi
if [[ "$initial" -eq 1 ]]; then
trap 'echo; echo "!! Umstellung abgebrochen. Die Seite bleibt im Wartungsmodus." >&2; echo "!! Rückweg: Verzeichnis aus $backup wiederherstellen (vorher .git entfernen), DB-Backup einspielen." >&2; rm -f "$DEPLOY_COPY"' ERR
else
trap 'echo; echo "!! Deploy abgebrochen. Die Seite bleibt im Wartungsmodus." >&2; echo "!! Rollback: bash $DEPLOY_SCRIPT $previous" >&2; rm -f "$DEPLOY_COPY"' ERR
fi
step "Wartungsmodus"
php artisan down --retry=60
step "Checkout $tag"
if [[ "$initial" -eq 1 ]]; then
if [[ -n "$server_only" ]]; then
while IFS= read -r file; do
mkdir -p "$parked/$(dirname "$file")"
mv "$file" "$parked/$file"
done <<< "$server_only"
echo "Nur-Server-Dateien verschoben nach $parked/"
fi
git checkout --quiet --force --detach "$tag"
: > .git/info/exclude
else
git checkout --quiet --detach "$tag"
fi
echo "$tag" > RELEASE
step "Composer"
composer install --no-dev --optimize-autoloader --no-interaction
if [[ -n "$new_migrations" ]]; then
step "Migrationen"
php artisan migrate --force
fi
for seeder in "${seeders[@]}"; do
step "Seeder $seeder"
php artisan db:seed --class="$seeder" --force
done
step "Öffentliche Assets"
if [[ ! -e public/storage ]]; then
php artisan storage:link
fi
if [[ -d public/vendor/livewire ]]; then
php artisan vendor:publish --tag=livewire:assets --force
fi
# Statisch ausgelieferte Flux-Skripte passend zur installierten Flux-Version
if [[ -d public/flux ]]; then
php artisan flux:publish-assets --force
fi
step "Caches"
php artisan config:cache
php artisan route:cache
php artisan view:clear
php artisan view:cache
php artisan event:cache
if [[ "$clear_schedule" -eq 1 ]]; then
php artisan schedule:clear-cache
fi
step "Worker und PHP-FPM"
php artisan queue:restart
default_fpm_reload="echo '' | sudo -S service $PHP_FPM_SERVICE reload"
fpm_reload="${DEPLOY_FPM_RELOAD-$default_fpm_reload}"
if [[ -n "$fpm_reload" ]]; then
bash -c "$fpm_reload"
else
echo "PHP-FPM jetzt im Server-Panel neu laden (Reload, nicht Restart), danach Enter."
read -r
fi
step "Wartungsmodus aus"
php artisan up
step "Version"
if [[ -n "$ABOUT_SECTION" ]]; then
php artisan about --only="$ABOUT_SECTION"
else
cat RELEASE
fi
trap - ERR
# Läuft ein Worker? `queue:restart` oben beendet ihn; neu starten muss ihn
# Supervisor. Läuft Supervisor nicht, steht die Queue still, ohne dass es
# jemand merkt.
if [[ "$QUEUE_PROBE" -eq 1 ]]; then
step "Queue-Worker"
if ! php artisan monitoring:queue-probe --wait=60; then
echo
echo "!! Deploy $tag ist live, aber KEIN Queue-Worker läuft." >&2
echo "!! Jetzt Supervisor und den Worker im Server-Panel prüfen — sonst bleiben Jobs und Mails liegen." >&2
exit 3
fi
fi
echo
echo "Deploy $tag fertig (vorher $previous). Weiter mit den Prüfungen aus der Deploy-Anleitung."