ci(release): refuse to publish with unfolded changelog fragments, and fold #752 where it shipped (#813) - #839
Conversation
… fold #752 where it shipped (#813) TWO DEFECTS IN ONE RECORD. 1. assemble_changelog.py is run BY HAND at the release commit, so any commit between that commit and the tag push ships its fix with its fragment still in changelog.d/ -- in no CHANGELOG.md section and in no release body. release.yml's slice step cannot notice: its two guards only ask whether the ## [<version>] section EXISTS and is NON-EMPTY, and a section missing one entry passes both. v0.6.0-rc1 shipped exactly that way. 3f39f34 (#752 -- the issues: write grant without which a v* tag failed the whole run with startup_failure, published nothing and produced no assets) is an ancestor of tag ad6470c, so the fix genuinely shipped; it landed after the assembler ran at e2f5021, so changelog.d/752.fixed.md was still sitting there at tag time. grep -c startup_failure CHANGELOG.md answered 0. --require-empty already existed (assemble_changelog.py:173, implemented :189 and :194) and was wired into ZERO workflows -- grep -rn 'require-empty|changelog.d|assemble_changelog' .github/ returned nothing. It now runs in release.yml's release job, immediately before 'Slice CHANGELOG section for the release notes'. --check is not a substitute; the script's own help calls it 'informational, NOT a gate' and it exits 0 always. Proven in both directions, not assumed: fragments present EXIT=1, ::error::unfolded changelog fragments remain ..., all 15 named changelog.d/ = README only EXIT=0 (synthetic --root, repo untouched) OPERATIONAL CONSEQUENCE, and it bites immediately: 15 fragments are pending on dev, so the next v* tag STOPS at this step until someone runs python3 python/scripts/assemble_changelog.py and commits the folded CHANGELOG.md. That is the gate working, not a regression -- but it is a new step in the release procedure, not a latent guard. 2. changelog.d/752.fixed.md is folded into ## [0.6.0-rc1]'s ### Fixed, where the fix actually shipped -- NOT into ## [0.6.0] -- Unreleased, which is where the next assembler run would have put it, misdating a fix that is an ancestor of the rc1 tag. Proven: git merge-base --is-ancestor 3f39f34 v0.6.0-rc1 exits 0. The entry says it was recorded late and why the published release body, sliced from this same section and now immutable, does not list it. The issue offered a second option (put it under the next version with a note); the ancestry measurement is what decided against it. NOT DONE, deliberately: the issue's third half asks for gh release edit v0.6.0-rc1 to fix a published Highlights bullet that says 'Seven commands' over six named items. That mutates a published release object and needs release-write access; it is not a worktree change and I did not do it unasked. release.yml YAML parses; gate lands in job 'release' between 'Attest build provenance' and 'Slice CHANGELOG section for the release notes' tests/gates 476 passed, 3 skipped
|
Correction to this PR's "Not done, deliberately" section: the third half is now done. - **Five commands and the Flow D planner stopped reporting success for something they did not do** — `flash`, `doctor`, `quality`, `init` and `monitor`, plus a declared-null `jlink_flash_device` no longer downgrading Flow D to the SE-UART path.instead of the "Seven commands" over six items it carried. Evidence, the exact diff, the preserved Nothing in this PR's diff changed — the release body is not a worktree file. The section above is simply stale now. Also stale in the same direction: the The operational warning in the description still stands and is the one thing worth a decision before merging: 15 fragments are pending on |
alpCaner
left a comment
There was a problem hiding this comment.
Approving. I checked the three things that would have made this a non-fix, and all three hold.
It fails closed. The step is a bare run: python3 python/scripts/assemble_changelog.py --require-empty — no || true, no continue-on-error, no pipe whose exit code comes from the wrong end. A non-zero exit reds the release job.
It runs on the path that matters, before the step that cannot notice. Placed in the release job immediately ahead of "Slice CHANGELOG section for the release notes", so the gate fires before the notes are cut rather than after. python3 is right for this job specifically: it is runs-on: ubuntu-latest and already invokes python3 twice in the same job (the envelope-contract.json generation and the release-notes slice), so no new interpreter assumption is introduced.
The retroactive fold is the honest option. Folding changelog.d/752.fixed.md into ## [0.6.0-rc1]'s ### Fixed rather than ## [0.6.0] — Unreleased puts the entry where the fix actually shipped — 3f39f34 is an ancestor of ad6470c — and the added note says outright that the published release body was sliced from this section before the entry existed and is now immutable. Editing a released section is a judgement call; recording why in the section itself is what makes it defensible rather than quiet history-rewriting.
One operational consequence worth restating in the merge notes, since the PR already identifies it: 15 fragments are pending on dev, so the next v* tag stops at this step until someone runs the assembler and commits the folded CHANGELOG.md. That is the gate doing its job, but it is a new mandatory step in the release procedure rather than a guard that only fires on a mistake — worth a line in whatever runbook the next tag follows.
Unrelated to this PR, one adjacent gap stays open: changelog.d/README.md still documents four fragment categories while assemble_changelog.py accepts six (security and deprecated are undocumented). That is tracked as #818 and this change neither worsens nor depends on it.
Closes #813 — two of its three halves. The third needs release-write access and is called out below rather than quietly dropped.
The general hole
assemble_changelog.pyis run by hand at the release commit. Anything merged between that commit and the tag push therefore ships its fix with its fragment still inchangelog.d/— in noCHANGELOG.mdsection, and in no release body.release.yml's slice step cannot catch it: its two guards only ask whether the## [<version>]section exists and is non-empty, and a section missing one entry passes both.--require-emptyalready existed atpython/scripts/assemble_changelog.py:173(implemented:189,:194) and was wired into zero workflows:It now runs in
release.yml'sreleasejob, immediately before "Slice CHANGELOG section for the release notes".--checkis not a substitute — the script's own help calls it "informational, NOT a gate" and it exits 0 always.v0.6.0-rc1 is the instance, and I measured it rather than took it
3f39f34— tan-cli#752, theissues: writegrant without which av*tag failed the entire run withstartup_failure, published nothing and produced no assets — is in the tag:So the fix shipped and was recorded nowhere. It landed after the assembler ran at
e2f5021 release: v0.6.0-rc1 (#750), so its fragment was still sitting inchangelog.d/at tagad6470c.Folded into
## [0.6.0-rc1]'s### Fixed, not the next version's section. The issue offered both; the ancestry measurement above is what decided it. Putting it under## [0.6.0] — Unreleased— exactly where the next assembler run would have put it — would claim a 0.6.0 fix for something that shipped in rc1. The entry carries a short note saying it was recorded late, and why the published body, sliced from this same section and now immutable, does not list it.Proven in both directions
A gate verified only in its failing state is half a gate:
Read this before merging — it changes the release procedure today
15 fragments are pending on
devright now. Once this lands, the nextv*tag stops at this step until someone runspython3 python/scripts/assemble_changelog.pyand commits the foldedCHANGELOG.md. That is the gate working as designed, not a regression — but it is a new required step in the release flow, not a latent guard that only fires on a mistake. If you would rather it warned for one cycle first, say so and I will make itcontinue-on-errorwith a::warning::plus an issue to flip it.Not done, deliberately
The issue's third half asks for
gh release edit v0.6.0-rc1to fix a published Highlights bullet that says "Seven commands" over six named items. That mutates a published release object, needs release-write access, and is not a worktree change — I have not touched it. It stays open work on the issue, or a one-line fix from you.Test plan
release.ymlparses; the new step lands in jobrelease, betweenAttest build provenanceandSlice CHANGELOG section for the release notes— verified by loading the YAML and printing the step's neighbours, not by eye--require-emptyexits 1 with fragments and 0 without (above)tests/gates— 476 passed, 3 skipped, includingtest_release_docs_match_the_workflow.py, which parsesrelease.ymlv*tag push. The two local runs above are the substitute; the first real exercise is the next tag, and per the warning above it is expected to be RED until the fragments are folded.