controller v0.269.0: whole restore from the second drive; crash loops stopped; exact image digests; steps judged by their own .felhom.yml (decisions 26-28, R-661 R-666 R-667 R-668 R-664 R-665 R-662, 09 6.4 part 6)
gates / gates (push) Successful in 27s

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-24 12:18:39 +02:00
parent 7c3b3a9694
commit 3c6b49b31c
141 changed files with 3401 additions and 237 deletions
+40 -5
View File
@@ -42,6 +42,8 @@ type LadderEntry struct {
To map[string]string `yaml:"to" json:"to"`
Digest map[string]string `yaml:"digest" json:"digest"`
Verdict string `yaml:"verdict" json:"verdict"`
// TestedAt is when the step was proven (RFC3339) — the badge compares it with the install (v0.269.0).
TestedAt string `yaml:"tested_at" json:"tested_at"`
}
type ladderDoc struct {
@@ -80,6 +82,11 @@ func StepFile(templateDir string, to map[string]string) string {
return filepath.Join(templateDir, "steps", StepKey(to)+".yml")
}
// StepMetaFile is the step's own `.felhom.yml` (R-664, v0.269.0): its probe, memory and applied record.
func StepMetaFile(templateDir string, to map[string]string) string {
return filepath.Join(templateDir, "steps", StepKey(to)+".felhom.yml")
}
func sameRefs(a, b map[string]string) bool {
if len(a) != len(b) {
return false
@@ -100,6 +107,10 @@ type LadderStep struct {
Left int
// Source is the compose file the step pins: a steps/ file, or the template's docker-compose.yml.
Source string
// Meta is the `.felhom.yml` that belongs to Source (R-664): steps/<key>.felhom.yml when the catalog
// carries it, else the template's own — never the stack dir's, which a restore may have rewritten
// with an older one (R-665).
Meta string
// Why is one operator-English sentence for the log.
Why string
}
@@ -109,12 +120,13 @@ type LadderStep struct {
// update refuses before anything moves (a jump past a tested step is the thing this exists to stop).
func nextLadderStep(templateDir string, pinned map[string]string) (LadderStep, error) {
current := filepath.Join(templateDir, "docker-compose.yml")
currentMeta := filepath.Join(templateDir, ".felhom.yml")
ladder, err := LoadLadder(filepath.Join(templateDir, ".felhom.yml"))
if err != nil && !os.IsNotExist(err) {
return LadderStep{}, err
}
if len(ladder) == 0 {
return LadderStep{Index: -1, Source: current, Why: "the template carries no update_ladder — the catalog's current definition"}, nil
return LadderStep{Index: -1, Source: current, Meta: currentMeta, Why: "the template carries no update_ladder — the catalog's current definition"}, nil
}
idx := -1
for i := len(ladder) - 1; i >= 0; i-- { // the NEWEST entry whose `from` is what runs
@@ -124,12 +136,12 @@ func nextLadderStep(templateDir string, pinned map[string]string) (LadderStep, e
}
}
if idx < 0 {
return LadderStep{Index: -1, Source: current,
return LadderStep{Index: -1, Source: current, Meta: currentMeta,
Why: fmt.Sprintf("the installed version %s matches no update_ladder entry (%d entries) — an app older than the ladder has no record to climb; the catalog's current definition", summarisePin(pinned), len(ladder))}, nil
}
left := len(ladder) - idx
if idx == len(ladder)-1 {
return LadderStep{Index: idx, Left: left, Source: current,
return LadderStep{Index: idx, Left: left, Source: current, Meta: currentMeta,
Why: fmt.Sprintf("the last step (%d of %d) — the catalog's current definition", idx+1, len(ladder))}, nil
}
src := StepFile(templateDir, ladder[idx].To)
@@ -140,8 +152,12 @@ func nextLadderStep(templateDir string, pinned map[string]string) (LadderStep, e
if !sameRefs(imgs, ladder[idx].To) {
return LadderStep{}, fmt.Errorf("step %d of %d: %s names %s, the ladder says %s", idx+1, len(ladder), src, summarisePin(imgs), summarisePin(ladder[idx].To))
}
return LadderStep{Index: idx, Left: left, Source: src,
Why: fmt.Sprintf("step %d of %d: %s → %s, from %s", idx+1, len(ladder), summarisePin(ladder[idx].From), summarisePin(ladder[idx].To), filepath.Base(src))}, nil
meta := StepMetaFile(templateDir, ladder[idx].To)
if _, err := os.Stat(meta); err != nil {
meta = currentMeta // a step written before R-664: the template's own, said in the log
}
return LadderStep{Index: idx, Left: left, Source: src, Meta: meta,
Why: fmt.Sprintf("step %d of %d: %s → %s, from %s (probe from %s)", idx+1, len(ladder), summarisePin(ladder[idx].From), summarisePin(ladder[idx].To), filepath.Base(src), filepath.Base(meta))}, nil
}
// ladderStepsLeft is the page's count: how many tested steps separate this pin from the catalog's
@@ -161,3 +177,22 @@ func ladderStepsLeft(templateDir string, pinned map[string]string) int {
}
return 0
}
// loadMetadataFile reads a `.felhom.yml` that is not named `.felhom.yml` (a step's
// `steps/<key>.felhom.yml`) through LoadMetadata — the ONE validating reader — by giving it a scratch
// directory. Only the health check and the resources are read from the result.
func loadMetadataFile(path string) (Metadata, error) {
data, err := os.ReadFile(path)
if err != nil {
return Metadata{}, err
}
tmp, err := os.MkdirTemp("", "felhom-step-meta-")
if err != nil {
return Metadata{}, err
}
defer os.RemoveAll(tmp)
if err := os.WriteFile(filepath.Join(tmp, ".felhom.yml"), data, 0o600); err != nil {
return Metadata{}, err
}
return LoadMetadata(tmp), nil
}