Das Auswahlfeld des Browsers sah aus wie das Auswahlfeld des Browsers. Hochgeladen wird jetzt über eine Ablagefläche: Bilder hineinziehen oder klicken, darunter steht, welche Dateien gewählt sind und welche übersprungen wurden. Darunter liegt weiter ein echtes input[type=file] - unsichtbar, aber fokussierbar, mit der Fläche als label und dem Fokusring über :focus-within. Ohne JavaScript bleibt genau das übrig: ein Dateifeld mit Beschriftung. Das Skript ergänzt Ziehen und benennt die Auswahl; abgeschickt wird nie von selbst. Kein "required", weil der Browser seine Meldung nicht an etwas Unsichtbares hängen kann - der Server weist einen leeren Upload mit einem lesbaren Satz ab. docs/UPDATE.md plant das Update über einen Git-Tag: Vorprüfung, Sicherung, Wartungszustand mit 503 für die Website und offenem /admin, Checkout, composer, Migration, Cache, Rauchtest, Protokoll - und den Rückweg in derselben Reihenfolge rückwärts. Migrationen bleiben nicht umkehrbar, der Rückweg ist die Sicherung. Kein Updater, der eine Adresse aus dem Formular zieht, und kein automatisches Update im Hintergrund. Zwei Entscheidungen hängen am Hosting: Composer auf dem Server oder vendor/ im Tag, und ob es Shell-Zugriff gibt. Gebaut ist davon nichts. 219 PHP-Tests / 881 Assertions unter PHP 8.4.22, darunter Upload über das Formular mit zwei Dateien, CSRF-Pflicht, Versionskonflikt, Archivfilter und Blättern mit 26 Bildern. Playwright prüft die Ablagefläche am echten Dateifeld; axe bleibt auf elf Bildschirmen ohne Verstöße. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.4 KiB
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
VERSIONim Projektstamm mit dem Tag, zum Beispiel1.4.0. Sie ist Teil des Tags selbst. - Tags heißen
v<major>.<minor>.<patch>. 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.
- Vorprüfung.
gitvorhanden und aufrufbar (proc_openist auf geteiltem Hosting oft abgeschaltet – dann endet der Updater hier mit einer klaren Meldung und dem Konsolenweg). Arbeitsverzeichnis unverändert (git status --porcelainleer), sonst würde ein Checkout lokale Änderungen verwerfen. Schreibrechte auf Projektstamm,var/und Medienordner. PHP-Version erfülltcomposer.json. Freier Plattenplatz mindestens die Größe der Datenbank plus 100 MB. - Ziel bestimmen.
git fetch --tags origin, dann den höchstenv*-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. - Sichern. Datenbank kopieren (bei SQLite Datei, bei MySQL
mysqldump), dazu.envund den aktuellen Commit-Hash, nachvar/backups/update-<zeitstempel>/. 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. - Wartungszustand an. Eine Datei
var/maintenance. Öffentliche Anfragen antworten mit 503, einer erklärenden Seite undRetry-After;/adminbleibt erreichbar, sonst kann niemand das Update beenden. Suchmaschinen verstehen 503 als „später wieder“, nicht als „weg“. - Code wechseln.
git checkout --force <tag>. Danach prüfen, dassVERSIONden erwarteten Wert hat – erst das beweist, dass der Wechsel wirklich stattgefunden hat. - Abhängigkeiten.
composer install --no-dev --optimize-autoloader, wenn Composer auf dem Server läuft. Wenn nicht – der wahrscheinliche Fall auf geteiltem Hosting – mussvendor/Teil des Tags sein. Das ist die Entscheidung, die ansteht: entweder Composer auf dem Server, oder ein Releasebranch, in demvendor/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. - 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 eindown(). - Cache.
cache:clear --env=prodund Aufwärmen. Der Grund steht indocs/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. - Rauchtest. Intern
/,/admin/loginund/sitemap.xmlabrufen und auf 200 prüfen, dazu eine Abfrage aufpageundrevision. Schlägt das fehl, gilt das Update als gescheitert, auch wenn jeder Schritt davor gemeldet hat, dass er fertig ist. - 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:
- Wartungszustand bleibt an.
git checkout --force <alter Commit>.- Abhängigkeiten des alten Stands wiederherstellen (derselbe Fall wie Schritt 6).
- 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.
- 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.--checkprüft nur,--to=v1.4.0wählt ein Ziel,--dry-runlä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
- Composer auf dem Server oder
vendor/im Tag? Hängt am Hosting aus Arbeitspaket J. - 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.
- 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.