engine-major gate: no database-engine pin crosses a MAJOR until Slice 4 (R-448) ships
The rule (CLAUDE.md, operator ruling 2026-09-13): until the Update button takes a verified backup as its precondition, no template may move a mariadb:/postgres: image across a major version. Four MariaDB and eleven PostgreSQL services; the gate finds them by image name, not by a list. scripts/check-engine-major.py — fast (git reads only), diffs each changed template's per-service image: line between the two ends of the push range, refuses a major move naming the rule and its expiry (R-448). Fourth row of catalog_gates.py; .githooks/pre-push now hands the push range through as --range=<remote sha>..<local sha>. HONEST LIMIT: it needs a parent commit and CI fetches at --depth 1 (the R-452 gap, not re-filed), so on a shallow clone the runner SKIPS it out loud instead of reddening every CI push. The hook, which has the full clone, is where it bites. Red-proof (scripts/test_gate_decoys.py, 7 cases, all seen to judge correctly): mariadb 11.6->12.3 REFUSED, postgres 16->17 REFUSED, mariadb:lts INCONCLUSIVE; 11.6->11.8 PASSES; the major moving only in a comment / kimai's serverVersion env / README / the app's own image PASSES. COVERS literal registered for felhom.eu's decoy_coverage_gate (which now reads 1 covered, 3 exempt, 0 unaccounted). test_catalog_gates.py pins the four-gate table and the announced shallow-clone skip. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -58,8 +58,36 @@ class CatalogGatesFastTest(unittest.TestCase):
|
||||
spec = importlib.util.spec_from_file_location("catalog_gates_under_test", ENTRY)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
self.assertEqual(len(mod.GATES), 3)
|
||||
self.assertEqual([g[0] for g in mod.GATES if g[3]], ["image-pins"])
|
||||
self.assertEqual(len(mod.GATES), 4)
|
||||
self.assertEqual([g[0] for g in mod.GATES if g[3]], ["image-pins", "engine-major"])
|
||||
# exactly one gate needs git history; it is the one the CI half cannot run (R-452)
|
||||
self.assertEqual([g[0] for g in mod.GATES if g[4]], ["engine-major"])
|
||||
|
||||
def test_engine_major_ran_under_fast(self):
|
||||
"""The 2026-09-13 gate is fast (git reads only) and must be IN --fast, or the hook that
|
||||
exists to enforce its rule never runs it."""
|
||||
self.assertIn("engine-major gate", self.out)
|
||||
|
||||
def test_shallow_clone_skips_engine_major_out_loud(self):
|
||||
"""On a --depth 1 clone (what CI has) the runner must SKIP engine-major and SAY so — never
|
||||
convict every push, never pass silently. Asserted on a real shallow clone of this repo."""
|
||||
import shutil, tempfile
|
||||
tmp = tempfile.mkdtemp(prefix="catalog-shallow-")
|
||||
try:
|
||||
subprocess.run(["git", "clone", "-q", "--depth", "1", "file://" + ROOT, tmp],
|
||||
check=True, capture_output=True)
|
||||
# test the WORKING-TREE runner and gate, not whatever HEAD happens to hold
|
||||
for fn in ("catalog_gates.py", "check-engine-major.py", "check-image-pins.py"):
|
||||
shutil.copy(os.path.join(ROOT, "scripts", fn), os.path.join(tmp, "scripts", fn))
|
||||
p = subprocess.run([sys.executable, os.path.join(tmp, "scripts", "catalog_gates.py"),
|
||||
"--fast"], cwd=tmp, capture_output=True, text=True)
|
||||
out = p.stdout + p.stderr
|
||||
self.assertIn("SHALLOW CLONE", out)
|
||||
self.assertIn("engine-major", out)
|
||||
self.assertNotIn("engine-major gate OK", out)
|
||||
self.assertEqual(p.returncode, 0, out)
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user