v0.263.2: the undo keeps the probe of the pinned version (R-637)
gates / gates (push) Successful in 25s

Found live on 9202 (romm): .felhom.yml flows into the stack dir on every
catalog sync, so "the old .felhom.yml" saved at update time was already the
new one, and the serving old version was judged with the new probe.

New record applied-meta/.felhom.yml, written whenever a version is pinned
(deploy, adoption, pin advance) and put back by the undo, like
applied-compose.yml. The fixture now places the new file at sync time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-23 11:53:53 +02:00
parent 5d38573a1a
commit 2cd66663f3
7 changed files with 197 additions and 20 deletions
+56 -6
View File
@@ -71,10 +71,47 @@ const undoCopyLabel = "felhom.undo-copy-of"
// undoCopyMarker is written last into a copy volume, beside the data (never inside it).
const undoCopyMarker = "felhom-undo-complete"
// preUpdateMetaDir holds the previous .felhom.yml. A DIRECTORY, so LoadMetadata can read it; the
// syncer copies only two files into the stack dir's root and never touches a subdirectory.
// preUpdateMetaDir holds the previous version's .felhom.yml for the undo. A DIRECTORY, so
// LoadMetadata can read it; the syncer copies only two files into the stack dir's root and never
// touches a subdirectory.
const preUpdateMetaDir = "pre-update-meta"
// appliedMetaDir (v0.263.2) holds the .felhom.yml that belongs to the PINNED version — the probe the
// running version answers. Written when a version is pinned (deploy, adoption, a guarded update) and
// put back by an undo. WHY IT EXISTS: `.felhom.yml` flows into the stack dir on every catalog sync
// (§5.4 — it is never frozen), so by the time anyone presses Update the stack dir already holds the
// NEW version's file. MEASURED LIVE on 9202 2026-09-23: v0.263.1 saved "the old .felhom.yml" at update
// time, got the new probe (romm :8999), and judged the serving old version "did not start".
const appliedMetaDir = "applied-meta"
// storeAppliedMeta records the .felhom.yml of the version just pinned. A failure is logged by the
// caller and never fails the act — without it the undo falls back to the current file, loudly.
func storeAppliedMeta(stackDir string, src []byte) error {
if len(src) == 0 {
return fmt.Errorf("refusing to store an empty applied .felhom.yml")
}
d := filepath.Join(stackDir, appliedMetaDir)
if err := os.MkdirAll(d, 0o755); err != nil {
return err
}
tmp := filepath.Join(d, ".felhom.yml.tmp")
if err := os.WriteFile(tmp, src, 0o644); err != nil {
return err
}
return os.Rename(tmp, filepath.Join(d, ".felhom.yml"))
}
// storeAppliedMetaFrom copies a .felhom.yml file into the applied record, logging (never failing).
func (m *Manager) storeAppliedMetaFrom(name, stackDir, src string) {
b, err := os.ReadFile(src)
if err == nil {
err = storeAppliedMeta(stackDir, b)
}
if err != nil {
m.logger.Printf("[WARN] [stacks] pin %s: could not record the pinned version's .felhom.yml (%v) — an undo would check health with whatever file is current then", name, err)
}
}
// undoHelperImage is the helper the backup legs already use for volume tars (backup.go, restore.go),
// so no new image is introduced onto a box.
const undoHelperImage = "alpine"
@@ -267,19 +304,32 @@ func (m *Manager) RemoveUndoCopies(name string) int {
return n
}
// savePreUpdateMeta keeps the previous .felhom.yml for the undo's health check.
// savePreUpdateMeta keeps the PINNED version's .felhom.yml for the undo's health check: the applied
// record when there is one; otherwise — an app pinned before v0.263.2 — the current file, which the
// catalog sync may already have replaced with the new version's (said in the returned note).
func savePreUpdateMeta(dir string) (string, error) {
src, err := os.ReadFile(filepath.Join(dir, ".felhom.yml"))
src, err := os.ReadFile(filepath.Join(dir, appliedMetaDir, ".felhom.yml"))
if err != nil {
return "", err
if src, err = os.ReadFile(filepath.Join(dir, ".felhom.yml")); err != nil {
return "", err
}
err = errNoAppliedMeta
}
note := err
md := filepath.Join(dir, preUpdateMetaDir)
if err := os.MkdirAll(md, 0o755); err != nil {
return "", err
}
return md, os.WriteFile(filepath.Join(md, ".felhom.yml"), src, 0o644)
if err := os.WriteFile(filepath.Join(md, ".felhom.yml"), src, 0o644); err != nil {
return "", err
}
return md, note
}
// errNoAppliedMeta: the app has no applied .felhom.yml record (pinned before v0.263.2), so the undo's
// probe is taken from the current file. The copy WAS made — callers log this and carry on.
var errNoAppliedMeta = fmt.Errorf("no applied .felhom.yml for the pinned version (pinned before v0.263.2) — the undo will probe with the current file")
func (m *Manager) undoHealth(ctx context.Context, name string, timeout time.Duration, meta *Metadata) (bool, string) {
if m.updateUndoHealthFn != nil {
return m.updateUndoHealthFn(ctx, name, timeout, meta)