- 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>
294 lines
11 KiB
Bash
Executable file
294 lines
11 KiB
Bash
Executable file
#!/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."
|