chore(changelog): land entries as per-change fragments in changelog.d/ - #128
Conversation
Concurrent PRs editing the shared '## Unreleased' section of CHANGELOG.md conflict whenever one PR outlives another merge (e.g. #115, whose only textual conflict against main is CHANGELOG.md). Entries now land as uniquely-named fragment files (changelog.d/<slug>.<category>.md), which git merges without conflict; the npm-version hook folds them into the release section and deletes them. Direct '## Unreleased' edits remain supported and are merged at the same point, so in-flight PRs need no rework. Tag-time publish guard and github-release job are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anarchid
left a comment
There was a problem hiding this comment.
🟠 NEEDS ATTENTION
Reviewer: Codex (GPT-5.6 Sol)
Reviewed head: 018547507c7b7d42c12743195cc492af0c607d3f
Two release-policy defects should be fixed and pinned with automated tests before merge.
-
.github/workflows/changelog.yml:25-30— deleting somebody else’s fragment satisfies the check.changed=$(git diff --name-only "$base...$head") # any matching changelog.d path is accepted
--name-onlydoes not distinguish additions/modifications from deletions. I reproduced a two-commit PR that modifiedsrc/example.tsand deletedchangelog.d/old.fixed.md; the exact workflow condition printedOK/passed, whilegit diff --name-statusshowedD changelog.d/old.fixed.md. That weakens the new policy in precisely the path meant to enforce it and can also remove an unrelated pending entry.Filter out deletions (for example with an appropriate
--diff-filter) and verify that the accepted fragment exists in the PR head. Preserve rename support by validating the destination path/status rather than falling back to name-only matching. -
scripts/release-changelog.mjs:50-56— only the fragment’s first line is validated as a bullet.if (!/^[-*] /.test(body)) fail(...)
The documented format requires one or more bullets with indented continuation lines, but this regex only anchors the beginning of the entire file. A fragment containing
- Valid first bullet.followed by unindented top-level prose was accepted, consumed, and emitted into the version section. This disproves the stated “refuse non-bullet fragment” behavior beyond the simplest first-line case.Validate every nonblank top-level line: it must begin a bullet, while continuation/nested lines must be indented. Add fixtures for multiple bullets, valid continuations, an invalid first line, and invalid later top-level prose.
Tooling results
git diff --check origin/main...HEAD: passed.npx tsc --noEmit: passed.node --import tsx --test test/framework.test.ts: passed.- Isolated release fold with current Unreleased entries plus the dogfood fragment: passed; category order and fragment deletion were correct.
- Deleted-fragment workflow probe: failed policy expectation; workflow passed a
Dfragment plussrc/modification. - Later non-bullet-line release probe: failed policy expectation; malformed fragment was released successfully.
- GitHub Ubuntu, macOS, and changelog checks: passed.
Verdict: the assembler’s normal path is sound, but the check and refusal contract each have a demonstrated hole. Fix those two cases and add a small committed regression harness; no runtime-framework blocker was found.
Brings zulip_mcp onto the ecosystem changelog policy directly in its fragment form (no shared-Unreleased era to transition from): entries land as uniquely-named files changelog.d/<slug>.<category>.md, which git merges without conflict; the npm-version hook folds them into the release section and deletes them. Direct '## Unreleased' edits are also accepted and merged at the same point. Since this repo has no publish workflow, the scheme is the pre-tag core only (CHANGELOG.md, fragments, release script, version hook, soft PR check); format doc lives in changelog.d/README.md as there is no CONTRIBUTING.md. Reference implementation: anima-research/agent-framework#128. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings zulip_mcp onto the ecosystem changelog policy directly in its fragment form (no shared-Unreleased era to transition from): entries land as uniquely-named files changelog.d/<slug>.<category>.md, which git merges without conflict; the npm-version hook folds them into the release section and deletes them. Direct '## Unreleased' edits are also accepted and merged at the same point. Since this repo has no publish workflow, the scheme is the pre-tag core only (CHANGELOG.md, fragments, release script, version hook, soft PR check); format doc lives in changelog.d/README.md as there is no CONTRIBUTING.md. Reference implementation: anima-research/agent-framework#128. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nts in CI Two findings from Greptile's review of the membrane companion PR (antra-tess/membrane#49), applicable wave-wide since the code is shared: a fragment whose first line was a bullet could smuggle top-level prose or a rogue '## ' heading (a fake section boundary) into the released changelog, and the CI check's path-only grep let a deleted or renamed fragment satisfy the entry requirement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Greptile's review of the membrane companion (antra-tess/membrane#49) found two issues in the shared code, fixed here in 7fc6a14: fragment validation now checks every line (top-level prose or a rogue |
…egression tests Codex QA pass on the membrane companion (antra-tess/membrane#49) found that a slug taken verbatim from a 'fix/foo' branch name — which the docs invited — created changelog.d/fix/foo.fixed.md, and the scanner skipped the 'fix' directory without a word: a silent release-note data-loss path that PR CI catches but direct pushes bypass. The scanner now aborts on any entry that is not README.md or a well-formed fragment file, and the docs prescribe filesystem-safe slugs ('/' -> '-'). The continuation-line check also treated any whitespace-prefixed line as valid, so an indented '## ' heading still passed. Continuations must now be indented two or more spaces, and headings/rules are refused at any indentation. test/release-changelog.test.ts runs the real script in a throwaway directory: fragment-only and mixed assembly, canonical ordering, audience-qualified headings, deletion + README preservation, and fourteen refusal cases asserting nothing is modified on failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
A Codex QA pass on the membrane companion (antra-tess/membrane#49, blocking) found two more shared-code issues, fixed here in d94b4d1: a fragment slug taken verbatim from a |
… and spaced rules Second Codex QA round on the membrane companion (antra-tess/membrane#49, head f72e00a): - Refusals used console.error + process.exit(1). Where stderr is a pipe (asynchronous on macOS), the exit can pre-empt the write, so callers — including the new tests — saw exit 1 with empty stderr. Refusals now throw a ReleaseError caught at the entry point, which prints and sets process.exitCode = 1 so the process drains stdio on its own. - The block-construct check missed thematic breaks with interior whitespace ('- - -') and headings used as item content ('- ## x'). Both are now refused; content after the bullet marker is inspected. Six regression cases added, plus one guarding against over-refusal of bullets that merely resemble rules or headings. - CI runs the suite on both OSes and on Node 20 and 24 (was: ubuntu only, Node 20), so platform-specific stdio behaviour is actually exercised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round 2 of the Codex QA pass on membrane (antra-tess/membrane#49) — fixed here in c2fa58a: refusals reported via |
Widening CI to macOS ran these for the first time there: three root-liveness re-attach cases fail on macOS with Node 24 (and pass with Node 20, and on Linux). They were written around Linux inode-reuse semantics; the file already keeps a sibling case Linux-only for a macOS watcher reason. Skipped on non-Linux with the same pattern; the underlying behaviour difference is tracked in #130. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The macOS × Node 24 leg failed on the first full-suite run on macOS — not the changelog code: 624/628 passed, and the 3 failures are |
Concurrent PRs editing the shared '## Unreleased' section of CHANGELOG.md conflict whenever one PR outlives another merge (e.g. anima-research#54, which edits CHANGELOG.md directly and is exposed to every entry that lands while it is in review). Entries now land as uniquely-named fragment files (changelog.d/<slug>.<category>.md), which git merges without conflict; the npm-version hook folds them into the release section and deletes them. Direct '## Unreleased' edits remain supported and are merged at the same point, so in-flight PRs need no rework. Tag-time publish guard and github-release job are unchanged. Part of the ecosystem-wide rollout of this scheme; reference implementation and full rationale in anima-research/agent-framework#128. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Concurrent PRs editing the shared '## Unreleased' section of CHANGELOG.md conflict whenever one PR outlives another merge — the same structural conflict generator the whole ecosystem's changelog policy carries, being fixed ecosystem-wide (see anima-research/agent-framework#128). Entries now land as uniquely-named fragment files (changelog.d/<slug>.<category>.md), which git merges without conflict; the npm-version hook folds them into the release section and deletes them. Direct '## Unreleased' edits remain supported and are merged at the same point, so in-flight PRs need no rework. Tag-time publish guard and github-release job are unchanged. The fragment-assembly logic is ported by hand from agent-framework's release-changelog.mjs into this repo's TypeScript hook script; the port also picks up two hardening pieces conhost's older script predated — refusing multiple '## Unreleased' headings, and splicing the version heading by match index rather than first-substring replace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Problem
The changelog policy has a structural conflict generator: every PR inserts prose at the same textual location (the top of
## UnreleasedinCHANGELOG.md), so any PR that outlives another merge conflicts there. Concretely: #115's only textual conflict against main isCHANGELOG.md—src/framework.tsand everything else merge cleanly — because #117, #121 and #126 each landed Unreleased entries while it was in review. With the current review cadence this recurs on essentially every long-lived PR. This is the same problem that pushed towncrier, Changesets, and GitLab to per-change fragment files.Changes
Entries now land as one uniquely-named file per change —
changelog.d/<slug>.<breaking|added|changed|fixed>.md, containing the entry's markdown bullets verbatim (slug = PR number or branch name). Distinct files never conflict in git.scripts/release-changelog.mjs(thenpm versionhook) folds pending fragments plus anything filed directly under## Unreleasedinto the new## X.Y.Z — datesection (subsections emitted in Breaking/Added/Changed/Fixed order; fragments join an existing matching subsection, including audience-qualified### Breaking (…)headings), deletes the consumed fragments, and keeps all existing refusals: no/multipleUnreleasedheadings, duplicate version section, nothing to release. New refusals: unrecognized category suffix and non-bullet fragment content — loud failure instead of silently stranding an entry.changelog.ymlsoft check now passes on a well-formed new fragment or aCHANGELOG.mdedit (transition-friendly: in-flight PRs like feat: tune-out — subconscious summaries instead of unsubscribing (#77) #115 that already editedCHANGELOG.mdkeep passing;no-changeloglabel escape unchanged).changelog.d/README.mdand badly-named fragments do not satisfy it.publish.ymlis untouched — the tag-time section guard and thegithub-releasemirror job both read the assembled versioned section, which exists at tag time exactly as before because assembly runs pre-tag.## Unreleasededits remain a supported input, merged at release time. The three entries currently sitting under Unreleased on main stay where they are and release normally.## Changelogsection rewritten, PR-template checkbox updated,changelog.d/README.mddocuments the format in-place, and this PR dogfoods the scheme with its own fragment (changelog-fragments.changed.md), so the next release exercises the pipeline.- **Module authors:** …) instead of in a subsection-heading qualifier; the heading-qualifier form still works via a direct Unreleased edit.Companion PRs land the same scheme across the ecosystem (same shape as the July policy rollout): antra-tess/membrane#49, anima-research/context-manager#72, anima-research/chronicle#16, anima-research/connectome-host#102. Safe to merge in any order — each repo's scheme is self-contained.
Tests
Release script exercised on a copy through all legs (script output pasted from the runs):
README.mdkept. Exit 0.a '## 0.11.0' section already exists.Exit 1.nothing to release as 0.12.0 — no fragments in changelog.d/ and no entries under '## Unreleased'.Exit 1.oops.md) — exit 1 with naming guidance.github-releaseawk simulated on the assembled output: guard passes, awk extracts the 32-line section, non-empty.changelog.ymlgrep logic simulated on six changed-file sets: src+fragment PASS, src+CHANGELOG PASS (legacy), src-alone FAIL, src+changelog.d/README.md-only FAIL, src+badly-named-fragment FAIL, docs-only PASS.Not verified
A real
npm versionrun end-to-end (next actual release is the live exercise — same status as the existing tag-time guards, which are also still unexercised since the policy landed). Thechangelogcheck will run live on this PR itself.changelog.d/changelog-fragments.changed.md.🤖 Generated with Claude Code