ci: use conventionalcommits preset for semantic-release #5

Merged
jercik merged 5 commits from ci/semantic-release-conventionalcommits into main 2026-08-01 14:49:15 +00:00
Owner

The Release run for PR #3's merge (run 12133) went green but released nothing: release.config.mjs used the commit-analyzer's default angular preset, whose header pattern cannot parse the Conventional Commits ! marker. The feat!: squash subject therefore scored as no release instead of major — semantic-release exited 0, the workflow stayed green, and v2.0.0 was silently never cut. Registry, tags, and Forgejo releases all still say 1.0.0.

This switches both the commit-analyzer and the release-notes-generator to preset: "conventionalcommits" and adds the conventional-changelog-conventionalcommits package it loads, pinned to ^9.3.1: release-notes-generator 14 loads conventional-changelog-writer 8, and the v10 preset emits the writer-9 API against it — generateNotes then renders only the version heading, with no sections and no error. Verified against the exact installed versions (commit-analyzer 13.0.1, notes generator 14.1.1, preset 9.3.1) by invoking analyzeCommits and generateNotes directly on the real squash subject:

  • old default: null (reproduces the silent skip)
  • preset 10.2.1: "major", but notes render as the bare 2.0.0 compare heading — empty release notes, no error
  • preset 9.3.1: "major", and notes render in full (BREAKING CHANGES section included)

The knip ignoreDependencies entry is needed because knip's semantic-release plugin extracts only plugin names from the config, never a preset option, so the dynamically-loaded preset package is invisible to it.

Merging this triggers the Release workflow, which re-analyzes every commit since v1.0.0 — including the feat!: squash — and cuts v2.0.0.

Follow-up outside this repo: the align template templates/repo/release-config.mjs.hbs has the same gap, so every aligned repo silently skips releases on feat!:/fix!: squash subjects; the identical preset change (with the same pin) landed in j4k-align PR #169 so this file stops counting as drift and the fleet gets the fix.

The Release run for PR #3's merge (run 12133) went green but released nothing: `release.config.mjs` used the commit-analyzer's default angular preset, whose header pattern cannot parse the Conventional Commits `!` marker. The `feat!:` squash subject therefore scored as *no release* instead of *major* — semantic-release exited 0, the workflow stayed green, and v2.0.0 was silently never cut. Registry, tags, and Forgejo releases all still say 1.0.0. This switches both the commit-analyzer and the release-notes-generator to `preset: "conventionalcommits"` and adds the `conventional-changelog-conventionalcommits` package it loads, pinned to `^9.3.1`: release-notes-generator 14 loads `conventional-changelog-writer` 8, and the v10 preset emits the writer-9 API against it — `generateNotes` then renders only the version heading, with no sections and no error. Verified against the exact installed versions (commit-analyzer 13.0.1, notes generator 14.1.1, preset 9.3.1) by invoking `analyzeCommits` and `generateNotes` directly on the real squash subject: - old default: `null` (reproduces the silent skip) - preset 10.2.1: `"major"`, but notes render as the bare `2.0.0` compare heading — empty release notes, no error - preset 9.3.1: `"major"`, and notes render in full (BREAKING CHANGES section included) The knip `ignoreDependencies` entry is needed because knip's semantic-release plugin extracts only plugin names from the config, never a `preset` option, so the dynamically-loaded preset package is invisible to it. Merging this triggers the Release workflow, which re-analyzes every commit since v1.0.0 — including the `feat!:` squash — and cuts v2.0.0. Follow-up outside this repo: the align template `templates/repo/release-config.mjs.hbs` has the same gap, so every aligned repo silently skips releases on `feat!:`/`fix!:` squash subjects; the identical preset change (with the same pin) landed in j4k-align PR #169 so this file stops counting as drift and the fleet gets the fix.
ci: use conventionalcommits preset for semantic-release
Some checks failed
commit-msg / commitlint (pull_request) Successful in 19s
Dedupe check / dedupe-check (pull_request) Successful in 34s
Checks / quality-checks (26.5.0) (pull_request) Successful in 34s
Checks / quality-checks (24.15.0) (pull_request) Successful in 34s
PR Review / Prepare immutable review tools (pull_request_target) Successful in 1m48s
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Successful in 3m17s
PR Review / forgejo-review-approach-smart-2 poster (pull_request_target) Successful in 12s
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Successful in 4m27s
PR Review / forgejo-review-approach-smart-1 poster (pull_request_target) Successful in 12s
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Successful in 5m56s
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Successful in 6m1s
PR Review / forgejo-review-code-smart-1 poster (pull_request_target) Successful in 15s
PR Review / forgejo-review-code-smart-2 poster (pull_request_target) Successful in 12s
PR Review / forgejo-review-approach-3 generator (pull_request_target) Failing after 25m33s
PR Review / forgejo-review-approach-3 poster (pull_request_target) Has been skipped
51cb0a71b9
forgejo-actions left a comment

Approach review: The approach is correct, minimal, and the canonical fix. No material alternative worth taking.

I verified the load-bearing claims against the exact pinned versions rather than taking the description at its word:

  • conventional-changelog-angular@8.3.1 (the version the lockfile resolves for both plugins) exposes headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/ and no breakingHeaderPattern. A feat!: subject therefore parses to type: null with zero notes, so neither {breaking: true} nor {type: "feat"} in commit-analyzer's default release rules matches, and analyzeCommits returns null. The diagnosis of the silent green run is right, and the ! gap is genuinely in the preset, not in the workflow or the config wiring.
  • Both @semantic-release/commit-analyzer@13.0.1 (lib/load-parser-config.js) and @semantic-release/release-notes-generator@14.1.1 (lib/load-changelog-config.js) resolve a preset as conventional-changelog-<preset> via importFrom.silent(__dirname, …) || importFrom(cwd, …). Under pnpm's isolated store the package is not reachable from the plugin directory, so it has to be resolvable from the project root — the direct devDependencies entry is required, not incidental. .forgejo/workflows/release.yml installs with pnpm install --frozen-lockfile (full dev tree), so it will be present at release time. The non-silent importFrom also means a future drop of the dependency fails the release loudly rather than reverting to the same silent skip.
  • Changing both plugins rather than only the analyzer is the right call: leaving the notes generator on angular would classify feat!: inconsistently between the version bump and the rendered notes.

The strongest argument for conventionalcommits over the surgical alternatives is consistency with what the repo already enforces. .forgejo/workflows/commit-msg.yml validates the PR title (the squash subject) with conventional-commit-msg, which accepts the spec's ! marker. The gate and the analyzer were speaking different dialects; this change makes the release side honor the same spec the merge side already requires, instead of encoding a second, hand-maintained interpretation of it.

Alternatives I considered and would not recommend:

  • Keeping angular and adding parserOpts: { breakingHeaderPattern: … }. Smaller diff, but it hand-rolls a regex that has to track the spec and would drift from the commit-msg gate again. Strictly worse for a config that is heading into a fleet template.
  • A releaseRules entry for breaking changes. It cannot work: the rules run on the parser's output, and the header never parses in the first place.
  • Avoiding the knip.json escape hatch by importing the preset statically. Not possible — both the preset and config plugin options take a package-name string that the plugin imports dynamically, so knip's semantic-release plugin has nothing to extract. ignoreDependencies is the correct mechanism here, and the inline comment on release.config.mjs records the non-obvious why for the preset choice itself.

One optional, non-blocking simplification: semantic-release passes root-level options to every plugin, so a single preset: "conventionalcommits" at the top of releaseConfig would cover both plugins and remove the possibility of the two drifting apart. The explicit per-plugin form is equally defensible and arguably clearer as template material, so this is a preference, not a defect.

Approach review by Claude Code Opus (opus)

**Approach review:** The approach is correct, minimal, and the canonical fix. No material alternative worth taking. I verified the load-bearing claims against the exact pinned versions rather than taking the description at its word: - `conventional-changelog-angular@8.3.1` (the version the lockfile resolves for both plugins) exposes `headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/` and **no** `breakingHeaderPattern`. A `feat!:` subject therefore parses to `type: null` with zero notes, so neither `{breaking: true}` nor `{type: "feat"}` in commit-analyzer's default release rules matches, and `analyzeCommits` returns `null`. The diagnosis of the silent green run is right, and the `!` gap is genuinely in the preset, not in the workflow or the config wiring. - Both `@semantic-release/commit-analyzer@13.0.1` (`lib/load-parser-config.js`) and `@semantic-release/release-notes-generator@14.1.1` (`lib/load-changelog-config.js`) resolve a `preset` as `conventional-changelog-<preset>` via `importFrom.silent(__dirname, …) || importFrom(cwd, …)`. Under pnpm's isolated store the package is not reachable from the plugin directory, so it has to be resolvable from the project root — the direct `devDependencies` entry is required, not incidental. `.forgejo/workflows/release.yml` installs with `pnpm install --frozen-lockfile` (full dev tree), so it will be present at release time. The non-silent `importFrom` also means a future drop of the dependency fails the release loudly rather than reverting to the same silent skip. - Changing both plugins rather than only the analyzer is the right call: leaving the notes generator on angular would classify `feat!:` inconsistently between the version bump and the rendered notes. The strongest argument for `conventionalcommits` over the surgical alternatives is consistency with what the repo already enforces. `.forgejo/workflows/commit-msg.yml` validates the PR title (the squash subject) with `conventional-commit-msg`, which accepts the spec's `!` marker. The gate and the analyzer were speaking different dialects; this change makes the release side honor the same spec the merge side already requires, instead of encoding a second, hand-maintained interpretation of it. Alternatives I considered and would not recommend: - Keeping angular and adding `parserOpts: { breakingHeaderPattern: … }`. Smaller diff, but it hand-rolls a regex that has to track the spec and would drift from the commit-msg gate again. Strictly worse for a config that is heading into a fleet template. - A `releaseRules` entry for breaking changes. It cannot work: the rules run on the parser's output, and the header never parses in the first place. - Avoiding the `knip.json` escape hatch by importing the preset statically. Not possible — both the `preset` and `config` plugin options take a package-name string that the plugin imports dynamically, so knip's semantic-release plugin has nothing to extract. `ignoreDependencies` is the correct mechanism here, and the inline comment on `release.config.mjs` records the non-obvious why for the preset choice itself. One optional, non-blocking simplification: semantic-release passes root-level options to every plugin, so a single `preset: "conventionalcommits"` at the top of `releaseConfig` would cover both plugins and remove the possibility of the two drifting apart. The explicit per-plugin form is equally defensible and arguably clearer as template material, so this is a preference, not a defect. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:c80b5a32-4dc9-41cb-84f6-30b2bb0474ad -->
forgejo-actions left a comment

Approach review: The diagnosis and the shape of the fix are right. I reproduced both halves against the exact resolved versions (commit-analyzer 13.0.1, release-notes-generator 14.1.1, writer 8.4.0): conventional-changelog-angular@8.3.1 really does ship a headerPattern with no breakingHeaderPattern, so analyzeCommits({}, ...) returns null for the feat!: squash subject and returns "major" with preset: "conventionalcommits". Switching the preset (rather than patching parserOpts.breakingHeaderPattern, which also yields major) is the better of the two — it fixes parsing and notes rendering together and matches what the fleet template should carry.

One material problem with the version chosen: conventional-changelog-conventionalcommits@^10 is ahead of what semantic-release 25 can render. Preset 10 emits @conventional-changelog/template function partials, but @semantic-release/release-notes-generator@14.1.1 (the current latest) depends on conventional-changelog-writer@^8, which is handlebars-based. The analyzer still returns major, so v2.0.0 gets cut — but the release notes silently collapse to the version heading with every commit section dropped. Under @9.3.1 the same input renders Features / Bug Fixes / BREAKING CHANGES correctly. Details and repro are in the inline comment.

Everything else reads fine: the knip.json ignore entry is the right escape hatch for a dynamically loaded preset, and both plugins being switched (not just the analyzer) is correct. Given this class of bug is silent — green workflow, wrong or empty output — a small vitest case that runs analyzeCommits/generateNotes from release.config.mjs against a feat!: subject would have caught both the original miss and the preset-10 notes regression; worth considering alongside the j4k-align rollout, since the version pin propagates to every aligned repo.

Approach review by Claude Code Opus (opus)

**Approach review:** The diagnosis and the shape of the fix are right. I reproduced both halves against the exact resolved versions (commit-analyzer 13.0.1, release-notes-generator 14.1.1, writer 8.4.0): `conventional-changelog-angular@8.3.1` really does ship a `headerPattern` with no `breakingHeaderPattern`, so `analyzeCommits({}, ...)` returns `null` for the `feat!:` squash subject and returns `"major"` with `preset: "conventionalcommits"`. Switching the preset (rather than patching `parserOpts.breakingHeaderPattern`, which also yields `major`) is the better of the two — it fixes parsing and notes rendering together and matches what the fleet template should carry. One material problem with the version chosen: `conventional-changelog-conventionalcommits@^10` is ahead of what semantic-release 25 can render. Preset 10 emits `@conventional-changelog/template` function partials, but `@semantic-release/release-notes-generator@14.1.1` (the current latest) depends on `conventional-changelog-writer@^8`, which is handlebars-based. The analyzer still returns `major`, so v2.0.0 gets cut — but the release notes silently collapse to the version heading with every commit section dropped. Under `@9.3.1` the same input renders Features / Bug Fixes / BREAKING CHANGES correctly. Details and repro are in the inline comment. Everything else reads fine: the `knip.json` ignore entry is the right escape hatch for a dynamically loaded preset, and both plugins being switched (not just the analyzer) is correct. Given this class of bug is silent — green workflow, wrong or empty output — a small vitest case that runs `analyzeCommits`/`generateNotes` from `release.config.mjs` against a `feat!:` subject would have caught both the original miss and the preset-10 notes regression; worth considering alongside the j4k-align rollout, since the version pin propagates to every aligned repo. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:b2e798b8-3ab1-466a-9871-a57ae9c30ea4 -->
package.json Outdated
@ -50,6 +50,7 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^25.9.2",
"@vitest/coverage-v8": "^4.1.8",
"conventional-changelog-conventionalcommits": "^10.2.1",

^10.2.1 is ahead of what semantic-release 25 can render, and the failure is silent.

Preset 10 builds its writerOpts partials with @conventional-changelog/template (function partials), but @semantic-release/release-notes-generator@14.1.1 — the latest published version — depends on conventional-changelog-writer@^8, which is handlebars-based, and the lockfile here resolves conventional-changelog-writer@8.4.0. The commit-analyzer half is unaffected (major either way), so v2.0.0 still gets cut; the notes half quietly loses every section.

Reproduced with commit-analyzer 13.0.1 + release-notes-generator 14.1.1 on commits feat!: breaking thing, feat: plain thing, fix: a bug:

  • with preset 10.2.1, generateNotes returns only ## [2.0.0](...compare/v1.0.0...v2.0.0) (date) — no Features, no Bug Fixes, no BREAKING CHANGES
  • with preset 9.3.1, it returns the full body: ### ⚠ BREAKING CHANGES, ### Features (both feat commits), ### Bug Fixes

Suggest "conventional-changelog-conventionalcommits": "^9.3.1" until release-notes-generator moves to writer 9. ^9 also won't drift into the incompatible major on its own. Worth carrying the same pin into the j4k-align template, otherwise every aligned repo cuts correct versions with empty release notes.

`^10.2.1` is ahead of what semantic-release 25 can render, and the failure is silent. Preset 10 builds its `writerOpts` partials with `@conventional-changelog/template` (function partials), but `@semantic-release/release-notes-generator@14.1.1` — the latest published version — depends on `conventional-changelog-writer@^8`, which is handlebars-based, and the lockfile here resolves `conventional-changelog-writer@8.4.0`. The commit-analyzer half is unaffected (`major` either way), so v2.0.0 still gets cut; the notes half quietly loses every section. Reproduced with commit-analyzer 13.0.1 + release-notes-generator 14.1.1 on commits `feat!: breaking thing`, `feat: plain thing`, `fix: a bug`: - with preset `10.2.1`, `generateNotes` returns only `## [2.0.0](...compare/v1.0.0...v2.0.0) (date)` — no Features, no Bug Fixes, no BREAKING CHANGES - with preset `9.3.1`, it returns the full body: `### ⚠ BREAKING CHANGES`, `### Features` (both feat commits), `### Bug Fixes` Suggest `"conventional-changelog-conventionalcommits": "^9.3.1"` until release-notes-generator moves to writer 9. `^9` also won't drift into the incompatible major on its own. Worth carrying the same pin into the j4k-align template, otherwise every aligned repo cuts correct versions with empty release notes.
jercik marked this conversation as resolved
forgejo-actions left a comment

Summary: The core diagnosis in this PR is correct and I reproduced it: with the exact pinned versions, analyzeCommits returns null for the feat!: squash subject under the default angular preset (conventional-changelog-angular@8.3.1 has no breakingHeaderPattern) and "major" with preset: "conventionalcommits". The lockfile additions and integrity hashes match the registry, and the knip ignoreDependencies entry is justified.

One blocking problem: the chosen preset version, ^10.2.1, is a major ahead of what the semantic-release stack can consume. Preset v10 emits the conventional-changelog-writer@9 function-template API, while @semantic-release/release-notes-generator@14.1.1 depends on writer ^8 (Handlebars strings). The bump would be computed correctly, but the v2.0.0 release notes/Forgejo release body would contain only the version heading — no Features, Bug Fixes, or ⚠ BREAKING CHANGES sections. Pinning ^9.3.1 produces the full notes with the same "major" bump.

Code review by Claude Code Opus (opus)

**Summary:** The core diagnosis in this PR is correct and I reproduced it: with the exact pinned versions, `analyzeCommits` returns `null` for the `feat!:` squash subject under the default angular preset (`conventional-changelog-angular@8.3.1` has no `breakingHeaderPattern`) and `"major"` with `preset: "conventionalcommits"`. The lockfile additions and integrity hashes match the registry, and the knip `ignoreDependencies` entry is justified. One blocking problem: the chosen preset version, `^10.2.1`, is a major ahead of what the semantic-release stack can consume. Preset v10 emits the `conventional-changelog-writer@9` function-template API, while `@semantic-release/release-notes-generator@14.1.1` depends on writer `^8` (Handlebars strings). The bump would be computed correctly, but the v2.0.0 release notes/Forgejo release body would contain only the version heading — no `Features`, `Bug Fixes`, or `⚠ BREAKING CHANGES` sections. Pinning `^9.3.1` produces the full notes with the same `"major"` bump. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:8f1c9dea-85e6-48c5-a582-d04296087495 -->
package.json Outdated
@ -50,6 +50,7 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^25.9.2",
"@vitest/coverage-v8": "^4.1.8",
"conventional-changelog-conventionalcommits": "^10.2.1",

🟠 High: conventional-changelog-conventionalcommits@10 is incompatible with the release-notes-generator in this lockfile, and the result is a release body containing only the version heading — every ### Features / ### Bug Fixes / ### ⚠ BREAKING CHANGES section is silently dropped.

Root cause: preset v10 emits the writer v9 API (function-valued template / headerPartial / commitPartial / footerPartial, via its new @conventional-changelog/template dependency), but @semantic-release/release-notes-generator@14.1.1 depends on conventional-changelog-writer@^8, which is Handlebars-string based (loadTemplates looks for mainTemplate, and compileTemplates registers the partials with Handlebars). Handlebars happens to accept a function as a partial, so the preset's headerPartial(context) still renders the heading — which is why the 2.0.0 compare heading in the PR description looked right — but commitPartial is then invoked as (commit, handlebarsOptions) instead of (context, commit) and returns '', and footerPartial returns '' unconditionally, so the notes body is the heading and nothing else. This is also why v10 needs no compare-func while writer-v8-compatible presets (conventional-changelog-angular@8.3.1, conventional-changelog-conventionalcommits@9.x) do.

Reproduced with the exact versions in this lockfile (commit-analyzer 13.0.1, release-notes-generator 14.1.1), calling generateNotes({preset:"conventionalcommits"}, ctx) on feat!: ..., feat: ... and fix: ... commits:

# preset 10.2.1
## [2.0.0](https://.../compare/v1.0.0...v2.0.0) (2026-07-31)
<nothing else>

# preset 9.3.1
## [2.0.0](https://.../compare/v1.0.0...v2.0.0) (2026-07-31)

### ⚠ BREAKING CHANGES

* non-blocking default, distinct exit codes, JSON error envelopes (#3)

### Features
...
### Bug Fixes
...

analyzeCommits is unaffected — it only consumes parser opts, and returns "major" under both 9.3.1 and 10.2.1, so the version bump this PR is fixing works either way; only the notes break.

Fix: pin "conventional-changelog-conventionalcommits": "^9.3.1" (last writer-v8-compatible line) and refresh the lockfile. Upgrading the writer instead is not an option — release-notes-generator's latest release still declares conventional-changelog-writer: ^8.0.0. Dropping the preset from the notes generator is also not an option: the angular parser discards the feat!: commit entirely, so its notes lose that commit too.

🟠 **High:** `conventional-changelog-conventionalcommits@10` is incompatible with the release-notes-generator in this lockfile, and the result is a release body containing only the version heading — every `### Features` / `### Bug Fixes` / `### ⚠ BREAKING CHANGES` section is silently dropped. Root cause: preset v10 emits the **writer v9** API (function-valued `template` / `headerPartial` / `commitPartial` / `footerPartial`, via its new `@conventional-changelog/template` dependency), but `@semantic-release/release-notes-generator@14.1.1` depends on `conventional-changelog-writer@^8`, which is Handlebars-string based (`loadTemplates` looks for `mainTemplate`, and `compileTemplates` registers the partials with Handlebars). Handlebars happens to accept a function as a partial, so the preset's `headerPartial(context)` still renders the heading — which is why the `2.0.0` compare heading in the PR description looked right — but `commitPartial` is then invoked as `(commit, handlebarsOptions)` instead of `(context, commit)` and returns `''`, and `footerPartial` returns `''` unconditionally, so the notes body is the heading and nothing else. This is also why v10 needs no `compare-func` while writer-v8-compatible presets (`conventional-changelog-angular@8.3.1`, `conventional-changelog-conventionalcommits@9.x`) do. Reproduced with the exact versions in this lockfile (commit-analyzer 13.0.1, release-notes-generator 14.1.1), calling `generateNotes({preset:"conventionalcommits"}, ctx)` on `feat!: ...`, `feat: ...` and `fix: ...` commits: ``` # preset 10.2.1 ## [2.0.0](https://.../compare/v1.0.0...v2.0.0) (2026-07-31) <nothing else> # preset 9.3.1 ## [2.0.0](https://.../compare/v1.0.0...v2.0.0) (2026-07-31) ### ⚠ BREAKING CHANGES * non-blocking default, distinct exit codes, JSON error envelopes (#3) ### Features ... ### Bug Fixes ... ``` `analyzeCommits` is unaffected — it only consumes `parser` opts, and returns `"major"` under both 9.3.1 and 10.2.1, so the version bump this PR is fixing works either way; only the notes break. Fix: pin `"conventional-changelog-conventionalcommits": "^9.3.1"` (last writer-v8-compatible line) and refresh the lockfile. Upgrading the writer instead is not an option — release-notes-generator's latest release still declares `conventional-changelog-writer: ^8.0.0`. Dropping the preset from the notes generator is also not an option: the angular parser discards the `feat!:` commit entirely, so its notes lose that commit too.
jercik marked this conversation as resolved
@ -5,0 +3,4 @@
// The default angular preset cannot parse the Conventional Commits "!" marker,
// so a "feat!:" squash subject silently produces no release instead of a major.
["@semantic-release/commit-analyzer", { preset: "conventionalcommits" }],
["@semantic-release/release-notes-generator", { preset: "conventionalcommits" }],

🟠 High: This is the line affected by the preset-version problem flagged on package.json — with conventional-changelog-conventionalcommits@10.2.1, this plugin renders only the version heading and drops every commit section and the ⚠ BREAKING CHANGES section (preset v10 targets conventional-changelog-writer@9's function templates; release-notes-generator 14.1.1 pins writer ^8, which is Handlebars-string based). Line 5 (commit-analyzer) is fine either way, since it consumes only parser opts. Pinning the preset to ^9.3.1 makes both lines correct.

🟠 **High:** This is the line affected by the preset-version problem flagged on `package.json` — with `conventional-changelog-conventionalcommits@10.2.1`, this plugin renders only the version heading and drops every commit section and the `⚠ BREAKING CHANGES` section (preset v10 targets `conventional-changelog-writer@9`'s function templates; release-notes-generator 14.1.1 pins writer `^8`, which is Handlebars-string based). Line 5 (`commit-analyzer`) is fine either way, since it consumes only `parser` opts. Pinning the preset to `^9.3.1` makes both lines correct.
jercik marked this conversation as resolved
forgejo-actions left a comment

Summary: Found 1 high-severity issue.

I reproduced the PR's diagnosis with the exact installed versions (@semantic-release/commit-analyzer@13.0.1, @semantic-release/release-notes-generator@14.1.1) against the real feat!: squash subject of 66c8896: the default angular preset (conventional-changelog-angular@8.3.1, whose headerPattern is /^(\w*)(?:\((.*)\))?: (.*)$/ with no breakingHeaderPattern) yields null, and preset: "conventionalcommits" yields major. The analyzer half of this change is correct and will cut v2.0.0.

The pinned preset version is the problem: conventional-changelog-conventionalcommits@10.2.1 ships writer-v9-style JS function templates, while release-notes-generator@14.1.1 still depends on conventional-changelog-writer@^8 (handlebars string templates). The combination renders only the version heading — every Features / Bug Fixes / BREAKING CHANGES section is silently dropped from the release body. Pinning ^9.3.1 keeps the major verdict and restores full notes; details in the inline comment.

Code review by Claude Code Opus (opus)

**Summary:** Found 1 high-severity issue. I reproduced the PR's diagnosis with the exact installed versions (`@semantic-release/commit-analyzer@13.0.1`, `@semantic-release/release-notes-generator@14.1.1`) against the real `feat!:` squash subject of `66c8896`: the default angular preset (`conventional-changelog-angular@8.3.1`, whose `headerPattern` is `/^(\w*)(?:\((.*)\))?: (.*)$/` with no `breakingHeaderPattern`) yields `null`, and `preset: "conventionalcommits"` yields `major`. The analyzer half of this change is correct and will cut v2.0.0. The pinned preset version is the problem: `conventional-changelog-conventionalcommits@10.2.1` ships writer-v9-style JS function templates, while `release-notes-generator@14.1.1` still depends on `conventional-changelog-writer@^8` (handlebars string templates). The combination renders only the version heading — every Features / Bug Fixes / BREAKING CHANGES section is silently dropped from the release body. Pinning `^9.3.1` keeps the `major` verdict and restores full notes; details in the inline comment. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:cdd1f864-8c4b-4d2f-957a-dd24318e3249 -->
package.json Outdated
@ -50,6 +50,7 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^25.9.2",
"@vitest/coverage-v8": "^4.1.8",
"conventional-changelog-conventionalcommits": "^10.2.1",

🟠 High: ^10.2.1 of this preset is incompatible with the release-notes-generator that semantic-release@25 bundles, so the v2.0.0 release body will contain nothing but the version heading.

conventional-changelog-conventionalcommits@10.2.1 exports writer-v9-style JS function templates (src/templates.js exports template/headerPartial/commitPartial as functions), but @semantic-release/release-notes-generator@14.1.1 depends on conventional-changelog-writer@^8, which expects handlebars template strings and reads writerOpts.mainTemplate (not template). The header partial happens to render as a function partial; the commit and note groups do not.

Verified with the exact installed versions, same commit objects, only the preset version differing:

  • preset 10.2.1## [2.0.0](…/compare/v1.0.0...v2.0.0) (2026-07-31) and nothing else (93 bytes)
  • preset 9.3.1 → the same heading plus ### ⚠ BREAKING CHANGES, ### Features, ### Bug Fixes with the commit links (436 bytes for the real feat!: squash)

analyzeCommits returns major under both preset versions, so downgrading costs nothing on the half this PR is actually fixing. Upgrading the generator is not an option — @semantic-release/release-notes-generator@latest is still 14.1.1 on writer ^8.

Fix: "conventional-changelog-conventionalcommits": "^9.3.1" (and refresh pnpm-lock.yaml; v9 drops the @conventional-changelog/template dep and adds compare-func). Worth carrying into the j4k-align template change mentioned in the PR body, since every aligned repo would otherwise get empty release notes.

🟠 **High:** `^10.2.1` of this preset is incompatible with the release-notes-generator that `semantic-release@25` bundles, so the v2.0.0 release body will contain nothing but the version heading. `conventional-changelog-conventionalcommits@10.2.1` exports writer-v9-style JS function templates (`src/templates.js` exports `template`/`headerPartial`/`commitPartial` as functions), but `@semantic-release/release-notes-generator@14.1.1` depends on `conventional-changelog-writer@^8`, which expects handlebars template *strings* and reads `writerOpts.mainTemplate` (not `template`). The header partial happens to render as a function partial; the commit and note groups do not. Verified with the exact installed versions, same commit objects, only the preset version differing: - preset `10.2.1` → `## [2.0.0](…/compare/v1.0.0...v2.0.0) (2026-07-31)` and nothing else (93 bytes) - preset `9.3.1` → the same heading plus `### ⚠ BREAKING CHANGES`, `### Features`, `### Bug Fixes` with the commit links (436 bytes for the real `feat!:` squash) `analyzeCommits` returns `major` under both preset versions, so downgrading costs nothing on the half this PR is actually fixing. Upgrading the generator is not an option — `@semantic-release/release-notes-generator@latest` is still `14.1.1` on writer `^8`. Fix: `"conventional-changelog-conventionalcommits": "^9.3.1"` (and refresh `pnpm-lock.yaml`; v9 drops the `@conventional-changelog/template` dep and adds `compare-func`). Worth carrying into the `j4k-align` template change mentioned in the PR body, since every aligned repo would otherwise get empty release notes.
jercik marked this conversation as resolved
@ -5,0 +3,4 @@
// The default angular preset cannot parse the Conventional Commits "!" marker,
// so a "feat!:" squash subject silently produces no release instead of a major.
["@semantic-release/commit-analyzer", { preset: "conventionalcommits" }],
["@semantic-release/release-notes-generator", { preset: "conventionalcommits" }],

ℹ️ Info: This is the entry affected by the preset-version mismatch flagged on package.json. The commit-analyzer entry on line 5 is unaffected — it only consumes parserOpts, which are plain objects in both preset major versions.

ℹ️ **Info:** This is the entry affected by the preset-version mismatch flagged on `package.json`. The commit-analyzer entry on line 5 is unaffected — it only consumes `parserOpts`, which are plain objects in both preset major versions.
jercik marked this conversation as resolved
ci: pin conventionalcommits preset to v9 for writer-8 notes compat
Some checks failed
commit-msg / commitlint (pull_request) Successful in 14s
Dedupe check / dedupe-check (pull_request) Successful in 28s
Checks / quality-checks (26.5.0) (pull_request) Successful in 33s
Checks / quality-checks (24.15.0) (pull_request) Successful in 33s
PR Review / Prepare immutable review tools (pull_request_target) Successful in 1m44s
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-3 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-smart-1 poster (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-smart-2 poster (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-3 poster (pull_request_target) Has been skipped
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Successful in 3m54s
PR Review / forgejo-review-code-smart-1 poster (pull_request_target) Failing after 15s
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Successful in 6m6s
PR Review / forgejo-review-code-smart-2 poster (pull_request_target) Failing after 11s
1a39834c0d
Author
Owner

Round-1 disposition (fix in 1a39834):

  • 32006 / 32008 / 32009 / 32011 / 32012 (preset v10 incompatible with writer v8 — notes collapse to the heading) — fixed with the unanimous suggestion: the devDependency is now ^9.3.1 and the lockfile refreshed. Reproduced both ways before pushing: with 10.2.1, generateNotes returns only the 93-byte compare heading; with 9.3.1 it returns the full 438-byte body (⚠ BREAKING CHANGES + Features sections) and analyzeCommits still scores the real feat!: squash as major. Candidly: my pre-PR verification ran generateNotes and printed exactly the truncated heading — I misread it as success; the reviewers' catch is exactly why this cycle exists. The same pin is applied to j4k/align#169 so the fleet template rollout carries v9, not v10.
  • 8031's optional root-level preset suggestion — acknowledged, keeping the per-plugin arrays: each plugin names its preset explicitly, the shape byte-matches the align template render, and the reviewer flagged it as a preference, not a defect.
  • 8032's vitest suggestion (drive analyzeCommits/generateNotes from the config against a feat!: subject) — agreed on the value, but the right home is fleet-side: this file is align-managed, so a behavioral guard belongs next to the template. Filed as a follow-up on j4k/align rather than growing this repo's suite around a generated file.
  • approach-3 lane failure — infrastructure, not code: the GLM-5.2 lane hit its whole-turn deadline (OPENCODE_PROMPT_TIMEOUT) and published nothing. Not retried: this push supersedes that head, approach lanes are opened-only by design, and two approach reviews (8031, 8032) had already published.

All five conversations resolved.

Round-1 disposition (fix in 1a39834): - **32006 / 32008 / 32009 / 32011 / 32012 (preset v10 incompatible with writer v8 — notes collapse to the heading)** — fixed with the unanimous suggestion: the devDependency is now `^9.3.1` and the lockfile refreshed. Reproduced both ways before pushing: with 10.2.1, `generateNotes` returns only the 93-byte compare heading; with 9.3.1 it returns the full 438-byte body (⚠ BREAKING CHANGES + Features sections) and `analyzeCommits` still scores the real `feat!:` squash as `major`. Candidly: my pre-PR verification ran `generateNotes` and printed exactly the truncated heading — I misread it as success; the reviewers' catch is exactly why this cycle exists. The same pin is applied to j4k/align#169 so the fleet template rollout carries v9, not v10. - **8031's optional root-level `preset` suggestion** — acknowledged, keeping the per-plugin arrays: each plugin names its preset explicitly, the shape byte-matches the align template render, and the reviewer flagged it as a preference, not a defect. - **8032's vitest suggestion (drive `analyzeCommits`/`generateNotes` from the config against a `feat!:` subject)** — agreed on the value, but the right home is fleet-side: this file is align-managed, so a behavioral guard belongs next to the template. Filed as a follow-up on j4k/align rather than growing this repo's suite around a generated file. - **approach-3 lane failure** — infrastructure, not code: the GLM-5.2 lane hit its whole-turn deadline (OPENCODE_PROMPT_TIMEOUT) and published nothing. Not retried: this push supersedes that head, approach lanes are opened-only by design, and two approach reviews (8031, 8032) had already published. All five conversations resolved.
forgejo-actions left a comment

Approach review: The approach is correct and minimal.

The fix swaps the default angular preset (which cannot parse the Conventional Commits ! marker, silently scoring feat!: as no-release) for preset: "conventionalcommits" on both @semantic-release/commit-analyzer and @semantic-release/release-notes-generator. This is the documented, canonical way to make semantic-release handle the ! marker — the only realistic alternative would be hand-rolled releaseRules/parserOpts, which would be more complex and less idiomatic for no gain.

conventional-changelog-conventionalcommits is correctly placed in devDependencies alongside semantic-release itself, which only runs in the Release CI workflow where devDependencies are installed; nothing in dependencies needs it.

The knip ignoreDependencies entry is the documented escape hatch for a package that semantic-release loads dynamically by string name, which knip's plugin cannot statically see. It is the right tool.

No materially better approach exists.

Approach review by Approach Review 3 (OpenCode Wafer) (GLM-5.2)

**Approach review:** The approach is correct and minimal. The fix swaps the default `angular` preset (which cannot parse the Conventional Commits `!` marker, silently scoring `feat!:` as no-release) for `preset: "conventionalcommits"` on both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator`. This is the documented, canonical way to make semantic-release handle the `!` marker — the only realistic alternative would be hand-rolled `releaseRules`/`parserOpts`, which would be more complex and less idiomatic for no gain. `conventional-changelog-conventionalcommits` is correctly placed in `devDependencies` alongside `semantic-release` itself, which only runs in the Release CI workflow where devDependencies are installed; nothing in `dependencies` needs it. The knip `ignoreDependencies` entry is the documented escape hatch for a package that semantic-release loads dynamically by string name, which knip's plugin cannot statically see. It is the right tool. No materially better approach exists. _Approach review by Approach Review 3 (OpenCode Wafer) (GLM-5.2)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiIxYTM5ODM0YzBkMzk3ZDYyNDBkZGI4YTI1MTgxMTA1ZmI3ZjliOTE1Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLTMiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIyODIiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiNDg3ZTczOGMtNWMwYS00OTFhLTljOTUtMTYzZDhmOTM5ZWRiIn0= -->
forgejo-actions left a comment

Approach review: The approach is correct and minimal, and I independently confirmed the diagnosis against the real packages rather than taking the description on trust:

  • conventional-changelog-angular@8.3.1 (src/parser.js) defines only headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/ and no breakingHeaderPattern, so a feat!: subject fails to parse, yields a null type with no notes, and matches none of commit-analyzer’s default release rules — exactly the silent no-release described.
  • conventional-changelog-conventionalcommits@9.3.1 defines both headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/ and breakingHeaderPattern, so the ! marker resolves to a major.
  • @semantic-release/commit-analyzer@13.0.1 resolves a preset through importFrom.silent(__dirname, ...) || importFrom(cwd, ...) and depends only on the angular preset, so the preset package genuinely has to be a root dependency of this repo — the devDependencies entry is required, not incidental, and the Release workflow’s pnpm install --frozen-lockfile installs it.

On alternatives I considered and rejected as not better:

  • Overriding parserOpts with a hand-written headerPattern/breakingHeaderPattern instead of adding the package would duplicate preset internals across two plugins and drift from upstream; adding the published preset is the standard facility.
  • The knip.json ignoreDependencies entry is the right escape hatch: knip’s semantic-release plugin extracts plugin names only, so a preset resolved by name at runtime is invisible to it, and there is no more precise knip mechanism for a dynamically resolved package.
  • Applying the preset per plugin rather than as a root-level shared option is slightly more verbose but more explicit and avoids handing an unknown preset key to @semantic-release/npm and @j4k/semantic-release-forgejo; that trade is a preference, not a defect.

The change also brings the release pipeline in line with the commit-msg workflow, which already accepts feat!: as a valid subject — the two were disagreeing, and now they do not. No material alternative to raise.

Approach review by Claude Code Opus (opus)

**Approach review:** The approach is correct and minimal, and I independently confirmed the diagnosis against the real packages rather than taking the description on trust: - `conventional-changelog-angular@8.3.1` (`src/parser.js`) defines only `headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/` and no `breakingHeaderPattern`, so a `feat!:` subject fails to parse, yields a null `type` with no notes, and matches none of commit-analyzer’s default release rules — exactly the silent no-release described. - `conventional-changelog-conventionalcommits@9.3.1` defines both `headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/` and `breakingHeaderPattern`, so the `!` marker resolves to a major. - `@semantic-release/commit-analyzer@13.0.1` resolves a preset through `importFrom.silent(__dirname, ...) || importFrom(cwd, ...)` and depends only on the angular preset, so the preset package genuinely has to be a root dependency of this repo — the `devDependencies` entry is required, not incidental, and the Release workflow’s `pnpm install --frozen-lockfile` installs it. On alternatives I considered and rejected as not better: - Overriding `parserOpts` with a hand-written `headerPattern`/`breakingHeaderPattern` instead of adding the package would duplicate preset internals across two plugins and drift from upstream; adding the published preset is the standard facility. - The `knip.json` `ignoreDependencies` entry is the right escape hatch: knip’s semantic-release plugin extracts plugin names only, so a preset resolved by name at runtime is invisible to it, and there is no more precise knip mechanism for a dynamically resolved package. - Applying the preset per plugin rather than as a root-level shared option is slightly more verbose but more explicit and avoids handing an unknown `preset` key to `@semantic-release/npm` and `@j4k/semantic-release-forgejo`; that trade is a preference, not a defect. The change also brings the release pipeline in line with the `commit-msg` workflow, which already accepts `feat!:` as a valid subject — the two were disagreeing, and now they do not. No material alternative to raise. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiIxYTM5ODM0YzBkMzk3ZDYyNDBkZGI4YTI1MTgxMTA1ZmI3ZjliOTE1Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLXNtYXJ0LTEiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIyODIiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiNzU5ZWE5N2EtZjJkNC00OThhLTlhZDktMGJjNzU1NDY4YzUwIn0= -->
forgejo-actions left a comment

Approach review: The approach looks good.

Switching both @semantic-release/commit-analyzer and @semantic-release/release-notes-generator to the official conventionalcommits preset is the standard facility for this problem, and it is clearly preferable to the alternatives: hand-rolling parserOpts.headerPattern to admit the ! marker, adding custom releaseRules, or mandating a BREAKING CHANGE: footer in squash subjects would each reimplement or work around what the preset already specifies, and would drift from the spec on the next parser change. Declaring conventional-changelog-conventionalcommits as a devDependency is required because semantic-release resolves preset by dynamic import, and knip.json's ignoreDependencies is the documented escape hatch for a package knip's semantic-release plugin structurally cannot see (it reads plugin names, not plugin options). The scope is minimal — no workflow, source, or test changes — and the fix lands entirely in config.

The major-alignment constraint is handled correctly: ^9.3.1 keeps the preset on the same major line as the conventional-changelog-writer@8 that commit-analyzer 13.0.1 and notes-generator 14.1.1 pull transitively, and the caret cannot cross into 10. The follow-up commit on this branch already made that pin, so the PR description's "preset 10.2.1" is stale relative to the head; since the squash subject and body feed the release notes this cuts, it is worth refreshing before merge.

One non-blocking observation, offered rather than requested: the failure class here — semantic-release exiting 0 having released nothing, with a green workflow — stays invisible after this fix, and ignoreDependencies additionally means knip will not flag the preset package if the preset option is ever dropped from release.config.mjs. If the fleet-wide change heading into j4k-align is a good moment for it, a single assertion that analyzeCommits returns major for a feat!: subject against the real config would pin both halves down. Not a reason to hold this PR.

Approach review by Claude Code Opus (opus)

**Approach review:** The approach looks good. Switching both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` to the official `conventionalcommits` preset is the standard facility for this problem, and it is clearly preferable to the alternatives: hand-rolling `parserOpts.headerPattern` to admit the `!` marker, adding custom `releaseRules`, or mandating a `BREAKING CHANGE:` footer in squash subjects would each reimplement or work around what the preset already specifies, and would drift from the spec on the next parser change. Declaring `conventional-changelog-conventionalcommits` as a devDependency is required because semantic-release resolves `preset` by dynamic import, and `knip.json`'s `ignoreDependencies` is the documented escape hatch for a package knip's semantic-release plugin structurally cannot see (it reads plugin names, not plugin options). The scope is minimal — no workflow, source, or test changes — and the fix lands entirely in config. The major-alignment constraint is handled correctly: `^9.3.1` keeps the preset on the same major line as the `conventional-changelog-writer@8` that commit-analyzer 13.0.1 and notes-generator 14.1.1 pull transitively, and the caret cannot cross into 10. The follow-up commit on this branch already made that pin, so the PR description's "preset 10.2.1" is stale relative to the head; since the squash subject and body feed the release notes this cuts, it is worth refreshing before merge. One non-blocking observation, offered rather than requested: the failure class here — semantic-release exiting 0 having released nothing, with a green workflow — stays invisible after this fix, and `ignoreDependencies` additionally means knip will not flag the preset package if the `preset` option is ever dropped from `release.config.mjs`. If the fleet-wide change heading into j4k-align is a good moment for it, a single assertion that `analyzeCommits` returns `major` for a `feat!:` subject against the real config would pin both halves down. Not a reason to hold this PR. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiIxYTM5ODM0YzBkMzk3ZDYyNDBkZGI4YTI1MTgxMTA1ZmI3ZjliOTE1Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLXNtYXJ0LTIiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIyODIiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiNjJiMjRhNTMtYWU2OC00NzQwLWI3NzgtNmNmZThhMjJmY2Y5In0= -->
forgejo-actions left a comment

Summary: Found 1 low-severity issue. The core change is correct and I reproduced it.

Verification (fresh installs of the exact locked versions — commit-analyzer 13.0.1, release-notes-generator 14.1.1, conventional-changelog-writer 8.4.0, preset 9.3.1 — invoking analyzeCommits/generateNotes on the real feat!: squash subject of 66c8896):

  • default angular preset: analyzeCommitsnull, notes render only the version heading — reproduces the silent skip described in the PR body.
  • preset: "conventionalcommits" @ 9.3.1: analyzeCommits"major", notes render the 2.0.0 compare heading plus ⚠ BREAKING CHANGES and Features sections with correct Forgejo /issues/ and /commit/ links.
  • Resolution also works under pnpm's isolated node_modules (repeated the run with pnpm 11.8.0), so the dynamically loaded preset is found at release time; the workflow's pnpm install --frozen-lockfile --ignore-scripts installs devDependencies, so it is present.

Lockfile is consistent with package.json (^9.3.1 → 9.3.1), the integrity hash matches the public registry entry, and compare-func stays deduped at 2.0.0. The knip ignoreDependencies entry is warranted: in a minimal repro, knip 6 reports the preset as an unused devDependency without it.

Code review by Claude Code Opus (opus)

**Summary:** Found 1 low-severity issue. The core change is correct and I reproduced it. Verification (fresh installs of the exact locked versions — commit-analyzer 13.0.1, release-notes-generator 14.1.1, conventional-changelog-writer 8.4.0, preset 9.3.1 — invoking `analyzeCommits`/`generateNotes` on the real `feat!:` squash subject of 66c8896): - default angular preset: `analyzeCommits` → `null`, notes render only the version heading — reproduces the silent skip described in the PR body. - `preset: "conventionalcommits"` @ 9.3.1: `analyzeCommits` → `"major"`, notes render the `2.0.0` compare heading plus `⚠ BREAKING CHANGES` and `Features` sections with correct Forgejo `/issues/` and `/commit/` links. - Resolution also works under pnpm's isolated `node_modules` (repeated the run with pnpm 11.8.0), so the dynamically loaded preset is found at release time; the workflow's `pnpm install --frozen-lockfile --ignore-scripts` installs devDependencies, so it is present. Lockfile is consistent with `package.json` (`^9.3.1` → 9.3.1), the integrity hash matches the public registry entry, and `compare-func` stays deduped at 2.0.0. The knip `ignoreDependencies` entry is warranted: in a minimal repro, knip 6 reports the preset as an unused devDependency without it. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiIxYTM5ODM0YzBkMzk3ZDYyNDBkZGI4YTI1MTgxMTA1ZmI3ZjliOTE1Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWNvZGUtc21hcnQtMSIsInNvdXJjZVdvcmtmbG93UnVuSWQiOiIxMjI4MiIsInNvdXJjZUdlbmVyYXRvckF0dGVtcHQiOiIxIiwicmVzdWx0SWQiOiI2NDQxMTk0Ni02NzE0LTQwYWMtOTAzNy02OWJmNTllY2VkNjIifQ== -->
@ -5,0 +3,4 @@
// The default angular preset cannot parse the Conventional Commits "!" marker,
// so a "feat!:" squash subject silently produces no release instead of a major.
["@semantic-release/commit-analyzer", { preset: "conventionalcommits" }],
["@semantic-release/release-notes-generator", { preset: "conventionalcommits" }],

🟢 Low: The ^9.3.1 range on the preset is load-bearing, but nothing in the tree records why — the only trace is the ci: pin conventionalcommits preset to v9 for writer-8 notes compat commit subject.

I reproduced the incompatibility with the locked plugin versions: with conventional-changelog-conventionalcommits@10.2.1 and conventional-changelog-writer@8.4.0 (what release-notes-generator 14.1.1 pulls in), analyzeCommits still returns "major", but generateNotes returns only the version heading — no BREAKING CHANGES section, no Features section, and no error. Preset 9.x declares no peer range on the writer, so neither npm nor pnpm can warn, and a future ^10 bump (manual pnpm up --latest or an automated updater) would silently empty every release note while CI stays green — the same silent-failure class this PR exists to fix.

Since package.json cannot carry comments, extend the comment block already added here to state the constraint, e.g. // Preset pinned to 9.x: 10.x renders empty notes against conventional-changelog-writer 8 (what release-notes-generator 14 installs).

🟢 **Low:** The `^9.3.1` range on the preset is load-bearing, but nothing in the tree records why — the only trace is the `ci: pin conventionalcommits preset to v9 for writer-8 notes compat` commit subject. I reproduced the incompatibility with the locked plugin versions: with `conventional-changelog-conventionalcommits@10.2.1` and `conventional-changelog-writer@8.4.0` (what release-notes-generator 14.1.1 pulls in), `analyzeCommits` still returns `"major"`, but `generateNotes` returns only the version heading — no `BREAKING CHANGES` section, no `Features` section, and no error. Preset 9.x declares no peer range on the writer, so neither npm nor pnpm can warn, and a future `^10` bump (manual `pnpm up --latest` or an automated updater) would silently empty every release note while CI stays green — the same silent-failure class this PR exists to fix. Since `package.json` cannot carry comments, extend the comment block already added here to state the constraint, e.g. `// Preset pinned to 9.x: 10.x renders empty notes against conventional-changelog-writer 8 (what release-notes-generator 14 installs).`
jercik marked this conversation as resolved
forgejo-actions left a comment

Summary: The fix is correct and I reproduced every claim in the PR body against the exact locked versions (@semantic-release/commit-analyzer 13.0.1, @semantic-release/release-notes-generator 14.1.1, preset 9.3.1). Found 1 low issue.

Verification performed:

  • analyzeCommits on the real squash subject feat!: non-blocking default, distinct exit codes, JSON error envelopes (#3) alone returns null with the default angular preset and "major" with preset: "conventionalcommits" — the silent-skip reproduces exactly as described.
  • generateNotes with the preset renders the 2.0.0 compare heading plus ⚠ BREAKING CHANGES, Features, and Bug Fixes sections.
  • Preset resolution works under pnpm's isolated node_modules (enableGlobalVirtualStore: true): commit-analyzer's importFrom(cwd, ...) fallback finds the root devDependency symlink, so pnpm install --frozen-lockfile --ignore-scripts in release.yml is sufficient.
  • perf: and revert: commits render identically under both presets, so the preset switch causes no release-notes regression for existing commit types.
  • The knip ignoreDependencies entry is genuinely load-bearing: knip 6.16.1's semantic-release plugin (dist/plugins/semantic-release/index.js) maps config.plugins to names only and never reads a preset option, so the package would otherwise be reported unused — and pnpm knip runs in checks.yml.
  • oxfmt --check passes on release.config.mjs, package.json, and knip.json; release.config.mjs still imports cleanly; the lockfile carries no leftover 10.2.1 reference.

The v9 pin from the follow-up commit is justified, and the failure mode is worse than "notes compat": preset 10.2.1 against writer 8.4.0 emits the compare heading and zero sections, with no error. Note the PR body still describes verification against preset 10.2.1 while the head pins ^9.3.1 — worth refreshing the description before merge so the record matches what ships.

Code review by Claude Code Opus (opus)

**Summary:** The fix is correct and I reproduced every claim in the PR body against the exact locked versions (`@semantic-release/commit-analyzer` 13.0.1, `@semantic-release/release-notes-generator` 14.1.1, preset 9.3.1). Found 1 low issue. Verification performed: - `analyzeCommits` on the real squash subject `feat!: non-blocking default, distinct exit codes, JSON error envelopes (#3)` alone returns `null` with the default angular preset and `"major"` with `preset: "conventionalcommits"` — the silent-skip reproduces exactly as described. - `generateNotes` with the preset renders the `2.0.0` compare heading plus `⚠ BREAKING CHANGES`, `Features`, and `Bug Fixes` sections. - Preset resolution works under pnpm's isolated `node_modules` (`enableGlobalVirtualStore: true`): commit-analyzer's `importFrom(cwd, ...)` fallback finds the root devDependency symlink, so `pnpm install --frozen-lockfile --ignore-scripts` in `release.yml` is sufficient. - `perf:` and `revert:` commits render identically under both presets, so the preset switch causes no release-notes regression for existing commit types. - The `knip` `ignoreDependencies` entry is genuinely load-bearing: knip 6.16.1's semantic-release plugin (`dist/plugins/semantic-release/index.js`) maps `config.plugins` to names only and never reads a `preset` option, so the package would otherwise be reported unused — and `pnpm knip` runs in `checks.yml`. - `oxfmt --check` passes on `release.config.mjs`, `package.json`, and `knip.json`; `release.config.mjs` still imports cleanly; the lockfile carries no leftover 10.2.1 reference. The v9 pin from the follow-up commit is justified, and the failure mode is worse than "notes compat": preset 10.2.1 against writer 8.4.0 emits the compare heading and **zero** sections, with no error. Note the PR body still describes verification against preset 10.2.1 while the head pins `^9.3.1` — worth refreshing the description before merge so the record matches what ships. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiIxYTM5ODM0YzBkMzk3ZDYyNDBkZGI4YTI1MTgxMTA1ZmI3ZjliOTE1Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWNvZGUtc21hcnQtMiIsInNvdXJjZVdvcmtmbG93UnVuSWQiOiIxMjI4MiIsInNvdXJjZUdlbmVyYXRvckF0dGVtcHQiOiIxIiwicmVzdWx0SWQiOiI4NjIwYWJjMy1lOTg4LTRlZDUtYjRmZC05ZjJhYTRmZDFmNDAifQ== -->
@ -3,2 +3,2 @@
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
// The default angular preset cannot parse the Conventional Commits "!" marker,
// so a "feat!:" squash subject silently produces no release instead of a major.

🟢 Low: The comment records why the preset is needed, but not the constraint that makes the ^9.3.1 pin in package.json load-bearing — and that constraint fails silently.

The preset's major must match the conventional-changelog-writer major used by @semantic-release/release-notes-generator. I ran the mismatch against the locked versions: preset 10.2.1 with release-notes-generator 14.1.1 (writer 8.4.0) produces

## [2.0.0](.../compare/v1.0.0...v2.0.0) (2026-08-01)

and nothing else — no BREAKING CHANGES, no Features, no Bug Fixes — with no thrown error. semantic-release would exit 0 and cut an empty-notes release, the same green-but-wrong shape this PR exists to fix.

The caret range itself is safe today (9.3.1 is the last 9.x, and semantic-release@^25.0.7 holds release-notes-generator at ^14.1.0/writer ^8), so this is not a live defect. It only bites on a deliberate major bump of either package. Extending the existing comment with one clause, e.g. pinned to v9 because release-notes-generator 14 loads conventional-changelog-writer 8; a v10 preset renders empty notes without erroring, keeps that reasoning where the next person bumping the dependency will see it.

🟢 **Low:** The comment records why the preset is needed, but not the constraint that makes the `^9.3.1` pin in `package.json` load-bearing — and that constraint fails silently. The preset's major must match the `conventional-changelog-writer` major used by `@semantic-release/release-notes-generator`. I ran the mismatch against the locked versions: preset **10.2.1** with release-notes-generator 14.1.1 (writer 8.4.0) produces ``` ## [2.0.0](.../compare/v1.0.0...v2.0.0) (2026-08-01) ``` and nothing else — no `BREAKING CHANGES`, no `Features`, no `Bug Fixes` — with no thrown error. semantic-release would exit 0 and cut an empty-notes release, the same green-but-wrong shape this PR exists to fix. The caret range itself is safe today (9.3.1 is the last 9.x, and `semantic-release@^25.0.7` holds release-notes-generator at `^14.1.0`/writer `^8`), so this is not a live defect. It only bites on a deliberate major bump of either package. Extending the existing comment with one clause, e.g. `pinned to v9 because release-notes-generator 14 loads conventional-changelog-writer 8; a v10 preset renders empty notes without erroring`, keeps that reasoning where the next person bumping the dependency will see it.
jercik marked this conversation as resolved
docs(release): record why the conventionalcommits preset is pinned to v9
Some checks failed
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-approach-3 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Has been cancelled
PR Review / Dispatch and observe exact review writers (pull_request_target) Has been cancelled
PR Review / Prepare immutable review tools (pull_request_target) Has been cancelled
commit-msg / commitlint (pull_request) Successful in 44s
Dedupe check / dedupe-check (pull_request) Successful in 47s
Checks / quality-checks (26.5.0) (pull_request) Successful in 1m13s
Checks / quality-checks (24.15.0) (pull_request) Successful in 1m13s
cd5cf60801
Author
Owner

Round 3 (head cd5cf60) — both findings addressed.

  • 32493 / 32510 (why the ^9.3.1 pin is load-bearing): implemented in cd5cf60 — the release.config.mjs comment now records that release-notes-generator 14 loads conventional-changelog-writer 8 and a v10 preset renders empty notes against it without erroring. Both reviews asked for the same clause; one comment block now carries it.
  • The PR description is refreshed to describe the pinned 9.3.1 verification (including the empty-notes reproduction on 10.2.1) instead of the pre-pin 10.2.1 wording, so the squash body that feeds the release notes is accurate.
Round 3 (head cd5cf60) — both findings addressed. - **32493 / 32510** (why the `^9.3.1` pin is load-bearing): implemented in cd5cf60 — the `release.config.mjs` comment now records that release-notes-generator 14 loads conventional-changelog-writer 8 and a v10 preset renders empty notes against it without erroring. Both reviews asked for the same clause; one comment block now carries it. - The PR description is refreshed to describe the pinned 9.3.1 verification (including the empty-notes reproduction on 10.2.1) instead of the pre-pin 10.2.1 wording, so the squash body that feeds the release notes is accurate.
forgejo-actions left a comment

Approach review: The approach is sound. Switching both commit-analyzer and release-notes-generator to preset: "conventionalcommits" is the standard, correct fix for the angular preset's inability to parse the Conventional Commits ! marker — the root cause of the silent no-release on feat!:. Applying it to both plugins is right: the analyzer decides the level, but the generator renders the BREAKING CHANGES section, so both must agree.

The ^9.3.1 pin is a justified defensive measure, empirically verified against the exact installed writer version (release-notes-generator 14 → conventional-changelog-writer 8) that v10 of the preset renders empty notes against. The knip ignoreDependencies entry is necessary because the preset is loaded dynamically by plugin name, invisible to static analysis. Adding the preset as a devDependency (not a runtime dep) is correct since semantic-release runs in CI only.

A custom releaseRules/parserOpts header-pattern extension to the angular preset would be more complex for no benefit, and upgrading release-notes-generator to chase preset v10 is not clearly preferable to the verified pin. The systemic follow-up for the align template fleet is noted. No materially better alternative.

Approach review by Approach Review 3 (OpenCode Wafer) (GLM-5.2)

**Approach review:** The approach is sound. Switching both `commit-analyzer` and `release-notes-generator` to `preset: "conventionalcommits"` is the standard, correct fix for the angular preset's inability to parse the Conventional Commits `!` marker — the root cause of the silent no-release on `feat!:`. Applying it to both plugins is right: the analyzer decides the level, but the generator renders the BREAKING CHANGES section, so both must agree. The `^9.3.1` pin is a justified defensive measure, empirically verified against the exact installed writer version (release-notes-generator 14 → conventional-changelog-writer 8) that v10 of the preset renders empty notes against. The knip `ignoreDependencies` entry is necessary because the preset is loaded dynamically by plugin name, invisible to static analysis. Adding the preset as a devDependency (not a runtime dep) is correct since semantic-release runs in CI only. A custom `releaseRules`/`parserOpts` header-pattern extension to the angular preset would be more complex for no benefit, and upgrading release-notes-generator to chase preset v10 is not clearly preferable to the verified pin. The systemic follow-up for the align template fleet is noted. No materially better alternative. _Approach review by Approach Review 3 (OpenCode Wafer) (GLM-5.2)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjZDVjZjYwODAxNzEzMTk2YjRhODBlNGQ4YTVjNjY1Yjg1ZmE1Yzc3Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLTMiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIzMzIiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiODMwMDI2ZDEtNGNmZS00NDI4LTlmZTUtODI4YjAwYjY4Y2YxIn0= -->
forgejo-actions left a comment

Approach review: The approach looks good — this is the canonical fix, and the supporting premises check out.

What I verified independently of the PR body:

  • The root cause is real. conventional-changelog-angular@8.3.1 (the version the lockfile resolves for both plugins) ships headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/ in src/parser.js, with no breakingHeaderPattern. A feat!: subject fails the header match entirely, so it scores as no release — exactly the silent skip described.
  • The v9 pin is structurally sound, not just documented. @semantic-release/release-notes-generator@14.1.1 is the current latest and depends on conventional-changelog-writer@^8.0.0, so there is no newer generator that would accept a v10 preset. ^9.3.1 cannot resolve past 9.x, so the caret itself enforces the writer-8 constraint; the comment in release.config.mjs explains a genuinely non-obvious why rather than standing in for the guard. The repo has no Renovate/Dependabot config, so the remaining bump path is a deliberate manual major upgrade.
  • Switching the notes generator's preset too is the right call, not scope creep. Leaving it on angular would keep feat!: commits out of the rendered sections even once the analyzer scored them correctly.
  • The knip.json entry is necessary. The preset is resolved dynamically by name, so knip's semantic-release plugin cannot see it; ignoreDependencies is the standard escape hatch here.

Alternatives I considered and rejected as worse:

  • Keeping the angular preset and overriding parserOpts (e.g. adding a breakingHeaderPattern) avoids the new devDependency and the knip entry, but it hand-maintains a regex fork of a preset's parser, and it would fix the major verdict while leaving type/subject unparsed for notes rendering. The official preset is both simpler and more complete.
  • Nothing here argues for a workflow-level "assert a release happened" guard: semantic-release legitimately no-ops on chore-only pushes, so such a check would produce false failures.

One thing worth confirming at merge, since it is the part I could not exercise without a node_modules tree: the conventionalcommits preset's default commitUrlFormat / compareUrlFormat / issueUrlFormat are GitHub-shaped (/commit/, /compare/, /issues/). Forgejo uses those same paths for code.j4k.dev/j4k-oss/tropkod-client, so the generated links should resolve without a presetConfig override — but the v2.0.0 notes are the first real render, so it is cheap to eyeball the links on the resulting release.

Approach review by Claude Code Opus (opus)

**Approach review:** The approach looks good — this is the canonical fix, and the supporting premises check out. What I verified independently of the PR body: - **The root cause is real.** `conventional-changelog-angular@8.3.1` (the version the lockfile resolves for both plugins) ships `headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/` in `src/parser.js`, with no `breakingHeaderPattern`. A `feat!:` subject fails the header match entirely, so it scores as no release — exactly the silent skip described. - **The v9 pin is structurally sound, not just documented.** `@semantic-release/release-notes-generator@14.1.1` is the current `latest` and depends on `conventional-changelog-writer@^8.0.0`, so there is no newer generator that would accept a v10 preset. `^9.3.1` cannot resolve past 9.x, so the caret itself enforces the writer-8 constraint; the comment in `release.config.mjs` explains a genuinely non-obvious *why* rather than standing in for the guard. The repo has no Renovate/Dependabot config, so the remaining bump path is a deliberate manual major upgrade. - **Switching the notes generator's preset too is the right call, not scope creep.** Leaving it on angular would keep `feat!:` commits out of the rendered sections even once the analyzer scored them correctly. - **The `knip.json` entry is necessary.** The preset is resolved dynamically by name, so knip's semantic-release plugin cannot see it; `ignoreDependencies` is the standard escape hatch here. Alternatives I considered and rejected as worse: - Keeping the angular preset and overriding `parserOpts` (e.g. adding a `breakingHeaderPattern`) avoids the new devDependency and the knip entry, but it hand-maintains a regex fork of a preset's parser, and it would fix the *major* verdict while leaving type/subject unparsed for notes rendering. The official preset is both simpler and more complete. - Nothing here argues for a workflow-level "assert a release happened" guard: semantic-release legitimately no-ops on chore-only pushes, so such a check would produce false failures. One thing worth confirming at merge, since it is the part I could not exercise without a `node_modules` tree: the `conventionalcommits` preset's default `commitUrlFormat` / `compareUrlFormat` / `issueUrlFormat` are GitHub-shaped (`/commit/`, `/compare/`, `/issues/`). Forgejo uses those same paths for `code.j4k.dev/j4k-oss/tropkod-client`, so the generated links should resolve without a `presetConfig` override — but the v2.0.0 notes are the first real render, so it is cheap to eyeball the links on the resulting release. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjZDVjZjYwODAxNzEzMTk2YjRhODBlNGQ4YTVjNjY1Yjg1ZmE1Yzc3Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLXNtYXJ0LTEiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIzMzIiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiMjU5ZjUyOTQtMGY2OC00OTVjLWE5M2QtY2UxMmY0ODdjYmMzIn0= -->
forgejo-actions left a comment

Approach review: The diagnosis and the fix both hold up when checked against the locked tree.

  • conventional-changelog-angular@8.3.1's parserOpts carries only a headerPattern requiring type(scope): subject and no breakingHeaderPattern, so a feat!: subject parses to a null type with no notes and scores as no release. The silent skip is real.
  • Neither @semantic-release/commit-analyzer@13.0.1 nor @semantic-release/release-notes-generator@14.1.1 bundles the conventionalcommits preset — the lockfile shows both pulling conventional-changelog-angular@8.3.1 only — so adding the preset as a direct devDependency is required, not incidental.
  • The v9-over-v10 pin is justified: v9.3.1's createWriterOpts returns mainTemplate (the writer-8 key), while v10.2.1 returns template and depends on @conventional-changelog/template. Under the locked conventional-changelog-writer@8.4.0 the v10 preset's main template is simply never found, which matches the empty-notes symptom described.

Switching both plugins to preset: "conventionalcommits" is the documented configuration for this problem and keeps the repo in step with the align template; the knip ignoreDependencies entry is the standard escape hatch for a preset loaded by name rather than imported. The alternative of keeping the angular preset and overriding parserOpts.breakingHeaderPattern would avoid the new dependency and the knip entry, but yields worse-grouped notes and diverges from the fleet template, so it is not preferable here.

One maintenance observation is left as a comment: the preset/writer major coupling is load-bearing but expressed only as a caret range plus prose.

Approach review by Claude Code Opus (opus)

**Approach review:** The diagnosis and the fix both hold up when checked against the locked tree. - `conventional-changelog-angular@8.3.1`'s `parserOpts` carries only a `headerPattern` requiring `type(scope): subject` and no `breakingHeaderPattern`, so a `feat!:` subject parses to a null type with no notes and scores as *no release*. The silent skip is real. - Neither `@semantic-release/commit-analyzer@13.0.1` nor `@semantic-release/release-notes-generator@14.1.1` bundles the conventionalcommits preset — the lockfile shows both pulling `conventional-changelog-angular@8.3.1` only — so adding the preset as a direct devDependency is required, not incidental. - The v9-over-v10 pin is justified: v9.3.1's `createWriterOpts` returns `mainTemplate` (the writer-8 key), while v10.2.1 returns `template` and depends on `@conventional-changelog/template`. Under the locked `conventional-changelog-writer@8.4.0` the v10 preset's main template is simply never found, which matches the empty-notes symptom described. Switching both plugins to `preset: "conventionalcommits"` is the documented configuration for this problem and keeps the repo in step with the align template; the knip `ignoreDependencies` entry is the standard escape hatch for a preset loaded by name rather than imported. The alternative of keeping the angular preset and overriding `parserOpts.breakingHeaderPattern` would avoid the new dependency and the knip entry, but yields worse-grouped notes and diverges from the fleet template, so it is not preferable here. One maintenance observation is left as a comment: the preset/writer major coupling is load-bearing but expressed only as a caret range plus prose. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjZDVjZjYwODAxNzEzMTk2YjRhODBlNGQ4YTVjNjY1Yjg1ZmE1Yzc3Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLXNtYXJ0LTIiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIzMzIiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiNGY0YjMxZjUtMTExMC00MjgxLWI5MTEtMjMzZGIzYzdlYjY2In0= -->
@ -50,6 +50,7 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^25.9.2",
"@vitest/coverage-v8": "^4.1.8",
"conventional-changelog-conventionalcommits": "^9.3.1",

The ^9 range and the comment in release.config.mjs are the only things holding this preset on the writer-8-compatible major — and the failure modes on both sides of that boundary are silent. The old default scored no release; a v10 preset against writer 8 renders an empty body. Neither is caught by knip, lint, typecheck, or the test suite, so a routine major-bump PR for this package would go green and the next release would again ship nothing useful, with no error anywhere.

Worth considering something that makes a violation loud rather than relying on the range holding: a small test that feeds a synthetic feat!: commit through analyzeCommits/generateNotes and asserts a major bump and a rendered BREAKING CHANGES section would catch both the original bug and any future preset/writer skew. If the fleet's dependency-bot config is in reach, a rule capping this package below 10 is a cheaper complement. As it stands the constraint is documented but unenforced, which is the same shape of problem this PR is fixing.

The `^9` range and the comment in `release.config.mjs` are the only things holding this preset on the writer-8-compatible major — and the failure modes on both sides of that boundary are silent. The old default scored *no release*; a v10 preset against writer 8 renders an empty body. Neither is caught by knip, lint, typecheck, or the test suite, so a routine major-bump PR for this package would go green and the next release would again ship nothing useful, with no error anywhere. Worth considering something that makes a violation loud rather than relying on the range holding: a small test that feeds a synthetic `feat!:` commit through `analyzeCommits`/`generateNotes` and asserts a `major` bump and a rendered BREAKING CHANGES section would catch both the original bug and any future preset/writer skew. If the fleet's dependency-bot config is in reach, a rule capping this package below 10 is a cheaper complement. As it stands the constraint is documented but unenforced, which is the same shape of problem this PR is fixing.
jercik marked this conversation as resolved
forgejo-actions left a comment

Summary: No blocking issues; 1 low-severity note.

I reproduced the change against the exact versions in the lockfile (@semantic-release/commit-analyzer@13.0.1, @semantic-release/release-notes-generator@14.1.1, conventional-changelog-conventionalcommits@9.3.1) on the real feat!: squash subject, under a pnpm install using this repo's enableGlobalVirtualStore: true layout:

  • default (angular) preset -> analyzeCommits returns null (confirms the silent skip described in the PR body)
  • preset: "conventionalcommits" at 9.3.1 -> "major", and generateNotes renders the full body (BREAKING CHANGES + Features sections)
  • 10.2.1 -> "major" but notes collapse to the bare version heading with no error (confirms the reason for the v9 constraint)

Preset resolution works in the pnpm layout: commit-analyzer's import-from-esm falls back to the cwd-rooted node_modules, where the new top-level devDependency lives. The release workflow installs devDependencies (pnpm install --frozen-lockfile --ignore-scripts), so the preset is present at release time.

Other checks: the lockfile entry matches the published 9.3.1 metadata (integrity, compare-func@^2.0.0, engines.node >=18) and introduces no duplicate versions, so pnpm dedupe --check and --frozen-lockfile stay satisfied; oxfmt --check passes on all three text files; the knip ignoreDependencies entry is required because the preset is referenced only as a config string, and pnpm knip runs in CI.

Code review by Claude Code Opus (opus)

**Summary:** No blocking issues; 1 low-severity note. I reproduced the change against the exact versions in the lockfile (`@semantic-release/commit-analyzer@13.0.1`, `@semantic-release/release-notes-generator@14.1.1`, `conventional-changelog-conventionalcommits@9.3.1`) on the real `feat!:` squash subject, under a pnpm install using this repo's `enableGlobalVirtualStore: true` layout: - default (angular) preset -> `analyzeCommits` returns `null` (confirms the silent skip described in the PR body) - `preset: "conventionalcommits"` at 9.3.1 -> `"major"`, and `generateNotes` renders the full body (BREAKING CHANGES + Features sections) - 10.2.1 -> `"major"` but notes collapse to the bare version heading with no error (confirms the reason for the v9 constraint) Preset resolution works in the pnpm layout: commit-analyzer's `import-from-esm` falls back to the cwd-rooted `node_modules`, where the new top-level devDependency lives. The release workflow installs devDependencies (`pnpm install --frozen-lockfile --ignore-scripts`), so the preset is present at release time. Other checks: the lockfile entry matches the published 9.3.1 metadata (integrity, `compare-func@^2.0.0`, `engines.node >=18`) and introduces no duplicate versions, so `pnpm dedupe --check` and `--frozen-lockfile` stay satisfied; `oxfmt --check` passes on all three text files; the knip `ignoreDependencies` entry is required because the preset is referenced only as a config string, and `pnpm knip` runs in CI. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjZDVjZjYwODAxNzEzMTk2YjRhODBlNGQ4YTVjNjY1Yjg1ZmE1Yzc3Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWNvZGUtc21hcnQtMSIsInNvdXJjZVdvcmtmbG93UnVuSWQiOiIxMjMzMiIsInNvdXJjZUdlbmVyYXRvckF0dGVtcHQiOiIxIiwicmVzdWx0SWQiOiJjOTYyNWZjZS1hMmRlLTQyMDAtOTljMi0yNjQ0MmRlZjA3OWQifQ== -->
@ -50,6 +50,7 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^25.9.2",
"@vitest/coverage-v8": "^4.1.8",
"conventional-changelog-conventionalcommits": "^9.3.1",

🟢 Low: The regression this guards against is silent, and the range wording overstates the guard. ^9.3.1 is a range, not a pin (the release.config.mjs comment calls it "pinned"), and it is the only thing keeping the preset off v10. I confirmed the v10 failure mode against the installed generator: 10.2.1 still returns "major", but generateNotes renders only the version heading — no sections, no error, semantic-release exits 0 and the workflow stays green, so a future major bump reproduces exactly the kind of invisible breakage this PR is fixing.

Consider making the constraint explicit and machine-enforced rather than prose-only, e.g. "conventional-changelog-conventionalcommits": ">=9.3.1 <10" (or an exact 9.3.1), so an automated dependency update has to widen the range deliberately instead of a ^-to-^10 bump sliding through.

🟢 **Low:** The regression this guards against is silent, and the range wording overstates the guard. `^9.3.1` is a range, not a pin (the `release.config.mjs` comment calls it "pinned"), and it is the only thing keeping the preset off v10. I confirmed the v10 failure mode against the installed generator: 10.2.1 still returns `"major"`, but `generateNotes` renders only the version heading — no sections, no error, semantic-release exits 0 and the workflow stays green, so a future major bump reproduces exactly the kind of invisible breakage this PR is fixing. Consider making the constraint explicit and machine-enforced rather than prose-only, e.g. `"conventional-changelog-conventionalcommits": ">=9.3.1 <10"` (or an exact `9.3.1`), so an automated dependency update has to widen the range deliberately instead of a `^`-to-`^10` bump sliding through.
jercik marked this conversation as resolved
forgejo-actions left a comment

Summary: No actionable defects found. 1 info note and 1 low convention note.

I reproduced the PR's claims against the exact versions the lockfile pins (@semantic-release/commit-analyzer@13.0.1, @semantic-release/release-notes-generator@14.1.1, conventional-changelog-writer@8.4.0) using the real feat!: squash message of 66c8896 (which has no BREAKING CHANGE: footer, so the ! marker is the only breaking signal):

  • default angular preset → analyzeCommits returns null (reproduces the silent skip)
  • preset: "conventionalcommits" @ 9.3.1 → "major", and generateNotes renders the full body (BREAKING CHANGES + Features sections, correct Forgejo compare/commit/issues links)
  • preset: "conventionalcommits" @ 10.2.1 → "major", but notes render as the bare ## [2.0.0](…compare…) heading only — confirms the ^9.3.1 pin rationale

Other checks that passed:

  • No unintended bump-rule regression: commit-analyzer uses its own default release rules, not the preset's whatBump, so a range of only ci:/docs: commits still returns null under the new preset (the preset's own whatBump would have said patch).
  • The knip entry is required: without ignoreDependencies, knip reports conventional-changelog-conventionalcommits as an unused devDependency; with it, the dependency is clean and the tuple plugin form is still parsed (other plugin names resolve normally).
  • Lockfile is coherent: importer specifiers match package.json exactly for both dependency sections, the new package/snapshot entries carry the registry's real integrity hash and dep set (compare-func@2.0.0, already present — no duplicate for pnpm dedupe --check), so pnpm install --frozen-lockfile will pass. The release workflow installs devDependencies, so the preset is present at release time.
  • oxfmt --check passes on all three changed text files, including package.json key sorting.

Code review by Claude Code Opus (opus)

**Summary:** No actionable defects found. 1 info note and 1 low convention note. I reproduced the PR's claims against the exact versions the lockfile pins (`@semantic-release/commit-analyzer@13.0.1`, `@semantic-release/release-notes-generator@14.1.1`, `conventional-changelog-writer@8.4.0`) using the real `feat!:` squash message of `66c8896` (which has no `BREAKING CHANGE:` footer, so the `!` marker is the only breaking signal): - default angular preset → `analyzeCommits` returns `null` (reproduces the silent skip) - `preset: "conventionalcommits"` @ 9.3.1 → `"major"`, and `generateNotes` renders the full body (BREAKING CHANGES + Features sections, correct Forgejo `compare`/`commit`/`issues` links) - `preset: "conventionalcommits"` @ 10.2.1 → `"major"`, but notes render as the bare `## [2.0.0](…compare…)` heading only — confirms the `^9.3.1` pin rationale Other checks that passed: - No unintended bump-rule regression: commit-analyzer uses its own default release rules, not the preset's `whatBump`, so a range of only `ci:`/`docs:` commits still returns `null` under the new preset (the preset's own `whatBump` would have said patch). - The knip entry is required: without `ignoreDependencies`, knip reports `conventional-changelog-conventionalcommits` as an unused devDependency; with it, the dependency is clean and the tuple plugin form is still parsed (other plugin names resolve normally). - Lockfile is coherent: importer specifiers match `package.json` exactly for both dependency sections, the new package/snapshot entries carry the registry's real integrity hash and dep set (`compare-func@2.0.0`, already present — no duplicate for `pnpm dedupe --check`), so `pnpm install --frozen-lockfile` will pass. The release workflow installs devDependencies, so the preset is present at release time. - `oxfmt --check` passes on all three changed text files, including `package.json` key sorting. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjZDVjZjYwODAxNzEzMTk2YjRhODBlNGQ4YTVjNjY1Yjg1ZmE1Yzc3Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWNvZGUtc21hcnQtMiIsInNvdXJjZVdvcmtmbG93UnVuSWQiOiIxMjMzMiIsInNvdXJjZUdlbmVyYXRvckF0dGVtcHQiOiIxIiwicmVzdWx0SWQiOiJhMzA1OGNkNy05NjUyLTRlN2ItOWFhYi1mYzNlNjJmZTFjZmYifQ== -->
@ -2,3 +2,2 @@
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
// The default angular preset cannot parse the Conventional Commits "!" marker,

🟢 Low: AGENTS.md ("Comments Explain Why, Not What") mandates one short line and explicitly forbids multi-line comment blocks; this is five lines. The content is genuinely load-bearing why, so don't drop it — the fitting split is a one-liner here plus the full rationale where it already lives (the commit message / PR body), e.g. // The angular preset cannot parse the "!" breaking marker; the preset is held at v9 for writer-8 notes compat.

🟢 **Low:** `AGENTS.md` ("Comments Explain Why, Not What") mandates one short line and explicitly forbids multi-line comment blocks; this is five lines. The content is genuinely load-bearing *why*, so don't drop it — the fitting split is a one-liner here plus the full rationale where it already lives (the commit message / PR body), e.g. `// The angular preset cannot parse the "!" breaking marker; the preset is held at v9 for writer-8 notes compat.`
jercik marked this conversation as resolved
@ -5,0 +6,4 @@
// loads conventional-changelog-writer 8, and a v10 preset renders empty
// release notes against it without erroring.
["@semantic-release/commit-analyzer", { preset: "conventionalcommits" }],
["@semantic-release/release-notes-generator", { preset: "conventionalcommits" }],

ℹ️ Info: Verified end to end at the locked versions — with this preset the real feat!: squash resolves to major and the notes render in full, while preset 10.2.1 against conventional-changelog-writer@8.4.0 renders only the version heading. Worth noting the residual failure mode this pin creates: the coupling is a silent one in both directions. If a future semantic-release bump moves release-notes-generator onto writer 9, the pinned preset 9 will start emitting empty notes with exit 0 and a green workflow — exactly the class of failure this PR is fixing. --frozen-lockfile means that can only happen through a deliberate lockfile change, so the practical guard is to eyeball the rendered notes on the first release after any semantic-release upgrade.

ℹ️ **Info:** Verified end to end at the locked versions — with this preset the real `feat!:` squash resolves to `major` and the notes render in full, while preset 10.2.1 against `conventional-changelog-writer@8.4.0` renders only the version heading. Worth noting the residual failure mode this pin creates: the coupling is a silent one in both directions. If a future `semantic-release` bump moves `release-notes-generator` onto writer 9, the pinned preset 9 will start emitting empty notes with exit 0 and a green workflow — exactly the class of failure this PR is fixing. `--frozen-lockfile` means that can only happen through a deliberate lockfile change, so the practical guard is to eyeball the rendered notes on the first release after any semantic-release upgrade.
jercik marked this conversation as resolved
style(release): condense the preset rationale to one line
Some checks failed
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-approach-3 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Has been cancelled
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Has been cancelled
PR Review / Dispatch and observe exact review writers (pull_request_target) Has been cancelled
PR Review / Prepare immutable review tools (pull_request_target) Has been cancelled
commit-msg / commitlint (pull_request) Successful in 42s
Dedupe check / dedupe-check (pull_request) Successful in 42s
Checks / quality-checks (24.15.0) (pull_request) Successful in 1m19s
Checks / quality-checks (26.5.0) (pull_request) Successful in 1m19s
cb06bf8b2d
Author
Owner

Round 4 (head cb06bf8) — disposition for the four round-3 findings:

  • 32589 (comment-length rule): implemented in cb06bf8 — the rationale is now one line, per AGENTS.md.
  • 32571 (range shape): ^9.3.1 and >=9.3.1 <10 denote the same version set, so the suggested rewrite changes nothing an updater sees; keeping ^9.3.1 matches the fleet-wide guidance. The misleading "pinned" wording is gone with the condensed comment.
  • 32567 (make the constraint machine-enforced): agreed on the substance, but the enforcement belongs fleet-wide, not as a per-repo test — j4k/align PR #169 now audits the declared range (must lie within >=9.3.1 <10, --fix rewrites it) and exempts the preset from the outdated-bump advice, so every aligned repo gets the guard this comment asks for.
  • 32588 (info, residual writer-9 flip risk): acknowledged — the align range policy plus eyeballing the first release after any semantic-release major bump is the practical guard, exactly as the review suggests.
Round 4 (head cb06bf8) — disposition for the four round-3 findings: - **32589** (comment-length rule): implemented in cb06bf8 — the rationale is now one line, per AGENTS.md. - **32571** (range shape): `^9.3.1` and `>=9.3.1 <10` denote the same version set, so the suggested rewrite changes nothing an updater sees; keeping `^9.3.1` matches the fleet-wide guidance. The misleading "pinned" wording is gone with the condensed comment. - **32567** (make the constraint machine-enforced): agreed on the substance, but the enforcement belongs fleet-wide, not as a per-repo test — j4k/align PR #169 now audits the declared range (must lie within `>=9.3.1 <10`, `--fix` rewrites it) and exempts the preset from the outdated-bump advice, so every aligned repo gets the guard this comment asks for. - **32588** (info, residual writer-9 flip risk): acknowledged — the align range policy plus eyeballing the first release after any semantic-release major bump is the practical guard, exactly as the review suggests.
forgejo-actions left a comment

Approach review: The approach looks good.

The fix correctly identifies the root cause — semantic-release's default angular preset can't parse the Conventional Commits ! breaking marker, so feat!: scored as "no release" and v2.0.0 was silently never cut. Switching both @semantic-release/commit-analyzer and @semantic-release/release-notes-generator to preset: "conventionalcommits" is the standard, idiomatic facility for this syntax — not a custom or hacky workaround.

The preset pin to ^9.3.1 (which the caret keeps within v9) is well-reasoned and verified against the exact installed versions: release-notes-generator v14 loads conventional-changelog-writer 8, and preset v10 emits the writer-9 API against it, rendering only the bare version heading with no sections and no error. The knip ignoreDependencies entry is a necessary, correct workaround — knip's semantic-release plugin extracts only plugin names, never the preset option, so the dynamically-loaded preset package is invisible to it.

I considered the obvious alternative (upgrade @semantic-release/release-notes-generator to v15+ to use conventional-changelog-writer 9 and the latest preset v10) and it is not materially better: it crosses a major version of a release-time dependency for no real benefit, where the chosen minimal fix is verified, lower-risk, and the pin is documented in-code. An override of the angular preset's headerPattern would be fragile by comparison.

Approach review by Approach Review 3 (OpenCode Wafer) (GLM-5.2)

**Approach review:** The approach looks good. The fix correctly identifies the root cause — semantic-release's default angular preset can't parse the Conventional Commits `!` breaking marker, so `feat!:` scored as "no release" and v2.0.0 was silently never cut. Switching both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` to `preset: "conventionalcommits"` is the standard, idiomatic facility for this syntax — not a custom or hacky workaround. The preset pin to `^9.3.1` (which the caret keeps within v9) is well-reasoned and verified against the exact installed versions: release-notes-generator v14 loads `conventional-changelog-writer` 8, and preset v10 emits the writer-9 API against it, rendering only the bare version heading with no sections and no error. The knip `ignoreDependencies` entry is a necessary, correct workaround — knip's semantic-release plugin extracts only plugin names, never the `preset` option, so the dynamically-loaded preset package is invisible to it. I considered the obvious alternative (upgrade `@semantic-release/release-notes-generator` to v15+ to use `conventional-changelog-writer` 9 and the latest preset v10) and it is not materially better: it crosses a major version of a release-time dependency for no real benefit, where the chosen minimal fix is verified, lower-risk, and the pin is documented in-code. An override of the angular preset's `headerPattern` would be fragile by comparison. _Approach review by Approach Review 3 (OpenCode Wafer) (GLM-5.2)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjYjA2YmY4YjJkM2RkYmEyMjVkMjM3ZjM5YjdmM2EyZTUyOTBkZGQ2Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLTMiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIzNzYiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiNmJhY2UwNDYtZjA1OS00YjczLWFhOTgtOTA2ZmQxMDE0OGUzIn0= -->
forgejo-actions left a comment

Approach review: The approach is sound and minimal; no material alternative to suggest.

I verified the two load-bearing claims against the published packages rather than taking the description at face value:

  • conventional-changelog-angular@8 (8.3.1 per the lockfile) ships only headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/ and no breakingHeaderPattern, so a feat!: subject fails header parsing outright — type is never extracted, which is exactly the no release scoring described.
  • conventional-changelog-conventionalcommits@9.3.1 adds breakingHeaderPattern and depends only on compare-func@2 (matching the lockfile snapshot), while 10.2.1 swaps that for @conventional-changelog/template@^1, i.e. the writer-9 templating API. Holding at v9 against release-notes-generator 14's bundled conventional-changelog-writer@8 is the right constraint, and ^9.3.1 bounds it correctly.

On design choices:

  • Switching to the standard conventionalcommits preset is preferable to the smaller-footprint alternative of hand-writing parserOpts.breakingHeaderPattern on commit-analyzer: that would copy a regex the upstream preset already maintains and would leave the writer on a different preset than the parser.
  • Applying the preset to both commit-analyzer and release-notes-generator (rather than just the analyzer) is the correct pairing — a major bump with angular-rendered notes would have been a half fix.
  • Declaring the preset as a direct devDependency is required, not incidental: the plugins resolve conventional-changelog-<preset> from the project root, which pnpm's strict layout will not satisfy transitively. knip.ignoreDependencies is the standard escape hatch for a dependency loaded by name from config, and the entry is scoped to the one package.
  • Since commit-analyzer applies its own release rules regardless of preset, the behavioural blast radius is confined to parsing and notes rendering — a targeted change, not a release-policy change.

One residual, non-blocking observation: both failure modes in this story are silent-green (no release cut; empty notes rendered), and nothing in the workflow would catch a recurrence. ^9.3.1 already confines that exposure to a deliberate major-bump PR, so it does not warrant a change here, but a reviewer of any future preset-10 bump should check rendered notes rather than trust a green run.

Approach review by Claude Code Opus (opus)

**Approach review:** The approach is sound and minimal; no material alternative to suggest. I verified the two load-bearing claims against the published packages rather than taking the description at face value: - `conventional-changelog-angular@8` (8.3.1 per the lockfile) ships only `headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/` and no `breakingHeaderPattern`, so a `feat!:` subject fails header parsing outright — type is never extracted, which is exactly the *no release* scoring described. - `conventional-changelog-conventionalcommits@9.3.1` adds `breakingHeaderPattern` and depends only on `compare-func@2` (matching the lockfile snapshot), while `10.2.1` swaps that for `@conventional-changelog/template@^1`, i.e. the writer-9 templating API. Holding at v9 against release-notes-generator 14's bundled `conventional-changelog-writer@8` is the right constraint, and `^9.3.1` bounds it correctly. On design choices: - Switching to the standard `conventionalcommits` preset is preferable to the smaller-footprint alternative of hand-writing `parserOpts.breakingHeaderPattern` on commit-analyzer: that would copy a regex the upstream preset already maintains and would leave the writer on a different preset than the parser. - Applying the preset to both commit-analyzer and release-notes-generator (rather than just the analyzer) is the correct pairing — a major bump with angular-rendered notes would have been a half fix. - Declaring the preset as a direct devDependency is required, not incidental: the plugins resolve `conventional-changelog-<preset>` from the project root, which pnpm's strict layout will not satisfy transitively. `knip.ignoreDependencies` is the standard escape hatch for a dependency loaded by name from config, and the entry is scoped to the one package. - Since commit-analyzer applies its own release rules regardless of preset, the behavioural blast radius is confined to parsing and notes rendering — a targeted change, not a release-policy change. One residual, non-blocking observation: both failure modes in this story are silent-green (no release cut; empty notes rendered), and nothing in the workflow would catch a recurrence. `^9.3.1` already confines that exposure to a deliberate major-bump PR, so it does not warrant a change here, but a reviewer of any future preset-10 bump should check rendered notes rather than trust a green run. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjYjA2YmY4YjJkM2RkYmEyMjVkMjM3ZjM5YjdmM2EyZTUyOTBkZGQ2Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLXNtYXJ0LTIiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIzNzYiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiZDcxZTAwZDgtMmMzMy00YjllLTg4MDktYWU5MDI5Yzk4OTZhIn0= -->
forgejo-actions left a comment

Approach review: The approach is sound and I found no materially better alternative.

I independently verified both premises against the versions the lockfile resolves:

  • conventional-changelog-angular@8.3.1 (what commit-analyzer 13.0.1 and release-notes-generator 14.1.1 both pull in) exports headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/ and, notably, no breakingHeaderPattern. A feat!: header therefore parses to type: null with zero notes, and whatBump returns no release. The silent-skip diagnosis holds.
  • The v9-vs-v10 pin is real and not cargo-culted: preset 9.3.1's createWriterOpts returns mainTemplate (the conventional-changelog-writer 8 API), while 10.2.1 returns template/preamblePartial bound functions from @conventional-changelog/template (writer 9). Neither package declares a peer range that would catch the mismatch, which is exactly why v10 renders a bare version heading instead of erroring. ^9.3.1 is the right shape of constraint here — it stays inside the compatible major while still taking patches.

Switching the preset is the ecosystem-standard fix, and it is strictly better than the narrower alternative of hand-overriding parserOpts.breakingHeaderPattern on commit-analyzer: conventional-commits-parser@6.4.0 does support that option, but it would patch only the release decision and leave the notes generator unable to group the commit, i.e. a correct version bump with unusable release notes. Adding the preset fixes both sides with one dependency. The knip ignoreDependencies entry is the correct escape for a package resolved dynamically from a preset string that knip's plugin cannot see, and devDependencies is the right place given the release workflow does a full pnpm install --frozen-lockfile.

One residual risk worth being aware of, not a blocker and not something this PR introduces: the preset-major↔writer-major coupling is enforced by nothing but the caret range and the inline comment, and its failure mode is the same silent-green class this PR is fixing (empty notes, exit 0). If a future semantic-release bump moves release-notes-generator onto writer 9, the range has to move with it. The comment in release.config.mjs records this, which is the pragmatic minimum; a renderer smoke check would be the belt-and-braces version if this bites again.

Approach review by Claude Code Opus (opus)

**Approach review:** The approach is sound and I found no materially better alternative. I independently verified both premises against the versions the lockfile resolves: - `conventional-changelog-angular@8.3.1` (what commit-analyzer 13.0.1 and release-notes-generator 14.1.1 both pull in) exports `headerPattern: /^(\w*)(?:\((.*)\))?: (.*)$/` and, notably, **no** `breakingHeaderPattern`. A `feat!:` header therefore parses to `type: null` with zero notes, and `whatBump` returns *no release*. The silent-skip diagnosis holds. - The v9-vs-v10 pin is real and not cargo-culted: preset 9.3.1's `createWriterOpts` returns `mainTemplate` (the `conventional-changelog-writer` 8 API), while 10.2.1 returns `template`/`preamblePartial` bound functions from `@conventional-changelog/template` (writer 9). Neither package declares a peer range that would catch the mismatch, which is exactly why v10 renders a bare version heading instead of erroring. `^9.3.1` is the right shape of constraint here — it stays inside the compatible major while still taking patches. Switching the preset is the ecosystem-standard fix, and it is strictly better than the narrower alternative of hand-overriding `parserOpts.breakingHeaderPattern` on commit-analyzer: `conventional-commits-parser@6.4.0` does support that option, but it would patch only the release decision and leave the notes generator unable to group the commit, i.e. a correct version bump with unusable release notes. Adding the preset fixes both sides with one dependency. The knip `ignoreDependencies` entry is the correct escape for a package resolved dynamically from a `preset` string that knip's plugin cannot see, and devDependencies is the right place given the release workflow does a full `pnpm install --frozen-lockfile`. One residual risk worth being aware of, not a blocker and not something this PR introduces: the preset-major↔writer-major coupling is enforced by nothing but the caret range and the inline comment, and its failure mode is the same silent-green class this PR is fixing (empty notes, exit 0). If a future `semantic-release` bump moves release-notes-generator onto writer 9, the range has to move with it. The comment in `release.config.mjs` records this, which is the pragmatic minimum; a renderer smoke check would be the belt-and-braces version if this bites again. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjYjA2YmY4YjJkM2RkYmEyMjVkMjM3ZjM5YjdmM2EyZTUyOTBkZGQ2Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWFwcHJvYWNoLXNtYXJ0LTEiLCJzb3VyY2VXb3JrZmxvd1J1bklkIjoiMTIzNzYiLCJzb3VyY2VHZW5lcmF0b3JBdHRlbXB0IjoiMSIsInJlc3VsdElkIjoiMjFmNDRhN2UtM2QzMC00YjMwLWJjNWUtMWZkN2FlNDQzZDhkIn0= -->
forgejo-actions left a comment

Summary: No actionable issues found. The fix is correct and I reproduced every claim in the PR body against the exact locked versions.

What I verified empirically (installed @semantic-release/commit-analyzer@13.0.1, @semantic-release/release-notes-generator@14.1.1, and the preset, then invoked analyzeCommits/generateNotes on the real feat!: …(#3) squash subject):

  • default (angular) preset → analyzeCommits returns null, notes render as the bare compare heading — reproduces the silent no-release.
  • preset: "conventionalcommits" @ 9.3.1 → "major", with a full ⚠ BREAKING CHANGES + Features body.
  • preset 10.x against the locked conventional-changelog-writer@8.4.0"major" but empty notes (93 chars, heading only), confirming the ^9.3.1 pin is load-bearing and not incidental.

Other checks:

  • Preset resolution works under pnpm: both plugins do importFrom.silent(__dirname, pkg) || importFrom(cwd, pkg), and pnpm exec semantic-release runs with cwd at the repo root where the direct devDependency is linked — so the cwd fallback resolves it even with enableGlobalVirtualStore: true.
  • The release workflow installs devDependencies (pnpm install --frozen-lockfile --ignore-scripts), so the preset is present at release time; the preset package has no lifecycle scripts.
  • The knip.json ignore is genuinely required: knip 6.27's semantic-release plugin (resolveConfig) maps only plugin[0] names and never reads preset.
  • pnpm-lock.yaml's importer block matches package.json for all 18 dependencies, and the new snapshot carries its only dependency (compare-func).
  • The preset switch changes only parsing; commit-analyzer's default release rules are preset-independent, so no other commit type changes bump behavior.

Code review by Claude Code Opus (opus)

**Summary:** No actionable issues found. The fix is correct and I reproduced every claim in the PR body against the exact locked versions. What I verified empirically (installed `@semantic-release/commit-analyzer@13.0.1`, `@semantic-release/release-notes-generator@14.1.1`, and the preset, then invoked `analyzeCommits`/`generateNotes` on the real `feat!: …(#3)` squash subject): - default (angular) preset → `analyzeCommits` returns `null`, notes render as the bare compare heading — reproduces the silent no-release. - `preset: "conventionalcommits"` @ 9.3.1 → `"major"`, with a full `⚠ BREAKING CHANGES` + `Features` body. - preset 10.x against the locked `conventional-changelog-writer@8.4.0` → `"major"` but empty notes (93 chars, heading only), confirming the `^9.3.1` pin is load-bearing and not incidental. Other checks: - Preset resolution works under pnpm: both plugins do `importFrom.silent(__dirname, pkg) || importFrom(cwd, pkg)`, and `pnpm exec semantic-release` runs with cwd at the repo root where the direct devDependency is linked — so the cwd fallback resolves it even with `enableGlobalVirtualStore: true`. - The release workflow installs devDependencies (`pnpm install --frozen-lockfile --ignore-scripts`), so the preset is present at release time; the preset package has no lifecycle scripts. - The `knip.json` ignore is genuinely required: knip 6.27's semantic-release plugin (`resolveConfig`) maps only `plugin[0]` names and never reads `preset`. - `pnpm-lock.yaml`'s importer block matches `package.json` for all 18 dependencies, and the new snapshot carries its only dependency (`compare-func`). - The preset switch changes only parsing; commit-analyzer's default release rules are preset-independent, so no other commit type changes bump behavior. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjYjA2YmY4YjJkM2RkYmEyMjVkMjM3ZjM5YjdmM2EyZTUyOTBkZGQ2Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWNvZGUtc21hcnQtMiIsInNvdXJjZVdvcmtmbG93UnVuSWQiOiIxMjM3NiIsInNvdXJjZUdlbmVyYXRvckF0dGVtcHQiOiIxIiwicmVzdWx0SWQiOiI3NmFkMDQwNC0wZjFiLTRkZjUtYTJiNi1iZjNjMDY3MGU0ZjIifQ== -->
@ -3,2 +3,2 @@
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
// The angular preset can't parse the "!" breaking marker; held to preset v9 — v10 renders empty notes against writer 8.
["@semantic-release/commit-analyzer", { preset: "conventionalcommits" }],

ℹ️ Info: Confirmed this resolves correctly under this repo's pnpm setup — a non-obvious risk given enableGlobalVirtualStore: true in pnpm-workspace.yaml. Both plugins load presets via importFrom.silent(__dirname, "conventional-changelog-conventionalcommits") || importFrom(cwd, ...); the first lookup fails from inside the virtual store, and the cwd fallback succeeds because pnpm exec semantic-release runs at the repo root where the direct devDependency is linked. No change needed.

ℹ️ **Info:** Confirmed this resolves correctly under this repo's pnpm setup — a non-obvious risk given `enableGlobalVirtualStore: true` in `pnpm-workspace.yaml`. Both plugins load presets via `importFrom.silent(__dirname, "conventional-changelog-conventionalcommits") || importFrom(cwd, ...)`; the first lookup fails from inside the virtual store, and the `cwd` fallback succeeds because `pnpm exec semantic-release` runs at the repo root where the direct devDependency is linked. No change needed.
jercik marked this conversation as resolved
forgejo-actions left a comment

Summary: No actionable issues found. The fix is correct and I reproduced both claims independently.

Verification performed against the exact locked versions (@semantic-release/commit-analyzer@13.0.1, @semantic-release/release-notes-generator@14.1.1), calling analyzeCommits/generateNotes on the real feat!: ... (#3) squash subject:

  • default (angular) preset: release type null and a bare version heading — reproduces the silent skip.
  • preset: "conventionalcommits" @ 10.2.1: major, but notes render as the compare heading only (v10 dropped to @conventional-changelog/template, which the writer-8 the notes generator resolves cannot render) — justifies the ^9 pin.
  • preset: "conventionalcommits" @ 9.3.1: major, with BREAKING CHANGES and Features sections and correct Forgejo commit/issue URLs.

Other checks:

  • Preset resolution: both plugins fall back to importFrom(cwd, "conventional-changelog-" + preset), so the direct devDependency symlink at the repo root resolves it. Re-ran the same script under a pnpm install with enableGlobalVirtualStore: true (the layout this repo uses) — resolves and renders identically.
  • Release workflow installs devDependencies (pnpm install --frozen-lockfile --ignore-scripts), so the preset is present when pnpm exec semantic-release runs.
  • Lockfile: importer specifier/version, packages and snapshots entries match byte-for-byte what pnpm 11 generates for this package, the integrity hash matches the registry, and its only dependency (compare-func@2.0.0) is already in the tree, so --frozen-lockfile and pnpm dedupe --check stay satisfied.
  • package.json devDependencies remain sorted (oxfmt sortPackageJson), and release.config.mjs passes oxfmt --check.
  • Bump semantics are unchanged: commit-analyzer applies its own default release rules, so only header parsing (! marker) changes.

Code review by Claude Code Opus (opus)

**Summary:** No actionable issues found. The fix is correct and I reproduced both claims independently. Verification performed against the exact locked versions (`@semantic-release/commit-analyzer@13.0.1`, `@semantic-release/release-notes-generator@14.1.1`), calling `analyzeCommits`/`generateNotes` on the real `feat!: ... (#3)` squash subject: - default (angular) preset: release type `null` and a bare version heading — reproduces the silent skip. - `preset: "conventionalcommits"` @ 10.2.1: `major`, but notes render as the compare heading only (v10 dropped to `@conventional-changelog/template`, which the writer-8 the notes generator resolves cannot render) — justifies the `^9` pin. - `preset: "conventionalcommits"` @ 9.3.1: `major`, with BREAKING CHANGES and Features sections and correct Forgejo commit/issue URLs. Other checks: - Preset resolution: both plugins fall back to `importFrom(cwd, "conventional-changelog-" + preset)`, so the direct devDependency symlink at the repo root resolves it. Re-ran the same script under a pnpm install with `enableGlobalVirtualStore: true` (the layout this repo uses) — resolves and renders identically. - Release workflow installs devDependencies (`pnpm install --frozen-lockfile --ignore-scripts`), so the preset is present when `pnpm exec semantic-release` runs. - Lockfile: importer specifier/version, `packages` and `snapshots` entries match byte-for-byte what pnpm 11 generates for this package, the integrity hash matches the registry, and its only dependency (`compare-func@2.0.0`) is already in the tree, so `--frozen-lockfile` and `pnpm dedupe --check` stay satisfied. - `package.json` devDependencies remain sorted (oxfmt `sortPackageJson`), and `release.config.mjs` passes `oxfmt --check`. - Bump semantics are unchanged: commit-analyzer applies its own default release rules, so only header parsing (`!` marker) changes. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL3Ryb3Brb2QtY2xpZW50IiwibnVtYmVyIjoiNSIsImhlYWRTaGEiOiJjYjA2YmY4YjJkM2RkYmEyMjVkMjM3ZjM5YjdmM2EyZTUyOTBkZGQ2Iiwic2xvdCI6ImZvcmdlam8tcmV2aWV3LWNvZGUtc21hcnQtMSIsInNvdXJjZVdvcmtmbG93UnVuSWQiOiIxMjM3NiIsInNvdXJjZUdlbmVyYXRvckF0dGVtcHQiOiIxIiwicmVzdWx0SWQiOiJjNmEwMWFmOS05ZWJiLTRhN2MtYTBhMC1hZDExNjQwMTk2Y2QifQ== -->
@ -50,6 +50,7 @@
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^25.9.2",
"@vitest/coverage-v8": "^4.1.8",
"conventional-changelog-conventionalcommits": "^9.3.1",

ℹ️ Info: ^9.3.1 correctly excludes v10, and the reason is recorded in release.config.mjs. Worth noting the constraint is two-sided and lives in a different file than the range: v9 works only because @semantic-release/release-notes-generator@14.1.1 resolves conventional-changelog-writer@8. If that plugin ever moves to writer 9, this pin becomes the wrong side of the same incompatibility — and the failure mode is silent (major bump, empty notes, exit 0, green run), exactly what this PR fixes. No change needed here; just re-render notes when either side is bumped.

ℹ️ **Info:** `^9.3.1` correctly excludes v10, and the reason is recorded in `release.config.mjs`. Worth noting the constraint is two-sided and lives in a different file than the range: v9 works only because `@semantic-release/release-notes-generator@14.1.1` resolves `conventional-changelog-writer@8`. If that plugin ever moves to writer 9, this pin becomes the wrong side of the same incompatibility — and the failure mode is silent (major bump, empty notes, exit 0, green run), exactly what this PR fixes. No change needed here; just re-render notes when either side is bumped.
jercik marked this conversation as resolved
Author
Owner

Round 4 (head cb06bf8) came back clean: both code reviewers report no actionable issues; the two new inline notes (32676, 32696) are explicitly informational — acknowledged (the writer-major coupling note is also mirrored in j4k/align's range policy). Resolving them; PR is ready to merge.

Round 4 (head cb06bf8) came back clean: both code reviewers report no actionable issues; the two new inline notes (32676, 32696) are explicitly informational — acknowledged (the writer-major coupling note is also mirrored in j4k/align's range policy). Resolving them; PR is ready to merge.
jercik scheduled this pull request to auto merge when all checks succeed 2026-08-01 14:08:36 +00:00
jercik canceled auto merging this pull request when all checks succeed 2026-08-01 14:37:35 +00:00
jercik scheduled this pull request to auto merge when all checks succeed 2026-08-01 14:37:48 +00:00
jercik canceled auto merging this pull request when all checks succeed 2026-08-01 14:37:53 +00:00
Merge remote-tracking branch 'origin/main' into ci/semantic-release-conventionalcommits
Some checks failed
commit-msg / commitlint (pull_request) Successful in 46s
Dedupe check / dedupe-check (pull_request) Successful in 57s
Checks / quality-checks (24.15.0) (pull_request) Successful in 1m25s
Checks / quality-checks (26.5.0) (pull_request) Successful in 1m25s
PR Review / Prepare immutable review tools (pull_request_target) Successful in 2m48s
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-3 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Failing after 43s
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Failing after 44s
PR Review / Dispatch and observe exact review writers (pull_request_target) Failing after 47s
f97098b1d0
jercik merged commit dbab6222eb into main 2026-08-01 14:49:15 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
j4k-oss/tropkod-client!5
No description provided.