deploy: Git-Deploy per Tag — dev/betrieb/deploy.sh, Ablauf und einmalige Umstellung in git-deploy.md, Runbook §10; public/build wird versioniert
- deploy.sh: bricht ohne Änderung ab bei Abweichung im Checkout oder neuen Migrationen ohne --migrations; down, Checkout, RELEASE, Composer, Caches, queue:restart, FPM, up; Rollback = derselbe Aufruf mit dem vorherigen Tag - git-deploy.md: Forgejo-Zugang, Umwandlung von ~/pressekonto.com in einen Checkout von v1.1.0 ohne Dateiänderung, Release lokal, Prüfungen, Rollback - Assets werden lokal gebaut und committet, auf dem Server kein Node Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd688e5a7a
commit
129de4fc76
4 changed files with 351 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,7 +1,6 @@
|
|||
# Laravel
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor
|
||||
|
|
|
|||
|
|
@ -372,7 +372,10 @@ Newsroom-Übersicht, eine alte PM-Detailseite (lädt Bild + Anhang?), Firmenlogo
|
|||
## 10. Betrieb nach Go-Live (Stand 20.07.2026 — System ist LIVE)
|
||||
|
||||
Live-Setup: Hetzner-Cloud-Server via **Ploi**, App unter `~/pressekonto.com`,
|
||||
Deploys laufen per **SFTP-Datei-Upload** (kein Git-Deploy). Echte Domains
|
||||
Deploys liefen bis `v1.1.0` per **SFTP-Datei-Upload**. **Ab `v1.1.1` per Git-Tag**
|
||||
mit `dev/betrieb/deploy.sh`; Ablauf, einmalige Umstellung und Rollback in
|
||||
`dev/betrieb/git-deploy.md`. Die SFTP-Checkliste unten gilt nur noch bis zur
|
||||
Umstellung. Echte Domains
|
||||
(pressekonto.com, presseecho.de, businessportal24.com) inkl. www-DNS geschaltet,
|
||||
Stripe im Live-Mode, `legacy:verify` 0 failures. Migration/Cutover: erledigt,
|
||||
Doku archiviert unter `dev/archive/optimize/cutover-runbook.md`.
|
||||
|
|
|
|||
145
dev/betrieb/deploy.sh
Executable file
145
dev/betrieb/deploy.sh
Executable file
|
|
@ -0,0 +1,145 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Deploy eines Git-Tags auf Live (Ablauf und Hintergrund: dev/betrieb/git-deploy.md).
|
||||
#
|
||||
# Aufruf im App-Verzeichnis auf dem Server:
|
||||
#
|
||||
# cd ~/pressekonto.com
|
||||
# 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:
|
||||
# PK_FPM_RELOAD Befehl zum Neuladen von PHP-FPM
|
||||
# (Standard: echo "" | sudo -S service php8.4-fpm reload).
|
||||
# Leer setzen, wenn FPM von Hand in Ploi neu geladen wird.
|
||||
#
|
||||
# Rollback: dasselbe Skript mit dem vorherigen Tag.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Git ersetzt diese Datei beim Checkout. Bash liest Skripte stückweise, deshalb
|
||||
# läuft der Deploy aus einer Kopie.
|
||||
if [[ -z "${PK_DEPLOY_COPY:-}" ]]; then
|
||||
copy="$(mktemp)"
|
||||
cp "$0" "$copy"
|
||||
PK_DEPLOY_COPY="$copy" exec bash "$copy" "$@"
|
||||
fi
|
||||
trap 'rm -f "$PK_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 dev/betrieb/deploy.sh 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 dev/betrieb/deploy.sh $previous" >&2; rm -f "$PK_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
|
||||
|
||||
if [[ -d public/vendor/livewire ]]; then
|
||||
php artisan vendor:publish --tag=livewire: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 php8.4-fpm reload'
|
||||
fpm_reload="${PK_FPM_RELOAD-$default_fpm_reload}"
|
||||
if [[ -n "$fpm_reload" ]]; then
|
||||
bash -c "$fpm_reload"
|
||||
else
|
||||
echo "PHP-FPM jetzt in Ploi neu laden (Server ▸ PHP ▸ Restart), danach Enter."
|
||||
read -r
|
||||
fi
|
||||
|
||||
step "Wartungsmodus aus"
|
||||
php artisan up
|
||||
|
||||
step "Version"
|
||||
php artisan about --only=pressekonto
|
||||
|
||||
trap - ERR
|
||||
echo
|
||||
echo "Deploy $tag fertig (vorher $previous). Weiter mit den Prüfungen in dev/betrieb/git-deploy.md, Abschnitt D."
|
||||
202
dev/betrieb/git-deploy.md
Normal file
202
dev/betrieb/git-deploy.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Git-Deploy
|
||||
|
||||
**Stand:** 06.10.2026 · **Status:** vorbereitet, Umstellung steht aus (erstes Release über Git: `v1.1.1`)
|
||||
|
||||
Bis `v1.1.0` gingen Deploys als Paket per SFTP auf den Server
|
||||
([`deploy-v1.1.0.md`](deploy-v1.1.0.md)). Künftig ist `~/pressekonto.com` ein
|
||||
Git-Checkout, und ausgeliefert wird ein Tag aus `origin`.
|
||||
|
||||
## Grundsätze
|
||||
|
||||
- **Remotes:** `origin` = `ssh://git@forge.adametz.media:222/kadmin/pressekonto.git`
|
||||
(Forgejo auf Hetzner, privat). Von dort holt der Live-Server.
|
||||
`NAS` = `git.adametz.media` im lokalen Netz, nur als Backup.
|
||||
- **Ausgeliefert wird nur ein Tag** (`vX.Y.Z`, SemVer wie bisher), nie ein Branch-Stand.
|
||||
- **Deploy nur von Hand** per SSH mit [`deploy.sh`](deploy.sh). Ploi-„Quick Deploy"
|
||||
und Push-Webhooks bleiben aus.
|
||||
- **Assets werden lokal gebaut und committet.** `public/build` ist versioniert
|
||||
(2,4 MB, 18 Dateien). Auf dem Server gibt es kein Node.
|
||||
- **Auf dem Server wird nichts von Hand geändert.** Das Skript bricht ab, sobald
|
||||
der Checkout vom Tag abweicht. Hotfixes laufen ebenfalls über Commit, Tag und Deploy.
|
||||
- **Kein `git clean` auf Live.** `.env`, `storage/`, `vendor/`, `auth.json`,
|
||||
`RELEASE` und `public/storage` sind ignoriert und bleiben unberührt.
|
||||
- **Kein Zero-Downtime-Modus mit Release-Verzeichnissen.** `storage/app` (Archiv,
|
||||
Quarantäne, Exporte) bleibt an seinem Ort. Deploy im selben Verzeichnis mit
|
||||
`php artisan down`, wie bisher.
|
||||
- `dev/`, `tests/` und die Doku liegen damit auch auf dem Server, aber außerhalb
|
||||
von `public/`. Ausgeliefert werden sie nicht.
|
||||
|
||||
## A — Einmalig: Zugang vom Server zu Forgejo
|
||||
|
||||
1. Auf dem Server (Benutzer der App) prüfen: `git --version`.
|
||||
2. Deploy-Key erzeugen und in Forgejo **nur lesend** am Repository eintragen
|
||||
(Repository ▸ Einstellungen ▸ Deploy-Schlüssel):
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/forge_pressekonto -N "" -C "pressekonto-live"
|
||||
cat ~/.ssh/forge_pressekonto.pub
|
||||
```
|
||||
|
||||
3. `~/.ssh/config` ergänzen:
|
||||
|
||||
```
|
||||
Host forge.adametz.media
|
||||
Port 222
|
||||
User git
|
||||
IdentityFile ~/.ssh/forge_pressekonto
|
||||
IdentitiesOnly yes
|
||||
```
|
||||
|
||||
4. Host-Key aufnehmen und den Fingerprint mit dem in Forgejo angezeigten vergleichen:
|
||||
|
||||
```bash
|
||||
ssh-keyscan -p 222 forge.adametz.media >> ~/.ssh/known_hosts
|
||||
ssh-keygen -lf <(ssh-keyscan -p 222 forge.adametz.media 2>/dev/null)
|
||||
```
|
||||
|
||||
5. Test: `git ls-remote ssh://git@forge.adametz.media:222/kadmin/pressekonto.git`
|
||||
zeigt `refs/tags/v1.1.0`.
|
||||
6. FPM-Neustart ohne Ploi testen: `echo "" | sudo -S service php8.4-fpm reload`.
|
||||
Geht das nicht, beim Deploy `PK_FPM_RELOAD=` setzen. Das Skript wartet dann,
|
||||
bis FPM in Ploi neu geladen ist.
|
||||
|
||||
## B — Einmalig: Live-Verzeichnis in einen Checkout umwandeln
|
||||
|
||||
Live läuft auf `v1.1.0`. Das Verzeichnis wird an Ort und Stelle zum Checkout
|
||||
dieses Tags. Dabei ändert sich zunächst keine Datei. Git zeigt nur, wo der
|
||||
Server vom Tag abweicht.
|
||||
|
||||
7. Dateibackup wie bei v1.1.0:
|
||||
|
||||
```bash
|
||||
cd ~ && tar czf backup-vor-git.tgz --exclude=./storage --exclude=./node_modules -C ~/pressekonto.com .
|
||||
```
|
||||
|
||||
8. Repository anlegen und auf `v1.1.0` setzen, **ohne** Dateien anzufassen:
|
||||
|
||||
```bash
|
||||
cd ~/pressekonto.com
|
||||
git init -q
|
||||
git remote add origin ssh://git@forge.adametz.media:222/kadmin/pressekonto.git
|
||||
git fetch --tags origin
|
||||
git update-ref --no-deref HEAD "$(git rev-parse 'v1.1.0^{commit}')"
|
||||
git read-tree HEAD
|
||||
```
|
||||
|
||||
9. Abweichungen sichten:
|
||||
|
||||
```bash
|
||||
git status --porcelain | grep -v -E '^.. (dev/|docs/|_docs/|tests/|\.agents/|\.codex/|\.cursorrules|\.devcontainer/|\.github/|\.mcp\.json|AGENTS\.md|CLAUDE\.md|boost\.json|docker-compose\.yml|phpstan|phpunit\.xml|pint\.json)'
|
||||
```
|
||||
|
||||
Herausgefiltert sind die Entwicklerdateien, die nie im Paket waren. Was danach
|
||||
übrig bleibt:
|
||||
|
||||
| Zeile | Bedeutung | Vorgehen |
|
||||
|---|---|---|
|
||||
| ` M datei` | Datei auf Live weicht vom Tag ab | `git diff -- datei` ansehen. Fehlt die Änderung im Repository, erst lokal nachziehen, neuer Tag, dann weiter. Sonst ist es ein Paketrest und wird überschrieben. |
|
||||
| ` D datei` | Datei fehlt auf Live | Meist Entwicklerdatei oder ein nie hochgeladener Teil. Einordnen, der Checkout legt sie an. |
|
||||
| `?? datei` | nur auf Live, nicht ignoriert | **Nicht löschen.** Nach `~/vor-git/` verschieben, wenn sie nicht gebraucht wird. Wird sie gebraucht, gehört sie in die `.gitignore` (lokal, neuer Tag). |
|
||||
|
||||
10. Wenn nichts Unerklärtes übrig ist, auf den Tag auschecken:
|
||||
|
||||
```bash
|
||||
git checkout -q -f --detach v1.1.0
|
||||
git status --porcelain # muss leer sein
|
||||
```
|
||||
|
||||
Der Code ist danach inhaltlich derselbe wie vorher. Ein Neustart ist nicht nötig.
|
||||
|
||||
## C — Release lokal (jedes Mal)
|
||||
|
||||
11. `main` sauber, volle Suite und Larastan grün (wie bisher).
|
||||
12. Assets bauen und mitcommitten:
|
||||
|
||||
```bash
|
||||
npm ci && npm run build
|
||||
git add public/build
|
||||
git diff --cached --quiet public/build || git commit -m "build: Assets für vX.Y.Z"
|
||||
```
|
||||
|
||||
Vite benennt Dateien nach ihrem Inhalt. Ohne Frontend-Änderung entsteht kein Diff.
|
||||
|
||||
13. Tag setzen und auf beide Remotes pushen:
|
||||
|
||||
```bash
|
||||
git tag -a vX.Y.Z -m "vX.Y.Z — <Inhalt>"
|
||||
git push origin main --follow-tags
|
||||
git push NAS main --follow-tags
|
||||
```
|
||||
|
||||
## D — Deploy auf Live (jedes Mal)
|
||||
|
||||
14. Bringt der Tag Migrationen mit: **DB-Backup** ziehen (`ARCHIVE_KEY` getrennt gesichert).
|
||||
15. Neue `.env`-Werte laut Release-Notiz vorher eintragen.
|
||||
16. Deploy:
|
||||
|
||||
```bash
|
||||
cd ~/pressekonto.com
|
||||
bash dev/betrieb/deploy.sh vX.Y.Z # ohne Migrationen
|
||||
bash dev/betrieb/deploy.sh vX.Y.Z --migrations # mit Migrationen
|
||||
```
|
||||
|
||||
Das Skript macht Folgendes:
|
||||
- Es prüft, dass der Checkout sauber ist, und holt die Tags.
|
||||
- Es zeigt neue Migrationen an. Ohne `--migrations` bricht es dann ab, **bevor** etwas verändert ist.
|
||||
- Danach in dieser Reihenfolge: `down`, Checkout, `RELEASE`, `composer install`, gegebenenfalls `migrate`, Livewire-Assets, Caches, `queue:restart`, FPM, `up`, Version.
|
||||
|
||||
`--clear-schedule` setzt zusätzlich die `withoutOverlapping`-Sperren zurück.
|
||||
Das ist nur nötig, wenn sich Sperrdauern im Scheduler geändert haben.
|
||||
Darf eine Migration länger auf Sperren warten:
|
||||
`DB_LOCK_WAIT_TIMEOUT=300 bash dev/betrieb/deploy.sh vX.Y.Z --migrations`.
|
||||
|
||||
17. Prüfungen:
|
||||
- `/health` mit Token liefert `"version":"vX.Y.Z"`.
|
||||
- Admin ▸ Reports ▸ Betriebsüberwachung ist grün.
|
||||
- `storage/logs` zeigt keine neuen Fehler.
|
||||
- Smoke-Test aus dem Runbook (Abschnitt 8).
|
||||
- Hat sich der Scheduler geändert: `php artisan schedule:list`.
|
||||
|
||||
Schlägt ein Schritt fehl, bleibt die Seite im Wartungsmodus. Das Skript nennt
|
||||
den Rollback-Befehl.
|
||||
|
||||
## E — Rollback
|
||||
|
||||
```bash
|
||||
cd ~/pressekonto.com
|
||||
bash dev/betrieb/deploy.sh <vorheriger Tag>
|
||||
```
|
||||
|
||||
Liegt die Abbruchstelle nach dem Checkout, ist der Checkout zwar sauber, aber
|
||||
`vendor/` und die Caches können halb erneuert sein. Der Rollback-Aufruf baut
|
||||
beides für den alten Tag neu auf. Migrationen sind additiv. Der alte Code läuft
|
||||
mit den neuen Spalten, ein `migrate:rollback` gibt es auf Live nicht. Nur bei
|
||||
Datenproblemen das DB-Backup einspielen und danach Runbook Abschnitt 9 (Archiv) befolgen.
|
||||
|
||||
## F — Erste Umstellung mit `v1.1.1`
|
||||
|
||||
`v1.1.1` dient nur dazu, den neuen Weg zu testen. Das Release enthält keine
|
||||
Migration und keine Fachänderung.
|
||||
|
||||
| Schritt | Inhalt |
|
||||
|---|---|
|
||||
| lokal | `/public/build` aus der `.gitignore`; `deploy.sh`, diese Datei und Runbook §10 committen; Doku-Commits seit `v1.1.0`; Build-Commit; Tag `v1.1.1`; Push auf beide Remotes |
|
||||
| Server | A (Zugang), B (Umwandlung auf `v1.1.0`), dann `bash dev/betrieb/deploy.sh v1.1.1` |
|
||||
| Prüfung | D 17. Zusätzlich `git status --porcelain`: Bis `v1.1.0` war `public/build` ignoriert, ab `v1.1.1` ist es versioniert. Alte Build-Reste auf Live (z. B. `public/build/admin/`, `public/build/assets/`, `public/build/manifest.json` aus der stillgelegten `vite.config.js`) erscheinen erst jetzt als `??`. Nach `~/vor-git/` verschieben, sonst bricht der nächste Deploy an der Abweichungsprüfung ab. |
|
||||
| danach | `~/deploy-v1.1.0.tgz`, `~/backup-vor-v1.1.0.tgz` und nach ein paar stabilen Tagen `~/backup-vor-git.tgz` löschen |
|
||||
|
||||
## Später: Changelog
|
||||
|
||||
Der Changelog-Entwurf
|
||||
([Decision-Update](../2026-10/Decision-Update_Changelog_Automatisierung_2026-10-04.md),
|
||||
Stufe 2) dockt in `deploy.sh` nach `up` an. Ein Artisan-Befehl liest die Commits
|
||||
zwischen dem vorherigen und dem neuen Tag. Die liegen jetzt auf dem Server vor.
|
||||
Daraus legt er die Entwürfe an. Der LLM-Schritt läuft als Queue-Job, damit ein
|
||||
Ausfall des Anbieters keinen Deploy abbricht.
|
||||
|
||||
## Protokoll
|
||||
|
||||
| Datum | Schritt | Ergebnis |
|
||||
|---|---|---|
|
||||
| 06.10.2026 | Vorbereitung | `origin` auf Forgejo (Hetzner) umgestellt, NAS als zweites Remote, Push erledigt. `deploy.sh` gegen ein simuliertes Repository geprüft: Abbruch bei neuen Migrationen ohne Option, Deploy, wiederholter Aufruf, Rollback, unbekannter Tag, Fehler mitten im Lauf. Die Umwandlung eines bestehenden Verzeichnisses (B) wurde ebenso simuliert: Abweichungen werden sichtbar, `.env` und ungetrackte Dateien bleiben, ignorierte Builds werden beim Checkout überschrieben. |
|
||||
| 06.10.2026 | `v1.1.1` lokal | `/public/build` versioniert. Alte Build-Reste von 2025 lokal entfernt (nicht mehr referenziert). Suite 2.316 grün, einer übersprungen (parallel, 96 s); Larastan ohne Meldung. Build-Commit und Tag `v1.1.1` gesetzt, Push auf `origin`/`NAS` nach Freigabe. |
|
||||
Loading…
Add table
Add a link
Reference in a new issue