fix(guard): assert the notes toolchain PAIR, and stop using require.resolve #12

Merged
ryangr0 merged 3 commits from fix/notes-toolchain-pair-guard into main 2026-09-04 18:11:51 +00:00
Owner

Replaces #11, which should not be merged (reasons at the bottom).

The guard was wrong twice over

It shipped as a floor — generator >= 15. That rejects the all-stable pair (generator 14 + preset 9), which renders fine, and says nothing about the preset, which is half of what decides whether notes render at all.

It also looked at the wrong thing. require.resolve throws ERR_PACKAGE_PATH_NOT_EXPORTED on preset v10 and generator 15 (both ESM-only) under CJS, and the try/catch around it turned that into a silent return null → "compatible". On exactly the combination the guard exists to catch. Version lookup now walks node_modules on disk.

The matrix, measured

Rendered real commits through generateNotes() against real installs of each pair:

generator preset result
14.1.1 10.2.1 100 chars, zero commit lines — silent empty
14.1.1 10.4.0 throws — requires conventional-changelog-writer@9 or newer
14.1.1 9.3.1 renders
15.0.0-beta.2 10.4.0 / 10.2.1 renders — what this package ships
15.0.0-beta.2 9.3.1 throws — headerPartial is not a function

Row one is the original bug reproduced exactly. Upstream only added the loud error by preset 10.4.0, so the months-long silent window was real, and a pair guard is the only thing that closes it.

Scope

Dependencies are unchanged. The generator 15 + preset 10 pairing is deliberate (#10) and stays. Both pairs are legal for consumers; a consumer's own manifest decides which it gets.

The error message now states the sanctioned state explicitly, and warns that npm install <pkg> cannot correct a bad pair — the beta doesn't satisfy semantic-release's own ^14, so npm nests the incompatible copy exactly where semantic-release loads it from. That misunderstanding already produced one wrong fix on the infrastructure side (e57b6f2, reverted in webgrip/infrastructure#133).

The matrix is a test now, not a comment, via an exported pure predicate. 31/31 pass.

Why #11 is being closed rather than merged

Its guard rewrite is good and is preserved here. The rest of it is not:

  1. Version downgrade 1.2.3 → 1.2.2. A tombstone from a false premise — I concluded 1.2.3 had been published with no commit on main. It has one: 184967d, an ancestor of main, from PR #10. My clone was 17 days stale and a git fetch had failed silently.
  2. engines downgrade, same origin.
  3. Re-adds { type: 'chore', release: false } and five siblings. compareReleaseTypes ranks false at -1, above major, so one matching false rule vetoes every positive rule — which is why chore(deps) bumps never released from 1.0.0 until 1.2.3 removed them. #11 restores that bug, justified by "1.2.3's rationale is unrecoverable: its release commit never landed" — the same false premise.
  4. It reverses the toolchain decision from #10, which the owner has since re-confirmed.
Replaces #11, which should not be merged (reasons at the bottom). ## The guard was wrong twice over It shipped as a floor — `generator >= 15`. That rejects the all-stable pair (generator 14 + preset 9), which renders fine, and says nothing about the preset, which is half of what decides whether notes render at all. It also looked at the wrong thing. `require.resolve` throws `ERR_PACKAGE_PATH_NOT_EXPORTED` on preset v10 and generator 15 (both ESM-only) under CJS, and the `try/catch` around it turned that into a silent `return null` → "compatible". On exactly the combination the guard exists to catch. Version lookup now walks `node_modules` on disk. ## The matrix, measured Rendered real commits through `generateNotes()` against real installs of each pair: | generator | preset | result | |---|---|---| | 14.1.1 | 10.2.1 | **100 chars, zero commit lines — silent empty** | | 14.1.1 | 10.4.0 | throws — `requires conventional-changelog-writer@9 or newer` | | 14.1.1 | 9.3.1 | renders | | **15.0.0-beta.2** | **10.4.0 / 10.2.1** | **renders — what this package ships** | | 15.0.0-beta.2 | 9.3.1 | throws — `headerPartial is not a function` | Row one is the original bug reproduced exactly. Upstream only added the loud error by preset 10.4.0, so the months-long silent window was real, and a pair guard is the only thing that closes it. ## Scope **Dependencies are unchanged.** The generator 15 + preset 10 pairing is deliberate (#10) and stays. Both pairs are legal for consumers; a consumer's own manifest decides which it gets. The error message now states the sanctioned state explicitly, and warns that `npm install <pkg>` cannot correct a bad pair — the beta doesn't satisfy semantic-release's own `^14`, so npm nests the incompatible copy exactly where semantic-release loads it from. That misunderstanding already produced one wrong fix on the infrastructure side (`e57b6f2`, reverted in webgrip/infrastructure#133). The matrix is a test now, not a comment, via an exported pure predicate. 31/31 pass. ## Why #11 is being closed rather than merged Its guard rewrite is good and is preserved here. The rest of it is not: 1. **Version downgrade 1.2.3 → 1.2.2.** A tombstone from a false premise — I concluded 1.2.3 had been published with no commit on main. It has one: `184967d`, an ancestor of main, from PR #10. My clone was 17 days stale and a `git fetch` had failed silently. 2. **`engines` downgrade**, same origin. 3. **Re-adds `{ type: 'chore', release: false }` and five siblings.** `compareReleaseTypes` ranks `false` at -1, above `major`, so one matching `false` rule vetoes every positive rule — which is why `chore(deps)` bumps never released from 1.0.0 until 1.2.3 removed them. #11 restores that bug, justified by "1.2.3's rationale is unrecoverable: its release commit never landed" — the same false premise. 4. It reverses the toolchain decision from #10, which the owner has since re-confirmed.
fix(guard): assert the notes toolchain PAIR, and stop using require.resolve
All checks were successful
Test / test (pull_request) Successful in 1m17s
be68dd8f19
The guard shipped as a floor (`generator >= 15`), which is wrong in both
directions: it rejects the all-stable pair (generator 14 + preset 9) that
renders perfectly well, and it says nothing about the preset, which is half of
what actually decides whether notes render.

Worse, it looked at the wrong thing. `require.resolve` throws
ERR_PACKAGE_PATH_NOT_EXPORTED on preset v10 and generator 15 (both ESM-only)
under CJS, and the try/catch around it turned that into a silent "compatible" —
on exactly the combination the guard exists to catch. Version lookup now walks
node_modules on disk, which has no such blind spot.

The compatibility matrix is measured, by rendering real commits through
generateNotes() against real installs:

  generator 14.1.1        + preset 10.2.1  -> 100 chars, ZERO commit lines
  generator 14.1.1        + preset 10.4.0  -> throws (writer@9 required)
  generator 14.1.1        + preset  9.3.1  -> renders
  generator 15.0.0-beta.2 + preset 10.4.0  -> renders      <- what we ship
  generator 15.0.0-beta.2 + preset  9.3.1  -> throws (headerPartial)

Row one is the original bug reproduced exactly: upstream only added the loud
error by preset 10.4.0, so the silent window was real.

Dependencies are unchanged. The generator 15 + preset 10 pair is deliberate
(#10) and the error message now says so, in the terms a future reader will need
— including that `npm install <pkg>` cannot correct a bad pair, because the
beta does not satisfy semantic-release's own ^14 and npm nests the incompatible
copy exactly where semantic-release loads it from. That misunderstanding has
already produced one wrong fix.

The matrix is now a test rather than a comment, via an exported pure predicate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every change here changes every repo's next release, so the invariants belong
next to the code: Forgejo is the sole release authority and the factory throws
without SEMANTIC_RELEASE_GITEA, release:false rules veto the deps patch rules,
and the notes toolchain is a PAIR whose mismatch produces empty notes instead
of an error. Also why the plugins hoist into the consumer's workspace, which is
what makes require.resolve the wrong tool for the guard.

CLAUDE.md is a symlink so every harness reads the same file.

VIK-828

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into fix/notes-toolchain-pair-guard
All checks were successful
Test / test (pull_request) Successful in 2m5s
65c52ac105
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
webgrip/semantic-release-config!12
No description provided.