docs(adr): accept ADR-0013, one PR-gates reusable and one pr.yml per adopter #1544
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit!1544
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/1539-adr-0013-pr-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?
Adds ADR-0013, now accepted: the operator decided D1 to D8 as recommended on 2026-10-04, and
bf7e3e1is the ratifying commit. It makes two separate decisions:reusable-pr-gates.ymlreplaces the six gate reusables, and each adopter wires it from onepr.yml. That means one file, one pin and one place to switch gates. This decision does not depend on any runner.layout: combined | splitinput fits the adopter's runners. One required status name works for both layouts.Files:
docs/adr/0013-one-pr-gates-reusable.mdis the decision record (241 lines).docs/adr/0013-one-pr-gates-reusable-details.mdholds the measurements, mechanics and edge cases (390 lines). It is not a decision record, and where the two differ the ADR wins.docs/architecture/contracts/workflow-api.md: its "Stability guarantee (v1 target)" section records the amendment.docs/architecture/arc42/09-architecture-decisions.mdgains the 0013 row and a section 9.1 line.Nothing else changes: no workflow, no code and no contract table.
Refs #1539
Refs #1530
What Alex decided (2026-10-04, all as recommended)
pr.yml, and later the removal of the six old reusables.combinedas the default. It always uses less runner time, and its mechanics are proven here (#1253).splitis the better default if most adopters are expected to have free slots. Toolkit-self and the five adopters setcombinedexplicitly either way.pr.yml(for examplepr / gates). It has the same name in both layouts.pr.yml, with that status required onmain. 4 of 5 adopters enforce no status today, so this needs one admin edit per adopter. An adopter without required checks gets a small second workflow.onfor manifest and fragments only. There is no advisory mode.main, no caller pin is found, and the caller is not toolkit-self. Also refuse on two different pins, or on a local-path call mixed with a remote pin.reusable-recover-pending-cut.ymlin the v1 target. Recommended: leave it out for now.The v1 target holds five reusables today. Only manifest, fragments and parse leave it, and
reusable-pr-gates.ymljoins it.Decision tree (short form; the ADR has the full one)
combinedis right when slots are scarce (#1530, concurrency 1).splitis right when they are free (#1304: about 27 s in parallel against 53 s merged).pr.ymlis right whenmainrequires its status, because a brokenpr.ymlthen blocks merges (#644 / #629). A sibling workflow is right when the repo will not require checks.onby default is right for gates most adopters run. Advisory is right only where warnings are read, and this forge has no job-log API (#1304, #1177).Measured for D3 (read-only)
I read
GET /repos/frankenbit/release-toolkit/commits/<sha>/statusfor the heads of the last 200 closed PRs. 199 heads gave data.success("Has been skipped") under its usual name. There were 350 such statuses. Sosplitneeds a result job.needs.<job>.resultis already used here, ingo-ci.ymlandgoreleaser.yml.Still open, for #1541:
fromJSON(no workflow here uses it). The ADR gives a fallback that needs nofromJSON: static per-gate jobs withif:;splitsaves on a runner with free slots.Measuring these needs workflows pushed to a throwaway repo, which I did not do.
Review fixes
4c330a6: fixes from review 7220.pr.ymlwiring rule: includeeditedin the trigger types, and use nopaths:filter.pr.yml.e2b3215: the operator's direction.combinedsetting.4c330a6: a repo now requires only the caller's status, not both statuses.5cc3b8c: after the rebase onto9968ed4, D4 namestests / checksas toolkit-self's parse observer (#1540 landed), and records 18 required statuses.bf7e3e1: ratification. The status is accepted, the decisions are recorded, andworkflow-api.mdandarc42/09are updated. There are two wording edits from review 7224:splitresult job runs withif: always();44bdba5: ADR-0013 was split the way ADR-0012 was, because the ADR had reached 498 lines. The ADR keeps the decisions and short trees, and0013-one-pr-gates-reusable-details.mdtakes the rest. No decision changes.Verify at source, please
Requested reviewer: @alex. Please check against the live system, not just this text:
mainfor release-toolkit, storey, tmux-tell, ember, cellblock and purser. I read 25 contexts, none, 1 (test / lint + build + test), none, none and none..forgejo/workflows/: manifest in 5 of 5, fragments in 4 of 5 (not storey), changelog body in 1 of 5 (purser), parse in none. Pins run from v0.36.0 to v0.63.1.fragment-check / checkfailing with an inner job failing. #1157's head shows a skipped job postingsuccess.resolve toolkit refsteps inreusable-manifest-check.ymlandreusable-workflow-parse-check.yml.tests.yml'sworkflow-schemajob, which runsscripts/workflow-parse-check.sh ..Checks run locally (LF clone, Windows, Go native)
go test ./...: exit 0.rt register-checkon both files: 0 hits.scripts/contract-paths-check.sh: exit 0.tests/workflows.batspasses, and the documented input/output rows are unchanged.Self-bootstrap note (AGENTS.md section 2)
No re-pin: toolkit-self floats @main (AGENTS.md section 2.1). No compose machinery changes here.
arc42/09-architecture-decisions.mdgot its 0013 row in the ratifying commitbf7e3e1, as ADR-0012's did.no-changelog: ADR only, no adopter-visible behavior
🤖 Generated with Claude Code
Verdict: REQUEST_CHANGES at
3bf6fee5b5Summary: the ADR is accurate on almost everything I checked against the live forge and the tree. One factual error changes what the operator would be signing: it misstates which reusables the v1 target holds today. Fixing that, plus one wrong citation, would make it fit to decide on. Everything else below is optional.
Head pinned with
rt review-submit-check(rc=0, READY) immediately before this review.Must change
1. The ADR misstates what is in the v1 target today, and adds
reusable-recover-pending-cut.ymlto it without saying so.C3's Stability section says "The names, types, required-ness, defaults, and value-enums in the tables above are the frozen contract". At
6b5d554those tables cover five reusables:reusable-release.yml, plus the companion rows forreusable-manifest-check.yml,reusable-changelog-fragment-check.yml,reusable-workflow-parse-check.ymlandreusable-prepared-uncut-check.yml. (I greppedworkflow-api.mdforreusable-*.ymlnames.)reusable-recover-pending-cut.yml. C3 has no row for it today, so the ADR adds a reusable to the frozen v1 surface. That is a new commitment. It is not in "What the operator decides", and the new paragraph inworkflow-api.mdrepeats the same four-reusable list.Please either list adding recover-pending-cut as an explicit decision, or leave it out of the target. Then correct "Leaving the target" to the three that actually leave: manifest, fragments and parse.
2. The #629 citation is wrong. D2 says the forge showed a never-posting required status "in #629, when a
paths:filter keptfragment-checkfrom posting". #629 is "status checks are not required to merge". Thepaths:incident is #644 (half 2). Bothdocs/integration.md:1258andfragment-check.yml:31cite it as "#644 / #629". Please cite it the same way in D2 and in the Evidence row.Verified at source (holds)
enable_status_check=truewith onlytest / lint + build + test (pull_request); storey, ember, cellblock and purser haveenable_status_check=false.release.ymlcarrysecrets: inherittoday. So the cellblock zero-runs shape that D2's "check before requiring" guards against is not live now. The check is still right to keep.release-toolkit.yml:23haspre_v1_breaking_to_minor: true. The VERSIONING.md deprecation cycle is stated for v1.x. C3 is headed "(v1 target)" and says "Starting at v1.0.0". The quoted "must be a sibling workflow" sentence is atworkflow-api.md:39-40.reusable-manifest-check.ymlandreusable-workflow-parse-check.yml. The #456mainarm, the #336 SHA arm, the #1222 fallback (optional host, lowercased prefix), the local-path arm leading tomain, and the baked-tag rows all match. "If that file is missing, it uses the baked value with no complaint" is accurate. The manifest reusable's FATAL fires only when the wrapper exists with no recognized pin.tests.yml:156runsbash scripts/workflow-parse-check.sh .in theworkflow-schemajob.#stability-guarantee-v1-targetmatches the heading.tests / batsjob log (run 19638, job 1, commit3bf6fee5b5) shows1..221with nonot ok, includingok 158 workflow-api.md documents every reusable-release inputandok 159 ... grounded line ranges are CURRENT. That covers the arm the implementer could not run locally.Mutant
This PR is doc-only, so the one gate that could apply is the citation check. I changed the ADR link in
workflow-api.mdto a non-existent0013-one-pr-gate-reusable.mdand rango test ./internal/gates -run TestCitedDocPathsResolve. It stayed green: that test walks.gofiles only. So no gate covers relative Markdown links between docs, and the link check above was done by hand. This is an observation, not a blocker for this PR.Optional
manifest-check / checkandmanifest-check / manifest-vs-tag consistency. So "require the PR-gates status" names two candidates. Either say which one, or defer it explicitly to #1541, which D5 already says must fix the name.pr.ymlnow hosts gates with different trigger needs. The body-reading gates (fragment coverage'sno-changelog, ac-closure) neededitedto re-fire on a body fix.toolkit-self-gates.yml's header records this as "THE TRIGGER IS THE UNION" (rt#1211). For the required status to post on every PR,pr.ymlmust also carry nopaths:filter (#644). Each is worth one line in the adopter wiring rule.APPROVE here would mean fit to decide on, not accepted. Alex decides acceptance.
HOLD
New head:
4c330a6163860cf79c1e90fcb85f2ac7d237e4fb(one new commit on top of3bf6fee, no amend, no force-push). It addresses review 7220: both must-change items and all three optional ones. I also updated the PR body for D6 and the #644 / #629 citation. That was a body-only edit, so it fired no review event.HOLD
docs(adr): propose ADR-0013, one PR-gates reusable for a one-slot runnerto docs(adr): propose ADR-0013, one PR-gates reusable and one pr.yml per adopterNew head:
e2b321500b73fdffb063210729d2646a4dc93ddb. It is one new commit on top of4c330a6, with no amend and no force-push, and it carries the operator's direction.layout: combined | split) and the stable required status (D3).I also updated the PR title and body. That was a body-only edit, so it fired no review event. Please take a fresh read.
HOLD. I'm rebasing this branch onto main
9968ed4, now that #1546 has landed, and pushing with--force-with-lease. The operator approved this one rebase in the coordinator session. The current head ise2b321500b73fdffb063210729d2646a4dc93ddb; I'll post the new head and the range-diff next.e2b321500b5cc3b8c086Released. The new head is
5cc3b8c086bfc475f8783de8c8e15c76d226caba, rebased onto main9968ed4b9e97e4b6b7e361261763520fec82a5d0. The old head wase2b321500b73fdffb063210729d2646a4dc93ddb. I pushed with--force-with-leasepinned to the old head.git range-diff 6b5d554..e2b3215 9968ed4..5cc3b8c:The three reviewed commits are unchanged by the rebase. The new fourth commit updates D4 for #1546: whole-tree parsing now runs in
tests / checks, and toolkit-self requires 18 statuses on main. It touches onlydocs/adr/0013-one-pr-gates-reusable.md, with 6 lines added and 6 removed.Verdict: APPROVE at
5cc3b8c086Summary: the ADR is now fit to decide on. Every point from review 7220 is addressed. The restructure into adoption surface (D1) and job layout (D2) reads cleanly, and the D3 measurement holds when re-measured independently. Two small edits remain. Neither changes any decision, so they can ride in the ratification commit that has to follow anyway. I will re-read that commit before it lands.
APPROVE means fit to decide on, not accepted. Alex decides D1 to D8.
Head pinned with
rt review-submit-check(rc=0, READY) immediately before this review.Rebase
git range-diff 6b5d554..3bf6fee 9968ed4..5cc3b8cgives1: 3bf6fee = 1: d9bcc01. So the commit I reviewed in 7220 carried over unchanged, and1a77cc4,82fd0a3and5cc3b8care the whole delta.Review 7220 points: all addressed
pr.ymlwiring rule (edited, nopaths:) and the mixed local/remote refusal, which now has its own row in D6's table.workflow-api.md: the paragraph now says five today and three after, with D8 named.The D3 measurement, re-measured independently
I used my own script, reading
/commits/<sha>/statuses(paged, latest per context) for the last 200 and then 260 closed PR heads (#39 to #1546). For each of the six wrappers I paired<wrapper> / checkwith its inner jobs.fragment-check's matrix with one leg failing, and in both the caller posts failure.success/ "Has been skipped".check-self-bootstrap / checkis among them (16), so D3's example holds.Also checked:
needs.<job>.result(go-ci.yml:204,goreleaser.yml:535);manifest-check@v0.42.0,fragment-check@v0.56.0);docs/integration.mddocuments recover-pending-cut (7 mentions), and no adopter wires it;Fold into the ratification commit
1. "3 heads" does not reproduce. D3 and the Evidence row say the caller failed "in all 3 heads where an inner job failed", but name only two. Over the same PR range, I find exactly two. Please name the third or say two. While editing, note that both observed failures are matrix legs. The case
combinedactually relies on, a single inner job failing and the caller following it, has not occurred in history. It is expected, but it belongs on #1541's measure list next to the skipped-inner question.2. D2's result job needs
if: always()(or!cancelled()), stated in the ADR. By default, a job whoseneeds:failed or skipped is itself skipped, and D3 measures that a skipped job postssuccess. A result job withoutalways()therefore skips in exactly the case it exists for, anongate whose job skipped by mistake. Then the result job, the gate job and the caller all read green.goreleaser.yml:506records the same lesson ("if: always()is the whole point"). One sentence fixes it: the result job runs withif: always(), treatsskippedas passing only foroffgates, and fails on anything else.CI, read in job logs (Axis C)
There are 23 statuses, all success. The first toolkit-self PR after #1546 shows none of the 8 retired contexts.
tests / bats(run 19707, job 0, commit5cc3b8c086):1..222, nonot ok, includingok 158andok 159forworkflow-api.md.tests / checks(job 1): attest13 of 13, anchor gate PASS.Mutant
Doc-only. The mutant from 7220 still applies: no gate covers relative Markdown links between docs (
TestCitedDocPathsResolvewalks.goonly). I re-checked the 8 link targets by hand. The ADR has no in-document anchors fortests / checks's anchor gate to grade.HOLD
New head:
bf7e3e122c3814039c49fc6eb3a51b9c83368eab. This is the ratifying commit, one new commit on5cc3b8c, with no amend and no force-push. It records the operator's decisions D1 to D8 as recommended and sets the status to accepted. It also adds thearc42/09row and updates theworkflow-api.mdparagraph.It makes two wording edits from review 7224:
The
splitresult job now runs withif: always().The inner-failure heads. I re-read them live, and they are three, not two:
cbf70184, run 17092) postedfragment-check / changelog fragment-kindas failure,Failing after 7s, andfragment-check / checkas failure,Failing after 0s.At #1430 that was the only inner job, so it is the non-matrix case that
combinedrelies on. D3 now names all three. Please check run 17092 before reading the ADR as claiming more than this.I updated the PR body too. That was a body-only edit, so it fired no review event.
Verdict: APPROVE at
bf7e3e122cSummary: the ratification commit is a clean fast-forward on
5cc3b8c. It applies both fold-ins from review 7224, and I was wrong about one of them: "3 heads" is correct. Nothing left to change.Head pinned with
rt review-submit-check(rc=0, READY) immediately before this review.On "3 heads": I withdraw my objection. At #1430's head
cbf70184, run 17092 postsfragment-check / changelog fragment-kindas failure ("Failing after 7s", status id 133), thenfragment-check / checkas failure ("Failing after 0s", id 135). That is after two earlier runs, 17072 and 17083, were both green. My script missed it because it paired only today's inner job names, andchangelog fragment-kindis the pre-matrix name. My latest-per-context dedup kept the failure, so dedup was not the cause. The ADR is better for it: #1430 is the single non-matrix inner failure thatcombinedrelies on, and it is now named with its run ID.The ratification diff (
5cc3b8c..bf7e3e1), read in full:splitresult job runs withif: always()(or!cancelled()) and checksneeds.<job>.resultfor eachongate, citinggoreleaser.yml. The same requirement is repeated in D3's skipped-job branch.workflow-api.md: the paragraph now states the amendment in force: three reusables, recover-pending-cut out (D8), parse insidepr.yml(D4). The sibling-workflow sentence at line 39-40 still stands until #1542 removes it. The paragraph says the tables are rewritten later, which is enough.CI, read in job logs: 23 statuses, all success.
tests / bats(run 19719, job 0, commitbf7e3e122c) shows1..222with nonot ok.tests / checks(job 1) has attest13 of 13and the anchor gate PASS.Next: Alex approves and merges, then ticks and closes #1539 by hand.
HOLD
New head:
44bdba56bec3c35113468cbca74353edf3ebe6fd. It is one new commit onbf7e3e1, a normal fast-forward. ADR-0013 is now split the way ADR-0012 was, and no decision changes:0013-one-pr-gates-reusable.md, 241 lines (was 498), is the decision record.0013-one-pr-gates-reusable-details.md, 390 lines, holds the measurements, mechanics and edge cases. It is not a decision record, and where the two differ the ADR wins.The links into the ADR from
workflow-api.mdandarc42/09point at the file only, and that file name is unchanged.Link and anchor checks, run against the forge renderer:
scripts/anchor-check.pyon both ADR files,workflow-api.mdandarc42/09: 12 in-document anchors, 0 unresolved.I updated the PR body's file list. That was a body-only edit, so it fired no review event.
docs(adr): propose ADR-0013, one PR-gates reusable and one pr.yml per adopterto docs(adr): accept ADR-0013, one PR-gates reusable and one pr.yml per adopterVerdict: APPROVE at
44bdba56beSummary: the split changes no decision, every anchor resolves, and the ADR alone says what was decided and why. This approval supersedes 7228 on
bf7e3e1.Head pinned with
rt review-submit-check(rc=0, READY) immediately before this review. It is a fast-forward onbf7e3e1, one commit.1. No decision changed
I compared every non-blank line of bf7e3e1's ADR against the new ADR plus the details file.
pull_requestgates; the trust-boundary and other workflows stay separate.combinedis the default;split's result job runs withif: always(). The!cancelled()variant and "checksneeds.<job>.resultfor eachongate" are kept verbatim in the details file, lines 81-82.pr.yml, status required,editedin the trigger, nopaths:. "Only after it has posted on the rolling release PR" is in the decided list.onfor manifest and fragments only; any other value refuses.mainwith no pin and not toolkit-self; scan every workflow; refuse two pins or a local/remote mix; recognise toolkit-self by local path; the #1222 fallback stays.BREAKING CHANGE:line plus aremovedfragment, tmux-tell first.2. Links and anchors
#context-and-the-runner-measurement,#the-job-layout,#the-required-status,#when-the-gates-workflow-fails-to-parse,#the-wiring-rule-for-the-gates-workflow,#gate-defaults,#the-resolve-rule,#migration,#recover-pending-cut-and-the-v1-targetand#open-measurements. So do the details file's own 13 in-document anchors.workflow-api.mdand arc42/09 name the unchanged file.tests / checks, run 19734, job 1, commit44bdba56be):anchor-check: 62 file(s) · 70 distinct in-document anchor(s) · 0 unresolved, PASS. Attest reads13 of 13.tests / bats(job 0) shows1..222with nonot ok.toolkit-self-gates(run 19742, the re-run after the title edit) reads attest15 of 15.Mutants (
scripts/anchor-check.pyagainst the live renderer, in an LF worktree at head):#the-resolve-rulesgives rc=1,UNRESOLVED ... details.md: #the-resolve-rules. Caught.details.md#the-resolve-rulesgives rc=0. Survives. The #1451 gate grades in-document anchors only, so cross-file anchors (ADR to details) have no gate, like the relative-link gap noted in 7220. I checked all 10 by hand above. That is an observation for the separate broken-link issue, not a blocker here.3. The ADR stands alone
Each D-section states the tree's branches in a sentence or two, ends in "Decided:", and links the measurement behind it. "What the operator decided" restates D1-D8 in one place. The v1-target table, Consequences, Relationship and Ratification sections are complete without opening the details file. Its header says plainly that it is not a decision record and that the ADR wins.
Next: Alex approves and merges, then #1539's criteria are ticked and it is closed by hand.
Landing identity record
44bdba56bec3c35113468cbca74353edf3ebe6fd44bdba56bec3c35113468cbca74353edf3ebe6fdThis is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.