fix(npm): tolerate workspace:* deps in version bump and bun.lock patching#805
Merged
fix(npm): tolerate workspace:* deps in version bump and bun.lock patching#805
Conversation
…hing (#804) `npm version --workspaces --include-workspace-root` successfully writes the new version to every workspace package.json but then exits non-zero with `EUNSUPPORTEDPROTOCOL` when any package declares a `workspace:*` dependency (npm/cli#8845). Craft treated the non-zero exit as failure and fell back to per-package bumping, which hit the same validator and failed the release. Separately, `bun pm pack` rewrites `workspace:*` specifiers to concrete versions at pack time by reading them from bun.lock — not from package.json. Neither npm nor bun updates those lockfile entries automatically, so published tarballs shipped with stale dependency versions and failed to install. This change: - After `npm version --workspaces` fails, post-checks every package.json on disk. If they all have the target version, we warn (citing npm/cli#8845) and proceed. Otherwise we fall through to individual bumping, which now applies the same per-package post-check before rethrowing. - After a successful bump, patches `bun.lock` workspace `version` entries in-place using the workspace's path-relative key and a non-greedy regex that leaves nested dependency pins untouched. No-op when bun.lock is absent. Uses safeFs.writeFileSync for dry-run safety. Dry-run is handled by short-circuiting the failure post-check (spawnProcess is a no-op in dry-run so files wouldn't have been written by npm anyway) and by safeFs.writeFileSync already respecting it.
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2af7e84. Configure here.
…kspaces
Two correctness fixes from self-review:
1. `patchBunLock` regex used `[\s\S]*?` which walked across the closing `}`
of a versionless workspace and silently rewrote the NEXT workspace's
version line. For example, with `"packages/empty": { "name": "..." }`
(no version) followed by `"packages/other": { ..., "version": "0.0.1" }`,
a bump targeting `packages/empty` would corrupt `packages/other`'s
version. Fixed by using `[^{}]*?` which stops at the workspace block's
closing brace OR the opener of a nested object (e.g. "dependencies").
This also removes the implicit assumption that the top-level `version`
line precedes nested objects inside a workspace block.
2. `allWorkspacePackageJsonsMatch` skipped private workspace packages on
the grounds that "npm version skips them" — but npm's `--workspaces`
flag bumps all workspaces regardless of `private`. Skipping them made
the post-check too permissive: if npm partially failed to bump a
private workspace, we'd wrongly report success. Now we verify every
workspace package.
Adds three regression tests:
- `bun.lock patching does NOT cross workspace boundaries when a workspace
lacks a version field` (catches regex bug)
- `bun.lock patching disambiguates path prefixes (packages/foo vs
packages/foo-bar)` (locks in the implicit guarantee from trailing `"`)
- `post-check verifies private workspace packages too (npm bumps them)`
(catches the private-skip bug)
Each regression test was verified to fail when the corresponding fix is
reverted.
…om utils Two bot-flagged issues addressed: 1. `patchBunLock` was called unconditionally in `bumpVersion`, which meant non-workspace bun projects with a bun.lock would get a spurious "lockfile out of sync" warning (workspaces.packages is empty → no entries match → warning fires). Move the call inside the `if (isWorkspace)` block so it only runs for monorepos. 2. The inline `escapeRegex` helper was a duplicate of the already-exported `escapeRegex` in `src/utils/filters.ts`. Import it from there instead.
7 tasks
BYK
added a commit
that referenced
this pull request
Apr 23, 2026
CHANGELOG.md is auto-generated from PR descriptions by the release tooling (see the #793, #807, #805 entries in CHANGELOG.md for examples where release-note titles contain `_*` sequences that prettier wants escaped to `\*`). Every time a release cuts new entries matching those patterns the format-check starts failing on every open PR against master until someone runs a manual fixup. Exclude the file from prettier so the release tooling is the sole authority on its contents.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes #804.
Context
Two bugs in
NpmTarget.bumpVersion(and the publish path) when a monorepo uses"workspace:*"dependency specifiers:Issue 1:
npm version --workspacesfalse-negative failurenpm version <NEW> --no-git-tag-version --allow-same-version --workspaces --include-workspace-rootsuccessfully bumps theversionfield in every workspacepackage.json, then fails withEUNSUPPORTEDPROTOCOLwhen npm's URL validator encounters"workspace:*"deps. Craft saw the non-zero exit, fell back to per-packagenpm version, hit the same error in each subdir, and failed the whole release.Upstream: npm/cli#8845 — known, unresolved.
workspace:is documented as supported but npm 11.x'sversioncommand validator still rejects it.Issue 2: stale
bun.lockworkspace versionsbun pm packrewritesworkspace:*specifiers to concrete versions at pack time, reading them frombun.lock, not frompackage.json. Neithernpm versionnorbun installupdates those lockfile entries, so published tarballs shipped with staledependenciespointing at old workspace versions that were never published. Install failed withETARGET.Changes
src/targets/npm.ts--workspacesfailure: ifnpm version --workspacesexits non-zero, read every relevantpackage.json(root + every workspace package) and checkversion === newVersion. All match → warn (citing [DOCS] workspace: protocol documented but throws EUNSUPPORTEDPROTOCOL in npm 11.6.4 npm/cli#8845) and proceed. Mismatch → fall back to individual bumping. Private workspaces are verified too, sincenpm version --workspacesbumps them.npm versionfailures now check whether the file was bumped anyway. Match → warn and continue; mismatch → rethrow. This keeps genuine failures (bad version strings, perm errors) loud while no longer failing onworkspace:*deps.patchBunLock(rootDir, packages, newVersion): after successful bumps, find each workspace's path-relative key inbun.lockand rewrite the first"version": "..."line inside its block. The regex uses[^{}]*?(not[\s\S]*?) so it stops at either the workspace block's closing}or the opener of a nested object — this prevents corrupting sibling workspaces when a workspace lacks aversionfield, and leaves nested dependency version pins untouched regardless of field ordering. Writes viasafeFs.writeFileSyncfor dry-run safety. No-op ifbun.lockis absent. Handles Windows path separators by normalizing to/.Dry-run handling: the failure post-check short-circuits under
isDryRun()becausespawnProcessis already a no-op in dry-run (files wouldn't have been written by npm), andsafeFs.writeFileSyncnatively respects dry-run.src/__tests__/versionBump.test.ts— 10 new tests underNpmTarget.bumpVersion > workspace:* handling:--workspacesbun.lockworkspace versions after a successful bumpbun.lockis absentpackages/foovspackages/foo-bar)Each regression test was verified to fail when the corresponding fix is reverted.
Backwards compatibility
Fully backwards compatible:
workspace:*deps: npm version succeeds as before, post-check isn't invoked.bun.lock:patchBunLockis a no-op.Verification
pnpm lint— 0 errors (7 pre-existing warnings, unchanged).pnpm vitest run src/__tests__/versionBump.test.ts— all 42 tests pass (32 existing + 10 new).pnpm build— clean.pnpm test— 994 pass, 7 fail (pre-existingprepare-dry-run.e2e.test.tsfailures in dumb terminals withoutEDITOR, documented environment issue).References