Skip to content

0014 — Deployment per rsync, Rollback als erneutes Deployment ​

Status: angenommen · Milestone: M8

Kontext ​

Die Auslieferung besteht aus zwei Teilen: dem Inhalt von dist/ und — bei forms.provider: 'standalone-php' — dem Verzeichnis des Formular-Handlers samt seinen Composer-Abhängigkeiten. Auf dem Zielsystem liegen zwei Dinge, die kein Deployment anfassen darf: config/config.local.php mit dem SMTP-Passwort und var/ mit den Rate-Limit-Zählern und dem Log (ADR 0010).

Mittwald bietet mehrere Wege. Die offizielle GitHub Action richtet sich an containerisierte Anwendungen; für ein statisches Deployment bleiben SSH-basierte Verfahren — rsync oder ein Deployment-Werkzeug wie Deployer.

Zu entscheiden sind drei Dinge: das Übertragungsverfahren, der Umgang mit dem SSH-Schlüssel und was „Rollback" hier bedeutet.

Entscheidung ​

Übertragen wird per rsync über SSH, in zwei getrennten Aufrufen. Der erste bringt dist/ in den Dokumentenstamm und nimmt das Handler-Verzeichnis aus. Der zweite bringt server/forms/ in dessen Zielverzeichnis und nimmt config/config.local.php und var/ aus. Beide mit --delete.

Was übertragen wird, entscheidet der Build. Die Integration kicktemp:deploy-plan schreibt eine Datei .deploy-plan.json neben die Ausgabe: Zielsystem, Umgebung, Hostname, ob der Handler mitgeht und wohin. Der Workflow liest sie.

Der SSH-Schlüssel wird nicht an ein fremdes Action übergeben. Der Workflow legt ihn selbst ab und pinnt den Hostschlüssel über die Variable DEPLOY_HOST_KEY. Fehlt sie, bricht er ab.

Ausgelöst wird über einen Tag v*, dazu workflow_dispatch für Staging und Rollback.

Rollback ist ein erneutes Deployment eines früheren Tags, kein Symlink-Tausch:

sh
gh workflow run deploy.yml --ref v0.6.0

Zwei Umgebungen als GitHub Environments: production baut mit PUBLIC_ENVIRONMENT=production, staging mit preview. Die Zuordnung steht im Workflow, nicht in einer Variable.

GitHub Pages bleibt als Alternative für Projekte ohne PHP-Formulare, in einem eigenen Workflow — und ausschließlich mit eigener Domain.

Begründung ​

Warum zwei rsync-Aufrufe und nicht einer. Für die beiden Teile gelten verschiedene Regeln. Ein einzelner Aufruf mit --delete über den Dokumentenstamm würde entweder das Handler-Verzeichnis löschen oder — mit gelockertem --delete — verwaiste Dateien einer früheren Version stehen lassen. Zwei Aufrufe machen die Ausnahmen sichtbar: Was in der Ausschlussliste steht, steht dort mit einem Grund.

Warum --delete überhaupt. Ohne es bleibt eine gelöschte Seite auf dem Server erreichbar, und eine Sitemap ohne Eintrag ist kein Hindernis für einen Aufruf. Verwaiste Dateien sind der Grund, aus dem Kundenwebsites Adressen aus dem Jahr 2019 ausliefern.

Warum der Plan aus dem Build kommt. Die Alternative wäre eine Repository-Variable wie DEPLOY_FORMS_HANDLER=true. Damit gäbe es zwei Stellen, an denen steht, ob ein Projekt ein PHP-Backend hat, und irgendwann sagen sie Verschiedenes. Die schlechtere Richtung wäre die gefährliche: ein hochgeladener, unkonfigurierter Formular-Endpunkt auf einem Server, der keinen haben soll. Er fiele nicht weiter auf — er antwortet mit einem Konfigurationsfehler und wartet.

Warum kein fremdes Action für rsync. Ein Deployment-Schlüssel ist Schreibzugriff auf die Kundenwebsite. Ein Action, das ihn in die Hand bekommt, ist eine Abhängigkeit mit genau diesem Recht — in zwanzig Kundenprojekten gleichzeitig. Die eigene Fassung sind zwölf Zeilen Shell und erlaubt zusätzlich, den Hostschlüssel zu pinnen statt ihn blind anzunehmen. Ohne dieses Pinning schützt die SSH-Verbindung gegen niemanden, der sich dazwischenstellen kann; und der bequeme Ausweg StrictHostKeyChecking=no steht in jeder Anleitung.

Warum Rollback kein Symlink-Tausch ist. Ein releases/-Verzeichnis mit einem Symlink current wäre schneller und in der Wirkung sauberer. Es setzt aber voraus, dass der Document Root in mStudio auf current zeigt und Apache Symlinks folgt — eine Vorbereitung von Hand, die einmal gemacht und beim einundzwanzigsten Projekt vergessen wird. Der Fehlerfall ist dabei bösartig: Das Deployment meldet Erfolg, und die Website zeigt weiter den alten Stand, weil der Document Root woanders hinzeigt. Ein erneutes Deployment eines Tags setzt keine Serverstruktur voraus, funktioniert auf jedem Mittwald-Projekt sofort und braucht keinen zweiten Mechanismus, der gepflegt werden muss. Es kostet dafür die Zeit eines vollständigen Durchlaufs samt Qualitätskette. Für eine Broschürenwebsite ist das der richtige Tausch — hier zählt, dass der Rollback funktioniert, nicht dass er dreißig Sekunden dauert.

Warum das Quality Gate im Deployment wiederholt wird. Ein Tag zeigt nicht zwingend auf einen Stand, der durch die CI gelaufen ist. Denselben Befehl noch einmal zu fahren kostet Minuten und verhindert, dass ein Deployment etwas ausliefert, das nie geprüft wurde.

Eingeschränkt durch ADR 0019: Die Kette läuft im Deployment nur noch, wenn die CI sie für genau diesen Commit nicht nachweisen kann.

Warum die Umgebungszuordnung im Workflow steht. Indexierbar ist nur PUBLIC_ENVIRONMENT=production (ADR 0006). Stünde der Wert in einer Environment-Variable, wäre ein indexierbares Staging ein Tippfehler weit weg — und eine Baustelle in den Suchergebnissen ist ein Schaden, der Wochen nachhallt. Der Workflow prüft zusätzlich, dass der gebaute Stand und das Ziel übereinstimmen.

Warum GitHub Pages nur mit eigener Domain. Project Pages liegen unter …/<repository>/. Die internen Links dieses Starters sind absolute Pfade ab der Wurzel und werden nirgends um ein Basisverzeichnis ergänzt; die Navigation lässt gar keine relativen Pfade zu (ADR 0001). Ein Unterpfad würde bauen und mit lauter toten Links ausgeliefert. Statt base durch jede Komponente, jede Sitemap-Zeile und jedes Canonical zu fädeln, lehnt die Konfigurationsprüfung einen Pfad in site.url ab und nennt den Grund.

Verworfene Alternativen ​

releases/ mit Symlink-Tausch. Siehe oben: schneller, aber mit einer Voraussetzung, die niemand im einundzwanzigsten Projekt prüft.

Deployer. Es kann Releases und Rollback von Haus aus und ist bei Mittwald dokumentiert. Es bringt aber PHP-Werkzeug und eine eigene Konfigurationssprache in ein Repository, dessen Deployment sonst aus zwei rsync-Aufrufen besteht — und es löst damit das Problem, das mit dem Symlink-Layout überhaupt erst entsteht.

Deployment bei jedem Push auf main. Schneller, aber jeder versehentliche Merge ist sofort öffentlich. Ein Release ist eine bewusste Handlung, und ein Tag drückt genau das aus.

Ein Tag, den ein Workflow mit dem voreingestellten GITHUB_TOKEN anlegt, löst allerdings keinen Lauf aus — GitHub unterbindet das, um endlose Ketten zu verhindern. Ein Kundenprojekt, das die Versionierung automatisiert, braucht dafür ein eigenes Token; sonst entsteht der Tag, und das Deployment bleibt stumm. Im Starter-Repository fällt das nicht auf, weil dort beide Deployment-Workflows abgeschaltet sind. Die Dokumentation nennt es beim Deployment.

Das Artefakt eines früheren Laufs für den Rollback wiederverwenden. Das wäre schneller als ein Neubau und exakt derselbe Stand. Es hängt aber an der Aufbewahrungsfrist der Artefakte, und ein Rollback, der nach dreißig Tagen stillschweigend nicht mehr geht, ist schlechter als einer, der immer geht.

Konsequenzen ​

  • Der Hostschlüssel muss einmal je Umgebung ermittelt werden (ssh-keyscan -t ed25519 <host>) und gegen die Angabe des Hosters geprüft werden. Ohne ihn bricht das Deployment ab — beabsichtigt.
  • Ein Rollback dauert so lange wie ein Deployment, weil er eines ist.
  • deploy.yml und deploy-pages.yml laufen nie im kanonischen Starter-Repository. Das Gegenstück ist docs.yml, die nur dort läuft — ein Repository hat genau eine Pages-Site.
  • Ein Kundenprojekt löscht nach dem Initialisieren den Workflow, den es nicht braucht. Bleibt er liegen, bricht er mit einer Meldung ab, die auf das Zielsystem in project.config.ts verweist, statt zu raten.
  • Wird dist/ von Hand kopiert, fehlen die Schritte, die kein Dateikopieren leistet: composer install --no-dev und das Auslassen von config.local.php und var/. Die Dokumentation nennt sie bei Mittwald.