Entwurf: Release-Decke — gezielt vorwaerts festnageln aus der Konsole
tests / pest (push) Waiting to run Details
tests / assets (push) Waiting to run Details
tests / release (push) Blocked by required conditions Details

Bei zehn Servern laesst sich eine Auslieferung heute nicht staffeln: entweder
alle nehmen die neueste Version oder niemand. Der einzige Griff, der eine
bestimmte setzt, ist `RELEASE=vX.Y.Z bash deploy/update.sh` auf der
Kommandozeile — zehnmal.

Zwei Funde haben den Entwurf geformt:

- Zurueckrollen ist kein fehlender Knopf, sondern eine bewusst verbotene
  Handlung (update.sh:222). Die Fehlermeldung dort verweist auf einen
  Datenbank-Schnappschuss vor dem Update — den nimmt niemand, in update.sh
  steht kein einziger Dump. Echtes Zurueckrollen ist deshalb eine eigene
  Baustelle, und ihr fehlendes Stueck ist der Schnappschuss, nicht der Knopf.
- `clupilot:auto-update` haette einen einmaligen Sprung beim naechsten
  Wartungsfenster sofort wieder auf die neueste Version gehoben. Festnageln
  muss also eine stehende Obergrenze sein, sonst haelt es nicht.

Der Ansatz ist dadurch klein: der Agent uebergibt in Zeile 669 ohnehin schon
`RELEASE="$TARGET_RELEASE"`. Wird diese eine Variable auf die Decke geklemmt,
folgt alles andere — `behind` faellt aus derselben Rechnung, und Knopf wie
Automatik lesen beide `UpdateChannel::state()`. Kein zweiter Weg in eine
Auslieferung.

Entscheidend beim Fehlerverhalten: die Decke faellt zu, nicht auf. Eine
unlesbare oder ins Leere zeigende Decke darf nicht auf „neueste Version"
zurueckfallen — das installierte genau das, wovon weggenagelt wurde. Dass ein
Tag verschwindet, ist dabei kein Randfall: der Release-Prozess loescht falsche
Tags und ueberspringt die Nummer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/versandtakt
nexxo 2026-08-04 14:25:21 +02:00
parent 6de7bf9db8
commit cddba14993
1 changed files with 194 additions and 0 deletions

View File

@ -0,0 +1,194 @@
# Release-Decke: gezielt vorwärts festnageln — Entwurf
**Stand:** 2026-08-04
**Zustand:** Vom Betreiber abschnittsweise freigegeben. Umfang, Semantik,
Ansatz, Fehlerverhalten und Testschnitt sind entschieden.
## Worum es geht
Ein CluPilot-Server nimmt heute immer die **neueste** Version. Wer zehn Server
betreibt, kann eine Auslieferung nicht staffeln: entweder alle bekommen v1.8.1,
oder niemand. Der einzige Griff, der eine bestimmte Version setzt, ist
```
sudo -u clupilot env RELEASE=vX.Y.Z bash /opt/clupilot/deploy/update.sh
```
— Handarbeit auf der Kommandozeile, zehnmal.
Gewünscht ist derselbe Griff in der Konsole.
## Was das ausdrücklich NICHT ist
**Kein Zurückrollen.** `deploy/update.sh:222` weist einen Rückschritt
ausdrücklich ab, und das bleibt so:
> *„Backwards is not an update. The database has already been migrated forward,
> and older code against a newer schema is the one failure this script exists to
> prevent — with the added cruelty that the migrations needed to roll back are
> not in the older checkout at all."*
Die Fehlermeldung des Skripts verweist auf einen Datenbank-Schnappschuss „taken
before the upgrade" — **den nimmt heute niemand.** In `update.sh` steht kein
`mysqldump`, kein `mariadb-dump`, kein Schnappschuss irgendeiner Art. Der
Ausweg, den das Skript nennt, existiert nicht.
Echtes Zurückrollen ist deshalb eine **eigene Baustelle**, und ihr fehlendes
Stück ist der Schnappschuss, nicht der Knopf. Sie ist hier bewusst nicht
enthalten.
## Was entschieden ist
| Frage | Entscheidung |
|---|---|
| Umfang | Gezielt **vorwärts** festnageln, kein Rückschritt |
| Semantik | **Stehende Obergrenze**, keine einmalige Bewegung |
| Ablage | Datei `storage/app/deploy/release-ceiling` im Checkout |
| Wirkung | Agent klemmt `TARGET_RELEASE`/`BEHIND` — alles Weitere folgt |
| Fehlerverhalten | Die Decke **fällt zu, nicht auf** |
| Berechtigung | `site.manage`, Operator-Guard (R21) |
### Warum eine stehende Obergrenze und kein einmaliger Sprung
`clupilot:auto-update` nimmt im Wartungsfenster, was verfügbar ist, und legt
dieselbe Anfrage ab wie der Knopf — **ohne** `RELEASE`, also immer auf die
neueste Version. Ein einmaliger Sprung auf v1.8.0 wäre beim nächsten
Fensteröffnen sofort wieder auf v1.8.1 gehoben. Festnageln, das nicht hält, ist
kein Festnageln.
### Warum eine Datei und keine Einstellung in der Datenbank
Der Agent läuft auf dem **Wirt** und sieht die Datenbank nicht. Er ist aber die
Stelle, die `behind` und `target_release` ausrechnet. Läge die Decke in der
Datenbank, müssten Knopf *und* Automatik sie jeweils selbst anwenden — zwei
Wege, zwei Regelwerke, die auseinanderlaufen. `AutoUpdate.php` reklamiert im
Kopfkommentar ausdrücklich das Gegenteil für sich:
> *„There is no second path into a deployment, because a second path is a second
> set of rules to keep in step with the first."*
Schlimmer noch: `behind`/`available` würden weiter gegen den neuesten Tag
gerechnet. Die Konsole meldete „1 Aktualisierung verfügbar" für eine Version,
die sie nie installieren wird.
### Warum nicht die bestehende `release_mode.source`
Die Datei gibt es schon und sie hält den festgenagelten Ref — aber
**`update.sh` schreibt sie nach jedem Lauf selbst**. Eine Decke, die dort läge,
würde von genau der Auslieferung überschrieben, die sie begrenzen soll. Und sie
vermischte „wo stehe ich" mit „wie weit darf ich".
## Wie es wirkt
Der Angelpunkt ist eine Zeile, die es schon gibt —
`deploy/update-agent.sh:669`:
```bash
RELEASE="$TARGET_RELEASE" "$ROOT/deploy/update.sh" > "$RUNLOG" 2>&1
```
Der Agent übergibt sein Ziel bereits ausdrücklich. Wird `TARGET_RELEASE` auf die
Decke geklemmt, geht ein Lauf exakt dorthin — ohne dass `update.sh` etwas von
Decken wissen muss.
Und weil `BEHIND` aus derselben Rechnung fällt, sagt `available` von selbst die
Wahrheit. Knopf und Automatik lesen beide `UpdateChannel::state()`; keiner von
beiden bekommt eine Sonderregel.
## Bauteile
| Bauteil | Änderung |
|---|---|
| `deploy/lib/release.sh` | `release_newest_tag` und `release_tags_ahead` bekommen ein optionales Decken-Argument. Beide liegen schon dort, „so a test can run them". |
| `deploy/update-agent.sh` | Liest `release-ceiling`, prüft sie, klemmt `NEWEST_TAG`/`BEHIND`, meldet zusätzlich `ceiling`, `ceiling_error` und `releases` in `update-status.json`. |
| `UpdateChannel` | `setCeiling(string $by, ?string $tag)` schreibt bzw. entfernt die Datei **atomar** und legt zusätzlich eine `KIND_CHECK`-Anfrage ab. `state()` reicht `ceiling`, `ceiling_error`, `releases` durch. |
| `Settings.php` + Blade | Auswahlfeld aus `releases`, „Festnageln" / „Decke abnehmen", Bestätigung im Modal (R23). |
### Datenfluss
1. Konsole schreibt `release-ceiling` (www-data im Container, Bind-Mount).
2. Konsole legt zusätzlich eine `KIND_CHECK`-Anfrage ab.
3. Die Pfad-Einheit `clupilot-update-agent.path` weckt den Agenten binnen einer
Sekunde — sonst zöge die Anzeige erst beim nächsten Takt nach.
4. Agent rechnet `behind`/`target_release` gegen die Decke, schreibt den Status.
5. Konsole zeigt „festgenagelt auf v1.8.0".
Die Decke selbst ist **keine Anfrage an den Agenten**, sondern Zustand, den er
liest. Die `KIND_CHECK`-Anfrage daneben dient nur der Auffrischung der Anzeige.
### Wählbare Versionen
Der Agent meldet die Tags, die als Decke in Frage kommen: alle `v*` ab der
ausgelieferten Version aufwärts, auf die **20 neuesten** begrenzt. Ohne Grenze
wüchse die Statusdatei mit der Tag-Historie mit.
**Die ausgelieferte Version selbst steht mit in der Liste**, und sie ist der
häufigste Fall: „hier einfrieren, nichts Neues nehmen". Genau das braucht der
Besitzer für die neun Server, die auf v1.8.0 bleiben sollen, während einer
v1.8.1 bekommt.
## Zwei Zustände, die klar lesen müssen
Beides sind gültige Lagen, keine Fehler:
- **Decke unter dem Ausgelieferten** (festgenagelt auf v1.8.0, läuft v1.8.1):
`behind = 0`. Die Konsole sagt „festgenagelt, aber bereits weiter" — **nicht**
„aktuell". Zurück geht es nicht, siehe oben.
Über das Auswahlfeld ist das nicht erreichbar (es bietet erst ab der
ausgelieferten Version an). Erreichbar ist es trotzdem, und zwar auf dem Weg,
den es weiter geben wird: die Decke steht auf v1.8.0, jemand fährt auf der
Kommandozeile `RELEASE=v1.8.1 update.sh`. Die Lage muss also lesbar sein,
statt als Fehler zu erscheinen.
- **Decke gleich dem Neuesten**: verhält sich wie heute ohne Decke.
## Fehlerbehandlung
**Der Kern: die Decke fällt zu, nicht auf.**
Ist die Datei unlesbar, halb geschrieben, oder zeigt sie auf einen Tag, den es
nicht (mehr) gibt, darf der Agent **nicht** stillschweigend auf „neueste
Version" zurückfallen — das installierte genau das, wovon der Besitzer
weggenagelt hat. Stattdessen: `behind = 0`, nichts wird angeboten, und
`ceiling_error` geht in die Statusdatei, damit die Konsole es sagen kann.
Eine Sicherung, die im Zweifel öffnet, ist keine.
Dass ein Tag verschwindet, ist hier kein Randfall: der Release-Prozess sieht
ausdrücklich vor, dass ein **falscher Tag gelöscht und die Nummer übersprungen**
wird.
Weiter:
- **Atomar schreiben.** Konsole schreibt `.tmp` und benennt um — dasselbe Muster
wie `write_alive`/`write_unblock` im Agenten. Sonst liest der Agent
irgendwann eine halbe Zeile.
- **Form und Existenz sind zwei Fragen.** `^v[0-9]+(\.[0-9]+)*$` *und* der Tag
muss vorhanden sein.
- **Ein laufender Lauf liest die Decke nicht neu.** Der nächste Takt korrigiert.
Keine zusätzliche Sperre — siehe `WatchdogLockContentionTest` dazu, warum
Sperren hier sparsam eingesetzt gehören.
## Tests
| Ebene | Was geprüft wird |
|---|---|
| `ReleaseComparisonTest` (besteht) | `release_newest_tag`/`release_tags_ahead` mit Decke — die echten Bash-Helfer |
| neu, Agent | Echtes Skript gegen ein Wegwerf-Repo mit Tags: Decke gesetzt → `behind` zählt nur bis dorthin, `target_release` **ist** die Decke |
| neu, fällt zu | Decke zeigt auf gelöschten Tag → `behind = 0` **und** `ceiling_error`, nicht „neueste" |
| neu, Automatik | `clupilot:auto-update` mit gesetzter Decke tut nichts |
| neu, Konsole | Festnageln schreibt die Datei, „Decke abnehmen" löscht sie, ohne `site.manage` geht beides nicht |
Der Automatik-Test ist der wichtigste: er belegt, dass Knopf und Automatik
denselben Zustand lesen — die Behauptung, auf der der ganze Entwurf ruht. Ohne
ihn ist „es gibt keinen zweiten Weg" eine Absichtserklärung im Kommentar.
## Was hier bewusst fehlt
- **Zurückrollen** und der dafür nötige Datenbank-Schnappschuss — eigene
Baustelle, siehe oben.
- **Flottenweites Setzen** („alle zehn Server auf v1.8.0"). Jeder Server hat
seine eigene Konsole; eine flottenweite Steuerung ist ein anderes Produkt.
- **Zeitgesteuertes Anheben** („ab Freitag v1.8.1 erlauben"). Das
Wartungsfenster gibt es schon; eine zweite Zeitsteuerung daneben wäre eine
zweite Regel, die mit der ersten in Takt gehalten werden müsste.