gates: the newest release header must state its MinAgent; backfill v0.233.0-v0.236.0 (R-470, R-472)
gates / gates (push) Successful in 12s

From hub v0.112.0 a floor above the golden is served only with a declared
MinAgent, read from this CHANGELOG's newest `## vX.Y.Z` header. New fast,
blocking gate minagent_header_gate.py: that block must contain a line
starting `**MinAgent: X.Y.Z**`; prose, a code span or a non-bold mention
does not count (decoy test; red-proof F: relaxing the match to a body
mention fails test_decoy_body_mention_is_not_the_line).

Backfill: v0.233.0, v0.234.0, v0.235.0 and v0.236.0 carried no MinAgent
line. Each now reads `**MinAgent: 0.129.0** (unchanged)`, the value of the
nearest earlier header (v0.232.0). Proof it did not move: zero commits
under controller/internal/agentapi since 2026-09-01 (last: bb50e12,
2026-08-14), and the highest featureMinAgent entry is 0.129.0.
This commit is contained in:
2026-09-13 17:09:33 +02:00
parent cf8a371c39
commit f946b0d0ca
6 changed files with 144 additions and 0 deletions
+8
View File
@@ -113,6 +113,8 @@ to fail** — three first ran INERT or did not compile and were fixed before the
## v0.236.0 — "delete my data too" deletes the data, or says that it could not (2026-09-13, R-442) ## v0.236.0 — "delete my data too" deletes the data, or says that it could not (2026-09-13, R-442)
**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01
**The defect.** A customer removes an app and ticks *also delete my data*. The box says it worked; **The defect.** A customer removes an app and ticks *also delete my data*. The box says it worked;
**the data is still on the disk** — measured 2026-09-01 on demo-hp: `HTTP 200` with **the data is still on the disk** — measured 2026-09-01 on demo-hp: `HTTP 200` with
`hdd_paths_removed: null, hdd_paths_preserved: null` and 128 MB of Nextcloud left at `hdd_paths_removed: null, hdd_paths_preserved: null` and 128 MB of Nextcloud left at
@@ -162,6 +164,8 @@ metrics) — left alone; not deleted here.
## v0.235.0 — freeze the version, keep the fixes flowing (2026-09-06, update arc slice 3) ## v0.235.0 — freeze the version, keep the fixes flowing (2026-09-06, update arc slice 3)
**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01
**OPERATOR RULING, 2026-09-06 — Option 1.** R-447 was `BLOCKED` because R-438 established that **OPERATOR RULING, 2026-09-06 — Option 1.** R-447 was `BLOCKED` because R-438 established that
`RestartStack`'s use of `up -d` to pick up template changes was **chosen** and written down in its own `RestartStack`'s use of `up -d` to pick up template changes was **chosen** and written down in its own
comment; reversing a chosen behaviour is a decision, not a bug fix. The decision is made and this comment; reversing a chosen behaviour is a decision, not a bug fix. The decision is made and this
@@ -273,6 +277,8 @@ catalog. `TestRenderTable_EmptyStoredDefinitionIsTreatedAsAbsent`.
## v0.234.0 — the label now appears on an app nobody has touched (2026-09-03, update arc slice 1b) ## v0.234.0 — the label now appears on an app nobody has touched (2026-09-03, update arc slice 1b)
**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01
**Found by the operator on demo-felhom the morning after v0.233.0, and it is a real gap, not a **Found by the operator on demo-felhom the morning after v0.233.0, and it is a real gap, not a
misunderstanding:** OpenGist had been up 15 hours, was running exactly what the catalog pins, and misunderstanding:** OpenGist had been up 15 hours, was running exactly what the catalog pins, and
showed **no badge at all**. v0.233.0 wrote the record only from the four bring-up paths, so an app showed **no badge at all**. v0.233.0 wrote the record only from the four bring-up paths, so an app
@@ -326,6 +332,8 @@ a backfill nothing invokes seeds nothing, and a `strings.Contains` would match a
## v0.233.0 — the box writes down what it installed, and one label says whether it is current (2026-09-02, update arc slices 1 & 2) ## v0.233.0 — the box writes down what it installed, and one label says whether it is current (2026-09-02, update arc slices 1 & 2)
**MinAgent: 0.129.0** (unchanged) — backfilled 2026-09-13 (R-470): read from v0.232.0's header; `internal/agentapi/` has no commit since 2026-09-01
**Neither slice changes any behaviour.** The Frissítés button, the restart path, the sync and the boot **Neither slice changes any behaviour.** The Frissítés button, the restart path, the sync and the boot
reconciler are byte-identical. This adds a RECORD and a LABEL, because the behaviour work (slice 3) reconciler are byte-identical. This adds a RECORD and a LABEL, because the behaviour work (slice 3)
is easier to judge once the fleet's real state is visible. Opened by is easier to judge once the fleet's real state is visible. Opened by
+2
View File
@@ -84,6 +84,8 @@ GATES = [
("instructions", SHARED_INSTRUCTIONS, [REPO], True, True), ("instructions", SHARED_INSTRUCTIONS, [REPO], True, True),
# R-389 — a REPORT.md observation with no register row behind it. Fast: stdlib file reads. # R-389 — a REPORT.md observation with no register row behind it. Fast: stdlib file reads.
("observations", SHARED_OBSERVATIONS, [REPO], True, True), ("observations", SHARED_OBSERVATIONS, [REPO], True, True),
# R-470/R-472 — the newest release header states its MinAgent; the hub's floor reads it. Fast.
("minagent-header", os.path.join(SCRIPTS, "minagent_header_gate.py"), [], True, True),
# R-404 — ADVISORY. Reports the golden debt where it is created; never refuses. # R-404 — ADVISORY. Reports the golden debt where it is created; never refuses.
("golden-notice", GOLDEN_NOTICE, [REPO], True, False), ("golden-notice", GOLDEN_NOTICE, [REPO], True, False),
] ]
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""minagent_header_gate.py — the newest release header must state its MinAgent (R-470, R-472).
Run from controller/: python3 scripts/minagent_header_gate.py
Exit 0 clean · 1 the newest `## vX.Y.Z` block has no `**MinAgent: X.Y.Z**` line · 2 inconclusive.
WHY THIS EXISTS. From hub v0.112.0 a controller release can reach the fleet by the managed floor
WITHOUT a golden, because the operator declares the release's agent requirement with the floor
(operator ruling 2026-09-13, R-472). That declaration is READ from this file's newest header — the
`**MinAgent: 0.129.0** (unchanged)` line `RUNBOOK-manual-build.md` §4.1 already told the vouch to
read. Four headers (v0.233.0 … v0.236.0) did not carry it (R-470), so the value the hub is now told
to trust would have had to be guessed. A rule with no gate is a wish; this is the gate.
WHAT COUNTS. The block from the newest `## vX.Y.Z` heading to the next `## ` heading must contain a
line that STARTS with `**MinAgent: <X.Y.Z>**`. A mention of the word anywhere else — prose saying
"MinAgent is unchanged", a code span, the heading itself — is the label without the fact (R-421)
and does not count.
"""
import io
import os
import re
import sys
SCRIPTS = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(os.path.dirname(SCRIPTS))
CHANGELOG = os.environ.get("MINAGENT_GATE_CHANGELOG", os.path.join(REPO, "CHANGELOG.md"))
HEADER_RE = re.compile(r"^##\s+v(\d+\.\d+\.\d+)\b")
MINAGENT_LINE_RE = re.compile(r"^\*\*MinAgent:\s*(\d+\.\d+\.\d+)\*\*")
def newest_block(lines):
"""(version, block_lines) for the newest `## vX.Y.Z` heading, or (None, [])."""
start, version = None, None
for i, line in enumerate(lines):
m = HEADER_RE.match(line)
if m:
start, version = i, m.group(1)
break
if start is None:
return None, []
block = []
for line in lines[start + 1:]:
if line.startswith("## "):
break
block.append(line)
return version, block
def main():
try:
lines = io.open(CHANGELOG, encoding="utf-8").read().splitlines()
except OSError as e:
print("minagent header gate INCONCLUSIVE: cannot read %s (%s)" % (CHANGELOG, e))
return 2
version, block = newest_block(lines)
if version is None:
print("minagent header gate INCONCLUSIVE: no '## vX.Y.Z' heading in %s" % CHANGELOG)
return 2
for line in block:
m = MINAGENT_LINE_RE.match(line.strip())
if m:
print("minagent header gate OK — v%s declares MinAgent %s" % (version, m.group(1)))
return 0
print("minagent header gate FAILED: the newest release header v%s has no '**MinAgent: X.Y.Z**' line." % version)
print("From hub v0.112.0 the floor carries a release past the golden only with a DECLARED MinAgent, and")
print("that value is read from this line (R-472). Add it directly under the heading, e.g.:")
print(" **MinAgent: 0.129.0** (unchanged)")
print("Read the value from internal/agentapi/features.go (the highest featureMinAgent) — never guess it.")
return 1
if __name__ == "__main__":
sys.exit(main())
@@ -32,6 +32,7 @@ FINGERPRINTS = [
("docker-v", "docker -v gate OK"), ("docker-v", "docker -v gate OK"),
("debug-routes", "debug route gate OK"), ("debug-routes", "debug route gate OK"),
("reuse-refs", "cited paths — exact"), ("reuse-refs", "cited paths — exact"),
("minagent-header", "minagent header gate OK"),
] ]
+1
View File
@@ -43,6 +43,7 @@ COVERS = {
"mojibake": "CONTROL: already walked; proves the planted file is really reachable", "mojibake": "CONTROL: already walked; proves the planted file is really reachable",
"debug-routes": "a live dispatcher case commented out - the button survives, the handler dies", "debug-routes": "a live dispatcher case commented out - the button survives, the handler dies",
"golden-notice": "R-410 in the other direction: an empty dir must not count as a bake", "golden-notice": "R-410 in the other direction: an empty dir must not count as a bake",
"minagent-header": "the word MinAgent in prose / a code span, not the header line (test_minagent_header_gate.py)",
} }
fails = [] fails = []
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Tests for minagent_header_gate.py (R-470/R-472) — each case writes a CHANGELOG and runs the REAL gate.
Run from controller/: python3 scripts/test_minagent_header_gate.py
"""
import os
import subprocess
import sys
import tempfile
import unittest
SCRIPTS = os.path.dirname(os.path.abspath(__file__))
GATE = os.path.join(SCRIPTS, "minagent_header_gate.py")
def run(body):
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f:
f.write(body)
path = f.name
try:
env = dict(os.environ, MINAGENT_GATE_CHANGELOG=path)
p = subprocess.run([sys.executable, GATE], capture_output=True, text=True, env=env)
return p.returncode, p.stdout + p.stderr
finally:
os.unlink(path)
class MinAgentHeaderGateTest(unittest.TestCase):
def test_header_line_passes(self):
rc, out = run("## v0.239.0 — x (2026-09-13)\n\n**MinAgent: 0.129.0** (unchanged)\n\nbody\n\n## v0.238.1 — y\n")
self.assertEqual(rc, 0, out)
self.assertIn("v0.239.0 declares MinAgent 0.129.0", out)
def test_missing_line_fails_naming_the_header(self):
rc, out = run("## v0.239.0 — x (2026-09-13)\n\nbody\n\n## v0.238.1 — y\n\n**MinAgent: 0.129.0** (unchanged)\n")
self.assertEqual(rc, 1, out)
self.assertIn("v0.239.0", out)
def test_decoy_body_mention_is_not_the_line(self):
"""R-421: the word in prose, a code span, or a non-bold form is the LABEL without the FACT.
COMPANION RED-PROOF F (REPORT.md): relax MINAGENT_LINE_RE to a plain search for 'MinAgent'.
This test then passes the decoy and fails."""
decoy = ("## v0.239.0 — x (2026-09-13)\n\n"
"MinAgent is unchanged from the previous release, see `**MinAgent: 0.129.0**` there.\n"
"The agent requirement (MinAgent: 0.129.0) did not move.\n\n## v0.238.1 — y\n")
rc, out = run(decoy)
self.assertEqual(rc, 1, out)
def test_no_header_is_inconclusive(self):
rc, out = run("# CHANGELOG\n\nnothing released\n")
self.assertEqual(rc, 2, out)
if __name__ == "__main__":
unittest.main()