#!/usr/bin/env bash # # Deploy eines Git-Tags auf Live (mivita.care, Ploi). Anleitung: dev/betrieb/README.md # # Aufruf im App-Verzeichnis auf dem Server: # # cd /home/ploi/mivita.care # bash dev/betrieb/deploy.sh v1.2.0 # ohne neue Migrationen # bash dev/betrieb/deploy.sh v1.2.0 --migrations # mit Migrationen, DB-Backup vorher! # # 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). # # Umgebung (optional): # DEPLOY_FPM_RELOAD Befehl zum Neuladen von PHP-FPM (Standard siehe unten). # Leer setzen, wenn FPM von Hand in Ploi neu geladen wird — # dann wartet das Skript auf Enter. Nie „Restart“: der # entfernt kurz den Socket, Nginx liefert 502. # # Exit-Codes: 0 fertig, 1 abgebrochen (siehe Ausgabe), 2 falscher Aufruf, # 3 live, aber Horizon hat den Test-Job nicht abgeholt. # # Rollback: dasselbe Skript mit dem vorherigen Tag. # # Bewusst KEIN route:cache und KEIN config:cache: # - Die Routen hängen vom Host ab (RouteServiceProvider::loadDomainAwareRoutes). # Ein auf der Kommandozeile erzeugter Cache enthielte nur die main-Routen — # Shop, CRM und Portal wären weg. # - Mehrere Stellen lesen env() außerhalb von config/ (z. B. env('APP_KEY') in # App\User::isPasswort). Mit gecachter Config liefert env() dort null. set -euo pipefail # ---- Projekt-Einstellungen -------------------------------------------------- DEPLOY_SCRIPT="dev/betrieb/deploy.sh" # Pfad dieses Skripts im Repository PHP_FPM_SERVICE="php8.4-fpm" # systemd-Name des FPM-Dienstes ABOUT_SECTION="mivita" # Abschnitt aus AboutCommand::add (AppServiceProvider) QUEUE_PROBE=1 # 1 = monitoring:queue-probe am Ende # ----------------------------------------------------------------------------- # 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 for option in "$@"; do case "$option" in --migrations) run_migrations=1 ;; --clear-schedule) clear_schedule=1 ;; *) 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] [--clear-schedule]" >&2 exit 2 fi if [[ ! -f artisan || ! -d .git ]]; then echo "Im App-Verzeichnis aufrufen (artisan und .git fehlen hier)." >&2 exit 2 fi step() { printf '\n==> %s\n' "$*"; } 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)" echo "Live: $previous → Ziel: $tag" if [[ "$(git rev-parse HEAD)" == "$(git rev-parse "$tag^{commit}")" ]]; then echo "$tag ist bereits ausgecheckt. Nichts zu tun." exit 0 fi new_migrations="$(git diff --name-only --diff-filter=A HEAD "$tag" -- database/migrations)" 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 trap 'echo; echo "!! Deploy abgebrochen. Die Seite bleibt im Wartungsmodus." >&2; echo "!! Rollback: bash $DEPLOY_SCRIPT $previous" >&2; rm -f "$DEPLOY_COPY"' ERR step "Wartungsmodus" php artisan down --retry=60 step "Checkout $tag" git checkout --quiet --detach "$tag" 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 step "Caches leeren" php artisan config:clear php artisan route:clear php artisan event:clear php artisan view:clear if [[ "$clear_schedule" -eq 1 ]]; then php artisan schedule:clear-cache fi step "Horizon und PHP-FPM" # Horizon hält den alten Code im Speicher; terminate beendet es nach den # laufenden Jobs, Supervisor (Ploi-Daemon) startet es neu. php artisan horizon:terminate 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 in Ploi neu laden (Reload, nicht Restart), danach Enter." read -r fi step "Wartungsmodus aus" php artisan up step "Version" php artisan about --only="$ABOUT_SECTION" trap - ERR # Läuft Horizon wieder? `horizon:terminate` oben beendet es; neu starten muss # es 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 Horizon hat den Test-Job nicht abgeholt." >&2 echo "!! Jetzt php artisan horizon:status und den Horizon-Daemon in Ploi 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 dev/betrieb/README.md."