Skip to content

ci: strip git trailers from changelog and default to dry-run release - #1099

Merged
nielspardon merged 2 commits into
substrait-io:mainfrom
nielspardon:fix/changelog-strip-git-trailers
Jun 18, 2026
Merged

ci: strip git trailers from changelog and default to dry-run release#1099
nielspardon merged 2 commits into
substrait-io:mainfrom
nielspardon:fix/changelog-strip-git-trailers

Conversation

@nielspardon

@nielspardon nielspardon commented Jun 8, 2026

Copy link
Copy Markdown
Member

Problem

In v0.88.0, a Signed-off-by line leaked into the changelog and GitHub release notes under BREAKING CHANGES.

Root cause: the conventional-commits parser only ends a BREAKING CHANGE note at a recognized reference (closes #…) or another note keyword — not at a git trailer. So when a Signed-off-by: trailer immediately follows the breaking-change description (with no intervening closes/fixes line), it gets absorbed into the note text. That is exactly why #1019 leaked but #994 (which had a closes #980 line before its sign-off) did not.

Because @semantic-release/changelog and @semantic-release/github both render nextRelease.notes, the single fix point is @semantic-release/release-notes-generator.

Changes

  • Convert .releaserc.json.releaserc.mjs. JSON can't carry functions, and the fix needs a writerOpts.transform. The new ESM config is a faithful translation of the old one plus the transform.
  • Strip trailing git trailers from note text. The transform wraps the preset's own transform and only rewrites commit.notes[].text, removing trailing trailer lines (Signed-off-by, Co-authored-by, etc.). Grouping, templates, and sorting from the preset are preserved (the generator merges { ...preset.writer, ...writerOpts }).
  • Default to a dry run as a fail-safe. The publishing plugins (@semantic-release/github, @semantic-release/git) and the publish/notify exec commands are only loaded when RELEASE_DRY_RUN=false. This matters because semantic-release still runs prepare/publish/success in dry-run mode (it only skips tag and push), so those side-effecting plugins must be omitted entirely rather than merely guarded by --dry-run.
    • ci/release/run.sh opts in to a real release via RELEASE_DRY_RUN=false.
    • ci/release/dry_run.sh now reads the config file (instead of overriding plugins on the CLI), so its preview reflects the real changelog. It inherits the safe default.

Verification

Tested against the actual conventional-changelog-conventionalcommits@8.0.0 + @semantic-release/release-notes-generator@14 packages:

  • Reproduced the v0.88.0 leak and confirmed the parser mechanism.
  • Loaded the real .releaserc.mjs, pulled its transform, and re-rendered the actual v0.87.0..v0.88.0 commits: the Signed-off-by is gone, while the breaking-change text, the closes #980 reference, and all other entries are intact.
  • Confirmed the config resolves correctly for RELEASE_DRY_RUN unset / true / false (safe reduced plugin set by default; full publishing set only on false).
  • bash -n and node --check pass on all three files.

Note

Tradeoff of the dry-run default: if CI ever fails to set RELEASE_DRY_RUN=false, a release silently becomes a no-op (green build, no new tag). That is visible and cheap to recover, versus an accidental release which is not.

🤖 Generated with Claude Code


This change is Reviewable

The conventional-commits parser folds trailing git trailers (e.g.
Signed-off-by:) into BREAKING CHANGE notes because it only ends a note at
a recognized reference or note keyword, not at a trailer. This leaked a
Signed-off-by line into the v0.88.0 changelog and GitHub release notes.

Convert .releaserc.json to .releaserc.mjs so the release-notes-generator
can use a writerOpts.transform that strips trailing trailer lines from
note text (JSON cannot carry functions). The transform wraps the preset's
transform and only overrides note text, so grouping/templates/sorting are
preserved.

Also make the config default to a dry run as a fail-safe: the publishing
plugins (@semantic-release/github, @semantic-release/git) and the
publish/notify exec commands are only loaded when RELEASE_DRY_RUN=false.
This matters because semantic-release still runs prepare/publish/success
in dry-run mode (it only skips tag and push), so those side-effecting
plugins must be omitted entirely rather than merely guarded by --dry-run.
ci/release/run.sh opts in to a real release via RELEASE_DRY_RUN=false;
ci/release/dry_run.sh inherits the safe default and now reads the config
file so its preview reflects the real changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The release scripts run semantic-release via `npx -p ...`, which installs
the conventionalcommits preset as a sibling of semantic-release in a
temporary node_modules. A bare `import` in .releaserc.mjs resolves
relative to the config file's directory (the repo, which has no
node_modules) and fails with ERR_MODULE_NOT_FOUND in the dry-run CI job.

Fall back to resolving the preset relative to the running semantic-release
binary (process.argv[1]) when the plain import fails; the plain import
still covers local dev where the preset is installed alongside the project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@benbellick

benbellick commented Jun 9, 2026

Copy link
Copy Markdown
Member

Hmm it looks like the "signed off by" issue is not present in other releases. What was actually different about that merge that caused this issue? Are we likely to see it again?

EDIT: Ah so this is the culprit commit

@nielspardon

nielspardon commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

EDIT: Ah so this is the culprit commit

Yes, when you have a BREAKING CHANGE: ... followed by anything not part of the breaking change message it gets included into the changelog since semantic release thinks it's part of the breaking change message.

Comment thread .releaserc.mjs
// recognized reference (closes #..., fixes #...) or another note keyword, not
// at a git trailer -- so a trailing `Signed-off-by:` gets absorbed into the
// note text. Strip such trailing trailer lines.
const stripTrailers = (text) => {

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.

wow... wish the commit message is actually structured, rather than relying on adhoc parsing 😮‍💨

@nielspardon
nielspardon merged commit f455ecb into substrait-io:main Jun 18, 2026
13 checks passed
@nielspardon
nielspardon deleted the fix/changelog-strip-git-trailers branch June 18, 2026 08:28
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.

3 participants