Skip to content

pack: resolve workspace:* to the dependency's package.json version, not bun.lock's - #36279

Closed
robobun wants to merge 5 commits into
mainfrom
farm/ee4d3f6c/pack-workspace-version-from-package-json
Closed

robobun wants to merge 5 commits into
mainfrom
farm/ee4d3f6c/pack-workspace-version-from-package-json

Conversation

@robobun

@robobun robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

When packing a workspace package that depends on another workspace package via workspace:*, workspace:^ or workspace:~, bun pm pack resolved the substituted version from lockfile.workspace_versions. That map holds whatever was in the dependency's package.json at the last bun install, so bumping a workspace package's version and packing a dependent without regenerating the lockfile produced a tarball pinned to the stale version. This is the common flow for changesets/lerna/release-please style release tooling, and it contradicts the docs ("replaced by the package's package.json version").

Repro

# workspace root with packages/pkg-a@1.0.0 and packages/pkg-b depending on pkg-a via workspace:*
bun install
# release tool bumps pkg-a
echo '{"name":"pkg-a","version":"2.0.0"}' > packages/pkg-a/package.json
cd packages/pkg-b && bun pm pack
tar -xzOf pkg-b-1.0.0.tgz package/package.json
# dependencies.pkg-a is "1.0.0", should be "2.0.0"

Fix

edit_root_package_json now reads the dependency's package.json from disk (its path is already in lockfile.workspace_paths) and uses its version field. It falls back to lockfile.workspace_versions only if the file cannot be read or parsed, so behavior is unchanged when the package.json is missing.

This matches pnpm's behavior.

How did you verify your code works?

Added parameterized tests to test/cli/install/bun-pack.test.ts covering workspace:*, workspace:^, workspace:~ with a lockfile that is stale relative to the on-disk package.json, plus a fallback case where the dependency's package.json has no version field. The first three fail on main and pass with this change; the fallback case passes in both. Existing workspace-protocol tests still pass.

Fixes #20477
Fixes #20829
Fixes #28935


[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/bun-pack.test.ts
bun test v1.4.0 (0fd26804f)

test/cli/install/bun-pack.test.ts:
(pass) basic [194.99ms]
(pass) in subdirectory [381.18ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [472.51ms]
(pass) package.json names and versions > missing name [151.49ms]
(pass) package.json names and versions > missing version [138.30ms]
(pass) package.json names and versions > missing name and version [151.12ms]
(pass) package.json names and versions > empty name [146.37ms]
(pass) package.json names and versions > empty version [146.87ms]
(pass) package.json names and versions > empty name and version [147.54ms]
(pass) package.json names and versions > missing [129.74ms]
(pass) package.json names and versions > scoped name: @scoped/pkg [169.20ms]
(pass) package.json names and versions > scoped name: @ [167.66ms]
(pass) package.json names and versions > scoped name: @/ [170.14ms]
(pass) package.json names and versions > scoped name: // [159.55ms]
(pass) package.json names
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (199974e57)

test/cli/install/bun-pack.test.ts:
(pass) basic [28.95ms]
(pass) in subdirectory [9.79ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [10.83ms]
(pass) package.json names and versions > missing name [2.62ms]
(pass) package.json names and versions > missing version [2.18ms]
(pass) package.json names and versions > missing name and version [2.41ms]
(pass) package.json names and versions > empty name [2.30ms]
(pass) package.json names and versions > empty version [2.31ms]
(pass) package.json names and versions > empty name and version [2.80ms]
(pass) package.json names and versions > missing [8.71ms]
(pass) package.json names and versions > scoped name: @scoped/pkg [4.69ms]
(pass) package.json names and versions > scoped name: @ [4.72ms]
(pass) package.json names and versions > scoped name: @/ [4.05ms]
(pass) package.json names and versions > scoped name: // [3.96ms]
(pass) package.json names and versions > scoped name: @// [4.11ms]
(pass) package.json names and versions > scoped name: @/s [4.02ms]
(pass) package.json names and versions > scoped name: @s [5.22ms]
(pass) fl
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/bun-pack.test.ts
bun test v1.4.0 (0fd26804f)

test/cli/install/bun-pack.test.ts:
(pass) basic [255.76ms]
(pass) in subdirectory [479.30ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [663.72ms]
(pass) package.json names and versions > missing name [233.93ms]
(pass) package.json names and versions > missing version [188.16ms]
(pass) package.json names and versions > missing name and version [189.46ms]
(pass) package.json names and versions > empty name [173.50ms]
(pass) package.json names and versions > empty version [242.59ms]
(pass) package.json names and versions > empty name and version [169.37ms]
(pass) package.json names and versions > missing [148.32ms]
(pass) package.json names and versions > scoped name: @scoped/pkg [206.56ms]
(pass) package.json names and versions > scoped name: @ [185.03ms]
(pass) package.json names and versions > scoped name: @/ [257.80ms]
(pass) package.json names and versions > scoped name: // [180.74ms]
(pass) package.json names
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 842ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_output v
... (truncated)
diff hotspot
src/runtime/cli/pack_command.rs   | 68 +++++++++++++++++++++++------
 test/cli/install/bun-pack.test.ts | 92 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 146 insertions(+), 14 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                               reads  edits  tests
src/runtime/cli/pack_command.rs        9      8      0
test/cli/install/bun-pack.test.ts      4      3      0

…ot bun.lock's

When packing a workspace package that depends on another workspace package via
workspace:*, workspace:^ or workspace:~, bun pm pack resolved the substituted
version from lockfile.workspace_versions. That value is whatever was in the
dependency's package.json at the last bun install, so bumping a workspace
package's version and packing a dependent without regenerating the lockfile
produced a tarball pinned to the old version.

Read the dependency's package.json from disk (located via
lockfile.workspace_paths) and use its version field. Fall back to
lockfile.workspace_versions only if the file is missing or unparseable so the
existing behavior is preserved when the package.json cannot be read.

Fixes #20477
Fixes #20829
@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:20 PM PT - Jul 28th, 2026

❌ @robobun, your commit 199974e has 1 failures in Build #84560 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36279

That installs a local version of the PR into your bun-36279 executable, so you can run:

bun-36279 --bun

Comment thread src/runtime/cli/pack_command.rs Outdated
Comment thread src/runtime/cli/pack_command.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. bun pm version does not update bun.lock in workspaces #28935 - The reported symptom is that bun pm pack writes stale workspace dependency versions from the lockfile after a version bump, which is exactly what this PR fixes by reading from the on-disk package.json instead.

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #28935

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The packing command now prefers current on-disk workspace versions when rewriting workspace: dependencies, with lockfile fallback behavior. Tests cover stale lockfiles for workspace:*, workspace:^, and workspace:~.

Workspace packaging

Layer / File(s) Summary
Read workspace package versions
src/runtime/cli/pack_command.rs
Adds filesystem-based package version lookup with parsing and validation.
Rewrite workspace protocols
src/runtime/cli/pack_command.rs, test/cli/install/bun-pack.test.ts
Uses disk versions before lockfile fallbacks and tests stale-lockfile packing behavior.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: resolving workspace dependencies from the package.json version instead of the lockfile.
Description check ✅ Passed The description includes the required problem statement and verification section, with clear details on the fix and tests run.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/cli/pack_command.rs`:
- Around line 3300-3329: Update read_workspace_version_from_package_json to trim
the parsed version using the existing whitespace utilities before validating or
copying it. Reject the value when the trimmed version is empty, and return the
trimmed version so workspace versions cannot produce malformed specifications.

In `@test/cli/install/bun-pack.test.ts`:
- Around line 694-745: Add fallback coverage alongside
staleLockfileWorkspaceProtocolTests by deleting or corrupting
pkgs/pkg-a/package.json after bun install for each workspace protocol. Assert
pack still succeeds and the generated package.json dependency uses the
lockfile-cached workspace version, exercising
read_workspace_version_from_package_json’s lockfile.workspace_versions fallback
through the existing pack flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 215e882d-aa2d-4ff2-959a-3c97f89e3950

📥 Commits

Reviewing files that changed from the base of the PR and between e820221 and cf2be12.

📒 Files selected for processing (2)
  • src/runtime/cli/pack_command.rs
  • test/cli/install/bun-pack.test.ts

Comment thread src/runtime/cli/pack_command.rs Outdated
Comment thread test/cli/install/bun-pack.test.ts
Comment thread src/runtime/cli/pack_command.rs
Comment thread src/runtime/cli/pack_command.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the comment-trim from the earlier review landed in 199974e, and the fallback-path test was added in b5d2644.

What was reviewed:

  • read_workspace_version_from_package_json path resolution: workspace_paths entries are relative to FileSystem::instance().top_level_dir(), which PackageManager::init sets to the workspace root even when packing from a subdirectory — the test at pkgs/pkg-b exercises this.
  • Lifetime of the returned slice: bytes are copied into the pack_bump() thread-local arena before source drops, so nothing borrows freed memory.
  • Sibling sites: edit_root_package_json is the only workspace_versions consumer under src/runtime/cli/; bun publish reuses pack, so no parallel fix needed.
  • Fallback ordering preserves the prior error path (break 'failed_to_resolve false) when both disk and lockfile miss.
Extended reasoning...

Overview

The PR changes bun pm pack's workspace-protocol rewriting so workspace:* / workspace:^ / workspace:~ resolve to the dependency's current on-disk package.json version rather than the version cached in bun.lock at last install. A new helper read_workspace_version_from_package_json reads and parses the dependency's manifest via the path already stored in lockfile.workspace_paths; on any I/O or parse failure it returns None and the caller falls back to the pre-existing lockfile.workspace_versions lookup, and from there to the pre-existing hard error. Four tests are added alongside the existing workspace-protocol coverage in test/cli/install/bun-pack.test.ts.

Security risks

None identified. The only new input surface is reading a workspace member's package.json — a file the user already controls and that bun install already parses. Path construction uses resolve_path::join_abs_string_buf_z against the workspace root with a lockfile-recorded relative path; no user-controlled traversal is introduced beyond what the lockfile already encodes.

Level of scrutiny

Low-to-moderate. This is a narrow behavioral fix inside a single CLI command, ~30 net lines of Rust plus tests, with a graceful fallback to the previous behavior. It touches no memory-lifetime-sensitive machinery beyond the existing pack_bump() arena pattern already used throughout the file, and the helper's owned Source outlives the borrowed version slice until it is copied into the arena. It fixes three linked user-reported issues and matches pnpm's documented semantics.

Other factors

All prior review threads on this PR are resolved: my earlier comment-style nit was addressed in 199974e (the src/ comment now reads // on-disk version is authoritative; the lockfile's cached version may be stale), CodeRabbit's fallback-coverage request was addressed in b5d2644, and the comment-cop flags are resolved. CodeRabbit's remaining whitespace-trim suggestion is a degenerate-input edge case (a "version": " " field) that is not a regression relative to the old lockfile path and was marked resolved. The bug-hunting system found nothing this run. CI build #84560 is running against the head commit.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the comment nits and fallback-coverage feedback from the earlier pass are all addressed.

What was reviewed:

  • read_workspace_version_from_package_json reuses the file's existing patterns (pack_bump() arena, File::read_from + parse_package_json_utf8 as at lines 65–67/1065); the arena-copied return is lifetime-correct.
  • Checked that the on-disk read is keyed off lockfile.workspace_paths joined against FileSystem::instance().top_level_dir(), which the sub-workspace tests exercise.
  • Tests cover workspace:*/^/~ with a stale lockfile plus the no-version fallback branch; PR evidence shows they fail on main and pass here.
  • CodeRabbit's whitespace-only-version note is a pre-existing edge case (lockfile path had the same behavior) and not a regression.
Extended reasoning...

Overview

Changes bun pm pack's edit_root_package_json so that when rewriting workspace:* / workspace:^ / workspace:~ dependency specifiers, it reads the dependency's current on-disk package.json version (via lockfile.workspace_paths) instead of the potentially-stale lockfile.workspace_versions. Adds a small helper read_workspace_version_from_package_json and falls back to the old lockfile-cached value on any I/O/parse failure. Four new tests in bun-pack.test.ts cover the three protocol variants with a stale lockfile plus the fallback-when-version-missing case.

Security risks

None. The only new I/O is reading a package.json at a path the lockfile already records, from within a CLI tool the user explicitly invoked. No untrusted input crosses a boundary that wasn't already crossed by the existing lockfile-loading code.

Level of scrutiny

Moderate. This is a focused ~40-line bugfix in a CLI command (not a hot runtime path), using established file-local idioms — the same File::read_from → Source::init_path_string_owned → JSON::parse_package_json_utf8 chain and pack_bump() arena already appear at lines 65–67 and 1065 of the same file. The returned &'static [u8] is arena-copied via alloc_slice_copy, so nothing borrows the dropped source/bytes. The else-chain preserves the pre-existing break 'failed_to_resolve false behavior when neither source yields a version.

Other factors

  • All prior review threads are resolved: the comment-cop / claude comment-style nit was addressed in 199974e (comment now reads // on-disk version is authoritative; the lockfile's cached version may be stale), and CodeRabbit's request for fallback-path coverage was addressed in b5d2644 with the "falls back to lockfile version when workspace package.json has no version" test.
  • PR evidence shows the new tests fail on main (3 FAILED under ASAN, 5 under release) and pass with the fix, satisfying the "prove the test fails for the right reason" bar.
  • CodeRabbit's remaining minor note about trimming whitespace-only version strings is a pre-existing edge case shared with the lockfile path and doesn't warrant blocking; the is_empty() guard already handles the common empty-string case.
  • Fixes three linked user-reported issues (#20477, #20829, #28935) and matches pnpm's documented behavior.

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI builds #84560 and #84681 both failed on unrelated infrastructure:

  • #84560: all test failures marked [flaky] (passed alone on retry), plus a Buildkite agent creation error for windows x64 verify-baseline. None touch bun-pack.test.ts or the install path.
  • #84681: darwin x64 and linux x64-asan build-bun steps completed the Rust compile (Finished release profile) but their sibling build-cpp jobs timed out / errored, so linking never ran. This diff touches no C++.

The new bun-pack.test.ts cases pass locally under bun bd and fail under the released binary. Ready for a maintainer to re-run CI and merge.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

#38813 takes a different route to the same bug and also covers catalog:: pack stops reading bun.lock and parses the root package.json plus workspaces from disk (the same parse bun install uses) when a workspace:/catalog: spec needs resolving. If that one lands, this PR is covered.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #38813, which fixes #20477 by not reading bun.lock in pack and publish at all (workspace versions and catalogs come from the package.json files on disk), so it also covers catalog: specs, workspaces added since the last install, and checkouts without a lockfile.

Checked against a build of #38813: the three stale-lockfile tests from this PR (workspace:*, ^, ~) pass unchanged there, and #38813 has an equivalent test of its own. The one test here that does not pass is the fallback to the lockfile's version when the workspace's package.json has no version; #38813 reports an error in that case instead, since the lockfile's version is the stale value the issue is about. The issue links (#20477, #20829; #28935 is closed as a duplicate of #18906) are on #38813 now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants