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
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user