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>
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>
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).
tests / release (push) Blocked by required conditionsDetails
Die Spec fuer die Host-Konsole (docs/superpowers/specs/2026-08-01-host-
konsole-zertifikat-design.md) war auf einer eigenen DNS-Zone und Proxmox'
Alias-Modus fuer DNS-01 gebaut. Hetzner kennt keine Subzonen (docs.hetzner.
com/networking/dns/faq/zones/, Artikel NE-7597D: "Subzones are not
supported") -- damit entfallen eigene Zone, CNAME je Host, Alias-Modus und
der pro Host verteilte DNS-Token ersatzlos. Der Grund bleibt in der Spec
stehen, statt geloescht zu werden, damit niemand denselben Weg in einem
halben Jahr erneut vorschlaegt.
Weg B, wie im Register vorgegeben: CluPilot stellt zentral aus, DNS-01
ueber den vorhandenen kontoweiten Hetzner-Token, Zertifikat per SSH
(RemoteShell::putFile + `pvenode cert set --force --restart`, geprueft
gegen die Proxmox-Dokumentation) auf den Host, Erneuerung als geplanter Job
auf der Bereitstellungs-Warteschlange (dieselbe Grenze wie SyncVpnPeers --
nur queue-provisioning erreicht einen Host ueber den Tunnel). Dazu ein
Vergleich mit der Kundeninstanz (ConfigureDnsAndTls, HTTP-01) und eine
genaue Bestandsaufnahme der Bereitschaftsseite: sie kennt heute kein
Zertifikat, weder fuer Hosts noch, trotz ersten Anscheins, uebertragbar
fuer die Plattform -- CertificateSweep/ProxyHost misst nur oeffentlich
erreichbare Namen und laeuft im falschen Container fuer einen Host-FQDN.
Im Code bestaetigt und in der Spec vermerkt: RrsetId::zone() ist heute fest
auf die Kundenzone verdrahtet, ein Host-FQDN liegt aber in der
Plattformzone -- das ist Bauarbeit, keine offene Entscheidung. Offen bleibt
nur, welches Werkzeug das ACME-Protokoll auf CluPilot-Seite spricht (keine
Bibliothek/kein Tool dafuer im Repo) und ob Plattform- und Kundenzone im
selben Hetzner-Projekt liegen -- beides als offene Fragen benannt, keine
davon blockiert den Rest des Ablaufs.
Registereintrag in OpenWork.php gestrichen: die Spec beschreibt keinen
toten Weg mehr, und genau das war der einzige Punkt, den der Eintrag
festhielt.
Getestet: php artisan test --filter=OpenWork, 8 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Important-Befunde aus dem Code-Review zu Task 3:
- writeAtomic() ignorierte den Rueckgabewert von File::put()/File::move()
und meldete setCeiling() als "true", selbst wenn ein I/O-Fehler (volle
Platte, Rechteproblem) nichts geschrieben oder eine .tmp liegen gelassen
hatte. Beide Rueckgabewerte werden jetzt geprueft, eine liegen gebliebene
.tmp wird im Fehlerfall aufgeraeumt, und der Fehlschlag wird bis zu
setCeiling() durchgereicht (Rueckgabe false).
- ceiling() konnte state() doch werfen lassen: zwischen File::exists() und
File::get() liegt ein Zeitfenster, und File::get() wirft eine
FileNotFoundException, wenn die Datei dazwischen verschwindet. readJson()
und lastLog() kapseln genau dieses Muster schon in try/catch(Throwable);
ceiling() zieht jetzt nach.
Beide Befunde tragen einen eigenen Test: ein Verzeichnis an der Ceiling-
Datei-Stelle (exists() wahr, get() wirft) fuer den zweiten, eine echte
Rechteverweigerung (chmod 0500 als nicht-root Testbenutzer) fuer den
ersten -- kein Facade-Mock noetig.
Der Zähler lag auf der Rechenzentrums-ZEILE (next_host_number). Ein leeres
Rechenzentrum liess sich löschen - bewusst so entschieden, was nichts mehr
hält, soll entfernbar bleiben -, aber die Zeile nahm den Zähler mit. Wer
denselben Code neu anlegte, bekam eine frische Zeile mit dem Schema-Default
1, und der nächste Host hiess wieder <code>-01, obwohl dieser Name schon in
alten Protokollen, Sicherungen und DNS-Zwischenspeichern auf eine ANDERE
Maschine zeigt.
Der Zähler zieht deshalb in eine eigene Tabelle host_name_sequences um,
geführt über den rohen Code statt über die id der Rechenzentrums-Zeile.
ConfirmDeleteDatacenter bleibt unangetastet: das Löschen war nie das
Problem, nur was es mitriss. Die Migration überträgt den Bestand (fsn/hel)
vor dem Löschen der alten Spalte und ist gegen echtes MariaDB in beide
Richtungen geprüft (hoch, Werte kontrolliert, zurück, wieder hoch).
Neuer Test in HostNamingTest stellt den ganzen Bruch nach: Rechenzentrum
anlegen, Host vergeben, Host entfernen, über den echten Bestätigungsdialog
löschen, mit demselben Code neu anlegen - der nächste Name bleibt fortlaufend
statt wieder bei 01 zu beginnen. Gegen den unveränderten Code lief er rot
(HostName::preview lieferte nbg-01 statt nbg-02).
Registereintrag "Ein Zähler kann durch Löschen eines Rechenzentrums
zurückfallen" gestrichen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Der Registereintrag nannte den falschen Grund: das Deaktivieren startet sehr
wohl eine Provisionierung. CustomDomainAccess::deactivate() ruft seit Langem
ReapplyInstanceAddress, das legt einen Lauf der `address`-Pipeline an und
schickt AdvanceRunJob auf die provisioning-Warteschlange; ConfigureNextcloud
loescht dort trusted_domains 2 und ConfigureDnsAndTls schreibt den Router ohne
den Namen neu. Das ist gebaut und geprueft.
Der Schaden war trotzdem echt, nur eine Tuer weiter. Erreicht wurde deactivate()
allein ueber PlanChange::settleCustomDomain, also ueber den Paketwechsel. Der
zweite und haeufigere Weg, auf dem das Recht endet — der Kunde bucht das Modul
in der Abrechnung ab, clupilot:end-cancelled-addons haelt den Termin am Ende des
bezahlten Zeitraums — ging an dieser Stelle vorbei: BookAddon::cancel() lieferte
Speicher nach und sprach mit Stripe, fragte aber niemanden nach der Adresse. Die
Domain verschwand aus jeder Ansicht und blieb auf der Maschine stehen.
BookAddon::cancel() fragt jetzt CustomDomainAccess::enforce() — die ganze Regel,
nicht den Modulschluessel: wer von Team auf Business aufgestuft hat und sein
altes Modul loswird, behaelt die Domain, weil das Paket sie selbst traegt.
Und der Anstoss darf die Entscheidung nicht kippen. deactivate() faengt jetzt
einen Fehlschlag der Nachfuehrung ab und schreibt ihn als Fehler ins Log: die
Wahrheit steht in der Datenbank, die Maschine zieht nach, und eine Kuendigung
haengt nicht daran, ob ein fremder Host gerade antwortet.
Die Gegenrichtung brauchte nichts: der Entzug loescht die Domain-Spalte, also
traegt der Kunde sie nach der Neubuchung neu ein und weist sie neu nach — und
genau dort haengt seit jeher der Lauf, der sie wieder ausliefert. Ein Test haelt
das fest, damit es keine Einbahnstrasse wird.
Registereintrag gestrichen.
Rot gesehen: ohne den settleCustomDomain-Aufruf fallen drei der vier neuen
Tests; ohne das try/catch faellt der vierte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Defekte im Vorab-Abgleich gefunden, beide im Testaufbau von Task 2:
- Der Test haette echte Tags (v9.9.8, v9.9.9) im GETEILTEN Repository
angelegt. Tags liegen im gemeinsamen .git und sind damit auch fuer den
Hauptbaum und jede Parallelsitzung sichtbar. Stirbt der Test vor seinem
Aufraeumen, beantwortet ein liegengebliebenes v9.9.9 die Frage
`git tag -l 'v*' --sort=-v:refname | head -1` falsch — und die entscheidet,
wohin ein Server aktualisiert. Jetzt laeuft der Agent in einem Wegwerf-
Checkout mit eigenem .git; er bestimmt seine Wurzel ohnehin aus dem eigenen
Pfad, es genuegt also, deploy/ dorthin zu kopieren.
- Falscher Manifest-Pfad: der Test schrieb nach storage/app/deploy/
deployment.json, gelesen wird storage/app/deployment.json (lib/release.sh:19)
— eine Ebene darueber. Der Test haette die ausgelieferte Version nie gesetzt
und etwas anderes gemessen, als er behauptet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Fuenf Tasks, jeder mit eigenem Testzyklus: Versions-Arithmetik mit Decke,
Agent klemmt Ziel und Zaehler, Kanal schreibt atomar und reicht durch, Beleg
dass das Wartungsfenster von selbst folgt, und der Griff in der Konsole.
Task 4 hat bewusst KEINEN Produktivcode. Wenn der Test ohne Aenderung an
AutoUpdate.php gruen wird, ist genau das der Befund — und wenn nicht, ist die
Antwort nicht eine Decken-Sonderregel in der Automatik, sondern ein Fehler in
state(). Der Plan sagt das ausdruecklich, weil ein zweiter Weg in eine
Auslieferung genau das ist, wovor AutoUpdate.php im Kopfkommentar warnt.
Bei der Selbstpruefung gegen die Spec fielen drei Fehler auf:
- Ein Apostroph in einem einfach zitierten Testnamen — Syntaxfehler.
- `Operator::factory()->create()->givePermissionTo(...)` gibt es hier nicht;
Berechtigungen haengen ueber Rollen am operator-Guard.
- Eine Spec-Anforderung ohne Task: der Zustand „Decke unter dem
Ausgelieferten". Er ist ueber das Auswahlfeld nicht erreichbar, ueber die
Kommandozeile schon, und er darf nicht als „aktuell" durchgehen. Jetzt mit
eigenem `ceiling_passed`, eigenem Satz und zwei Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Ein Plan-Wechsel wird nirgends angewendet" stimmt seit laengerem nicht mehr.
ApplyPlanChange faehrt die plan-change-Pipeline, aufgerufen vom OrderObserver
bei einer Aufstufung und von clupilot:apply-due-plan-changes bei einer
Abstufung zum Laufzeitende. settleCustomDomain() hat sehr wohl einen Aufrufer
(ApplyPlanChange:256), und sieben Testdateien mit 43 Pruefungen decken den Weg.
Aufgeschrieben, weil der Kopfkommentar dieser Datei genau das ausschliesst:
"Ein Punkt verschwindet, wenn die Arbeit im selben Commit fertig wird, der ihn
streicht — und damit kann die Liste nicht behaupten, etwas sei offen, das es
laengst nicht mehr ist." Genau das ist passiert. Wer eine Liste fuehrt, deren
einziger Zweck Ehrlichkeit ist, muss sie mit der Arbeit streichen, nicht
danach.
Gepruefte Restliste: neun Punkte. Zwei davon (zweiter Sicherungsort, Office
Pro) haengen an Infrastruktur, die es noch nicht gibt; einer (Hostnamen-
Abnahme) an der echten Anlage; einer (Support-Mail) an einem SMTP-Konto, das
der Betreiber anlegen muss.
tests / release (push) Blocked by required conditionsDetails
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>
tests / release (push) Blocked by required conditionsDetails
Am Telefon brach "Mitarbeiter-Zugang" in der Plakette um und machte aus ihr
einen doppelt hohen Kasten mit einem Bindestrich am Zeilenende. whitespace-nowrap.
Das ist die kleinste der Beschwerden an dieser Zeile, aber die einzige mit einer
eindeutigen Ursache. Die uebrige Mobilansicht — jede Angabe auf einer eigenen
Zeile, Statusplakette und Knoepfe oben rechts weit weg von den Daten — ist eine
Gestaltungsfrage und keine Klasse.
tests / release (push) Blocked by required conditionsDetails
flex-wrap allein liess die zwei Knoepfe in ihrer natuerlichen Breite unter dem
Text stehen: zwei ungleich breite Kaesten, linksbuendig, die aussahen wie
verrutscht statt wie eine Gruppe.
Am Telefon jetzt volle Breite und gestapelt — eine erkennbare Handlungsleiste.
Ab sm stehen sie wie bisher nebeneinander, dort ist Platz.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Der Update-Agent meldete dauerhaft "kommt nicht an die Arbeit", obwohl nichts
kaputt war. Wächter und Agent haben beide einen minütlichen Zeitgeber und nahmen
beide `.agent.lock` in ihren ersten Zeilen — bevor sie wussten, ob sie überhaupt
etwas tun würden. Der Wächter tut an fast jedem Tag nichts: seine vier Prüfungen
sind allesamt lesend. Er hielt die Sperre trotzdem, jede Minute; der Agent kam
leer aus und schrieb einen Übersprung-Vermerk. Weil der Agent der ist, der an die
Konsole berichtet, stand dort eine Dauerstörung, während beide Dienste genau das
taten, was sie sollten.
Der Wächter sieht jetzt ungesperrt nach und nimmt die Sperre erst, wenn wirklich
etwas zu richten ist — im gesunden Fall fasst er sie nie an. Wo er eingreift,
wird die Lage nach dem Nehmen der Sperre noch einmal geprüft: zwischen dem
ungesperrten Blick und der Sperre kann ein Update fertig geworden sein, und
`--force-recreate` auf gesunde Container reißt jede offene Verbindung ab.
Versetzte Zeitgeber wären nur seltener gewesen, nicht weg — zwei Takte derselben
Länge wandern gegeneinander, und systemd zieht sie über AccuracySec aktiv auf
gemeinsame Weckpunkte zusammen. Eine zweite Sperre wäre schlimmer als der Fehler
gewesen: dann liefe der Wächter mitten in ein Update hinein.
Dazu zwei Dinge, die derselbe Vorfall aufgedeckt hat:
- Vierzehn Aufrufe nach draußen standen im Wächter ohne Frist, alle unter der
Sperre. Der Agent hat `timeout -k` am 4. August gelernt, der Wächter nie — ein
`docker compose exec`, das auf den Daemon wartet, hätte die Sperre unbegrenzt
gehalten.
- Ein übersprungener Lauf sah im Journal aus wie ein erfolgreicher: der Agent
beendet sich sauber, systemd meldet Starting → Deactivated. Das hat die
Fehlersuche zweimal in die falsche Richtung geschickt. Er sagt es jetzt, mit
Halter und Zähler in der Zeile.
WatchdogLockContentionTest lässt beide echten Skripte gegeneinander laufen statt
die Sperrenlogik nachzurechnen — und prüft beide Richtungen: der Agent kommt an
die Arbeit, während der Wächter nur nachsieht, und der Wächter hält still,
während ein Update die Sperre hält.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Bis heute lief die Maschine eines gekuendigten Kunden fuer immer weiter und
band einen Platz auf dem Host, den niemand mehr verkaufen konnte. Vierzehn
Tage nach dem Laufzeitende wird sie jetzt heruntergefahren, archiviert und
geloescht — und der Platz ist wieder verkaufbar.
Die Regel, die ueber allem steht: niemals loeschen, bevor das Archiv
NACHWEISLICH existiert. Ein vzdump kann mit einer Auftragskennung enden und
trotzdem nichts hinterlassen. Der Abbau sieht deshalb nach, ob wirklich eine
Datei daliegt — und ob sie von DIESEM Lauf stammt und nicht von der
naechtlichen Sicherung, der der letzte Tag fehlt.
Dasselbe gilt fuer jede andere Stufe: Herunterfahren, Sichern und Loeschen
liefern bei Proxmox alle drei nur eine Auftragskennung, keine Zusage. Auf jede
wird gewartet und jede wird nachgeprueft.
Dazu aus Teil A (v1.7.1): beim Kuendigen wird gefragt, ob der Kunde seine
Daten will, und er wird gewarnt, bevor ihn seine eigene Cloud aussperrt.
NACH DEM UPDATE: nichts zu tun. Der Abbau laeuft taeglich um 05:30 und fasst
hoechstens fuenf Instanzen je Lauf an, damit er die bezahlte Bereitstellung
nicht aushungert. Was haengt, steht rot ueber der Instanzliste.
K1 — abgebaute Instanzen zaehlten weiter als belegend. `scopeOccupyingHost`
filtert jetzt `torn_down_at`: eine abgebaute Instanz behaelt `ended`, `vmid`
und `disk_gb` als Nachweis, `status != 'failed'` war fuer sie also wahr. Der
Knoten wurde physisch leer und die Buchhaltung blieb voll — die naechste
bezahlte Bestellung derselben Groesse waere geparkt und verworfen worden.
K2 — `deleteVm()` gibt die UPID zurueck, der Abbau wartet den qmdestroy-Auftrag
ab und schreibt `torn_down_at` erst danach. Ein 200 heisst nur, dass Proxmox
den Auftrag angenommen hat; scheitert die Zerstoerung danach, stand bisher eine
laufende Maschine mit einem Datensatz da, der "abgebaut" sagte — und niemand
sah sie je wieder an. Die Attrappe bildet die Asynchronitaet ab
(`destroyedVmids`, `destroyExitStatus`, `destroyHangs`). Die Fristen des
Auftrags wandern mit: Sichern 900 s, Zerstoeren 300 s, Summe unveraendert 1800 s
unter der Uhr des Arbeiters.
K3 — vor `shutdownVm()` steht derselbe `vmStatus()`-Riegel wie im
Nachbarschritt. Eine wegen offener Zahlung gesperrte Cloud und jeder
Wiederholungslauf nach einem Teilfehlschlag treffen einen gestoppten Gast; der
Wurf haette die Instanz unheilbar gemacht und den echten Grund am Datensatz
ueberschrieben. Die Attrappe weist eine Bitte gegen eine stehende Maschine
jetzt ab, und ein Test faehrt erstmals einen zweiten Lauf nach einem
Fehlschlag durch.
W2 — der rote Kasten "Abbau haengt" filtert `status = 'ended'`. Eine
wiederbelebte Instanz waere sonst fuer immer darin stehengeblieben.
Neun Pruefungen im Mahnwesen lassen ihre Cloud jetzt laufen, bevor sie gesperrt
wird — die geschaerfte Attrappe legt offen, dass `SuspendInstance` denselben
fehlenden Riegel hat (Folgepunkt im Bericht).
Zu jedem der vier Punkte eine Zusicherung, die ohne den Fix rot ist; die
Rotproben stehen im Bericht.
Gekuendigte Kundenmaschinen liefen bisher fuer immer weiter. Task 3 hat den
Abbau gebaut; hier kommen der Griff, der ihn faehrt, und der Ort, an dem man
sieht, was passiert ist.
Die Wartezeit-Frage, entschieden: ein Auftrag je Instanz auf der
provisioning-Warteschlange. Das ist keine Abwaegung — nur queue-provisioning
steht im Netz-Namensraum des vpn-hub, der scheduler-Container nicht. Ein
Befehl, der selbst mit Proxmox spraeche, haette gar keine Route zu einem Host.
Die Fristen stehen ausdruecklich ineinander: die Aktion bekommt 600+1200 =
1800 s, der Auftrag hat $timeout 2100 s, retry_after der Verbindung ist
2400 s. Nur die unterste Uhr hinterlaesst einen lesbaren Grund am Datensatz;
die mittlere toetet den Arbeiterprozess stumm, die oberste startet einen
ZWEITEN Abbau gegen eine Maschine mitten im vzdump. $tries=1, weil ein
sofortiger zweiter Versuch am Herunterfahren einer gesperrten VM scheitern und
den richtigen Grund ueberschreiben wuerde. Die Staffelung ist als Pruefung
festgenagelt. Der Preis — zwanzig statt sechzig Minuten fuers Sichern — steht
im Kopfkommentar ausgeschrieben.
Dazu zwei Entscheidungen, nach denen niemand gefragt hat: eine Obergrenze je
Lauf, weil ueber dieselbe serielle Warteschlange bezahlte Bestellungen laufen;
und eine Reihenfolge, die einen Dauerfall die uebrigen nicht aushungern laesst.
Zeitplan taeglich um 05:30 — der Abbau hat keinen Moment, auf den es ankommt,
aber er darf nicht ins naechtliche vzdump-Fenster um 02:00 fallen.
Sichtbarkeit: zwei Kaesten in der Konsole. „Abbau haengt" (rot, ganz oben) —
eine Instanz mit gefuelltem teardown_error steht unbegrenzt und belegt weiter
einen Platz. Und „Archiviert und abgebaut" (unter der Liste) mit archive_volid
im Klartext. Beide sortieren absteigend und beide haben ein Ende: der Fehler
raeumt sich beim naechsten erfolgreichen Lauf selbst ab, das Archiv faellt
nach zwoelf Monaten heraus. Der Folgepunkt vom Export-Kasten also nicht noch
einmal.
Suite 2929 gruen. 25 neue Pruefungen, vier Mutationsproben rot gesehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier Befunde aus der Pruefung.
Der schwerste: der Nachtrags-Zweig konnte eine LAUFENDE Kundenmaschine als
abgebaut verbuchen. vmExists() ist ->get(...)->successful() ohne ->throw(), und
Proxmox antwortet auf die Konfiguration einer nicht vorhandenen VM mit 500 —
demselben Code wie ein hakender Knoten. "Gibt es nicht" und "ich konnte nicht
fragen" sahen damit gleich aus, und der Zustand entsteht im Regelbetrieb: ein
Lauf sichert, scheitert am Loeschen, und der naechste findet archive_volid
gesetzt und einen Knoten, der nichts beantwortet. Die Ablagenpruefung steht
deshalb jetzt VOR dem Zweig: nodeStorage() ruft ->throw(), laeuft sie durch, hat
der Knoten geantwortet, und erst dann ist ein "nein" aus vmExists() ein Befund
statt einer Vermutung.
Drei Ausnahmen lagen ausserhalb des try und haetten im Zeitplan die uebrigen
Instanzen mitgerissen: das Anlegen der Sperre (im Betrieb Redis), das Vermerken
des Grundes im catch, und die Freigabe im finally — die sogar am Erfolgsfall
vorbei. Alle drei abgesichert.
Die Sperrfrist war mit backupWaitSeconds + 600 knapper als der laengste Lauf
(Herunterfahren UND Sichern) und konnte kurz vor dem Loeschen auslaufen. Jetzt
shutdownWaitSeconds + backupWaitSeconds + Puffer — und sie hat endlich eine
eigene Pruefung.
Ein einzelnes Archiv ohne Zeitpunkt bleibt gueltig, mehrere nicht: sie lassen
sich nicht ordnen, und vermerkt wuerde womoeglich die Sicherung von vorgestern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Bei verborgener Website kam der Betreiber auf admin. und stand auf www., app.
und status. vor der Baustellenseite seines eigenen Portals — obwohl er im VPN
war und seine Adresse in der Konsole freigegeben hatte.
Zwei Tore hatten zwei Vorstellungen davon, wer "wir" sind:
RestrictConsoleNetwork VPN-Subnetz + die Freigabeliste aus der Konsole
PublicSiteGate nur admin_access.trusted_ranges
Wer seine Bueroadresse eintrug, oeffnete damit nur das eine Tor. Das andere
kannte die Liste nicht und liess ihn stehen — was wie ein kaputtes VPN aussah
und keines war.
Jetzt fragt das Seiten-Tor dieselbe Stelle wie das Konsolentor. Eine Liste, ein
Begriff davon, wer hereindarf: wer die Konsole sehen darf, darf die versteckte
Seite auch sehen. Das ist dieselbe Person.
2901 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gekuendigte Kundenmaschinen wurden nie abgebaut. EndInstanceService nimmt der
Instanz am Laufzeitende die Adresse weg und laesst die Maschine ausdruecklich
stehen; danach kam nichts mehr, und sie belegte fuer immer einen Platz auf dem
Host. ArchiveAndTearDown ist der Schritt danach: vierzehn Tage nach dem Ende
wird die Maschine archiviert und geloescht.
Die eine Regel: niemals loeschen, bevor das Archiv nachweislich existiert. Ein
vzdump kann mit einer Auftragskennung enden und trotzdem nichts hinterlassen —
volle Ablage, abgebrochener Lauf, ein Fehler im Gast. Deshalb steht zwischen
Sichern und Loeschen eine echte Nachschau auf der Ablage (backupsFor), und der
juengste Eintrag muss nach dem Beginn dieses Laufes entstanden sein: eine
naechtliche Sicherung von gestern ist kein Archiv, dem der letzte Tag fehlen
darf.
Beide Proxmox-Aufrufe liefern nur eine Kennung, keine Zusage, also wird auf den
Auftrag gewartet und danach nachgesehen — beim Herunterfahren, ob der Gast
wirklich steht, beim Sichern, ob die Datei liegt. Unmittelbar vor dem Loeschen
wird der Datensatz neu gelesen: eine Instanz, die in der Zwischenzeit
wiederbelebt wurde, wird nicht geloescht. Keine Ausnahme entkommt — ein
Fehlschlag ist false und ein Grund in teardown_error, weil dieser Ablauf spaeter
im Zeitplan ueber viele Instanzen laeuft.
Die Ablage kommt aus einer Einstellung (provisioning.proxmox.archive_storage,
Vorgabe local wie bei den naechtlichen Sicherungen) UND wird gegen die Ablagen
des Knotens geprueft. Fehlt sie oder nimmt sie keine Sicherungen auf, bricht der
Abbau ab, bevor eine Kundenmaschine dafuer heruntergefahren wurde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests / release (push) Blocked by required conditionsDetails
Auf dem Telefon erschien das Vollbild-Fenster "Aktualisierung laeuft" immer
wieder, obwohl nichts lief; nach einem Neuladen war es weg.
Die Ursache stand im catch des Waechters:
} catch {
this.wasRunning = true; // ohne jede Bedingung
Jede fehlgeschlagene Anfrage schaltete damit das Fenster ein — auch eine, die
mit einer Aktualisierung nichts zu tun hatte. Am Telefon passierte das
staendig: visibilitychange prueft sofort bei jeder Rueckkehr in den
Vordergrund, das Funkmodul ist dann noch nicht wach, die Anfrage scheitert.
Nach einem Neuladen war es weg, weil die Seite dann den echten Serverwert
mitbrachte — was es wie einen Geist aussehen liess statt wie den Fehler, der es
war.
Jetzt gilt eine gescheiterte Anfrage erst dann als Deployment, wenn vorher
bekannt war, dass eines laeuft (wasRunning oder serverConfirmed). Waehrend
eines echten Deployments ist das erfuellt — der Server meldet running, bevor
die Behaelter heruntergehen —, also bleibt das Fenster dort stehen wie bisher.
2879 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
archive_volid: speichert den Ort des Archivs, ohne den es nach 12 Monaten
nicht wiederzufinden ist.
archived_at: speichert den Zeitpunkt der Archivierung, daraus berechnet sich
die 12-Monats-Frist.
torn_down_at: speichert den Zeitpunkt der Maschinenlöschung, getrennt von
archived_at, damit der Zwischenstand sichtbar bleibt, wenn Archivierung
erfolgreich war aber Löschung noch nicht versucht oder gescheitert ist.
teardown_error: speichert die Fehlermeldung beim Löschen in Klartext, damit
ein Betreiber ohne Log-Suche reagieren kann.
Vier Tests zeigen, dass die Felder auf null stehen, in Carbon casten und
dass der Zwischenstand (archiviert, nicht abgebaut) ein gültiger Zustand ist.
Eine Mutation-Prüfung bestätigt, dass die Tests greifen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
backupNow() faehrt einen einmaligen vzdump (mode=stop, weil der Abbau die
Maschine vorher ohnehin herunterfaehrt) und liefert die Auftragskennung.
backupsFor() ist das eigentliche Fundament fuer den spaeteren Abbau: sie
fragt nach, was auf der Ablage wirklich liegt, statt der Auftragskennung zu
vertrauen — ein vzdump kann enden und trotzdem kein Archiv hinterlassen.
$storage ist an beiden Methoden ein Pflichtparameter, keine Einstellung: der
Client raet nicht, welche Ablage gemeint ist, und wiederholt damit nicht den
fest verdrahteten "local"-Namen aus createBackupJob().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Ausarbeiten hat sich der Entwurf als zu vorsichtig erwiesen. Zwei Funde:
Die Hosts machen LAENGST Momentaufnahmen — RegisterBackup legt fuer jede
Kundeninstanz einen naechtlichen vzdump an, mode: snapshot, storage: local.
Die Frage, die im Entwurf als riskanteste Voraussetzung stand ("friert Proxmox
eine laufende Maschine brauchbar ein?"), ist im Betrieb seit Monaten
beantwortet.
Und der Ablageort ist da: dieselbe lokale Ablage. Der Speicherserver ist der
ZWEITE Ort, nicht der erste.
Damit ist die Haelfte von Teil B baubar, die Geld kostet: heute laeuft jede
gekuendigte Maschine fuer immer weiter und bindet einen Platz, den niemand mehr
verkaufen kann. Der Kunden-Export (B2) wartet weiter — ein zusaetzlicher
Tarball von 175 GB neben dem Archiv braucht ein Ziel ausserhalb des Hosts.
Die tragende Regel des Plans steht ueber allem: niemals loeschen, bevor das
Archiv NACHWEISLICH existiert. Ein vzdump kann mit einer Auftragskennung enden
und trotzdem nichts hinterlassen — volle Ablage, abgebrochener Lauf. Wer sich
auf die Kennung verlaesst statt nachzusehen, loescht eine Maschine, deren
Archiv es nicht gibt.
tests / release (push) Blocked by required conditionsDetails
v1.7.0 war getaggt, aber nie zusammengefuehrt. Der Update-Agent folgt Tags, die
Arbeit war also live — main kannte sie nicht, und main trug VERSION=1.6.6, also
UNTER dem existierenden Tag. Ein naechstes Release aus main haette 1.6.7
geheissen und waere nie ausgeliefert worden.
Drei Konflikte, alle so aufgeloest, dass beide Seiten ueberleben:
- VERSION auf 1.7.1, damit die Reihenfolge wieder steigt.
- deploy/install-agent.sh: main brachte install-server-terminal, der Zweig
release-update-lock. Zwei unabhaengige case-Arme an derselben Stelle — beide
bleiben, vier Schritte und vier sudoers-Zeilen.
- deploy/update.sh: nur ein Kommentar zu HOST_STEP_NEEDS=3.
Der timeout -k 10 aus 1.6.5 ist dabei erhalten geblieben. Ohne ihn haengt der
Agent unbegrenzt an docker compose exec, und genau das war der Ausfall, der
diese ganze Kette ausgeloest hat.
Dazu ein Merge-Artefakt: RbacMoveTest zaehlte 22 Berechtigungen. Beide Seiten
haben je eine ergaenzt — server.terminal und deployment.unblock — also sind es
23.
2868 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>