steps/<key>.felhom.yml: gate rule 4b, writer, backfill (8) (R-664)
gates / gates (push) Successful in 2s
gates / gates (push) Successful in 2s
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:
@@ -22,6 +22,8 @@ WHAT IT CHECKS, per template that carries `update_ladder:` (format and field rul
|
||||
4. every entry but the newest carries its OWN definition at `steps/<step_key(to)>.yml`, whose
|
||||
images per service are EXACTLY that entry's `to` (`09` §6.4 part 5: the box pins that file, one
|
||||
step per press). The name alone is not the fact: the file's own `image:` lines are read.
|
||||
4b. ... and its own `.felhom.yml` at `steps/<step_key(to)>.felhom.yml` (R-664), with a healthcheck
|
||||
when the template has one.
|
||||
|
||||
A template WITHOUT a ladder passes here — it has never been moved since the gate existed, and its
|
||||
first move is refused by the twin unless that move brings the first entry.
|
||||
@@ -65,6 +67,13 @@ def check_app(app_dir):
|
||||
got = ladder.images_in(open(sp, encoding="utf-8").read())
|
||||
if got != to:
|
||||
problems.append("%s names %s, but entry %d's `to` is %s" % (ladder.step_file(to), got, i + 1, to))
|
||||
# rule 4b (R-664, 2026-09-24): the step's own .felhom.yml — the box judges the step with it.
|
||||
smp = os.path.join(app_dir, ladder.step_meta_file(to))
|
||||
if not os.path.isfile(smp):
|
||||
problems.append("entry %d of %d has no %s — the box would judge the step with the head's probe (R-664)"
|
||||
% (i + 1, len(entries), ladder.step_meta_file(to)))
|
||||
elif "healthcheck:" not in open(smp, encoding="utf-8").read() and "healthcheck:" in open(os.path.join(app_dir, ".felhom.yml"), encoding="utf-8").read():
|
||||
problems.append("%s carries no healthcheck: while the template does — not a real step file" % ladder.step_meta_file(to))
|
||||
if entries:
|
||||
current = ladder.images_in(open(comp, encoding="utf-8").read())
|
||||
head = entries[-1].get("to")
|
||||
|
||||
@@ -210,3 +210,19 @@ def step_key(to):
|
||||
def step_file(to):
|
||||
"""The step definition's path RELATIVE to the template directory."""
|
||||
return "steps/%s.yml" % step_key(to)
|
||||
|
||||
|
||||
def step_meta_file(to):
|
||||
"""The step's own `.felhom.yml` (R-664, 2026-09-24): its probe, memory limits and applied record — the
|
||||
box judges the step with it. RELATIVE to the template directory."""
|
||||
return "steps/%s.felhom.yml" % step_key(to)
|
||||
|
||||
|
||||
def strip_ladder_block(felhom_text):
|
||||
"""A `.felhom.yml` without its update_ladder block (and the header comment the writer puts above it):
|
||||
a step's own copy carries no ladder — only the template's is read for one."""
|
||||
lines = felhom_text.splitlines()
|
||||
for i, l in enumerate(lines):
|
||||
if l.startswith("# update_ladder") or l.startswith("update_ladder:"):
|
||||
return "\n".join(lines[:i]).rstrip("\n") + "\n"
|
||||
return felhom_text
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""steps_meta_backfill.py — ONE-OFF (2026-09-24 night, R-664): write `steps/<key>.felhom.yml` beside every step
|
||||
file, from the NEWEST commit whose compose names that step's images — the .felhom.yml served WITH that
|
||||
definition — without its update_ladder block. Never overwrites."""
|
||||
import os, subprocess, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ladder # noqa: E402
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def git(*a):
|
||||
return subprocess.run(["git", "-C", ROOT] + list(a), capture_output=True, text=True, check=True).stdout
|
||||
|
||||
|
||||
def main():
|
||||
n = 0
|
||||
for app in sorted(os.listdir(os.path.join(ROOT, "templates"))):
|
||||
d = os.path.join(ROOT, "templates", app)
|
||||
fy = os.path.join(d, ".felhom.yml")
|
||||
if not os.path.isfile(fy):
|
||||
continue
|
||||
entries, _, errs = ladder.parse(open(fy, encoding="utf-8").read())
|
||||
if errs or len(entries) < 2:
|
||||
continue
|
||||
commits = git("log", "--format=%H", "--", "templates/%s" % app).split()
|
||||
for i, e in enumerate(entries[:-1]):
|
||||
dst = os.path.join(d, ladder.step_meta_file(e["to"]))
|
||||
if os.path.exists(dst):
|
||||
continue
|
||||
for c in commits:
|
||||
try:
|
||||
comp = git("show", "%s:templates/%s/docker-compose.yml" % (c, app))
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
if ladder.images_in(comp) == e["to"]:
|
||||
meta = git("show", "%s:templates/%s/.felhom.yml" % (c, app))
|
||||
open(dst, "w", encoding="utf-8").write(ladder.strip_ladder_block(meta))
|
||||
print("WROTE %-10s entry %d -> %s from %s" % (app, i + 1, ladder.step_meta_file(e["to"]), c[:12]))
|
||||
n += 1
|
||||
break
|
||||
else:
|
||||
print("MISS %-10s entry %d" % (app, i + 1))
|
||||
print("steps_meta_backfill: %d written" % n)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -437,16 +437,23 @@ def test_record_cases(clone):
|
||||
two = ladder_of(tr_entry({"navidrome": prev}, mid, {"navidrome": TR_D1}),
|
||||
tr_entry(mid, frm, {"navidrome": TR_D1}))
|
||||
step_rel = "templates/navidrome/" + _l.step_file(mid)
|
||||
meta_rel = "templates/navidrome/" + _l.step_meta_file(mid)
|
||||
meta_body = lambda t: _l.strip_ladder_block(io.open(os.path.join(clone, NF), encoding="utf-8").read())
|
||||
step_body = lambda t: swap_image("navidrome", old, mid["navidrome"])(io.open(os.path.join(clone, NC), encoding="utf-8").read())
|
||||
case_tr_static("FACT: a two-step ladder with NO steps/ file for the first step", clone,
|
||||
[(NF, two)], 1, ("has no definition",))
|
||||
case_tr_static("GENUINE: a two-step ladder whose first step carries its definition", clone,
|
||||
[(NF, two), (step_rel, step_body)], 0)
|
||||
[(NF, two), (step_rel, step_body), (meta_rel, meta_body)], 0)
|
||||
case_tr_static("DECOY: the steps/ file has the right NAME and names the head's image", clone,
|
||||
[(NF, two), (step_rel, lambda t: io.open(os.path.join(clone, NC), encoding="utf-8").read())],
|
||||
[(NF, two), (step_rel, lambda t: io.open(os.path.join(clone, NC), encoding="utf-8").read()), (meta_rel, meta_body)],
|
||||
1, ("names",))
|
||||
case_tr_static("DECOY: the step's definition sits beside the template under another name", clone,
|
||||
[(NF, two), ("templates/navidrome/steps/0.2.0.yml", step_body)], 1, ("has no definition",))
|
||||
# R-664: the step's own .felhom.yml
|
||||
case_tr_static("FACT: the step has its compose but no .felhom.yml", clone,
|
||||
[(NF, two), (step_rel, step_body)], 1, ("R-664",))
|
||||
case_tr_static("DECOY: the step's .felhom.yml exists by NAME and holds no healthcheck", clone,
|
||||
[(NF, two), (step_rel, step_body), (meta_rel, lambda t: "display_name: Navidrome\n")], 1, ("not a real step file",))
|
||||
|
||||
def main():
|
||||
gate = os.path.join(ROOT, "scripts", "check-engine-major.py")
|
||||
|
||||
@@ -122,6 +122,9 @@ class WriterTest(unittest.TestCase):
|
||||
sp = os.path.join(self.tmp, "templates", "navidrome", ladder.step_file({"navidrome": "deluan/navidrome:" + NEXT}))
|
||||
self.assertTrue(os.path.isfile(sp), out)
|
||||
self.assertEqual(open(sp).read(), at_step1, "the step file must be the definition AS SERVED, fixes included")
|
||||
smp = os.path.join(self.tmp, "templates", "navidrome", ladder.step_meta_file({"navidrome": "deluan/navidrome:" + NEXT}))
|
||||
self.assertTrue(os.path.isfile(smp), "the step's own .felhom.yml (R-664) was not written")
|
||||
self.assertNotIn("update_ladder:", open(smp).read())
|
||||
import subprocess
|
||||
r = subprocess.run([sys.executable, os.path.join(HERE, "check-test-record.py"), "--root", self.tmp,
|
||||
"navidrome"], capture_output=True, text=True)
|
||||
|
||||
@@ -832,6 +832,10 @@ def write_ladder(argv) -> int:
|
||||
sp.parent.mkdir(parents=True, exist_ok=True)
|
||||
sp.write_text(comp)
|
||||
print(f"STEP {app}: the superseded step {cur} keeps its definition at {ladder.step_file(cur)}")
|
||||
smp = tdir / ladder.step_meta_file(cur)
|
||||
if not smp.exists(): # R-664: and its own .felhom.yml, without the ladder
|
||||
smp.write_text(ladder.strip_ladder_block(fy_p.read_text()))
|
||||
print(f"STEP {app}: … and its .felhom.yml at {ladder.step_meta_file(cur)}")
|
||||
# move the compose, per service, on that service's own image: line
|
||||
out, svc = [], None
|
||||
for line in comp.splitlines():
|
||||
|
||||
Reference in New Issue
Block a user