# Updateverfahren: neue Version über Git ziehen Stand: 12.09.2026. **Das ist ein Plan, noch keine Umsetzung.** Nichts davon ist gebaut; die Reihenfolge und die Entscheidungen stehen hier, damit sie vor dem Bauen kritisierbar sind. ## Was ein Update heißt Ein Update ist ein Wechsel des Codes auf einen bestimmten Git-Tag, gefolgt von einer Datenbankmigration. Drei Dinge werden dabei **nie** angefasst, weil in ihnen alles steckt, was nicht wiederherstellbar ist: - `var/cms.db` (beziehungsweise später MySQL) – die Inhalte, - der Medienordner aus `MEDIA_DIRECTORY` – die Originale und alle abgeleiteten Größen, - `.env` – Datenbankzugang, `APP_SECRET`, Ursprungsadresse. Alle drei stehen in `.gitignore`. Das ist die Voraussetzung dafür, dass ein `git checkout` sie nicht berührt, und der erste Punkt, den der Updater prüfen muss, statt ihn anzunehmen. ## Die Version Es braucht eine Angabe, die man ohne Git lesen kann, sonst weiß die Oberfläche nicht, worauf sie steht: - Eine Datei `VERSION` im Projektstamm mit dem Tag, zum Beispiel `1.4.0`. Sie ist Teil des Tags selbst. - Tags heißen `v..`. Ein Tag, kein Branch: ein Branch verschiebt sich unter den Füßen, ein Tag nicht. - Die Verwaltung zeigt Version, Commit-Kurzhash und das Datum des Tags, sowie die höchste angewandte Migration. Das ist die Angabe, die im Fehlerfall zählt. ## Ablauf Jeder Schritt hat eine Abbruchbedingung. Bricht ein Schritt ab, bleibt der Wartungszustand an und der Updater sagt, welcher Schritt gescheitert ist – er versucht nichts zu reparieren, was er nicht selbst angelegt hat. 1. **Vorprüfung.** `git` vorhanden und aufrufbar (`proc_open` ist auf geteiltem Hosting oft abgeschaltet – dann endet der Updater hier mit einer klaren Meldung und dem Konsolenweg). Arbeitsverzeichnis unverändert (`git status --porcelain` leer), sonst würde ein Checkout lokale Änderungen verwerfen. Schreibrechte auf Projektstamm, `var/` und Medienordner. PHP-Version erfüllt `composer.json`. Freier Plattenplatz mindestens die Größe der Datenbank plus 100 MB. 2. **Ziel bestimmen.** `git fetch --tags origin`, dann den höchsten `v*`-Tag nach Semantik, oder einen ausdrücklich gewählten. Die Herkunft (`origin`) steht in der Konfiguration, **nicht** in der Anfrage: ein Updater, der eine Adresse aus dem Formular zieht, ist eine Fernsteuerung für fremden Code. 3. **Sichern.** Datenbank kopieren (bei SQLite Datei, bei MySQL `mysqldump`), dazu `.env` und den aktuellen Commit-Hash, nach `var/backups/update-/`. Der Medienordner wird **nicht** mitkopiert – er ist zu groß, und kein Update schreibt darin. Das steht auch so in der Oberfläche, damit niemand die Sicherung für vollständig hält. 4. **Wartungszustand an.** Eine Datei `var/maintenance`. Öffentliche Anfragen antworten mit 503, einer erklärenden Seite und `Retry-After`; `/admin` bleibt erreichbar, sonst kann niemand das Update beenden. Suchmaschinen verstehen 503 als „später wieder“, nicht als „weg“. 5. **Code wechseln.** `git checkout --force `. Danach prüfen, dass `VERSION` den erwarteten Wert hat – erst das beweist, dass der Wechsel wirklich stattgefunden hat. 6. **Abhängigkeiten.** `composer install --no-dev --optimize-autoloader`, wenn Composer auf dem Server läuft. Wenn nicht – der wahrscheinliche Fall auf geteiltem Hosting – muss `vendor/` Teil des Tags sein. **Das ist die Entscheidung, die ansteht:** entweder Composer auf dem Server, oder ein Releasebranch, in dem `vendor/` mitgeliefert wird und den ein Build-Rechner erzeugt. Beides ist vertretbar, aber es muss vor dem ersten Livegang entschieden sein, weil der Updater danach anders aussieht. 7. **Migrieren.** `doctrine-migrations migrate --no-interaction`. Die Migrationen sind bewusst nicht umkehrbar: `down()` würde Spalten entfernen, in denen Inhalte stehen. Der Rückweg ist die Sicherung aus Schritt 3, nicht ein `down()`. 8. **Cache.** `cache:clear --env=prod` und Aufwärmen. Der Grund steht in `docs/QUALITY.md`: ein laufender Prozess kann einen älteren kompilierten Container halten und dann Seiten mit 500 beantworten, obwohl der Code richtig ist. Bei PHP-FPM gehört ein Neuladen dazu. 9. **Rauchtest.** Intern `/`, `/admin/login` und `/sitemap.xml` abrufen und auf 200 prüfen, dazu eine Abfrage auf `page` und `revision`. Schlägt das fehl, gilt das Update als gescheitert, auch wenn jeder Schritt davor gemeldet hat, dass er fertig ist. 10. **Wartungszustand aus, Protokoll schreiben.** Nach `var/updates.log`: Zeitstempel, alte und neue Version, angewandte Migrationen, Dauer, Ergebnis. ## Wenn es schiefgeht Der Rückweg ist derselbe Weg rückwärts, in dieser Reihenfolge, und er ist genauso Teil des Plans wie der Hinweg: 1. Wartungszustand bleibt an. 2. `git checkout --force `. 3. Abhängigkeiten des alten Stands wiederherstellen (derselbe Fall wie Schritt 6). 4. **Datenbank aus der Sicherung zurückschreiben** – aber nur, wenn Schritt 7 begonnen hatte. Sonst nicht: ein Rückschreiben verliert alles, was seit dem Update entstanden ist, und wenn keine Migration lief, ist auch nichts zu reparieren. 5. Cache räumen, Rauchtest, Wartungszustand aus. Der teuerste Fall ist ein Update, das durchläuft und erst Tage später als falsch erkannt wird. Dagegen hilft kein Updater, sondern die Versionsangabe in der Verwaltung und das Protokoll: man muss ohne Rätselraten sagen können, was wann eingespielt wurde. ## Was gebaut wird - `src/Ops/Updater.php` – die Schritte als einzeln aufrufbare Methoden, jede mit einer Prüfung statt einer Annahme, und ein Protokoll je Schritt. - `src/Command/UpdateCommand.php` (`cms:update`) – der Weg, der immer funktioniert, auch ohne Weboberfläche. `--check` prüft nur, `--to=v1.4.0` wählt ein Ziel, `--dry-run` läuft ohne Checkout und Migration. - Ein Bildschirm unter „Einstellungen“ oder „System“, **nur für Administratoren**, mit CSRF: aktuelle Version, verfügbare Version, Protokoll der letzten Updates, ein Knopf. Kein automatisches Update nach Zeitplan – ein Update ist eine Entscheidung, nicht ein Hintergrunddienst. - Wartungszustand als Abonnent, der vor allem anderen greift. - Tests: Vorprüfung schlägt bei unsauberem Arbeitsverzeichnis fehl; ein gescheitertes Update lässt den Wartungszustand an; die Sicherung entsteht vor dem Checkout; ein erfolgreiches Update schreibt Protokoll und neue Version. Geprüft gegen ein wegwerfbares Git-Repository im Temp-Verzeichnis, nicht gegen das Projekt selbst. ## Was ausdrücklich nicht gebaut wird - Kein Updater, der eine Adresse, einen Branch oder ein ZIP aus dem Formular lädt. Die Herkunft steht in der Konfiguration. - Kein automatisches Update im Hintergrund. - Kein `down()`-Rückweg für Migrationen. Der Rückweg ist die Sicherung, und das ist eine Entscheidung, nicht eine Lücke. - Kein Update ohne Sicherung, auch nicht mit Häkchen „ich weiß, was ich tue“. ## Offen, bevor das gebaut wird 1. **Composer auf dem Server oder `vendor/` im Tag?** Hängt am Hosting aus Arbeitspaket J. 2. **Wie kommt der Code auf den Server?** Git-Klon im Projektstamm setzt Shell-Zugriff voraus. Gibt es den nicht, ist der Updater in dieser Form nicht möglich, und der Weg ist ein Deployment von außen (rsync eines Releaseverzeichnisses, Umschalten eines Symlinks). Das ist derselbe Ablauf, nur mit einem anderen Schritt 5. 3. **Ein Repository oder zwei?** Für den Piloten reicht eines. Sobald mehrere Websites dasselbe CMS nutzen, gehört der Code in ein eigenes Repository und die Inhalte bleiben je Installation.