R-459 CLOSED (MariaDB converts itself, proven by harness + live), golden 0.236.0 (R-467), the golden waiver (R-468)
Operator rulings 2026-09-13, both shipped the same day: - MariaDB finishes its own conversion (catalog eec1228/bd32830/3525e35). Harness E3/E3b `proven` with engine_state_after "already upgraded to 12.3.3-MariaDB [exit=1]", the skip line gone, C3 still `failed`; landed on demo-hp through the real 15-min cycle, nothing recreated, one deliberate restart logged "MariaDB upgrade not required" with the app serving. Evidence: documentation/audits/r459-close-2026-09-13/. The engine-major rule + gate keep every engine inside its major until Slice 4 (R-448) — removal tracked as R-469. - Goldens on a cadence, not per release. golden_currency_gate.py reads a dated waiver (documentation/tests/golden-waiver.yml, <= 14 days, row-bound): valid + BEHIND -> loud advisory, exit 0; expired -> red again naming the date; UNRECORDED (R-385) never covered; malformed -> 2, never 0. Tests cases 5-15 incl. the R-421 decoy; red-proof old-vs-new on the real behind tree. R-242's vouch half stays open. Cadence in RUNBOOK-manual-build.md §4.2 + the checklist. - Golden 0.236.0 baked, round-tripped, vouched, floor raised 0.232.0 -> 0.236.0 (documentation/tests/golden-0.236.0-2026-09-13/) — the last per-release bake; the waiver was issued AFTER it landed. No --no-verify anywhere in this session. Rows: R-459 CLOSED, R-467 CLOSED, R-242 narrowed; R-468/R-469/R-470/R-471 opened. 09 §3 gains decisions 5 and 6; STATUS items 11 and 12 closed; CONTEXT records the cadence ruling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,3 +1,31 @@
|
||||
## the golden waiver — goldens on a cadence, not per release (2026-09-13, R-468 / R-242) — NOT A RELEASE
|
||||
|
||||
**No product code, no version bump, no image.** A scripts change is not a release.
|
||||
|
||||
`golden_currency_gate.py` now reads a dated waiver, `documentation/tests/golden-waiver.yml` (`issued`,
|
||||
`expires`, `reason`, `register_row`). Operator ruling 2026-09-13: **bake on a cadence — weekly, and
|
||||
always before any drill or fresh install — not per release.** The gate had tripped on every release
|
||||
by design and the only honest ways past it were a bake or a declared `--no-verify`; August measured
|
||||
25 goldens in 26 days and thirteen bypasses.
|
||||
|
||||
- **Valid waiver + golden BEHIND the record** → a loud ADVISORY naming the waiver, its expiry and how
|
||||
many releases the golden lags; exit 0. A waived conviction stays visible on every push.
|
||||
- **Expired waiver** → exit 1 exactly as before, and the message says it EXPIRED on `<date>`.
|
||||
- **Unrecorded golden (R-385)** → exit 1 regardless; the waiver is named and declared NOT to cover
|
||||
it. **The asymmetry is the design:** behind is a cadence choice, unrecorded is the fleet running
|
||||
something nobody wrote down.
|
||||
- **Malformed waiver** — more than 14 days, absent or unparseable date, empty reason, a row that does
|
||||
not exist in `OPEN-ITEMS.md` — → exit 2 INCONCLUSIVE, never 0, never silently ignored. The 14-day
|
||||
cap lives in the gate (`WAIVER_MAX_DAYS`), not in prose. The register is read for ONE fact (does
|
||||
the row exist), never for meaning (R-421).
|
||||
- `GOLDEN_GATE_*` environment variables are a **test seam**: they move where the gate reads, never
|
||||
what it decides. `test_golden_currency_gate.py` gains cases 5–15 (Scenarios E/F/G/H, each with its
|
||||
wrong direction; the R-421 decoy — a file saying only `expires` — returns 2). **Red-proof:** the
|
||||
old gate on the real tree with a valid waiver planted → exit 1 (it cannot read one); the new gate on
|
||||
synthetic "behind" trees → 0 with the advisory present.
|
||||
- The docstring's *"honest fix is a recorded waiver, never a habit of bypassing"* is now a mechanism.
|
||||
**R-242's OTHER half — nothing gates the VOUCH — is unchanged and still open.**
|
||||
|
||||
## the decoy sweep — can this gate be fooled by a label? (2026-09-01, R-421) — NOT A RELEASE
|
||||
|
||||
**No product code, no version bump, no image, no golden.** A scripts change is not a release.
|
||||
|
||||
@@ -37,7 +37,8 @@ That limit is forced, not chosen, and the reasoning is recorded so nobody re-der
|
||||
and is therefore the step this repo can see; the vouch is an operator act against the hub and needs a
|
||||
different mechanism. That gap is real and is recorded as R-242's remaining half, NOT papered over
|
||||
here. In practice the two are minutes apart in the same session, and the recurrence this gate is
|
||||
built for was a missing BAKE.
|
||||
built for was a missing BAKE. **That vouch half is STILL open after 2026-09-13** — the waiver below
|
||||
covers a different thing, and this sentence is kept so the two are not confused.
|
||||
|
||||
WHY VERSION AND NOT BEHAVIOUR. It compares version numbers, so a controller release that changed
|
||||
nothing a customer can see also trips it. That is accepted deliberately: deciding "customer-visible"
|
||||
@@ -45,7 +46,8 @@ mechanically is not possible, judging it by hand is what already failed twice, a
|
||||
false trip is one bake — which is the operation the project wants to be routine anyway. **A gate that
|
||||
cries wolf is one people learn to bypass, and `--no-verify` exists**, so the tolerance is stated
|
||||
rather than assumed: if this ever fires on a release nobody wants a golden for, the honest fix is a
|
||||
recorded waiver in the register, never a habit of bypassing.
|
||||
recorded waiver in the register, never a habit of bypassing. **That waiver is now BUILT — see
|
||||
"THE WAIVER" below (2026-09-13).**
|
||||
|
||||
FAIL-CLOSED, BUT HONEST ABOUT NOT KNOWING. An absent controller clone, or a CHANGELOG whose top
|
||||
header cannot be parsed, exits **2 (INCONCLUSIVE)** — never 0. The runner reports 2 distinctly for
|
||||
@@ -72,7 +74,47 @@ it is absent. An unrecorded golden is convicted (exit 1) exactly like a stale on
|
||||
**Why membership and not `baked > released`.** A comparison against the newest heading alone would go
|
||||
green again the moment ANY later entry was written, leaving 0.221.1 permanently unrecorded and the
|
||||
gate permanently silent about it. Membership cannot be satisfied by an unrelated later release.
|
||||
|
||||
── THE WAIVER — goldens on a CADENCE, not per release (added 2026-09-13, operator ruling) ────────
|
||||
|
||||
What happened between 2026-08-07 and 2026-09-01: **25 goldens in 26 days**, almost one per release,
|
||||
because this gate trips on every release by design (see WHY VERSION AND NOT BEHAVIOUR) and the only
|
||||
honest ways past it were a bake or a `--no-verify`. Thirteen bypasses were counted by 2026-09-01
|
||||
(R-404/R-417). The operator ruled on 2026-09-13: **bake on a cadence — weekly, and always before any
|
||||
drill or fresh install — not per release.** Every release still raises the FLOOR, so the fleet keeps
|
||||
getting each release in ~20 s; only the golden, which protects a fresh install and nothing else,
|
||||
moves to a cadence.
|
||||
|
||||
The mechanism is a small tracked file, `documentation/tests/golden-waiver.yml`:
|
||||
|
||||
issued: 2026-09-13
|
||||
expires: 2026-09-27 # at most 14 days after issued, or the gate REFUSES the waiver
|
||||
reason: pre-customer development; goldens on a weekly cadence (operator ruling 2026-09-13)
|
||||
register_row: R-468 # must exist as a `**R-468**` row in OPEN-ITEMS.md
|
||||
|
||||
While the waiver is VALID, the "behind" conviction becomes a **loud ADVISORY** (exit 0) that names
|
||||
the waiver, its expiry and how many releases the golden lags. When it runs out, the gate is red
|
||||
again until someone bakes or renews. **A dated waiver cannot be forgotten — it just expires.** That
|
||||
is what makes it different from R-242's original rule, which recurred the day after it was written:
|
||||
renewal is a new commit with a diff, a deliberate act someone can see.
|
||||
|
||||
THE ASYMMETRY, stated because it is the whole design: **the waiver covers a golden that is BEHIND the
|
||||
record. It never covers a golden that is UNRECORDED (R-385).** The first is a cadence choice; the
|
||||
second is the fleet running something nobody wrote down, and no schedule makes that acceptable.
|
||||
|
||||
WHAT REFUSES THE WAIVER (exit 2, INCONCLUSIVE — never 0, never silently ignored): an expiry more
|
||||
than 14 days after issue; an absent or unparseable date; a missing or empty reason; a register row
|
||||
that is absent or whose `**R-n**` row does not exist. The 14-day cap is enforced HERE, not in the
|
||||
runbook, because a cap in prose is the thing that failed. A file at the right path containing only
|
||||
the word `expires` is the R-421 decoy and is refused like any other malformed waiver.
|
||||
|
||||
The register is read for exactly ONE fact — does the named row exist — never for meaning. Reading
|
||||
prose for meaning is the R-421 class.
|
||||
|
||||
WHAT IT DOES NOT COVER: the vouch (still R-242's open half); an unrecorded golden (R-385); anything
|
||||
after the first external install — the waiver's own row says it is a pre-customer arrangement.
|
||||
"""
|
||||
import datetime
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
@@ -81,8 +123,21 @@ import sys
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
# The controller clone sits beside this one. The same sibling assumption reuse_refs_check.py and
|
||||
# instructions_gate.py already make — an absent sibling is INCONCLUSIVE, never a silent pass.
|
||||
CONTROLLER_CHANGELOG = os.path.join(os.path.dirname(ROOT), "felhom-controller", "CHANGELOG.md")
|
||||
EVIDENCE_DIR = os.path.join(ROOT, "documentation", "tests")
|
||||
#
|
||||
# The GOLDEN_GATE_* environment variables are a TEST SEAM (2026-09-13): they move WHERE the gate
|
||||
# reads, never WHAT it decides, so test_golden_currency_gate.py can build a "behind" or an
|
||||
# "unrecorded" tree without depending on the real controller's release history. Unset in every real
|
||||
# run — the hook and CI export nothing.
|
||||
CONTROLLER_CHANGELOG = os.environ.get(
|
||||
"GOLDEN_GATE_CHANGELOG",
|
||||
os.path.join(os.path.dirname(ROOT), "felhom-controller", "CHANGELOG.md"))
|
||||
EVIDENCE_DIR = os.environ.get("GOLDEN_GATE_EVIDENCE_DIR", os.path.join(ROOT, "documentation", "tests"))
|
||||
WAIVER_PATH = os.environ.get("GOLDEN_GATE_WAIVER",
|
||||
os.path.join(ROOT, "documentation", "tests", "golden-waiver.yml"))
|
||||
REGISTER_PATH = os.environ.get("GOLDEN_GATE_REGISTER",
|
||||
os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md"))
|
||||
# The HARD LIMIT on a waiver's life. Enforced here and nowhere else — see the module docstring.
|
||||
WAIVER_MAX_DAYS = 14
|
||||
|
||||
# `## v0.206.0 — …` on the FIRST such line: the CHANGELOG is newest-first by convention.
|
||||
RELEASED_RE = re.compile(r"^##\s+v(\d+)\.(\d+)\.(\d+)\b")
|
||||
@@ -182,6 +237,82 @@ def newest_baked():
|
||||
return found[-1]
|
||||
|
||||
|
||||
WAIVER_KEY_RE = re.compile(r"^\s*([a-z_]+)\s*:\s*(.*?)\s*$")
|
||||
ROW_RE = re.compile(r"^R-\d+$")
|
||||
|
||||
|
||||
def read_waiver(path=None, register=None, today=None):
|
||||
"""(state, info) — state is 'absent', 'valid', 'expired' or 'malformed'.
|
||||
|
||||
The file is four `key: value` lines; comments (`# …`) and blank lines are ignored. It is parsed by
|
||||
hand so the gate stays stdlib-only and `--fast`. Every way the file can be wrong returns
|
||||
'malformed' with the reason in info["why"] — the caller turns that into exit 2, never into 0 and
|
||||
never into a silent 'absent'. A decoy — the word `expires` with no date — lands here too.
|
||||
"""
|
||||
path = path or WAIVER_PATH
|
||||
register = register or REGISTER_PATH
|
||||
today = today or datetime.datetime.now(datetime.timezone.utc).date()
|
||||
if not os.path.isfile(path):
|
||||
return "absent", {"path": path}
|
||||
try:
|
||||
text = io.open(path, encoding="utf-8").read()
|
||||
except OSError as e:
|
||||
return "malformed", {"path": path, "why": "cannot be read (%s)" % e}
|
||||
fields = {}
|
||||
for line in text.splitlines():
|
||||
line = line.split("#", 1)[0]
|
||||
m = WAIVER_KEY_RE.match(line)
|
||||
if m and m.group(2):
|
||||
fields[m.group(1)] = m.group(2).strip().strip("'\"")
|
||||
info = {"path": path, "fields": fields}
|
||||
|
||||
def date_of(key):
|
||||
v = fields.get(key, "")
|
||||
try:
|
||||
return datetime.date.fromisoformat(v)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
issued, expires = date_of("issued"), date_of("expires")
|
||||
if issued is None:
|
||||
info["why"] = "`issued:` is absent or not a YYYY-MM-DD date (got %r)" % fields.get("issued", "")
|
||||
return "malformed", info
|
||||
if expires is None:
|
||||
info["why"] = "`expires:` is absent or not a YYYY-MM-DD date (got %r)" % fields.get("expires", "")
|
||||
return "malformed", info
|
||||
if expires <= issued:
|
||||
info["why"] = "`expires:` (%s) is not after `issued:` (%s)" % (expires, issued)
|
||||
return "malformed", info
|
||||
span = (expires - issued).days
|
||||
if span > WAIVER_MAX_DAYS:
|
||||
info["why"] = ("`expires:` is %d days after `issued:` — the hard limit is %d. A waiver that "
|
||||
"tries to be permanent is refused; renew it with a new dated commit instead."
|
||||
% (span, WAIVER_MAX_DAYS))
|
||||
return "malformed", info
|
||||
if not fields.get("reason"):
|
||||
info["why"] = "`reason:` is absent or empty"
|
||||
return "malformed", info
|
||||
row = fields.get("register_row", "")
|
||||
if not ROW_RE.match(row):
|
||||
info["why"] = "`register_row:` is absent or not of the form R-<n> (got %r)" % row
|
||||
return "malformed", info
|
||||
# The register is read for ONE fact — does the row exist — never for meaning (R-421).
|
||||
try:
|
||||
reg = io.open(register, encoding="utf-8").read()
|
||||
except OSError as e:
|
||||
info["why"] = "register %s cannot be read (%s)" % (register, e)
|
||||
return "malformed", info
|
||||
if ("**%s**" % row) not in reg:
|
||||
info["why"] = "`register_row: %s` names a row that does not exist in %s" % (
|
||||
row, os.path.relpath(register, ROOT) if register.startswith(ROOT) else register)
|
||||
return "malformed", info
|
||||
info.update({"issued": issued, "expires": expires, "row": row, "reason": fields["reason"],
|
||||
"days_left": (expires - today).days})
|
||||
if today >= expires:
|
||||
return "expired", info
|
||||
return "valid", info
|
||||
|
||||
|
||||
def vstr(v):
|
||||
return ".".join(str(p) for p in v)
|
||||
|
||||
@@ -201,6 +332,23 @@ def main():
|
||||
print(" newest released controller : %s (%s)" % (vstr(released), rel_note))
|
||||
print(" newest golden baked : %s (documentation/tests/%s)" % (vstr(baked), bake_note))
|
||||
|
||||
# The waiver is read BEFORE any verdict, because a malformed one is a result of its own (exit 2)
|
||||
# whatever the golden's state — a file that says `expires` and means nothing must not lie there
|
||||
# looking like cover.
|
||||
wstate, winfo = read_waiver()
|
||||
if wstate == "malformed":
|
||||
print("")
|
||||
print("GOLDEN CURRENCY GATE INCONCLUSIVE: the waiver at %s is MALFORMED — %s"
|
||||
% (os.path.relpath(winfo["path"], ROOT), winfo["why"]))
|
||||
print("A malformed waiver is neither cover nor absence. Fix it (four lines: issued, expires "
|
||||
"<= %d days later, reason, register_row) or delete it." % WAIVER_MAX_DAYS)
|
||||
sys.exit(2)
|
||||
if wstate == "valid":
|
||||
print(" waiver : VALID until %s (%d day(s) left) — %s, reason: %s"
|
||||
% (winfo["expires"], winfo["days_left"], winfo["row"], winfo["reason"]))
|
||||
elif wstate == "expired":
|
||||
print(" waiver : EXPIRED on %s (%s)" % (winfo["expires"], winfo["row"]))
|
||||
|
||||
# R-385 — UNRECORDED, checked before "behind". A golden whose version has no heading of its own
|
||||
# was built from something never written down, and that is a different (worse) fault than a
|
||||
# forgotten bake: there is nothing to read to find out what the fleet is running.
|
||||
@@ -208,6 +356,12 @@ def main():
|
||||
print("")
|
||||
print("GOLDEN CURRENCY GATE FAILED: golden %s is baked but UNRECORDED — the controller "
|
||||
"CHANGELOG has no '## v%s' heading." % (vstr(baked), vstr(baked)))
|
||||
if wstate == "valid":
|
||||
# THE ASYMMETRY. A waiver is a cadence choice about a golden that is BEHIND; it says
|
||||
# nothing about a golden nobody wrote down, and must not be read as if it did.
|
||||
print("A waiver exists (%s, until %s) and DOES NOT COVER THIS: it covers a golden that is "
|
||||
"behind the record, never one that is unrecorded (R-385)."
|
||||
% (winfo["row"], winfo["expires"]))
|
||||
print("The newest heading is %s. A golden ahead of the record was built from a version "
|
||||
"nobody wrote down, so no one can read what the fleet is running." % vstr(released))
|
||||
print("Fix: give v%s its own '## v%s — <what changed>' heading in "
|
||||
@@ -220,17 +374,40 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
if released > baked:
|
||||
lag = sorted(v for v in every_released if v > baked)
|
||||
if wstate == "valid":
|
||||
print("")
|
||||
print("GOLDEN CURRENCY GATE ADVISORY — WAIVED, NOT CLEAN: controller v%s is released and "
|
||||
"NO golden carries it (newest bake is %s; %d release(s) behind: %s)."
|
||||
% (vstr(released), vstr(baked), len(lag), ", ".join(vstr(v) for v in lag)))
|
||||
print("A machine installed right now would receive v%s and reach v%s by self-update."
|
||||
% (vstr(baked), vstr(released)))
|
||||
print("Waived by %s until %s (%d day(s) left): %s"
|
||||
% (winfo["row"], winfo["expires"], winfo["days_left"], winfo["reason"]))
|
||||
print("This is the operator's 2026-09-13 cadence ruling, not a pass: bake weekly and "
|
||||
"before ANY drill or fresh install (RUNBOOK-manual-build.md §4.2). When the waiver "
|
||||
"expires this gate is red again.")
|
||||
print("golden currency gate OK (WAIVED) — the newest released controller has NO golden; "
|
||||
"a valid waiver covers it (NOTE: this checks the BAKE, not the vouch)")
|
||||
return
|
||||
print("")
|
||||
print("GOLDEN CURRENCY GATE FAILED: controller v%s is released and NO golden carries it "
|
||||
"(newest bake is %s)." % (vstr(released), vstr(baked)))
|
||||
"(newest bake is %s; %d release(s) behind)." % (vstr(released), vstr(baked), len(lag)))
|
||||
if wstate == "expired":
|
||||
print("The waiver at documentation/tests/golden-waiver.yml EXPIRED on %s (%s). It ran "
|
||||
"out, as a dated waiver is meant to: bake a golden, or renew it with a new dated "
|
||||
"commit (at most %d days)." % (winfo["expires"], winfo["row"], WAIVER_MAX_DAYS))
|
||||
print("A machine installed right now would receive v%s — the release is written, tested and "
|
||||
"pushed, and NOT delivered." % vstr(baked))
|
||||
print("Fix: bake a golden per documentation/runbooks/RUNBOOK-manual-build.md §4.1, then vouch "
|
||||
"it (a THREE-field change: golden_version + agent_version + min_agent).")
|
||||
print("If this release deliberately needs no golden, record a waiver in "
|
||||
"documentation/backlog/OPEN-ITEMS.md — never a bypass.")
|
||||
print("If the cadence ruling covers this release, issue a DATED waiver at "
|
||||
"documentation/tests/golden-waiver.yml (RUNBOOK-manual-build.md §4.2) — never a bypass.")
|
||||
sys.exit(1)
|
||||
|
||||
if wstate == "expired":
|
||||
print(" NOTE: the waiver expired on %s and covers nothing today (the golden is current). "
|
||||
"Renew or delete it so it does not read as cover." % winfo["expires"])
|
||||
print("golden currency gate OK — the newest released controller has a golden "
|
||||
"(NOTE: this checks the BAKE, not the vouch — see the module docstring)")
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ satisfied by a gate that fails on everything.
|
||||
|
||||
Run: python3 scripts/test_golden_currency_gate.py
|
||||
Exit 0 all pass · 1 a case failed.
|
||||
|
||||
2026-09-13: the WAIVER (Scenarios E-H of the task that built it) is tested below in `waiver_cases`,
|
||||
against a synthetic tree through the gate's GOLDEN_GATE_* seam — a valid waiver passes with the
|
||||
advisory PRESENT; an expired one convicts and NAMES the date; a valid one does NOT save an
|
||||
unrecorded golden (R-385); a 15-day, absent-expiry, unparseable, bad-row and empty-reason waiver
|
||||
each return 2; and the R-421 decoy (a file saying only `expires`) returns 2.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
@@ -111,13 +117,132 @@ def main():
|
||||
else:
|
||||
print("CASE 4 ok: the tree is unchanged; the gate's verdict is the same as the baseline")
|
||||
|
||||
fails += waiver_cases()
|
||||
|
||||
if fails:
|
||||
print()
|
||||
for f in fails:
|
||||
print("FAIL: %s" % f)
|
||||
return 1
|
||||
print("\ngolden-currency gate self-test OK — a directory name alone cannot satisfy it (R-410)")
|
||||
print("\ngolden-currency gate self-test OK — a directory name alone cannot satisfy it (R-410), "
|
||||
"and the waiver is judged on its dates, its row and its direction (2026-09-13)")
|
||||
return 0
|
||||
|
||||
|
||||
# ── THE WAIVER (2026-09-13) — Scenarios E, F, G, H, each with its red-proof ─────────────────────
|
||||
#
|
||||
# These run the gate against a SYNTHETIC tree through the GOLDEN_GATE_* seam (a fake controller
|
||||
# CHANGELOG, a fake evidence dir with a real-shaped bake log, a fake register, a waiver file), so the
|
||||
# verdicts do not depend on what the real controller happens to have released this week. The seam
|
||||
# moves where the gate reads, never what it decides.
|
||||
|
||||
import datetime
|
||||
import tempfile
|
||||
|
||||
|
||||
def run_gate_env(env):
|
||||
e = dict(os.environ)
|
||||
e.update(env)
|
||||
p = subprocess.run([sys.executable, GATE], capture_output=True, text=True, env=e)
|
||||
return p.returncode, p.stdout + p.stderr
|
||||
|
||||
|
||||
def synthetic_tree(released, baked, waiver_text, register_rows=("R-468",)):
|
||||
"""Build a tree where the CHANGELOG lists `released` (newest first) and one golden `baked` has a
|
||||
real-shaped bake log. Returns (env, tmpdir)."""
|
||||
tmp = tempfile.mkdtemp(prefix="golden-waiver-")
|
||||
ch = os.path.join(tmp, "CHANGELOG.md")
|
||||
with io.open(ch, "w", encoding="utf-8") as fh:
|
||||
for v in released:
|
||||
fh.write("## v%s — synthetic (2026-01-01)\n\nbody\n\n" % v)
|
||||
ev = os.path.join(tmp, "tests")
|
||||
os.makedirs(os.path.join(ev, "golden-%s-2026-01-01" % baked))
|
||||
with io.open(os.path.join(ev, "golden-%s-2026-01-01" % baked, "bake.log"), "w",
|
||||
encoding="utf-8") as fh:
|
||||
fh.write("[golden] upload OK (HTTP 201)\nGOLDEN_VERSION=%s\nGOLDEN_SHA256=%s\n" % (baked, REAL_SHA))
|
||||
reg = os.path.join(tmp, "OPEN-ITEMS.md")
|
||||
with io.open(reg, "w", encoding="utf-8") as fh:
|
||||
for r in register_rows:
|
||||
fh.write("| **%s** | synthetic row | READY | CC |\n" % r)
|
||||
wv = os.path.join(tmp, "golden-waiver.yml")
|
||||
if waiver_text is not None:
|
||||
with io.open(wv, "w", encoding="utf-8") as fh:
|
||||
fh.write(waiver_text)
|
||||
env = {"GOLDEN_GATE_CHANGELOG": ch, "GOLDEN_GATE_EVIDENCE_DIR": ev,
|
||||
"GOLDEN_GATE_WAIVER": wv, "GOLDEN_GATE_REGISTER": reg}
|
||||
return env, tmp
|
||||
|
||||
|
||||
def waiver(issued, expires, reason="pre-customer development; weekly cadence", row="R-468"):
|
||||
lines = ["# synthetic waiver"]
|
||||
if issued is not None:
|
||||
lines.append("issued: %s" % issued)
|
||||
if expires is not None:
|
||||
lines.append("expires: %s" % expires)
|
||||
if reason is not None:
|
||||
lines.append("reason: %s" % reason)
|
||||
if row is not None:
|
||||
lines.append("register_row: %s" % row)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def waiver_cases():
|
||||
fails = []
|
||||
today = datetime.date.today()
|
||||
d = lambda n: (today + datetime.timedelta(days=n)).isoformat()
|
||||
BEHIND = (["9.9.9", "9.9.8", "9.9.7"], "9.9.7") # released 9.9.9, golden 9.9.7: two behind
|
||||
UNRECORDED = (["9.9.9", "9.9.7"], "9.9.8") # golden 9.9.8 has no heading (R-385)
|
||||
|
||||
def check(label, released_baked, wtext, want_rc, must=(), must_not=(), rows=("R-468",)):
|
||||
env, tmp = synthetic_tree(released_baked[0], released_baked[1], wtext, rows)
|
||||
try:
|
||||
rc, out = run_gate_env(env)
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
bad = [m for m in must if m not in out] + ["NOT expected: " + m for m in must_not if m in out]
|
||||
if rc != want_rc or bad:
|
||||
fails.append("%s: rc=%d (wanted %d) %s\n%s" % (label, rc, want_rc, bad, out[-900:]))
|
||||
else:
|
||||
print("%s ok" % label)
|
||||
|
||||
# BASELINE for the synthetic tree — behind, no waiver: convicted exactly as before this change.
|
||||
check("CASE 5 (baseline, behind, no waiver -> 1)", BEHIND, None, 1,
|
||||
must=("GOLDEN CURRENCY GATE FAILED", "2 release(s) behind"))
|
||||
# E — a valid waiver turns the conviction into a LOUD advisory, exit 0.
|
||||
check("CASE 6 (E: valid waiver, behind -> ADVISORY, 0)", BEHIND, waiver(d(0), d(13)), 0,
|
||||
must=("ADVISORY", "WAIVED", "R-468", d(13), "2 release(s) behind", "OK (WAIVED)"),
|
||||
must_not=("GOLDEN CURRENCY GATE FAILED",))
|
||||
# F — the same waiver, expired: red again, and the reader learns it RAN OUT.
|
||||
check("CASE 7 (F: expired waiver, behind -> 1, names the date)", BEHIND,
|
||||
waiver(d(-10), d(0)), 1, must=("GOLDEN CURRENCY GATE FAILED", "EXPIRED on %s" % d(0)))
|
||||
check("CASE 7b (F: waiver expired yesterday -> 1)", BEHIND, waiver(d(-10), d(-1)), 1,
|
||||
must=("EXPIRED on %s" % d(-1),))
|
||||
# G — a valid waiver never saves an UNRECORDED golden (R-385).
|
||||
check("CASE 8 (G: valid waiver, UNRECORDED golden -> 1)", UNRECORDED, waiver(d(0), d(13)), 1,
|
||||
must=("UNRECORDED", "DOES NOT COVER THIS", "R-385"))
|
||||
# H — a waiver that tries to be permanent, or is malformed, is INCONCLUSIVE — never 0.
|
||||
check("CASE 9 (H: 15-day waiver -> 2)", BEHIND, waiver(d(0), d(15)), 2,
|
||||
must=("MALFORMED", "hard limit is 14"))
|
||||
check("CASE 9b (H: exactly 14 days is the limit and PASSES)", BEHIND, waiver(d(0), d(14)), 0,
|
||||
must=("ADVISORY",))
|
||||
check("CASE 10 (H: absent expiry -> 2)", BEHIND, waiver(d(0), None), 2,
|
||||
must=("MALFORMED", "expires"))
|
||||
check("CASE 11 (H: unparseable expiry -> 2)", BEHIND, waiver(d(0), "next week"), 2,
|
||||
must=("MALFORMED", "expires"))
|
||||
check("CASE 12 (H: register row does not exist -> 2)", BEHIND, waiver(d(0), d(13), row="R-999999"),
|
||||
2, must=("MALFORMED", "R-999999", "does not exist"))
|
||||
check("CASE 12b (H: empty reason -> 2)", BEHIND, waiver(d(0), d(13), reason=""), 2,
|
||||
must=("MALFORMED", "reason"))
|
||||
# THE DECOY (R-421): a file at the right path saying only `expires` must not pass.
|
||||
check("CASE 13 (DECOY: a file containing only the word `expires` -> 2)", BEHIND, "expires\n", 2,
|
||||
must=("MALFORMED",), must_not=("ADVISORY", "OK (WAIVED)"))
|
||||
# A malformed waiver is refused EVEN WHEN the golden is current — it must not lie there as cover.
|
||||
check("CASE 14 (malformed waiver, golden CURRENT -> still 2)", (["9.9.9"], "9.9.9"),
|
||||
waiver(d(0), d(30)), 2, must=("MALFORMED",))
|
||||
# An expired waiver with a current golden: OK, but the expiry is NAMED.
|
||||
check("CASE 15 (expired waiver, golden current -> 0 with a note)", (["9.9.9"], "9.9.9"),
|
||||
waiver(d(-10), d(-1)), 0, must=("expired on %s" % d(-1), "golden currency gate OK"))
|
||||
return fails
|
||||
|
||||
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user