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
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:
@@ -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
|
||||||
|
|||||||
@@ -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"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user