Skip to content

ci(release): refuse to publish with unfolded changelog fragments, and fold #752 where it shipped (#813) - #839

Merged
alpCaner merged 1 commit into
devfrom
fix/813-fold-752-and-gate-fragments
Aug 18, 2026
Merged

ci(release): refuse to publish with unfolded changelog fragments, and fold #752 where it shipped (#813)#839
alpCaner merged 1 commit into
devfrom
fix/813-fold-752-and-gate-fragments

Conversation

@hkngln

@hkngln hkngln commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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.py is run by hand at the release commit. Anything merged between that commit and the tag push therefore 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 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-empty already existed at python/scripts/assemble_changelog.py:173 (implemented :189, :194) and was wired into zero workflows:

$ grep -rn "require-empty\|changelog.d\|assemble_changelog" .github/
$ echo $?
1

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.

v0.6.0-rc1 is the instance, and I measured it rather than took it

3f39f34 — tan-cli#752, the issues: write grant without which a v* tag failed the entire run with startup_failure, published nothing and produced no assets — is in the tag:

$ git merge-base --is-ancestor 3f39f34 v0.6.0-rc1 ; echo $?
0
$ grep -c startup_failure CHANGELOG.md
0

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 in changelog.d/ at tag ad6470c.

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:

# fragments present
$ python3 python/scripts/assemble_changelog.py --require-empty
::error::unfolded changelog fragments remain -- run `python3 python/scripts/assemble_changelog.py` and commit the result before tagging
… all 15 named …
15 fragment(s) pending
EXIT=1

# a synthetic root whose changelog.d/ holds only README.md (repo untouched, via --root)
$ python3 python/scripts/assemble_changelog.py --require-empty --root <tmp>
0 fragment(s) pending
EXIT=0

Read this before merging — it changes the release procedure today

15 fragments are pending on dev right now. Once this lands, 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 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 it continue-on-error with a ::warning:: plus an issue to flip 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, 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.yml parses; the new step lands in job release, between Attest build provenance and Slice CHANGELOG section for the release notes — verified by loading the YAML and printing the step's neighbours, not by eye
  • --require-empty exits 1 with fragments and 0 without (above)
  • tests/gates476 passed, 3 skipped, including test_release_docs_match_the_workflow.py, which parses release.yml
  • The step itself has never run in CI — it only executes on a v* 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.

… 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
@hkngln

hkngln commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Correction to this PR's "Not done, deliberately" section: the third half is now done. gh release edit v0.6.0-rc1 has landed, and the Highlights bullet reads

- **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 prerelease/asset state, and one adjacent accuracy problem I deliberately did NOT fix are on #813: #813 (comment)

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 first install -- install.sh -> bootstrap -> init -> build failure that was red on this PR was never caused by this diff. zephyrproject-rtos/sdk-ng's releases-list endpoint was returning [], west sdk install reports an empty list as Unavailable SDK version: 1.0.1, and the pin was correct throughout. Filed as #840; upstream has since recovered (length is back to 100) and I have re-run the job on this PR.

The operational warning in the description still stands and is the one thing worth a decision before merging: 15 fragments are pending on dev, so the next v* tag stops at the new gate until someone runs python3 python/scripts/assemble_changelog.py and commits the folded CHANGELOG.md. Say the word if you would rather it warned for one cycle first.

@alpCaner alpCaner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@alpCaner
alpCaner added this pull request to the merge queue Aug 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 17, 2026
@alpCaner
alpCaner added this pull request to the merge queue Aug 18, 2026
Merged via the queue into dev with commit d1c5e08 Aug 18, 2026
33 of 34 checks passed
@alpCaner
alpCaner deleted the fix/813-fold-752-and-gate-fragments branch August 18, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

changelog.d/752.fixed.md shipped unfolded at v0.6.0-rc1 and --require-empty gates no workflow

2 participants