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:
@@ -138,6 +138,21 @@ type AppConfig struct {
|
||||
// WRITTEN BY: Manager.recordInstalledImages ONLY, from StartStack / RestartStack / UpdateStack
|
||||
// and the deploy path. READ BY: web.updateBadge (v0.233.0). Nothing takes a DECISION from it.
|
||||
InstalledImages map[string]InstalledImage `yaml:"installed_images,omitempty" json:"installed_images,omitempty"`
|
||||
// PinnedImages is what this app is SUPPOSED to run, per compose service — the customer's
|
||||
// INTENT, and the input the catalog render obeys (v0.235.0, operator ruling 2026-09-06:
|
||||
// "freeze the version, keep the fixes flowing").
|
||||
//
|
||||
// IT IS NOT InstalledImages. That field is an OBSERVATION ("what is running"), written by
|
||||
// looking at containers. This one is a DECISION ("what should run"), written only by an act
|
||||
// that is entitled to move a version: a deploy, a deliberate update, a restore, or the
|
||||
// one-time adoption pass. Letting an observation feed a decision would make a bad reading
|
||||
// become a bad deployment — the same category error the desired_state field exists to avoid
|
||||
// (R-166). They will normally agree; when they disagree that is a signal, not a bug to
|
||||
// paper over.
|
||||
//
|
||||
// ABSENT MEANS UNPINNED, and unpinned means the app behaves exactly as it did before
|
||||
// v0.235.0. It never means "pin to whatever the catalog says now".
|
||||
PinnedImages map[string]string `yaml:"pinned_images,omitempty" json:"pinned_images,omitempty"`
|
||||
}
|
||||
|
||||
// InstalledImage is one compose service's observed image. See AppConfig.InstalledImages.
|
||||
@@ -482,6 +497,16 @@ func (m *Manager) runComposeDeploy(name, stackDir string, env map[string]string,
|
||||
deployEnv := m.stackEnv(stackDir)
|
||||
m.recordInstalledImages(name, stackDir, deployEnv)
|
||||
|
||||
// Pin what we just deployed FROM (v0.235.0). The stack dir's compose file IS what the deploy
|
||||
// used, so it is both the pin's source and the definition stored beside it. A failure here is
|
||||
// logged and never fails the deploy — the app is up, and an unpinned app simply keeps
|
||||
// pre-v0.235.0 behaviour.
|
||||
if pin, data, err := PinFromCompose(ComposePathIn(stackDir)); err != nil {
|
||||
m.logger.Printf("[WARN] [stacks] pin %s: cannot pin from the deployed compose file: %v", name, err)
|
||||
} else if err := m.SetPin(name, stackDir, pin, data); err != nil {
|
||||
m.logger.Printf("[ERROR] [stacks] pin %s: %v", name, err)
|
||||
}
|
||||
|
||||
// Post-deploy container state check (async, non-blocking)
|
||||
m.logPostStartStatus(name, stackDir, deployEnv)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user