Skip to content

fix(semantic-release-pnpm): bump the version without invoking npm - #403

Merged
prisis merged 11 commits into
mainfrom
fix/pnpm-prepare-version-without-npm
Aug 20, 2026
Merged

fix(semantic-release-pnpm): bump the version without invoking npm#403
prisis merged 11 commits into
mainfrom
fix/pnpm-prepare-version-without-npm

Conversation

@prisis

@prisis prisis commented Aug 19, 2026

Copy link
Copy Markdown
Member

Fixes #375.

Problem

On pnpm >= 10 the prepare step bumped the version by shelling out to npm pkg set version=…. npm >= 10.9 validates devEngines on every invocation, so a package declaring

{ "devEngines": { "packageManager": { "name": "pnpm", "onFail": "error" } } }

made that call exit with EBADDEVENGINES. prepare aborted, publish never ran, and nothing was released — for exactly the projects this plugin exists for.

Fix

prepare writes the manifest itself on pnpm >= 10: read with readJson, set version, write back. No package manager binary is involved in the version bump any more (verified by the test asserting execa is never called on that path).

Formatting is preserved as npm pkg set did it: indentation (writeJson({ detectIndent: true })), trailing newline, and CRLF line endings (restored via @visulima/fs/eol). readJson rather than JSON.parse also means a BOM-prefixed manifest still parses, and a malformed manifest reports which file is broken — both of which the raw-JSON.parse version I first wrote got wrong.

pnpm version stays in place on pnpm < 10 for its side-effects (npm-shrinkwrap sync, version lifecycle scripts). On pnpm >= 10 the preversion/version/postversion scripts do not run — that was already true with npm pkg set, it is now documented in the JSDoc.

Tests

__tests__/unit/prepare.test.ts, 11 tests, all passing; full package unit suite 96/96.

New coverage: no shell-out on v10+ with a devEngines-enforcing manifest (the regression guard), BOM manifest, malformed manifest naming the file, pkgRoot. Duplicate v9/v10 scenarios were merged and the repeated context literal pulled into a runPrepare helper.

Known limits of the direct write

Behaviour that differs from in-place npm pkg set, all judged acceptable and none of them new failure modes for normal setups:

  • a symlinked package.json is replaced rather than followed (the temp-file + rename write);
  • the file mode is not preserved (umask default wins);
  • a mixed-EOL manifest normalises to its majority line ending, where npm picked the first newline it saw.

Reviewed but not done

  • Dropping the pnpm version branch entirely. It would delete the fork, but pnpm < 10 users would silently lose the version lifecycle scripts. Out of scope for a bug fix.
  • Sharing one "bump a version, preserve formatting" helper across packages. multi-semantic-release and semantic-release-clean-package-json each have their own; the latter has no EOL handling and still clobbers CRLF manifests. Worth its own issue rather than a cross-package refactor here.
  • The second write for CRLF manifests. Avoiding it means adding detect-indent to this package to build the output in one pass. Each write is atomic on its own, so the extra pass buys correctness nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MqVjBDe8SkgMK7LpDpgjJ7

Summary by CodeRabbit

  • New Features

    • Improved package version updates for pnpm 10 and newer without invoking an external package manager.
    • Preserved package manifest formatting, including byte-order marks and CRLF line endings.
    • Added support for package roots and synchronization of shrinkwrap files.
  • Bug Fixes

    • Improved handling of missing shrinkwrap files and preserved development engine settings.
    • Malformed package manifests now report the affected file path.
    • pnpm versions before 10 continue using the expected versioning workflow.

On pnpm >= 10 `prepare` bumped the version by shelling out to
`npm pkg set version=…`. npm >= 10.9 validates `devEngines` on every
invocation, so a package declaring
`devEngines.packageManager: { name: "pnpm", onFail: "error" }` — the
standard way to enforce pnpm — made that call exit with EBADDEVENGINES.
`prepare` aborted, `publish` never ran, and nothing was released.

Write the manifest directly instead: read it with `readJson` (BOM-safe,
and it names the file on malformed input), set `version`, and write it
back preserving indentation, trailing newline and CRLF line endings.
`pnpm version` stays in place on pnpm < 10 for its side-effects; on
pnpm >= 10 the `preversion`/`version`/`postversion` lifecycle scripts do
not run, as was already the case with `npm pkg set`.

Known limits of the direct write, unchanged behaviour otherwise: a
symlinked `package.json` is replaced rather than followed, the file mode
is not preserved, and a mixed-EOL manifest normalises to its majority
line ending.

Fixes #375

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MqVjBDe8SkgMK7LpDpgjJ7
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@prisis, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: be20f669-d4ff-432d-81f7-df7dbc9ed0b2

📥 Commits

Reviewing files that changed from the base of the PR and between de86d49 and 371c54a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • package.json
  • packages/multi-semantic-release/__tests__/utils/recognize-format.test.ts
  • packages/multi-semantic-release/__tests__/utils/serialize-manifest.test.ts
  • packages/multi-semantic-release/package.json
  • packages/multi-semantic-release/src/types.ts
  • packages/multi-semantic-release/src/update-deps.ts
  • packages/multi-semantic-release/src/utils/recognize-format.ts
  • packages/semantic-release-clean-package-json/__tests__/index.test.ts
  • packages/semantic-release-clean-package-json/package.json
  • packages/semantic-release-clean-package-json/src/index.ts
  • packages/semantic-release-pnpm/__tests__/unit/prepare.test.ts
  • packages/semantic-release-pnpm/package.json
  • packages/semantic-release-pnpm/src/prepare.ts
  • shared/serialize-manifest.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b251c8ab-2737-4fb5-b5f1-2994bb6e26a0

📥 Commits

Reviewing files that changed from the base of the PR and between 2efd5a6 and de86d49.

📒 Files selected for processing (2)
  • packages/semantic-release-pnpm/__tests__/unit/prepare.test.ts
  • packages/semantic-release-pnpm/src/prepare.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

prepare now writes package.json directly for pnpm v10+ and preserves formatting. Older pnpm versions still use pnpm version. Tests cover manifest formatting, errors, pkgRoot, devEngines, shrinkwrap files, and command arguments.

Changes

pnpm prepare versioning

Layer / File(s) Summary
Direct manifest version updates
packages/semantic-release-pnpm/src/prepare.ts
pnpm v10+ updates package.json directly, preserves BOM and line endings, and retains shrinkwrap synchronization. Older pnpm versions use pnpm version.
Prepare compatibility and test coverage
packages/semantic-release-pnpm/__tests__/unit/prepare.test.ts
Tests use a shared helper and cover formatting, malformed manifests, pkgRoot, tarball settings, devEngines, absent shrinkwrap files, and pnpm command arguments.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to de86d

The change is narrowly scoped to version handling and includes focused regression coverage; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant SemanticRelease
  participant prepare
  participant PackageManifest
  participant pnpm

  SemanticRelease->>prepare: prepare(version, pluginConfig)
  alt pnpm v10+
    prepare->>PackageManifest: load and update package.json
    PackageManifest-->>prepare: preserve BOM and line endings
  else pnpm below v10
    prepare->>pnpm: version version --no-git-tag-version --allow-same-version
    pnpm-->>prepare: return command output
  end
  prepare-->>SemanticRelease: complete version preparation
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: version bumps no longer invoke npm for semantic-release-pnpm.
Linked Issues check ✅ Passed The implementation addresses issue #375 by directly updating package.json for pnpm 10+ without invoking npm.
Out of Scope Changes check ✅ Passed The code and tests remain within scope, covering the fix, formatting preservation, compatibility, and regression scenarios.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pnpm-prepare-version-without-npm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for following the naming conventions! 🙏

prisis and others added 2 commits August 19, 2026 22:35
…e pass

The version bump wrote `package.json` twice on CRLF manifests: once via
`writeJson`, which always emits LF, then again to restore the line
endings. Between the two writes the manifest sat on disk with the wrong
line endings, and it cost an extra read and write per release.

Serialize the manifest ourselves instead — `detect-indent` for the
indentation, the detected EOL for the line endings, the trailing newline
only if the original had one — and write the result exactly once.

Adds `detect-indent` (catalog:prod), already used by
`multi-semantic-release` for the same purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MqVjBDe8SkgMK7LpDpgjJ7
Three packages each had their own "write a manifest back preserving its
formatting" code, and two of them rewrote a CRLF manifest as LF:

- multi-semantic-release kept indent and trailing newline via
  recognizeFormat, but serialized the body with LF;
- semantic-release-clean-package-json kept only the indent, through
  writeJson({ detectIndent: true });
- semantic-release-pnpm kept indent, trailing newline and line endings.

Move the pnpm behaviour into shared/serialize-manifest.ts and use it in
all three. The folder is plain source that packem inlines into each
package at build time, so nothing new is published; the packages keep
declaring detect-indent and @visulima/fs themselves, and the root
manifest declares them too so the shared file type-checks in place.

recognizeFormat and the FileFormat/RecognizeFormatFunction types go
away with their only caller, as does the detect-newline dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MqVjBDe8SkgMK7LpDpgjJ7
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.04%. Comparing base (b5efc89) to head (371c54a).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #403      +/-   ##
==========================================
- Coverage   86.67%   86.04%   -0.63%     
==========================================
  Files          47       44       -3     
  Lines        1426     1362      -64     
  Branches      387      371      -16     
==========================================
- Hits         1236     1172      -64     
- Misses        178      179       +1     
+ Partials       12       11       -1     
Flag Coverage Δ
multi-semantic-release 87.64% <100.00%> (-0.06%) ⬇️
rc ?
semantic-release-clean-package-json 77.27% <100.00%> (+1.08%) ⬆️
semantic-release-pnpm 84.30% <100.00%> (-0.58%) ⬇️
Files with missing lines Coverage Δ
packages/multi-semantic-release/src/update-deps.ts 90.38% <100.00%> (-0.05%) ⬇️
...s/semantic-release-clean-package-json/src/index.ts 97.36% <100.00%> (+0.22%) ⬆️
packages/semantic-release-pnpm/src/prepare.ts 100.00% <100.00%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

prisis added 3 commits August 20, 2026 09:31
…izer

refactor: serialize manifests through one shared helper
refactor(semantic-release-pnpm): write the bumped manifest in a single pass
@pkg-pr-new

pkg-pr-new Bot commented Aug 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@anolilab/multi-semantic-release

npm i https://pkg.pr.new/@anolilab/multi-semantic-release@403

@anolilab/rc

npm i https://pkg.pr.new/@anolilab/rc@403

@anolilab/semantic-release-clean-package-json

npm i https://pkg.pr.new/@anolilab/semantic-release-clean-package-json@403

@anolilab/semantic-release-pnpm

npm i https://pkg.pr.new/@anolilab/semantic-release-pnpm@403

@anolilab/semantic-release-preset

npm i https://pkg.pr.new/@anolilab/semantic-release-preset@403

commit: 371c54a

@prisis
prisis merged commit 0bb6ea8 into main Aug 20, 2026
8 of 9 checks passed
@prisis
prisis deleted the fix/pnpm-prepare-version-without-npm branch August 20, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prepare shells out to npm pkg set on pnpm ≥ 10 → fails with EBADDEVENGINES when the project enforces devEngines.packageManager: pnpm

1 participant