v0.235.0: freeze the version, keep the fixes flowing (operator ruling 2026-09-06)
gates / gates (push) Successful in 12s
gates / gates (push) Successful in 12s
Slice 3. R-447 was BLOCKED because R-438 established that RestartStack's use of up -d to pick up template changes was CHOSEN and written down in its own comment. The operator ruled Option 1, and this implements it. The rule: while the catalog offers the same version you run, its fixes flow to you; the moment it moves to a newer version you are frozen until you update. NOTHING was added to any of the thirteen compose up -d call sites. Most of them are repairs - the boot reconciler, the drive-return gate, the app-stop guard - and a repair path that refuses to repair leaves a customer's app down, which is worse than the problem. They are made safe by removing the reason. app.yaml gains pinned_images: what the app is SUPPOSED to run. It is NOT installed_images, which is an observation; letting a reading become a deployment is the R-166 category error one field over. Four writers, each also storing the exact definition as applied-compose.yml. UpdateStack advances the pin and re-renders BEFORE the pull, because pull and up -d act on the file on disk, and a pin set afterwards would pull the frozen version and report success. The syncer renders instead of copying, through one nil-safe seam. Catalog images equal the pin -> verbatim, so fixes and self-healing both survive; they differ -> the WHOLE stored definition, never a substitution of refs into a newer template (wger 2.6 needs a DB config the older template cannot supply). This is deliberately not 'skip deployed apps', which was option B and was rejected. AdoptPins runs once at boot after the backfill, files only, and skips loudly rather than inventing a pin. syncer.Start() moved to after it: the initial sync would otherwise run while every app was unpinned and overwrite a deployed app's version once per boot. THE BADGE HAD TO CHANGE OR SLICE 2 WOULD HAVE INVERTED SILENTLY. TemplateImages reads the LIVE compose file, which is now the frozen one, so the comparison would have answered Naprakesz on exactly the apps that are behind - with every test green, because the new field has the same type. It now reads CatalogImages. +16 tests (1729 -> 1745), 28 packages green. Three red-proofs run and reverted. A test also caught the syncer writing an empty compose file over a live app.
This commit is contained in:
@@ -389,7 +389,12 @@ func main() {
|
||||
syncer := catalogsync.New(cfg, logger, stackMgr.ScanStacks, func(updated []string) {
|
||||
stackMgr.InjectMissingFields(updated)
|
||||
})
|
||||
syncer.Start()
|
||||
// v0.235.0 — the render seam. Without this line the syncer copies the catalog verbatim, i.e.
|
||||
// the pre-v0.235.0 product; the freeze is INERT unless it is wired here, which is why a test
|
||||
// walks this file's AST for the call rather than trusting the package to be correct alone.
|
||||
syncer.SetRenderPlanFn(stackMgr.RenderPlanFor)
|
||||
// Start() is deliberately NOT called here — see the comment beside the call further down, after
|
||||
// the pin adoption pass.
|
||||
defer syncer.Stop()
|
||||
|
||||
// --- Graceful shutdown context ---
|
||||
@@ -447,6 +452,21 @@ func main() {
|
||||
// reason: a just-restarted app is observed in its settled state.
|
||||
stackMgr.BackfillInstalledImages()
|
||||
|
||||
// --- v0.235.0: pin adoption (complete AND matching observations only) ---
|
||||
// Give every deployed app a pin for what it is already running, so the render has something to
|
||||
// obey. Runs immediately after the backfill so an app that pass has just observed can be pinned
|
||||
// in the same boot. It reads and writes FILES only — no container is started, stopped or touched.
|
||||
// An app it cannot pin confidently is left UNPINNED and keeps pre-v0.235.0 behaviour, loudly.
|
||||
stackMgr.AdoptPins()
|
||||
|
||||
// --- Start the catalog syncer, AFTER adoption ---
|
||||
// ORDERING IS LOAD-BEARING. Start() fires an immediate sync in a goroutine. Started at its
|
||||
// original place — before adoption — that first sync would run while every app was still
|
||||
// unpinned, copy the catalog verbatim over a deployed app, and hand the next restart a version
|
||||
// change: precisely the behaviour this release removes, once per boot. Adoption first means the
|
||||
// very first sync of a boot already obeys the pins.
|
||||
syncer.Start()
|
||||
|
||||
// --- R-52: boot desired-state reconciliation ---
|
||||
// A deployed app that missed its boot start used to stay down until a human noticed (F5: immich
|
||||
// and calibre-web sat Exited for ~18 h while ten siblings came back). One bounded start-once
|
||||
@@ -2595,7 +2615,27 @@ func (a *stackAdapter) RecreateStackDefinitionFromUnit(name, composeSrcDir strin
|
||||
return fmt.Errorf("restoring %s from unit: %w", fname, err)
|
||||
}
|
||||
}
|
||||
return a.mgr.PersistUnitRedeployConfig(name, fullEnv)
|
||||
if err := a.mgr.PersistUnitRedeployConfig(name, fullEnv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// v0.235.0 — PIN TO WHAT THE UNIT CAPTURED. THIS IS WHAT CLOSES R-441.
|
||||
//
|
||||
// Measured before this line existed: this function writes the recovery unit's compose — carrying
|
||||
// the OLD image pin — into the live stack dir, and `Syncer.copyIfChanged` then overwrote it from
|
||||
// the catalog on the next 15-minute tick. So a restore's image-level recovery had a <=15-minute
|
||||
// half-life, and the next `compose up -d` from any of the thirteen call sites re-applied the
|
||||
// catalog's version. Pinning here makes the render obey the restored definition instead.
|
||||
//
|
||||
// Pinned from the file just written, so the pin and the stored definition cannot disagree.
|
||||
// A failure is loud and does NOT fail the restore: an unpinned app is exactly today's behaviour,
|
||||
// and refusing a completed restore over a bookkeeping write would be the worse trade.
|
||||
if pin, data, err := stacks.PinFromCompose(stacks.ComposePathIn(stackDir)); err != nil {
|
||||
log.Printf("[WARN] [stacks] pin %s: cannot pin from the restored definition: %v", name, err)
|
||||
} else if err := a.mgr.SetPin(name, stackDir, pin, data); err != nil {
|
||||
log.Printf("[ERROR] [stacks] pin %s: %v", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartStackServices brings up only the named compose services (the DB-only replay window, R-47).
|
||||
|
||||
Reference in New Issue
Block a user