Files
admin f946b0d0ca
gates / gates (push) Successful in 12s
gates: the newest release header must state its MinAgent; backfill v0.233.0-v0.236.0 (R-470, R-472)
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.
2026-09-13 17:09:33 +02:00

76 lines
3.1 KiB
Python

#!/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())