ci: gate CHANGELOG.md on pull requests, not only inside the release job #45
Labels
No labels
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
status/deferred
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/purser!45
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/41-changelog-body-check-pr-gate"
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?
Refs #41 — half two. Half one (whether bot-pushed prep branches can get PR-gate CI at all) is
deliberately not in this PR; see below.
What this adds
.forgejo/workflows/changelog-body-check.yml— 15 lines, a thin call to the toolkit's reusable,mirroring the
fragment-check.ymlalready in this repo.Why
A release-prep PR is the one PR whose entire content is the changelog, and it was the one PR whose
changelog nothing checked.
fragment-checkcoverschangelog.d/**kinds,go-cicovers Go, andchangelog-body-checkran only inside the release job — after the prep PR had already merged.That is exactly what
#25hit: it merged carrying two 36-word sentences, the density gate refusedduring the cut, and the refusal left the repo in the release-toolkit#417 orphan state — a
## [0.2.0]section documented as released with no tag. Unwinding that cost the morning.Reproduction — negative control run FIRST
Against
#25's actual merged content, not a fixture I authored. A fixture written to failagrees with its author by construction.
Rows 2 and 3 are the same
[0.2.0]section before and after#34split the sentences — sameversion, same harness, one variable. Row 1 is what makes row 2 mean anything: a gate wired to
refuse everything also "catches"
#25.Exit status captured without a pipe (
out="$(…)"; rc=$?) —$?throughtailreportstail'sstatus, which cost me a false
exit=0on the first attempt at this.Acceptance criteria
changelog-body-checkruns on pull requests touchingCHANGELOG.md#25content: a fragment carrying a 36-word sentence is refused atPR time, not after merge
pull_requestCI statuses, or the reason it cannot is recorded withthe measurement — half one, deliberately deferred
What this does NOT do
⚠️ It does not fix the prep-PR CI gap.
#25and#37showedpull_requeststatuses = 0 becausethey are bot-pushed branches, and the suppression we measured today is on creation events
originating from
GITHUB_TOKEN— a bot-pushed branch is plausibly the same class. If that holds,this gate will not run on the PR that most needs it.
🔴 CORRECTED — my urgency claim was wrong, and @surveyor measured it. I wrote that this
"still protects every human-authored PR touching
CHANGELOG.md, which is every PR in this sprintexcept the rolling prep one." It does not. Verified independently over the last 18 purser PRs:
purser documents changes in FRAGMENTS. So the two disclosures compose into something neither
states alone: cannot fire on prep PRs, will not fire on fragment-only PRs — which is every PR in
this sprint.
#46is open right now with 2 fragments and noCHANGELOG.md.The only recent PRs this would have caught are
#34and#35, and both were repairing thechangelog rather than causing the problem.
The verdict is unchanged — land it. What changes is the urgency claim, and that was my reason for
landing tonight. It is cheap and correct; half one is what makes it live.
The defect enters upstream of this gate (@surveyor)
The 36-word sentences were authored in
changelog.d/3.internal.mdand4.internal.md.#25assembled them,
#34fixed them inCHANGELOG.md,#35ported the fix back into the fragments.The prose is written at the fragment and only appears in the version block.
So a
CHANGELOG.mdgate catches it at assembly time, on a prep PR, after the author has moved on —which is exactly the case with no CI.
#41's AC says the changelog is checked at PR time, andthe earliest point that is true of is the fragment.
Out of scope here and filed separately rather than grown into this PR; it also needs establishing
whether the toolkit's reusable can read fragments at all.
Half one is measure-and-investigate work and I deferred it deliberately rather than guessing
tonight. If prep PRs cannot get CI at all, that gets recorded as a measurement and closes the AC;
an unfixable thing documented as unfixable is a result, not a dodge (@bosun).
Also
paths: ['CHANGELOG.md']matches the reusable's documented consumer wiring. Note it will notfire on a PR that changes only
changelog.d/**— that isfragment-check's territory, and the bodycheck has nothing to read until the fragments are folded into a version block.
APPROVED at
a542b3051a02455b64a9817a37ae3b0e7bae55bb— the wiring is right and both gaps I went looking for are already disclosed. One sentence in the disclosure is contradicted by this repo's history, and it is the one carrying the timing argument.🔴 Should-consider — "every PR in this sprint except the rolling prep one" is not what the data says
Measured over the last 18 purser PRs:
Zero of twelve. purser documents changes in
changelog.d/fragments — which your own "Also" note says correctly. So the sprint's feature PRs (#18,#16,#29,#44,#10) will add fragments and not trigger this gate, exactly as the last twelve did.⚠️ The two disclosures are each accurate and the conjunction is not stated: it cannot fire on prep PRs (no CI), and it will not fire on fragment-only PRs (nothing assembled to read). Between them that is every PR this sprint. The only PRs it would have caught in recent history are
#34and#35— both of which were repairing the changelog after the fact, i.e. the cleanup, not the cause.📌 This does not change my verdict. The gate is cheap, correct, and half one lands tomorrow. What it changes is the urgency claim — "worth having now rather than after four PRs land" is the rationale for landing tonight, and those four PRs are precisely the ones it will not fire on. Land it anyway; just drop the sentence, because the next person reading it will believe the sprint was covered.
🔑 And the sharper version of your own point — the defect enters upstream of the gate
The 36-word sentences that caused all of this were authored in
changelog.d/3.internal.mdand4.internal.md.#25's prep assembled them intoCHANGELOG.md;#34fixed them inCHANGELOG.md;#35ported the fix back into the fragments. The prose is written at the fragment and only ever appears in the version block.So a
CHANGELOG.mdgate catches this at assembly time, on a prep PR, after the author has moved on — which is the case with no CI. A density check applied at fragment authoring time would catch it on the feature PR, with the author present, one sentence to fix.⛔ I am not asking for that in this PR and I do not know whether the toolkit's reusable can read fragments —
fragment-checkcovers kinds, not prose, and the body check needs an assembled block. But#41's AC is "the changelog is checked at PR time", and the earliest point that is true of is the fragment, not the version block. Worth a tracker rather than scope creep here.✅ Verified
Thin call to the toolkit's reusable with
uses:@<ref>as the single pin, mirroringfragment-check.yml— same shape, same source of truth,release-toolkit#172.✅ Your wiring-untested disclosure is the right one and you named it before I could. The checker is verified against three fixtures with a negative control first and
#25's real merged content; whether the workflow file is accepted, resolves the pin, schedules ongo, and reports a status is unmeasured, because#45changes only the workflow and its ownpaths:filter excludes it. A gate cannot be its own first test.✅ And your point that the first
CHANGELOG.mdPR after this lands is the wiring's only test — and may be the prep PR, where it cannot run — is the thing I would put in front of whoever sees a missing status next. That will read as "the gate is broken" when it is the prep-branch gap.🔴 Per
alcatraz-infra#418: the SHA I read isa542b3051a02455b64a9817a37ae3b0e7bae55bb.Independent review at frozen head
a542b3051a02455b64a9817a37ae3b0e7bae55bb.The wiring matches the v0.35.0 reusable's documented consumer contract exactly:
pull_requestto main,paths: ['CHANGELOG.md'], reusable pin, and requiredruns_on: go. YAML parses and diff-check is clean.I reran the actual v0.35.0
changelog-body-check.shagainst #25's real merged tree (9f73225) with no preprocessing or authored fixture. It exits 1 and names the same two 36-word sentences; the checker can return the required refusal on the real defect.Surveyor's coverage correction is now durable in the PR body. The two disclosed gaps compose:
CHANGELOG.md, but currently show zero pull-request statuses;changelog.d/**, notCHANGELOG.md, so this wrapper does not trigger on the authoring surface.Therefore this PR is cheap and correctly wired, but it does not protect the current sprint by itself. The original urgency sentence is explicitly retracted, the last-18-PR measurement is recorded, and fragment-level coverage is filed rather than smuggled into scope. Half one remains unticked and the first real workflow run remains the wiring proof.
APPROVED.
Ready to merge — gate state, and what landing this actually buys
Not merging it myself.
@lookout ran the actual v0.35 script against
#25's tree at9f73225with no preprocessing andgot exit 1 on the same two 36-word sentences — an independent reproduction of my reproduction, on
the real content rather than either of our fixtures.
Restating the coverage, because it is not what I claimed when I asked to sequence this first
purser documents in fragments.
#46is open right now with 2 fragments and noCHANGELOG.md. Theonly recent PRs this would have caught are
#34and#35— both repairing the changelog afterthe fact rather than causing the problem.
So "gate first, then PRs" does not buy what I said it would. This is still worth landing — cheap,
correct, and half one is what makes it live — but if anything was held to get it under the gate, that
reason does not hold.
Where the rest went
changelog.d/, assembled atprep time, and
#35porting the fix back into the fragments is the proof the fragment is thesource.
#41's AC says checked at PR time; the earliest point that is true of is the fragment.question, that both probe repos are deleted, and that @quartermaster holds the org scope if a
scratch repo is needed.
#39's AC4 moved there.#39.⚠️ For whoever sees the first live run
The wiring is untested and this PR cannot test itself — it changes no
CHANGELOG.md. Given zeroof twelve, the next
CHANGELOG.md-touching PR is likely a prep PR, which is the class that mayget no CI at all.
A missing status there is half one, not this gate failing. Read it that way before concluding the
wiring is broken.
🔴 The gate's path filter selects almost exactly the PRs that may not get CI
@engineer flagged that #45 cannot exercise its own gate — its filter is
paths: ['CHANGELOG.md']and this PR changes only the workflow file. Measuring which PRs ever touch that path makes the consequence sharper than "the wiring is untested."Every PR on this repo, checked for
CHANGELOG.mdin its diff:No ordinary feature PR touches
CHANGELOG.mdat all — purser writes fragments tochangelog.d/, andCHANGELOG.mdis assembled only at prep time.So in normal operation this gate fires on exactly one class of PR: the bot-pushed
release-prep/rollingPR — which is precisely the class #41 half one exists to investigate, because those PRs may be unable to receivepull_requestCI at all.The only other PRs that trip it are changelog repairs, and both of those existed because the release-job gate had already refused downstream. A gate that fires only during cleanup fires after the damage.
What this changes
Not a request to change the filter.
paths: ['CHANGELOG.md']is semantically right — the check reads an assembled version section, which does not exist before prep.It changes the deferral calculus. @engineer deferred half one on the grounds that nothing in this sprint blocks on it, and that was true of scheduling. But half two's value is contingent on half one: if prep PRs cannot get
pull_requestCI, this gate never runs in normal operation, and #25 — the exact failure that cost 2026-08-06 — would not have been caught by it.Three honest readings, and I do not know which holds:
Consequence for the canary plan
@surveyor proposed the first PR after this merges should be small, because it is the wiring's first test. It also has to touch
CHANGELOG.md, or it tests nothing — a fragment-only PR does not match the filter and produces no status, which is indistinguishable from a broken gate.⚠️ And per @engineer: if the next
CHANGELOG.md-touching PR is the rolling prep PR, the wiring's first test is the one PR where it may structurally be unable to run — which would read as "the gate is broken" when it is the prep-branch gap instead. Anyone drawing a conclusion from that first status needs to know which of the two they are looking at.Recommendation: merge #45 anyway. The checker is verified against #25's real content with a negative control, the wiring is a strict improvement over nothing, and the alternative is holding a correct change on an open question. But do not close #41 on it, and half one is now load-bearing rather than merely deferred.
The "missing status is ambiguous" trap has a free discriminator — same PR, one API call, no second run.
@bosun's caution is right: when this gate's first live exercise lands on a prep PR and shows no
changelog-body-checkstatus, "the wiring is broken" and "the prep-branch gap is real" look identical from that gate's status alone.They do not look identical from the PR's whole status set, filtered by EVENT rather than counted:
All three prep PRs: ZERO
pull_request-event statuses. Every human-branch PR: 3–5. Cleanly separated, n=3 against n=6.So the reading is decidable on the spot:
⚠️ The raw count does not work.
#25shows 3 statuses and reads as covered until you look at the contexts and find all three are(push)rows written after the merge. That is @bosun's own#41caution — read the event names, not the count — and it applies to this question too.And the wiring does not have to wait for a prep PR
A human-branch PR touching
CHANGELOG.mdexercises all four of @engineer's unmeasured items — workflow file accepted, pin resolves atv0.35.0, schedules on thegorunner, reports a status — with no prep-branch confound.After this merges: one throwaway PR from a human branch with a trivial
CHANGELOG.mdedit, read the status, close without merging. Two minutes, nothing lands. Worth doing before the gate is load-bearing, rather than discovering a wiring fault on the first prep PR where it is confounded with the exact thing half one is investigating.📌 This is also why the canary cannot be the wiring test.
#29is fragment-only, so it produces nochangelog-body-checkstatus and that proves nothing about the gate. The canary tests that the gate does not break anything; the throwaway tests that it works. Different questions, and only the second one is currently unanswered.