Skip to content

ci: fail the build when a commit will be dropped from the changelog - #449

Merged
IgorShevchik merged 2 commits into
mainfrom
ci/guard-unparseable-commits
Aug 20, 2026
Merged

ci: fail the build when a commit will be dropped from the changelog#449
IgorShevchik merged 2 commits into
mainfrom
ci/guard-unparseable-commits

Conversation

@IgorShevchik

@IgorShevchik IgorShevchik commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Linked issue

Closes #436.

Type of change

  • Documentation (updates to the documentation or readme)
  • Bug fix (a non-breaking change that fixes an issue)
  • Enhancement (improving an existing functionality)
  • New feature (a non-breaking change that adds functionality)
  • Chore (updates to the build process or auxiliary tools and libraries)
  • Breaking change (fix or feature that would cause existing functionality to change)

Description

release-please parses every commit with @conventional-commits/parser. When the parse throws it catches, writes two logger.debug lines and moves on — CI stays green, the release PR renders normally, and the commit is gone. dcb3bacf went missing from the 2.12.0 notes that way: seven test: commits in range, six listed.

Fidelity — measured

Over all 3240 commits on main, this guard's oracle and release-please's own parseConventionalCommits were run side by side:

guard rejects:                   67
release-please yields nothing:   67
guard flags, release-please emits an entry:   0
release-please drops, guard green:            0

Perfect agreement, both directions. And the hazard is live rather than historic: all 29 line-leading call tokens in the entire history are in the last 400 commits, and the rate over the release-please era is ~1 in 100.

The trigger — corrected from the first revision

The first revision said "a body line that BEGINS with a call-like token whose parentheses nest". That is half of it.

A line beginning with a call-like token arms the parser's scope rule, which then rejects unless the very next (, ) or line-end is a ). So both of these fail:

shape at line start
`.toEqual([join(a, b)])` nested
writeTemplates(config.root unclosed before end of line
a call wrapped across two lines unclosed

And it is narrower than stated too: any : or ! before the paren disarms it, one space of indentation disarms it, and only the first group on the line is scanned — foo(a)(b(c)) parses.

The reliable repairs are indenting the line or putting a word, : or ! before the call. Reflowing — the only remedy the first revision offered — works only where the parentheses balance once joined, so it fails on intrinsic nesting, on a paragraph-initial line, and inside a code fence.

The error message was pointing most readers the wrong way

64 of the 67 real rejections are on the SUBJECT line, not in the body — missing colons (40), merge commits (13), Revert "…", a space between type and scope. Only 3 are the body shape the first revision described.

So someone whose subject is malformed was told to hunt for nested parentheses in a body that has none, while the parser's own message printed right above pointed at line 1. The guard now branches on the reported position:

The failure is on the SUBJECT line. Shapes that cause it:

  Revert "feat(x): …"     GitHub's revert button — retitle it `revert(x): …`
  chore(lint) fix         no colon after the scope
  fix (Button): …         space between type and scope
  …

Merge commits are exempt

Their subject is Merge pull request …, which the parser rejects and release-please is right to drop — there is nothing to put in a changelog. Failing there would redden main for correct behaviour. Thirteen exist in this history, the last from 2026-05-07, so it is rare rather than impossible.

The parser is now an ordinary devDependency

The first revision installed it into $RUNNER_TEMP at CI time, on the grounds that pnpm add had downgraded TypeScript 6.0.3 → 5.9.3. The downgrade is real and reproduces — but the package is not the cause. typescript is a non-optional peer that is not listed in devDependencies, while @nuxt/module-builder peers ^5.9.3, so any root pnpm add does it. Filed separately as #451.

Appending to package.json by hand and running pnpm install gives a 33-line pure-addition lockfile diff with typescript untouched, an integrity hash recorded, and --frozen-lockfile passing before and after.

That removes, in one move: the network install on every CI run, the only npm fetch in this repository outside --frozen-lockfile, the unpinned transitive tree, and the new coupling between registry availability and npm-publish.yml's await-ci gate on the release SHA.

push only

ci.yml's push trigger is branches: [main], so every push event reaching this workflow is the message that actually lands, and the release PR's squash is checked.

Correcting the first revision, which said "a PR's own commits are never the ones parsed". They usually are — for #427 the branch commit's body was byte-identical to the squash. The honest reason for push is that the squash subject and body can be rewritten in the merge dialog, and #440's were, so only the message on main is certain.

Tests

Moved from test/workflows/ to test/utils/commit-parses.spec.tsrun.sh executes before pnpm install, and the parser now lives in node_modules. It also drops the old suite's install fallback, which printed SKIP and exited 0 when offline.

33 assertions. Eighteen accepted shapes, weighted by what this repository actually writes, because a guard that reddened main on ordinary messages would be removed within the week:

pinned shape prevalence in recent commits
(#NNN) subject 98%
Co-authored-by: trailer 83%
Claude-Session: URL trailer 74%
indented body line — the repair the guard recommends 28%
Port of … trailer 22%
revert(Scope): … policy-mandated by #440, previously unpinned

Plus twelve rejected shapes across both positions, including the reduced dcb3bacf case, and three assertions that drive the real script for exit codes and for the subject/body branch.

What this does not do

It does not recover dcb3bacf. That is possible — 2.12.0 is not cut, and release-please reads a BEGIN_COMMIT_OVERRIDE block on the merged PR body in place of the commit message — but the missing entry describes a test-only change, and adding a machine directive to PR #427's description to recover one line was judged not worth the friction on a release that has been open for nine days.

Correcting the first revision, which said "a release already cut cannot be corrected retroactively": it is not cut, and it can be.

Verification

vitest run test/6870 passed, 6 skipped. eslint clean · vue-tsc --noEmit clean · shellcheck -x -S warning clean · assert-actions-pinned.py clean · test/workflows/run.sh 66 passed · pnpm install --frozen-lockfile clean before and after.

Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

release-please parses every commit with `@conventional-commits/parser`, and when
the parse throws it catches, writes two `logger.debug` lines and moves on.
Nothing turns red: CI is green, the release PR renders normally, and the commit
is simply absent. dcb3bac — `test(components): build path expectations with
pathe, not node:path (#427)` — went missing from the 2.12.0 notes that way; the
range holds seven `test:` commits and the notes list six.

Fidelity is measured, not assumed. Over all 3240 commits on `main`, this oracle
and release-please's own `parseConventionalCommits` agree on every one: 67
rejected by both, zero disagreements in either direction. The hazard is live at
roughly one commit in a hundred in the current style, and every one of the 29
line-leading call tokens in the whole history sits in the last 400 commits.

The trigger is broader than nested parentheses, which is what this commit first
claimed. A body line beginning with a call-like token arms the parser's scope
rule, which then rejects unless the very next `(`, `)` or line-end is a `)`. So
an unclosed call — `writeTemplates(config.root` running to end of line, or a call
wrapped across two lines — fails identically. Any `:` or `!` before the paren
disarms it, as does one space of indentation, and only the first group on the
line is scanned. The reliable repairs are indenting or prefixing; reflowing works
only where the parentheses balance once joined.

More importantly, 64 of the 67 real rejections are on the SUBJECT, not in the
body — missing colons, `Revert "…"`, a space between type and scope. The first
version of the error text sent all of them hunting for nested parens in a body
that does not have them, while the parser's own message pointed at line 1. The
guard now branches on the reported position and lists the shapes for each.

Merge commits are exempt. Their subject is `Merge pull request …`, which the
parser rejects and release-please is right to drop — failing there would redden
`main` for correct behaviour. Thirteen exist in this history, the last from
2026-05-07.

The parser is an ordinary devDependency. `pnpm add`-ing it downgraded TypeScript
6.0.3 to 5.9.3, which this commit first blamed on the package; the cause is that
`typescript` is a non-optional peer not listed in devDependencies while
`@nuxt/module-builder` peers `^5.9.3`, so any root `pnpm add` does it (#451).
Appending to `package.json` by hand and running `pnpm install` gives a 33-line
pure-addition lockfile diff with `typescript` untouched — so the CI-time install,
the only npm fetch in this repository outside `--frozen-lockfile`, is gone.

Tests moved from `test/workflows/` to `test/utils/commit-parses.spec.ts`, because
`run.sh` executes before `pnpm install` and the parser now lives in
`node_modules`. Eighteen accepted shapes are pinned, weighted by what this
repository actually writes — `Co-authored-by:` appears in 83% of recent commits,
`Claude-Session:` in 74%, a `(#NNN)` subject in 98%, an indented body line in
28% — because a guard that reddened `main` on ordinary messages would be removed
within the week.

Closes #436.
@IgorShevchik
IgorShevchik force-pushed the ci/guard-unparseable-commits branch from 2502e66 to 1e4066b Compare August 20, 2026 06:58
@IgorShevchik
IgorShevchik merged commit 2812722 into main Aug 20, 2026
1 check passed
@IgorShevchik
IgorShevchik deleted the ci/guard-unparseable-commits branch August 20, 2026 07:20
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.

release: test(components): … pathe … (#427) is silently missing from the 2.12.0 release notes

2 participants