Entwurf: Release-Decke — gezielt vorwaerts festnageln aus der Konsole
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
parent
6de7bf9db8
commit
cddba14993
|
|
@ -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.
|
||||
Loading…
Reference in New Issue