tests / release (push) Blocked by required conditionsDetails
Der Waechter ist in der Konsole sichtbar, samt seinem Ruecktritt und seinem
Schweigen. Die Vertragsversion des Wirt-Helfers wird gemeldet, bevor jemand
einen Knopf drueckt, der daran scheitert. Die Tunnel-Rettung ist ein Knopf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Waechter, die Vertragsversion des Wirt-Helfers und die Tunnel-Rettung
brauchten bisher die Kommandozeile auf dem Wirt. Bei zehn Servern zehnmal.
WAECHTER: Er redete nur ins systemd-Journal — und das liegt auf dem Wirt,
waehrend die Konsole in einem Container laeuft. Sie sah ihn also gar nicht.
Genau das hat am 4. August die Fehlersuche gekostet: er hielt die Sperre, der
Agent kam nicht an die Arbeit, und die einzige Stelle, an der das gestanden
haette, war unerreichbar. Er hinterlaesst jetzt seinen Ausgang, einschliesslich
des Ruecktritts, und die Konsole sagt auch, wenn er seit Minuten schweigt.
WIRT-HELFER: Hebt ein Release seine Vertragsversion an, braucht jeder Wirt
einmal "sudo bash deploy/install-agent.sh". Das wird gemeldet statt
automatisiert, und zwar bevor jemand einen Knopf drueckt, der daran scheitert.
Die sudoers-Grenze bleibt: sie gewaehrt dem Dienstbenutzer genau drei benannte
Befehle, und etwas Root-Eigenes, das ungeprueft aus dem beschreibbaren Checkout
ausfuehrt, gaebe jedem, der je an diesen Benutzer kommt, Root auf dem Wirt.
Nebenbei aufgeloest: die gebrauchte Vertragsversion stand zweimal im Repo, und
kein Test hielt die beiden Enden mehr zusammen.
TUNNEL-RETTUNG: Ein Knopf, ueber den bestehenden Postkasten. Kein zweiter Weg.
Der rote Faden war, dass die Konsole in JEDEM Zustand die Wahrheit sagt, und er
ist dabei dreimal gerissen: ein gescheiterter Tunnelaufbau meldete erst
"geheilt", nach dem Fix "nichts zu tun", und die Rettung meldete "erfolgreich",
wo das Skript bei seinen wichtigsten Teilfehlschlaegen 0 zurueckgab. Alle drei
behoben und im gerenderten HTML nachgewiesen — samt Gegenprobe, dass ein
gesunder Lauf NICHT warnt. Ein Warnkasten, der immer steht, wird nach zwei
Tagen nicht mehr gelesen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C1: rescue-tunnel.sh zaehlt Probleme (kein Zugang geladen, conntrack ohne
Root) und gibt sie als Exit-Code zurueck, statt bei jedem Fehlschlag Exit 0
zu melden. Die Konsole zeigt jetzt zusaetzlich das Protokoll der letzten
Tunnel-Rettung (UpdateChannel::rescueLog(), dasselbe <details>-Muster wie
das Update-Protokoll), und "erfolgreich" ist einem zurueckhaltenderen
"durchgelaufen"/"completed" gewichen, das nicht mehr behauptet als das
Skript wirklich weiss.
I1: watchdog.sh gewann `geheilt=true` bisher unmittelbar nach `up -d`,
`--force-recreate` und `artisan up`, ohne nachzupruefen, ob der Griff
gewirkt hat. Ein neues `fehlgeschlagen`-Flag laesst einen Misserfolg den
Lauf-Ausgang gewinnen, auch wenn ein anderer Zweig im selben Lauf
erfolgreich war -- die Konsole zeigt jetzt outcome=tried statt sich hinter
outcome=healed zu verstecken.
M3: watchdog_stood_down nennt nicht mehr nur "ein Update" als Sperrenhalter
-- die Sperre haelt inzwischen auch proxy-hosts, restart, archive-key und
rescue-tunnel.
M4: das Runbook nennt den Konsolen-Knopf jetzt vor der Kommandozeile.
M7: ConfirmRescueTunnel hat jetzt einen Test, nach dem Vorbild von
ConfirmReleaseUpdateLock -- mount()s authorize und die Ereignisverdrahtung
Modal -> Seite waren ungeprueft.
Volle Suite: 3036 bestanden, 0 fehlgeschlagen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kritisch: ein doppelt genannter Name — APP_HOST auch in SITE_HOST, ein Name
zweimal in SITE_HOST, oder einer davon gleich dem Konsolennamen — liess
vpn-entrypoint.sh zwei identische Site-Bloecke schreiben. Caddy lehnt das nicht
bloss ab, es startet dann ueberhaupt nicht ("ambiguous site definition"), der
Container laeuft in eine Neustartschleife, und mit ihm ist die Konsole aus dem
Tunnel verschwunden. Also genau der Ausfall, den dieser Zweig verhindern soll,
erreicht durch einen Tippfehler in der .env. Belegt mit caddy validate in beide
Richtungen.
Dazu: clupilot:publish-tunnel-names wurde von nichts aufgerufen. Die
Gateway-Haelfte liest die .env bei jedem up -d neu, die Resolver-Haelfte wurde
einmal von Hand geschrieben und nie wieder — eine neue Installation, ein
zusaetzlicher STATUS_HOST oder ein neu angelegtes dns-hosts-Volume haetten sie
still veralten lassen. Sie laeuft jetzt im Deploy mit.
Und drei Kleinigkeiten: die Spec beschrieb den Gesundheits-Port noch nach der
alten Annahme, der Resolver uebernahm ungeprueft, was in der .env steht (auf der
Produktivmaschine steht dort noch ein Markdown-Link), und die Testattrappe
raeumte weniger auf als die echte Umsetzung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kritisch: sync_vpn_certificate() rief docker compose exec als blanke
Zuweisung unter set -Eeuo pipefail auf — schlaegt das fehl (kein
laufender vpn-gateway), beendet set -e den ganzen Agenten, still,
nachdem write_alive schon lief. Beide Zuweisungen bekommen jetzt
`|| stamp=''`, mit Begruendung im Kommentar.
Wichtig: der Gesundheits-Port antwortete immer mit 204, auch wenn das
Startskript fuer den Konsolennamen kein Zertifikat fand und den
Gateway ohne jede Seite auf 443 rendert. VPN_READY wurde dann wahr,
und ausgegebene Client-Konfigurationen nannten einen Resolver, der die
Verbindung ablehnt. vpn-entrypoint.sh prueft jetzt, ob die
Konsolen-Seite wirklich gerendert wurde, und antwortet sonst mit 503 —
wget --spider (gegen das echte caddy:2-alpine-Image verifiziert, nicht
angenommen) behandelt das als Fehlschlag, vpn_ready bleibt false, und
die bestehende Warnung in update.sh greift wieder: sie ist nicht
verschwunden, sondern hierher gewandert. Zwei Tests in
VpnGatewayConfigTest decken beide Richtungen ab; der alte Test mit der
jetzt falschen Annahme "unabhaengig von jedem Zertifikat" wich dem
Test fuer den Fall mit Zertifikat.
Kleinigkeit: ein Satz im Kommentar von sync_vpn_certificate() haelt
fest, dass die Zertifikatsliste einmalig beim Start geschrieben wird
und ein nachtraeglich ausgestelltes Zertifikat erst den naechsten
Neustart des Gateways sieht.
Erstens: die neue Zuweisung stamp="$(docker compose exec …)" ist blank und laeuft
unter set -Eeuo pipefail. Auf jedem Wirt ohne laufenden Gateway beendet sie den
ganzen Update-Agenten — nach write_alive, der Takt sieht also gesund aus,
waehrend der Update-Knopf still nicht mehr funktioniert. Der Docblock derselben
Funktion warnt vor genau dieser Falle, nur fuer sed. Jetzt "|| stamp=''".
Zweitens: der Gesundheits-Port meldete Bereitschaft, sobald der Gateway laeuft.
Seit dieses Skript Namen ohne Zertifikat auslaesst, kann er laufen und auf 443
gar nichts bedienen — VPN_READY stuende auf true und ausgegebene
Client-Konfigurationen nennten einen Resolver, der auf eine ablehnende Adresse
zeigt. Er antwortet jetzt nur mit 204, wenn der Konsolenblock wirklich gerendert
wurde. Damit erscheint auch die Betreiber-Warnung wieder, ueber die vorhandene
Pruefung in update.sh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compose mountet das Startskript aus Task 2 statt einer festen Caddyfile und
gibt ihm VPN_TUNNEL_HOSTS mit; die alte vpn.Caddyfile entfaellt. update.sh
haengt das vpn-Profil nur noch an VPN_INTERNAL_HOST, nicht mehr an
VPN_CERT_PATH/VPN_KEY_PATH, weil der Gateway jeden Namen einzeln prueft statt
mit leeren tls-Pfaden abzustuerzen. update-agent.sh ueberwacht zur
Zertifikatserneuerung die Liste, die das Startskript schreibt, mit Rueckfall
auf den alten Einzelpfad fuer einen Gateway von vor dieser Fassung.
Der Test bestand bislang auch dann, wenn das Skript ueberhaupt nichts
rendert — die einzige Behauptung war eine Verneinung. Jetzt prueft er
zusaetzlich, dass admin.clupilot.test wirklich im Ergebnis steht.
Der Test "nimmt keinen Namen auf, den niemand konfiguriert hat" behauptete nur
die Abwesenheit von files. — er waere auch dann gruen gewesen, wenn das Skript
ueberhaupt nichts gerendert haette. Die anderen Tests mit not->toContain haben
ihre positive Haelfte, dieser hatte sie nicht.
Dazu die Testanzahl im Schritt nachgezogen: durch die Vorabkorrektur an der
Zertifikatsliste sind es sieben, nicht sechs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vorabcheck vor dem ersten Dispatch. sync_vpn_certificate() in update-agent.sh
ueberwacht GENAU EINEN Pfad aus VPN_CERT_PATH und startet den Gateway neu, wenn
er sich aendert — weil Caddys tls-Direktive die Datei nur beim Start liest. Mit
mehreren Namen haette eine Erneuerung von www. keinen Neustart ausgeloest, und
der Tunnel liefe mit einem abgelaufenen Zertifikat weiter. Genau der Ausfall,
den die Funktion laut ihrem eigenen Kommentar verhindern soll, nur eine Ebene
groesser.
Das Startskript schreibt jetzt mit, welche Zertifikate es geladen hat; die Wache
liest diese Liste und faellt auf den alten Pfad zurueck, solange ein Gateway von
vor dieser Fassung laeuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Startskript des Gateways ist im Plan keine Skizze: es lief. Mit einem Namen
mit Zertifikat, einem ohne, einem mit Zertifikat aber ohne Schluessel und einem
leeren Feld — die drei letzten werden ausgelassen und benannt, der
Gesundheits-Port steht unabhaengig davon, Rueckgabewert 0.
Dabei drei Stellen nachgezogen: eine if-Abfrage statt der AND-OR-Liste, an der
install-agent.sh sich schon einmal selbst beendet hat, und "find -print -quit"
mit "|| true" statt einer Pipe nach head, die SIGPIPE liefern kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Am Telefon mit aktivem VPN zeigt app.clupilot.com die Platzhalterseite. Nicht
falsch eingestellt, sondern nie gebaut: der Tunnel traegt nur 10.66.0.0/24, und
der interne Resolver biegt genau einen Namen um — den der Konsole. Portal,
Website und Statusseite loesen oeffentlich auf und gehen am Tunnel vorbei.
Der Gateway sucht seine Zertifikate kuenftig selbst und laesst Namen ohne
Zertifikat aus, statt gar nicht zu starten. Sonst nimmt ein fehlendes Zertifikat
fuer www. den Tunnel-Zugang zur Konsole mit — genau der Ausfall, vor dem
update.sh heute warnt.
files. bleibt absichtlich draussen: ein Server im Rettungssystem holt dort sein
Archiv und ist per Definition nicht im Tunnel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Der Festnageln-Knopf stand weiter rechts als der Bildschirm hergibt. Zweiter
Anlauf am selben Fehler: Ich hatte shrink-0 von sm auf lg verschoben, statt es zu
entfernen. Bei 1024 px nimmt die Seitenleiste 248 — fuer zwei Knoepfe, ein
Auswahlfeld und Festnageln bleibt zu wenig.
shrink-0 ist jetzt ganz weg. flex-wrap allein genuegt: passt es nebeneinander,
steht es nebeneinander; passt es nicht, bricht es um. Keine geratene
Breakpoint-Zahl mehr, die beim naechsten Geraet wieder danebenliegt.
148 Layout-Tests gruen.
tests / release (push) Blocked by required conditionsDetails
Auf dem Tablet lief die Zeile ueber den Rand: vier Elemente — zwei Knoepfe, das
Auswahlfeld und der Festnageln-Knopf — in einem Container, der ab sm auf
shrink-0 stand und damit nicht mehr schrumpfen durfte.
Das war mein eigener Fix von vorhin. sm greift schon bei 640 px, ein Tablet hat
768 — genau dazwischen sass der Fehler.
Jetzt wird die Leiste erst ab lg starr. Bis dahin bleibt sie volle Breite und
umbricht, was auf Telefon UND Tablet richtig ist. Das Auswahlfeld bekommt
zusaetzlich eine Obergrenze, damit eine lange Versionsliste die Zeile nicht
aufblaeht, und dieselbe Hoehe wie die Knoepfe daneben.
150 Layout-Tests gruen.
CRITICAL: der wg0-Fix aus Runde 0 (geheilt=true erst nach der zweiten
Probe) liess einen fehlgeschlagenen Rettungsversuch auf outcome=idle
fallen -- die einzige Zeile, die actions zeigte, war der healed-Zweig.
Die Konsole meldete "nichts zu tun", waehrend kein Host erreichbar war.
Neuer @elseif ($watchdog['actions'])-Zweig vor @else, text-warning,
Schluessel watchdog_tried (de/en). Kein Eingriff in watchdog.sh noetig.
IMPORTANT: rescue_last_run wurde geschrieben, aber von keinem Blade
gelesen, und write_status idle (ohne Argument) verschluckte den
Agentenfehler. write_status idle "$RESCUE_ERROR" wie beim proxy-hosts-
Zweig daneben; UpdateChannel::state() liefert rescue_last_run jetzt
strukturiert (finished_at als Carbon, error uebersetzt); neue Zeile in
der Update-Karte, neuer Uebersetzungscode update_error.rescue_failed.
Fuenf neue Tests, alle nachweislich rot gegen den vorherigen Stand
(per git stash isoliert geprueft) und gruen mit dem Fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neue Anfrageart rescue-tunnel im bestehenden Update-Postkasten (kein
zweiter Weg): UpdateChannel::requestRescueTunnel(), der Agent fuehrt
deploy/rescue-tunnel.sh mit Frist aus, die Konsole zeigt Ergebnis,
Waechter-Stand und Wirt-Helfer-Vertrag in der Update-Karte, Bestaetigung
im Modal (R23) nach dem Muster von ConfirmReleaseUpdateLock.
Zusatzfix in deploy/watchdog.sh: der wg0-Block meldete geheilt=true
schon, bevor geprueft war, ob wg-quick up wg0 wirklich gewirkt hat --
ein fehlgeschlagener Tunnelaufbau haette der Konsole "healed" vorgemacht.
geheilt wird jetzt erst nach der zweiten wg-show-Probe gesetzt, mit
Regressionstest in WatchdogVisibilityTest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Sie standen bisher nur in Arbeitsdateien unter .superpowers/, und die sind
nicht Teil des Repos — beim naechsten Aufraeumen waeren sie weg. Das Register
ist der Ort, an dem der Betreiber sie sieht.
Zwei davon sind echte Fehler im laufenden Betrieb:
Die Zahlungssperre ruft shutdownVm() unbedingt, ohne zu fragen, ob der Gast
laeuft. Ein Kunde, dessen Maschine aus anderem Grund steht, bekommt seine
Sperre nie — der Lauf wirft, jede Nacht neu, und die Nachricht geht nie hinaus.
Gefunden, als eine geschaerfte Proxmox-Attrappe aufdeckte, dass neun
Mahnwesen-Pruefungen nur deshalb gruen waren, weil die alte Attrappe die Bitte
gegen eine stehende Maschine klaglos annahm.
Und ein abgebrochener vzdump laeuft nach dem Wurf weiter und schreibt sein
Vollarchiv fertig — auf dieselbe Ablage, auf der die naechtlichen Sicherungen
aller anderen Kunden dieses Hosts liegen.
Damit stehen zwoelf Punkte im Register, aber KEINER mehr in der Gruppe
"verkauft und nicht geliefert". Das war heute Morgen anders.
Seit Task 2 steht in update.sh HOST_STEP_NEEDS="$(release_host_step_needs)"
statt einer nackten Zahl. Str::between() zog daraufhin die
Kommandoersetzung selbst aus dem Text, (int) davon war 0 — der Test verglich
3 gegen 0 und war rot. Der vorgegebene Filter HostStepContract|... traf
HostStepTest.php nicht (falscher Teilstring), deshalb fiel das erst im
Review auf.
Die rechte Seite des Vergleichs ruft jetzt release_host_step_needs() aus
deploy/lib/release.sh tatsächlich per Shell auf (gleiches Muster wie
HostStepContractTest), statt sie aus update.sh herauszulesen. Damit kommen
beide verglichenen Zahlen aus zwei verschiedenen Dateien
(install-agent.sh CONTRACT vs. release.sh release_host_step_needs) und die
Kopplung ist wieder erzwungen — verifiziert, indem CONTRACT testweise auf 4
gesetzt wurde und der Test daraufhin rot wurde ("4 is identical to 3"), dann
zurückgesetzt.
tests / release (push) Blocked by required conditionsDetails
$running steht mit "app\nredis" vorbelegt — ein echter Zeilenumbruch. Ohne
Anführungszeichen in den case-Zweig interpoliert, zerfiel der Zweig in zwei
Shell-Anweisungen: `printf '%s\n' app` und ein eigenständiges `redis`. Die
Attrappe meldete auf `ps` also nur einen Dienst, während `config` weiterhin
beide nannte — der Wächter sah eine Lücke, die es nicht gab, und heilte sie.
Hier bislang folgenlos, weil keine Zusicherung an der Entscheidung des Wächters
hängt. Die nächste hätte still danebengelegen und nach einem Wächter-Fehler
ausgesehen, nicht nach einem Attrappen-Fehler.
Dieselbe Stelle wurde in daeea1d (feature/wirt-konsole) für
WatchdogVisibilityTest.php auf demselben Weg gerichtet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Zwei Zusatzmodule waren kaeuflich und lieferten nichts:
Office mit vollem Funktionsumfang (22,80 EUR/Monat) — in den Kundeninstanzen
laeuft ueberhaupt keine Office-Software. Die Vorlage traegt nur nextcloud und
db, die Bereitstellung installiert keine Office-App, und Nextcloud bringt von
sich aus keine mit. Beworben wurde es an drei Stellen: als Merkmal auf der
Landingpage, als Paketmerkmal in Team und Business, und als Zusatzmodul.
Zweiter Sicherungsort (6 EUR/Monat) — die naechtliche Sicherung laeuft, aber
der zweite Ort, mit dem geworben wird, existiert nicht.
Beide sind jetzt gesperrt, und zwar an BEIDEN Wegen: sie verschwinden von der
Preistafel UND eine Buchung am Formular vorbei wird abgewiesen. Ein Modul, das
die Seite nicht mehr zeigt und das ein direkter Aufruf noch buchen kann, waere
nicht geschlossen.
Jedes hat seinen EIGENEN Schalter, weil sie an verschiedenen Voraussetzungen
haengen: Office am Dokumentenserver, der Sicherungsort am Speicherserver. Sie
werden zu verschiedenen Zeitpunkten wieder freigegeben.
Bestandsbuchungen sind unberuehrt — der Riegel gilt nur fuer neue. Auf dieser
Maschine gab es keine; der Live-Betrieb ist von hier aus nicht einsehbar und
gehoert nachgesehen.
Office wird gebaut: ONLYOFFICE ueber einen gemeinsamen Dokumentenserver.
ONLYOFFICE hat im Mai 2026 die Verbindungsgrenze der freien Fassung gestrichen,
es faellt also keine Lizenzgebuehr an.
tests / release (push) Blocked by required conditionsDetails
extra_backups verspricht einen zweiten, getrennten Sicherungsort, den es
nicht gibt (nur die erste, tägliche Sicherung läuft). Gesperrt nach dem
Muster von collabora_pro, aber mit eigener Konstante
(EXTRA_BACKUPS_ON_SALE statt OFFICE_ON_SALE) und eigenem Abweisungssatz,
weil beide Module aus unterschiedlichen Gründen fehlen und zu
unterschiedlichen Zeitpunkten zurückkommen. Keine Bestandsbuchung
betroffen (subscription_addons enthielt auf dieser Maschine keine Zeile
dazu). OpenWork-Eintrag von SOLD auf PLANNED umgestellt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 2 hat HOST_STEP_NEEDS=3 in update.sh durch release_host_step_needs()
ersetzt (deploy/lib/release.sh). Dieser Test prüfte bislang den nackten
literalen String und wäre sonst der einzige verbliebene Ort, der die alte
Verdopplung verlangt.
tests / release (push) Blocked by required conditionsDetails
Der Vortag nahm Office nur von der Preistafel — ein eingeloggter Team-
oder Business-Kunde hätte collabora_pro technisch weiterhin über
Billing::purchase() buchen können, weil AddonCatalogue den Preis
unverändert kannte und die Abrechnungsseite nie danach fragte. Der
Schalter (OFFICE_ON_SALE) wandert deshalb vom LandingController in
AddonCatalogue, der einzigen Stelle, die Preistafel UND Abrechnungsseite
kennen; eine neue Methode saleRefusal() weist die Buchung an beiden
Enden ab (Kauf-Aufruf und Karten-Anzeige), ohne BookAddon/GrantAddon
anzufassen — Bestandsfälle und Geschenke des Betreibers bleiben möglich.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Waechter redete bisher nur ins Journal auf dem Wirt — die Konsole
im Container sieht ihn also nicht. Er schreibt jetzt zusaetzlich
storage/app/deploy/watchdog-last-run.json (atomar, .tmp + mv) mit
Ausgang (idle/healed/stood_down) und den say()-Meldungen des Laufs.
WatchdogLog::lastRun() liest das robust (fehlend/kaputt -> null,
stale-Erkennung nach 5 Minuten).
Die mitgelieferte Testvorlage hatte selbst einen Fehler: die
docker-Attrappe setzte ihren mehrzeiligen Vorgabewert ungequotet in
generierten Shell-Code ein, wodurch die "ps"-Antwort einen Dienst
verschluckte und der idle-Test faelschlich "healed" sah. Behoben
durch Anfuehrungszeichen um den eingesetzten Wert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Waechter sichtbar machen, Wirt-Helfer ehrlich melden, Tunnel-Rettung als
Anfrageart. Alle drei folgen dem Muster, das Agent und Update-Kanal schon
benutzen: der Wirt schreibt Zustand als JSON in den Bind-Mount, die Konsole
liest ihn, und neue Handlungen gehen durch den bestehenden Postkasten.
Beim Wirt-Helfer wird bewusst NICHT automatisiert: sudoers gewaehrt dem
Dienstbenutzer genau drei benannte Befehle, und etwas Root-Eigenes, das
ungeprueft aus dem beschreibbaren Checkout ausfuehrt, gaebe jedem, der je an
diesen Benutzer kommt, Root auf dem Wirt. Gemeldet wird dafuer vollstaendig
und BEVOR jemand einen Knopf drueckt, der daran scheitert.
Nebenbei aufgeloest: die gebrauchte Vertragsversion stand zweimal im Repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Office (ONLYOFFICE über einen künftigen gemeinsamen Dokumentenserver) wurde
an drei Stellen beworben, ohne dass irgendwo im Repository ein Dokumentenserver
existiert: der Kachel "Office im Browser" auf der Preistafel, dem Paketmerkmal
`office` in Team und Business, und dem Zusatzmodul `collabora_pro` für
22,80 €/Monat. Alle drei sind jetzt stillgelegt, aber nicht gelöscht — ein
einziger dokumentierter Schalter (LandingController::OFFICE_ON_SALE) plus eine
neu veröffentlichte Planversion holen das Versprechen zurück, sobald der
Dokumentenserver steht.
Das Paketmerkmal wird über das im Katalog bereits etablierte Handover-Muster
entfernt (neue Migration, analog zu switch_to_new_plan_ladder): die laufende
Version von Team/Business wird geschlossen und durch eine identische ohne
`office` ersetzt. Bestehende Verträge bleiben auf ihrer alten, eingefrorenen
Version stehen und behalten das Merkmal unverändert.
Die dritte Planversion für Team/Business hat 24 Bestandstests berührt, die
eine feste Versionsnummer oder eine feste Preis-/Versionszahl annahmen —
repariert, überwiegend durch dynamisches Lesen der aktuellen Version statt
eines eingetippten Werts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bei zehn Servern liess sich eine Auslieferung nicht staffeln: entweder alle
nehmen die neueste Version oder niemand. Der einzige Griff, der eine bestimmte
setzt, war RELEASE=vX.Y.Z bash deploy/update.sh auf der Kommandozeile.
Der Kern ist eine geklemmte Variable. Der Agent uebergab in Zeile 669 ohnehin
schon RELEASE="$TARGET_RELEASE"; wird die an der Decke geklemmt, faellt
`behind` aus derselben Rechnung, und Knopf wie Wartungsfenster folgen von
selbst. Task 4 belegt genau das mit einem Test, der KEINEN Produktivcode
braucht: es gibt keinen zweiten Weg in eine Auslieferung.
Die Decke faellt zu, nicht auf. Unlesbar, formwidrig oder ins Leere zeigend
heisst: nichts wird angeboten. Ein Rueckfall auf "neueste Version"
installierte genau das, wovon weggenagelt wurde.
Nicht enthalten: Zurueckrollen. Das ist verboten (update.sh:222), und der
Datenbank-Schnappschuss, auf den die Fehlermeldung dort verweist, wird
nirgends genommen. Eigene Baustelle, ihr fehlendes Stueck ist der
Schnappschuss, nicht der Knopf.
Unterwegs gefunden und mitbehoben: zwei Stellen, an denen eine Zuweisung aus
einer Kommandoersetzung unter set -e + pipefail den Agenten toetete, BEVOR er
eine Statusdatei schreiben konnte (sync_vpn_certificate, release_manifest_
version) — dieselbe Ausfallart, die die Konsole eine nie endende Pruefung
zeigen laesst.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sechs Befunde aus dem Schlussreview, in einer Welle behoben:
- BEFUND 1 (Important): eine leere Auswahl im Festnageln-Feld liess
pinRelease() ueber `$this->ceilingChoice ?: null` in setCeiling(null)
laufen — die Gegenhandlung (Decke abnehmen) — und meldete dabei die
Erfolgsmeldung des Festnagelns. ConfirmPinRelease::confirm() schickt die
Version jetzt als Event-Nutzlast (wie ConfirmSaveSecret den Schluessel),
und pinRelease(string $version) weist eine leere Version ausdruecklich
ab, mit einer eigenen Meldung (release_pin_empty).
- BEFUND 2 (Minor, durch 1 miterledigt): Modal und Seite lasen bisher zwei
getrennte Eigenschaften ($version vs. $ceilingChoice). Der Fix oben
beseitigt die Trennung.
- BEFUND 3 (Important, Text only): der Kommentar bei release_tag_exists()
in deploy/lib/release.sh und der Fehlerbehandlungs-Abschnitt der Spec
behaupteten, ceiling_missing schuetze gegen einen vom Release-Prozess
geloeschten Tag. Tut es nicht: `git fetch --tags --force` (ohne
--prune-tags, bewusst) entfernt keine lokal bereits geholten Tags, die
drueben verschwunden sind. Beide Stellen beschreiben jetzt, wogegen die
Pruefung tatsaechlich schuetzt (ein nie geholter oder nie existierender
Tag) und wogegen nicht. Kein --prune-tags hinzugefuegt.
- BEFUND 4 (Minor): ConfirmPinRelease hatte keinen Test. Zwei neue Tests
nach dem Vorbild von ConfirmSaveSecret in IntegrationsPageTest.
- BEFUND 5 (Minor): ceilingChoice wurde nie aus dem gesetzten Zustand
vorbelegt. UpdateChannel::ceiling() ist jetzt public, Settings::mount()
belegt das Feld damit vor.
- BEFUND 6 (Minor): eine von Hand geleerte Deckendatei liest die Konsole
als "keine Decke" (ceiling() -> null), der Agent meldet dafuer aber
ceiling_error. Der "Decke abnehmen"-Knopf stand nur hinter
@if($update['ceiling']) und verschwand damit genau in dem Zustand, aus
dem er zurueckfuehren muesste. Bedingung erweitert auf
($update['ceiling'] || $update['ceiling_error']).
Jeder Befund traegt einen eigenen Test in ReleaseCeilingConsoleTest.php.
Volle Suite: 2973 passed (10389 assertions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fuenf Befunde aus der abschliessenden Review, in einer Runde behoben:
- EnvFileEditor::backup() liess PHPs copy() die Umask entscheiden statt den
Modus der Quelle zu uebernehmen — .env stand mit deploy/install.sh auf 0600,
jede Sicherung landete trotzdem weltlesbar bei 0644, mit APP_KEY,
DB_PASSWORD, VPN_CONFIG_KEY und STRIPE_SECRET darin. Reproduziert (per
kurzzeitigem git stash des Fixes: 420 statt 384) und jetzt durch einen
expliziten chmod nach dem Kopieren sowie einen neuen Test verhindert.
- HostSeparationTest pruefte "jede Route hat einen Hostnamen" nur scheinbar
allgemein — deploy/install.sh schreibt ADMIN_HOST_EXCLUSIVE=false als
Vorgabe, und im nicht-exklusiven Fallback registriert routes/web.php jede
/admin/*-Route absichtlich ohne Domain. Der Test setzte zwar
ADMIN_HOST_EXCLUSIVE=true, sagte aber nirgends, dass genau das die
Voraussetzung der Pruefung ist. hostSeparationTable() gibt AdminArea::
isExclusive() jetzt als Out-Parameter zurueck, gelesen waehrend die zweite
Anwendung noch gebootet ist, und der Test besteht darauf.
- clupilot:bind-hosts existierte, aber nichts sagte einem Operator, dass es
ihn braucht. deploy/update.sh druckt jetzt einen eigenen Hinweis, wenn
APP_HOST in .env leer ist — mit der vollen docker-compose-Zeile statt der
internen in_app-Abkuerzung, weil der Operator sie in seiner eigenen Shell
eintippt.
- ask STATUS_DOMAIN und ask FILES_DOMAIN versprachen "blank to keep it auf
jedem Host", fuellten Enter aber ueber den dritten ask()-Parameter mit dem
Default. Fuer FILES_DOMAIN war das kein Schoenheitsfehler: der Default
verschiebt /bootstrap.tar.gz vom Portal weg, bevor DNS fuer den neuen Namen
existiert. Beide Defaults entfernt.
- ask WWW_DOMAIN erklaerte nicht, dass SITE_HOST mehrere kommagetrennte Namen
traegt (der erste kanonisch, der Rest leitet dauerhaft um) — ein Operator,
der die Apex-Domain zusaetzlich zu www. binden wollte, hatte keinen Weg,
das aus dem Prompt zu erfahren. Nur der Prompt-Text geaendert, kein neuer
Prompt, Default unveraendert.
Voller Testlauf: 2953 passed (Baseline 2952 + der neue Backup-Berechtigungs-
Test), 0 failed. routes/web.php, RestrictAdminHost, config/fortify.php und
PublicSiteGate unangetastet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wie zuerst geschrieben haette er den Anhaenge-Zweig getroffen und waere auch
gegen die fehlerhafte Fassung gruen gewesen. Der Implementer hat das gemerkt und
SITE_HOST= leer vorbelegt — genau die Form, die .env.example ausliefert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
apply() ersetzte eine vorhandene Zeile per preg_replace($pattern, $line, ...)
- $line kommt von der Befehlszeile, und preg_replace deutet $1/\1/\\ im ERSATZ
als Rueckverweis, auch ohne Klammern im Muster. Ein Hostname mit solchen
Zeichen wuerde still verstuemmelt in die Datei geschrieben, die jedes
Geheimnis der Installation haelt - und EnvFileEditor::isValidLine() kann das
nicht fangen, das Ergebnis ist syntaktisch weiter KEY=value. apply() arbeitet
jetzt zeilenweise ohne jede Regex im Ersatzpfad.
Zusaetzlich: EnvFileEditor::write() wirft InvalidEnvContentException, wenn der
neue Inhalt nicht parst - das war ungefangen und zeigte dem Betreiber einen
Stapelabzug auf der Zugangsdatendatei statt eines Satzes wie jeder andere
Fehlerpfad in diesem Befehl.
Neuer Test deckt den Rueckverweis-Fall ab, ueber die bereits vorhandene
(leere) SITE_HOST-Zeile - der Pfad, den preg_replace tatsaechlich traf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
preg_replace deutet $1, \1 und \\ im ERSATZ als Rueckverweise, auch ohne
Gruppen im Muster. Der Wert kommt von der Befehlszeile, das Ziel ist die Datei
mit allen Zugangsdaten, und das Ergebnis waere still verstuemmelt statt
abgelehnt — isValidLine() sieht weiterhin ein gueltiges KEY=value.
Jetzt zeilenweise, ohne Regex auf dem Schreibweg. Dazu faengt der Befehl
InvalidEnvContentException ab, statt dem Betreiber einen Stapelabzug zu zeigen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Am Haupt-Checkout arbeiten parallel andere Sitzungen. Deren unfertige Aenderungen
gerieten in den eigenen Testlauf und haben eine Fehlersuche in die falsche
Richtung geschickt. Der Worktree ist jetzt eigenstaendig: vendor hartverlinkt,
.env kopiert, storage-Verzeichnisse angelegt.
Ein Symlink auf vendor taeugt dafuer nicht — Composer leitet seine Basispfade aus
dem aufgeloesten Verzeichnis ab und laedt dann die fremden Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Testcode im Plan sicherte $_SERVER und stellte daraus zurueck. phpunit haelt
seine <env>-Werte aber in $_ENV und putenv() — gemessen: ADMIN_HOSTS und
FILES_HOST fehlen in $_SERVER voellig. Das Zurueckstellen loeschte sie damit,
und jeder Test danach las ADMIN_HOSTS aus der echten .env des Containers. Die
Suite fiel an zwei Stellen, die mit Hostnamen nichts zu tun haben.
Gefunden hat es nicht der Filterlauf, den der Plan verlangte — der war gruen.
Gefunden hat es der Vergleich zweier voller Laeufe, mit und ohne die Aenderung:
2939 gruen gegen zwei Fehlschlaege. Deshalb verlangt Schritt 5 jetzt den vollen
Lauf und nennt den Kontrollversuch beim Namen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beides beim Vorabcheck aufgefallen: git switch main scheitert in einem
Worktree, in dem main anderswo ausgecheckt ist, und main ist waehrend des
Planens auf v1.8.2 weitergezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Testcode im Plan ist keine Skizze. Er lief: 123 Routen, zwoelf ohne Domain,
und die Original-Anwendung danach lesend UND schreibend intakt. Letzteres nicht
selbstverstaendlich — die zweite Anwendungsinstanz zeigt Eloquents statischen
Connection-Resolver auf ihre eigene, leere :memory:-Datenbank, und der halbe
Testlauf faende danach keine Tabelle mehr. Genau so ist es beim ersten Versuch
passiert; das Zurueckstellen steht deshalb mit Begruendung im Plan.
Ebenfalls vorher geprueft statt geraten: dass Pest-Expectations eine eigene
Fehlermeldung als zweites Argument nehmen, und dass dynamische Properties in
beforeEach hier Hausstil sind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Hostnamen-Trennung ist gebaut und wird nie eingeschaltet. Gemessen: mit
gesetzten APP_HOST/SITE_HOST/STATUS_HOST/FILES_HOST bleiben von 123 Routen
zwoelf ohne Domain, und davon sind zehn die begruendeten. Fortify haengt
laengst an fortify.domain, livewire/* bleibt geteilt, PublicSiteGate behaelt
seine Routennamen. Die .env der laufenden Maschine kennt die vier Schluessel
nur nicht — weil install.sh nach den Namen fragt und die Antworten wegwirft,
und dabei an einer Funktion stirbt, die erst dreizehn Zeilen spaeter definiert
wird.
Der zweite Entwurf haelt eine Entscheidung fest, bevor sie gebaut wird: wer ein
Release veroeffentlichen kann, kann damit die sudoers aller Server aendern. Das
ist der Preis dafuer, dass der root-Helfer sich selbst nachzieht, und er wird
bewusst bezahlt. Das Dienstkonto bekommt dabei nichts dazu — deshalb ein
root-eigener Spiegel und nicht der Checkout, den es besitzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 5: Bedienoberfläche zum Festnageln einer Release-Version, in der
bestehenden Update-Handlungsleiste statt einer zweiten daneben.
Bestätigt im Modal (R23), Auswahlfeld selbst ohne Modal (R20).