#!/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= 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 # ~/-vor-git-.tgz, zeigt Dateien, die es nur auf dem # Server gibt, und verschiebt sie nach Rückfrage nach ~/-vor-git-/. # 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=] [--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."