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>
This commit is contained in:
parent
fcb10be120
commit
b5ed756d67
98 changed files with 3947 additions and 82 deletions
294
dev/betrieb/deploy.sh
Executable file
294
dev/betrieb/deploy.sh
Executable file
|
|
@ -0,0 +1,294 @@
|
|||
#!/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."
|
||||
Loading…
Add table
Add a link
Reference in a new issue