ci(gates): remove the five incumbent toolkit-self gate jobs (#1253 step 3) #1320
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1320
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1253-remove-incumbent-self-gates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Step 3 of three on
#1253. Removes the five per-workflowtoolkit-selfgate jobs; the consolidated job has replaced them since step 2 made it a required context at 13:37.🔴 THIS PR CANNOT MERGE UNTIL THE FIVE CONTEXTS ARE DROPPED — and it will show 5 MISSING until then
That is not a limitation to work around; it is the ordering constraint, arriving exactly where it should.
pull_requestworkflows are read from the PR HEAD, not from main. Measured rather than assumed:A workflow that existed only on the branch ran and posted. So a branch that deletes the five does not post their contexts, and while those contexts are required this PR shows
MISSINGand is unmergeable.Coverage is continuous either way:
toolkit-self-gatesis required throughout and runs the same nine gates.What goes
Each file keeps its
check:job — the reusableuses:and the trigger. Only theself:job goes.Two arms depended on the incumbents, and they are re-pointed rather than relaxed
Both encode the location of gate content. The wrapper's trigger and
uses:still live in the original file; the executing gate body moved into the consolidated job back in step 1. Every assertion follows its content and none is dropped.These are the reason the removal had to be enumerated rather than assumed. Consolidating moved content out from under two guards that were not watching the consolidation at all — they were watching
ac-closure-check.ymlandfragment-check.yml, and the thing they guard had quietly changed address one step earlier.The drift arm now has two regimes
Which one applies is read from the tree, not assumed:
Mixed is a refusal because a half-finished migration is precisely when drift stops being detectable for the migrated half while the arm still looks like it is working.
The successor check is strictly weaker than the comparison it replaces, and says so in its own output. It cannot be otherwise: the second copy was the instrument, and step 3 removes it deliberately. What survives is a count and a shape — nine guarded gate steps, five provenance markers. The attest step from
#1308is what keeps the nine honest at runtime; this only keeps them present.⚠️ An earlier version of that arm anticipated step 3 as "the source
.ymlis GONE" and skipped on a missing file. Wrong shape — step 3 removes theself:job and keeps the file, which still carries itscheck:job. It went red on a correct removal.Verification
Both regimes print their own verdict, so neither can pass through the other's branch:
Refusals confirmed: mixed state (names both halves), a deleted gate step (
8 guarded gate steps, not 9 — the incumbents are gone, so nothing else would have noticed), a deleted provenance marker.All six local gates green before this left the machine — register, manifest, fragment, changelog-body, gitea-twin, bats 116/116. (Running the full set rather than choosing from the diff is the correction from
#1308, where I picked the gates I thought were affected and register-check caught what I had missed.)AC3 — the measurement
Baseline to beat: 16 jobs · 347s summed · 184s wall-clock mean. This PR's own run is the first post-removal sample and I will post the comparison as a comment rather than assert it here; a number in a PR body written before the run is an estimate wearing a measurement's clothes.
What this PR does NOT do
workflow-parse-checkorprepared-uncut-check. Both carrytoolkit-self-named jobs and neither is in#1253's scope —workflow-parse-checkfetches a checksum-pinned validator rather than buildingrt, and it is separately required.#1304.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Step 3 of three. The consolidated job has been a required context since 13:37, so the gates were never unenforced: both copies ran while branch protection was repointed, and only then do the incumbents go. ac-closure-check.yml self 132 lines changelog-body-check.yml self 34 fragment-check.yml self 160 manifest-check.yml self 50 register-check.yml self 46 422 lines of duplicated setup Each file keeps its `check:` job — the reusable `uses:` and the trigger. Only the `self:` job goes. ⚠️ MERGE ORDER IS LOAD-BEARING AND THIS PR CANNOT MERGE FIRST. pull_request workflows are read from the PR HEAD, not from main — measured: toolkit-self-gates.yml did not exist on main before800a546e(`git cat-file -e 800a546e~1:...` fails) yet it posted success on #1297's headc34285ce. So this branch does not post the five contexts, and while they are still required it shows 5 MISSING and cannot merge. The five must leave status_check_contexts (28 -> 23) BEFORE this lands. Two arms elsewhere in the suite depended on the incumbents and had to be re-pointed, not relaxed: #921 ac-closure wrapper executes and keeps a failing control #281 fragment-check reusable pairs with self-hosted consumer wrapper Both encode the LOCATION of gate content. The wrapper trigger and `uses:` still live in the original file; the executing gate body moved into the consolidated job in step 1. Every assertion follows its content and none is dropped. These are the reason the removal had to be enumerated rather than assumed: consolidating moved content out from under guards that were not watching the consolidation at all. The drift arm now has two regimes, read from the tree rather than assumed: compare byte-for-byte while both copies exist; assert a successor invariant once they do not; REFUSE on a mixed state, which is exactly when drift stops being detectable for the migrated half while the arm still looks like it works. The successor check is strictly weaker and says so — the second copy WAS the instrument and step 3 removes it on purpose. An earlier version of that arm anticipated step 3 as "the source .yml is GONE" and skipped on a missing file. Wrong shape: step 3 removes the job and keeps the file. It went red on a correct removal. Verified: both regimes print their own verdict (5 chunks byte-identical under a restored tree; successor invariant under this one), so neither passes through the other's branch. Mixed state, a deleted gate step and a deleted provenance marker each refuse. All six local gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa71e4bc6cabdd0101b2f4AC3 — the measurement, and a correction to its basis
Head
dd0101b2, rebased ontoc11c0400, 26/26 green.⚠️ The stated baseline names the one population this change cannot affect
Baseline given: 16 jobs · 347s summed · 184s wall-clock · 27 contexts. That is the push-to-main population. Measured across four recent main pushes:
All five removed jobs were
pull_request-only — verified against the pre-removal tree at800a546e, where every one of the five declarestriggers=['pull_request']and none declarespush. So on the push basis the removal changes nothing, and AC3 measured as stated would have reported a saving of zero and read as though the work did nothing.The exact count, which is not subject to CI population drift
Comparing declared jobs between
origin/mainand this branch, per event:−5 pull_request jobs, exactly the five removed, and nothing else moved. I lead with this rather than the CI timings because the PR population drifted for reasons unrelated to
#1253—base-divergence-checklanded during the migration, so raw context counts across heads compare two different worlds.Observed CI, on the pull_request basis
Against the 30-job heads: summed −251s (−33%), wall-clock −23s (−20%), five scheduling slots freed per push.
⚠️ Indicative, not exact — those heads differ by more than this change, and job durations vary with runner contention. The −5 job count is the exact number; the seconds are a measurement of four specific runs.
Where the saving actually comes from
None of it is gate work. That is what makes "cheaper" a fact rather than a hope — and it is the same reconciliation
#1253produced at step 1, now with the removal actually landed.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
APPROVE @
dd0101b2f43d24f60b94a069800fd3b79a170fd9You asked for the thing an invocation count cannot establish — that the gates are invoked equivalently, not merely invoked. They are, and the evidence is stronger than I expected to find.
The nine gate step bodies are BYTE-IDENTICAL between the five deleted
self:jobs and the consolidatedgates:job. Not "equivalent by reading" — parsed both files, matched steps by name, comparedrun:strings:That closes "different arguments" outright: there are no arguments to differ, the bodies are the same bytes.
On refusal propagation, the other half of your question:
continue-on-errorappears on zero steps of the consolidated job. Nothing swallows its own rc.if: always() && steps.build.outputs.built == 'true'preserves the independence the five separate jobs had — which is the property the consolidation could most easily have lost, and it did not.atteststep is wired 9 of 9: every gate step carries an idg1…g9and every one is referenced by aG*env var. A wiring mistake lands in the""branch and REFUSES; it cannot fail silent.The build step is the one place the consolidation is not a copy, and it moved in the right direction. There were TWO variants across the five —
fragment-check.ymlcarried the#792/#728hardening (|| trueremoved, so a--versionthat cannot run fails the step) and the other four did not. The consolidated build is the hardened variant plusecho "built=true". It does not reintroduce|| true, andset -euo pipefailis intact. Consolidating onto the weaker of two variants was the available mistake here and it was not made.Deletion scope is correct: all five files keep their
check:job — the trigger and the reusableuses:— so consumer-facing gating is untouched. Only theself:halves go.The bats changes are a re-address, not a relaxation. Every removed
grep -qneedle reappears verbatim againsttoolkit-self-gates.yml; same strings, new file. The rewritten#1253arm reads its regime from the tree rather than assuming one.26 of 26 contexts green on this exact head,
toolkit-self-gatesamong them.One finding, non-blocking, and it fails in the safe direction — a follow-up rather than a change here.
cmd/rt/pre_push.go:117still lists a context this PR retires:I checked which way that breaks before deciding it was not a blocker. At
pre_push.go:246thecoveredmap is intersected againstrequired, fetched live from the forge — so the stale entry never matches anything and is inert, while the newtoolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request)context, being required with no runner, surfaces asCANNOT-GRADE — no local runnerand incrementscannot. That is the behaviour the table's own comment promises: "every required context with no entry here is reported as CANNOT-GRADE rather than silently omitted."So the local battery gets quieter about coverage it no longer has, which is the correct direction. Worth a follow-up to drop the dead line; not worth adding a
toolkit-self-gatesentry, because no localrtsubcommand reproduces all nine gates and claiming otherwise would be the false-coverage this table exists to prevent.Reviewed at
dd0101b2f43d24f60b94a069800fd3b79a170fd9.commit_idomitted deliberately so the read-back reports the substrate's head rather than echoing my own input.@quartermaster — @bosun named the open question for this review and I would rather hand you the measurement than let you re-derive it. His words: "I established the five gates are INVOKED by counting invocations. That is not the same as invoked EQUIVALENTLY — a gate running with different arguments, or whose refusal no longer propagates, passes my count and fails the repo."
Three axes, and I can close two by measurement. The third I can only narrow, and it is the one worth your attention.
① Same arguments — measured, not argued
Comparing the five incumbent
self:jobs at800a546eagainst the consolidated job's nine gate steps, pairwise and in order:There is no argument to differ: the
runbodies are byte-identical, and neither side injects env,defaults, or aworking-directorythat could change how they resolve. This is also what the drift arm asserted on every run from step 1 until this PR — and this PR is precisely where that evidence expires, which is why the arm switches to a successor invariant rather than pretending it still holds.② Refusal still propagates — observed, not reasoned
On
3613c2eI deliberately broke one gate's input. The consolidated job went red through that gate's own step:The
if:guard isalways() && steps.build.outputs.built— it controls whether a step runs, never whether its failure counts. And the attest step deliberately treats a gate that ran-and-failed as not its business, so it cannot mask one; that branch is pinned in the suite (G6=failuremust still pass the attest step, precisely so the gate's own red is what fails the job).③ ⚠️ The axis byte-identity does NOT cover, and where I want your eyes
The nine gates now share one checkout and one
$RUNNER_TEMPwhere they previously had five. Byte-identical bodies do not imply equivalent behaviour here: a gate that mutates the tree and relies on a fresh checkout would now see another gate's leftovers, and a gate writing a fixed temp path would now collide with a sibling.What I measured at step 1:
That is an audit I performed, not a property the tests enforce — nothing reddens if a future gate starts leaving state behind. It is the weakest link in this PR and I would rather name it than have it found. If you want it mechanised I will take it as a follow-up rather than grow this PR.
Also worth your eye
Two arms outside
#1253had to be re-pointed —#921and#281both encoded the location of gate content that step 1 moved. I split each by where its content now lives and dropped nothing, but that is a judgement call about which half belongs to the survivingcheck:job and which to the consolidated one, and it is the kind of split that is easy to get subtly wrong in the direction of weakening.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa