v0.233.0: record what each compose service actually installed, and badge whether it is current
gates / gates (push) Successful in 12s
gates / gates (push) Successful in 12s
Update arc slices 1 and 2. NEITHER CHANGES ANY BEHAVIOUR — no new endpoint, no auto-update, the three lifecycle buttons byte-identical. Slice 1 — app.yaml gains installed_images, keyed by compose SERVICE name, each entry carrying ref + repo digest + first-seen timestamp. Written by Manager.recordInstalledImages after a successful compose up from StartStack, RestartStack, UpdateStack and runComposeDeploy. Read from the CONTAINER, never from docker-compose.yml: the syncer overwrites a deployed app's compose on a 15-minute cycle and the two disagreed for 25 minutes in the spike's own measurement. A failed write NEVER refuses the action - the deliberate opposite of SetDesiredState, because this is an observation and that is an intent. Not called from StartStackServices (the R-47 DB-only window). Its own docker seam with a context and a 30s timeout, which neither existing exec helper has. Slice 2 — .felhom.yml gains optional catalog_since; web.updateBadge compares the recorded ref per service against what the current template pins and returns a *MetaBadge through the EXISTING meta_badge partial. No new markup, no new CSS. NO RECORD RENDERS NOTHING: absent means unknown and never means current. No version number reaches the customer and no registry is queried. Known limitation, filed not hidden: 23 catalog pins float, so those apps can read Naprakesz when the image behind the tag has moved. +17 tests (1707 -> 1724), 28 packages green. Wiring proven through a real RestartStack plus an AST walk of the four call sites. Three companion red-proofs run and reverted.
This commit is contained in:
@@ -121,6 +121,39 @@ type AppConfig struct {
|
||||
// the primitive would make a nightly backup indistinguishable from the customer pressing Stop,
|
||||
// which is the exact confusion this field exists to end. Writers: SetDesiredState's callers.
|
||||
DesiredState string `yaml:"desired_state,omitempty" json:"desired_state,omitempty"`
|
||||
// InstalledImages records what each compose service is ACTUALLY RUNNING, read from the
|
||||
// containers after a successful compose up — never from docker-compose.yml, which the catalog
|
||||
// syncer overwrites on a 15-minute cycle with no deployed check at all (measured live:
|
||||
// SPIKE-app-update-2026-09-01 §3, where the file said v2.8.5 while the container ran v2.8.6 for
|
||||
// 25 minutes). The file is the value that has already moved; the container is the fact.
|
||||
//
|
||||
// Keyed by COMPOSE SERVICE NAME, not container name: the service name is what the compose file
|
||||
// and the catalog template both key on, so it is the only key a comparison can be made against.
|
||||
//
|
||||
// ABSENT MEANS UNKNOWN AND NEVER MEANS CURRENT (the R-166 rule, applied to an observation
|
||||
// instead of an intent). Every app.yaml written before v0.233.0 has no entry here, so absent is
|
||||
// the common value on upgrade; a reader that treated it as "up to date" would tell every
|
||||
// customer on the fleet that their months-old app is current.
|
||||
//
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// InstalledImage is one compose service's observed image. See AppConfig.InstalledImages.
|
||||
type InstalledImage struct {
|
||||
// Ref is the reference the container was created FROM, i.e. docker inspect .Config.Image —
|
||||
// e.g. "lscr.io/linuxserver/bookstack:26.05.2". This is what the template pins and what the
|
||||
// comparison uses.
|
||||
Ref string `yaml:"ref" json:"ref"`
|
||||
// Digest is the repo digest of the image behind that reference — the only identifier that
|
||||
// cannot move. Empty for an image that was never pulled from a registry (a locally built or
|
||||
// imported image has no RepoDigests); an empty digest is recorded, never a skipped entry.
|
||||
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
|
||||
// At is RFC3339 UTC: when this exact Ref+Digest pair was FIRST observed for this service. It is
|
||||
// deliberately NOT re-stamped on every restart — an unchanged observation must not rewrite
|
||||
// app.yaml (the SetDesiredState rule), and "running since" is more useful than "last looked at".
|
||||
At string `yaml:"at" json:"at"`
|
||||
}
|
||||
|
||||
// DeployRequest contains the user-provided values from the deploy form.
|
||||
@@ -443,8 +476,13 @@ func (m *Manager) runComposeDeploy(name, stackDir string, env map[string]string,
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Post-deploy container state check (async, non-blocking)
|
||||
// Record what this deploy actually installed, per compose service (v0.233.0). Runs AFTER the
|
||||
// SaveAppConfig above so it loads an app.yaml that already reads deployed=true. A failure here
|
||||
// never fails the deploy — see recordInstalledImages.
|
||||
deployEnv := m.stackEnv(stackDir)
|
||||
m.recordInstalledImages(name, stackDir, deployEnv)
|
||||
|
||||
// Post-deploy container state check (async, non-blocking)
|
||||
m.logPostStartStatus(name, stackDir, deployEnv)
|
||||
|
||||
_ = m.RefreshStatus()
|
||||
|
||||
Reference in New Issue
Block a user