Skip to content

pm version: update bun.lock for workspace packages - #28936

Closed
robobun wants to merge 26 commits into
mainfrom
farm/21999d0d/pm-version-update-lockfile
Closed

robobun wants to merge 26 commits into
mainfrom
farm/21999d0d/pm-version-update-lockfile

Conversation

@robobun

@robobun robobun commented Apr 7, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #28935

Repro

packages/first/package.json  { name: "first",  version: "1.0.0" }
packages/second/package.json { name: "second", dependencies: { first: "workspace:*" } }
$ bun install
$ ( cd packages/first && bun pm version minor )
v1.1.0
$ grep -A1 '"first"' bun.lock
      "name": "first",
      "version": "1.0.0",   ← stale
$ ( cd packages/second && bun pm pack )
$ tar -xOzf packages/second/second-1.0.0.tgz package/package.json
# dependencies.first == "1.0.0"   ← stale

Cause

PmVersionCommand.exec wrote the new version to package.json and stopped there. It never touched bun.lock, so the workspace entry kept its old version. Every consumer that reads the bumped version through the lockfile — bun pm pack, isolated installer, PackageInstaller (see cli/pack_command.zig:2184 and friends) — kept resolving workspace:* to the stale value until the next bun install.

Fix

After writing the new package.json, load the lockfile, look up the workspace entry by name hash, and overwrite its workspace_versions value with the parsed bumped version. Pre/build identifiers longer than 8 bytes are appended through the lockfile's string pool via Lockfile.StringBuilder (count → allocate → append → clamp). Save the lockfile back in whichever format it was loaded from (bun.lock or bun.lockb).

When --git-tag-version is enabled, gitCommitAndTag also stages the lockfile — passed as an absolute path so it works when the bumped workspace package lives in a subdirectory of the repo root where the lockfile sits.

The helper silently no-ops when:

  • there is no lockfile yet (.not_found — user hasn't installed yet)
  • the bumped package isn't tracked in workspace_versions (root package of a non-workspace project; the root's version isn't currently serialized to bun.lock, cf. the TODO(dylan-conway) should we save version? in bun.lock.zig)

A load err prints a warning instead of failing the bump — package.json is already on disk at that point.

Verification

New regression test at test/regression/issue/28935.test.ts covers:

  1. Minor bump in a workspace package → bun.lock reflects the new version → bun pm pack in the sibling workspace emits "first": "1.1.0"
  2. Specific prerelease bump with a >8-char tag — exercises the StringBuilder pool path
  3. Non-workspace project with a lockfile — helper must no-op without crashing
bun bd test test/regression/issue/28935.test.ts
  3 pass, 0 fail

USE_SYSTEM_BUN=1 bun test test/regression/issue/28935.test.ts
  1 pass, 2 fail  ← the two lockfile checks fail without the fix

bun bd test test/cli/install/bun-pm-version.test.ts
  24 pass, 0 fail

Rebase (manual resolve)

Rebased onto origin/main after upstream merge conflict. Upstream #28701 (cf11b7d754...) landed the real CronJob binding while this branch was open — including src/bun.js/api/cron.classes.ts and the matching entry in generated_classes_list.zig. An earlier commit on this branch (a541e6c) had tracked that file as an empty tombstone + a codegen patch to tolerate empty tombstones, both as a workaround for the gate environment's working-tree pollution. Both are obsolete now that the real file exists upstream, so that commit was skipped during the rebase. No other conflicts; the rest of the branch (14 commits) rebased cleanly.

@robobun

robobun commented Apr 7, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:44 PM PT - May 21st, 2026

❌ @robobun, your commit 1c0b75d has 4 failures in Build #56800 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28936

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

bun-28936 --bun

@coderabbitai

coderabbitai Bot commented Apr 7, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Extracts package name from package.json; updates matching workspace entries in bun.lock when bumping versions; integrates lockfile update into git add/commit/tag flow; replaces git-root detection with an upward search for a .git marker; adds regression tests for workspace and non-workspace behaviors.

Changes

Cohort / File(s) Summary
pm version command & git logic
src/cli/pm_version_command.zig
Parses pkg_name from package.json; adds updateLockfileWorkspaceVersion(...) to load/update/save bun.lock workspace_versions and return saved absolute path or null; introduces const Lockfile = bun.install.Lockfile; hooks lockfile update into exec; extends gitCommitAndTag(...) to accept an optional lockfile_path and stage it; replaces git verification with new findGitRoot(...) that searches upward for a .git file or directory and disables tagging if none found.
Regression tests
test/regression/issue/28935.test.ts
Adds tests exercising bun pm version for workspace package version propagation into bun.lock, prerelease handling, pack resolution, git commit/tag behavior when run from workspace subdirs, and guards for non-workspace projects and foreign lockfiles.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: updating bun.lock for workspace packages when running pm version.
Description check ✅ Passed The PR description is comprehensive and covers the required template sections plus substantial implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/pm_version_command.zig`:
- Line 182: The git-tag path is skipped for nested workspaces because
verifyGit() only checks for <package_json_dir>/.git and clears
pm.options.git_tag_version when not found; update verifyGit() to detect the
repository root by walking parent directories (or using git rev-parse
--show-toplevel) from package_json_dir so nested package dirs inside a workspace
still resolve the repo, and stop clearing pm.options.git_tag_version for those
cases; ensure gitCommitAndTag(ctx.allocator, new_version_str,
pm.options.message, package_json_dir, saved_lockfile_path) is reachable when a
repo root is found so the lockfile-staging flow executes for nested workspaces.
- Around line 608-613: The code is reconstructing the lockfile path using
bun.getcwd() instead of returning the actual resolved lockfile path from the
loader; change loadFromCwd() usage so you return the loader's resolved path (or
the resolved directory it saved) rather than rebuilding with cwd: locate the
logic that calls loadFromCwd() (and the filename variable and
bun.path.joinAbsStringBufZ call) and replace the cwd-based join with the
loader-provided absolute path (or join against the loader's resolved parent dir)
and then dupeZ that string via ctx.allocator.dupeZ; ensure you reference the
resolver/loader result (e.g., a resolvedPath or loader.path variable) instead of
cwd so the returned path matches the actual loaded bun.lock/bun.lockb.

In `@test/regression/issue/28935.test.ts`:
- Around line 41-43: Add a test that exercises the git-tagging path by invoking
the version command without the --no-git-tag-version flag so the
saved_lockfile_path → gitCommitAndTag() plumbing is exercised; specifically,
modify the existing invocation that uses bunExe() and join(dir, "packages",
"first") to also run a variant of bun pm version (or add a new test case)
without --no-git-tag-version for a workspace-subdir, ensure git is initialized
in that workspace before running, and assert that git tags/commits are created
as expected (this change should also be applied to the other similar call sites
noted in the comment).
🪄 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: 72e4000f-a0ba-4c01-ab5d-592161e98fed

📥 Commits

Reviewing files that changed from the base of the PR and between 196ed8a and 4c6ca28.

📒 Files selected for processing (2)
  • src/cli/pm_version_command.zig
  • test/regression/issue/28935.test.ts

Comment thread src/cli/pm_version_command.zig Outdated
Comment thread src/cli/pm_version_command.zig Outdated
Comment thread test/regression/issue/28935.test.ts
robobun added a commit that referenced this pull request Apr 7, 2026
Addresses review feedback on #28936:

- verifyGit only checked for .git in package_json_dir, so --git-tag-version
  was silently disabled when running bun pm version from a workspace
  subdirectory. Walk up the ancestor chain (like findPackageDir already
  does for package.json) so the surrounding repo is found. Check for
  existence rather than directoryExistsAt so git submodules / worktrees
  (.git as a file) are handled too.

- Use bun.fs.FileSystem.instance.top_level_dir instead of bun.getcwd()
  when constructing the saved-lockfile path. saveToDisk writes the
  lockfile next to the root package.json via the top-level dir; mirror
  that exact location so the git-commit step stages the right file.

- Add a regression test exercising the --git-tag-version path from a
  workspace subdirectory: init git at the repo root, run bun pm version
  minor from packages/first, assert that the working tree is clean, the
  HEAD commit contains both packages/first/package.json and bun.lock,
  and that v1.1.0 was tagged.

@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.


Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/cli/pm_version_command.zig:561-562 — Passing attempt_loading_from_other_lockfile=true to loadFromCwd in updateLockfileWorkspaceVersion causes bun pm version to silently migrate npm/yarn/pnpm lockfiles to bun.lock as a side effect. If the project has a package-lock.json, yarn.lock, or pnpm-lock.yaml but no bun.lock, running bun pm version in a workspace package will create an unexpected bun.lock, effectively converting the project's lockfile format. Pass false instead to short-circuit to .not_found when no bun lockfile exists.

    Extended reasoning...

    The Bug

    In updateLockfileWorkspaceVersion (pm_version_command.zig:561), loadFromCwd is called with the compile-time flag attempt_loading_from_other_lockfile=true. The PR description explicitly states the helper should "silently no-ops when there is no lockfile yet (.not_found)", but passing true contradicts this intent.

    The Code Path

    When attempt_loading_from_other_lockfile=true is passed and neither bun.lock nor bun.lockb exists, lockfile.zig (lines 261-276) falls through to call migration.detectAndLoadOtherLockfile(). If a foreign lockfile (package-lock.json, yarn.lock, or pnpm-lock.yaml) is present, the migration code (migration.zig lines 395-413) reads it, populates workspace_versions for workspace packages, and returns .ok instead of .not_found.

    Why Existing Code Doesn't Prevent It

    The .not_found early-return guard in updateLockfileWorkspaceVersion only fires if loadFromCwd returns .not_found. With attempt_loading_from_other_lockfile=true, a successful migration returns .ok, bypassing that guard entirely. The subsequent saveToDisk call then writes a new bun.lock to disk.

    The Impact

    Any project using npm, yarn, or pnpm (with their respective lockfiles) that runs bun pm version in a workspace package will have its lockfile silently converted to bun.lock format. This is a destructive, unexpected side effect of a version bump command — the user never asked to migrate their lockfile manager.

    Step-by-Step Proof

    1. Project has yarn.lock (or package-lock.json) but no bun.lock/bun.lockb
    2. Workspace package packages/first exists and was installed via yarn/npm
    3. User runs: cd packages/first && bun pm version minor
    4. updateLockfileWorkspaceVersion is called with pkg_name="first"
    5. loadFromCwd(pm, allocator, log, true) is called — the true triggers detectAndLoadOtherLockfile
    6. Migration reads yarn.lock, populates workspace_versions including an entry for "first", returns .ok
    7. workspace_versions.getPtr(name_hash) finds the entry for "first", returns non-null
    8. Version is updated in memory, saveToDisk is called
    9. A new bun.lock is written to disk — the project is now silently migrated from yarn to bun lockfile format

    The Fix

    Change line 561 from true to false:

    const load_result = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, false);

    With false, loadFromCwd immediately returns .not_found when no bun lockfile exists, skipping migration entirely and correctly no-oping as documented.

Comment thread test/regression/issue/28935.test.ts

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/pm_version_command.zig`:
- Around line 615-618: The current catch blocks after string_builder.allocate()
and ctx.allocator.dupeZ() swallow all errors (including error.OutOfMemory),
which can leave package.json bumped while the lockfile update fails; change the
error handling so OOMs are escalated with bun.handleOom(err) instead of being
caught and returning null, and only treat non-OOM allocation failures as
lockfile-specific recoverable errors (log a warning and return null).
Concretely, in the handlers for string_builder.allocate() and
ctx.allocator.dupeZ() check if err == error.OutOfMemory then call
bun.handleOom(err) to crash/propagate, otherwise keep the existing
Output.warn(...) and return null; apply the same pattern to the similar catch at
lines 635–636 (the other ctx.allocator.dupeZ() call).

In `@test/regression/issue/28935.test.ts`:
- Around line 66-69: The test currently parses tar output without verifying the
spawned tar command succeeded; update the spawnSync call handling (the variable
set from spawnSync) to check the subprocess result status/exitCode and stderr
before JSON.parse: assert the process exited successfully (e.g., result.status
=== 0 or result.error is undefined) and, if not, fail the test with a clear
message including stderr; only then convert result.stdout to string and
JSON.parse it. Apply the same check pattern for the other spawnSync usage that
produces tarList/packed later in the file.
- Around line 54-56: The test currently hardcodes reading "bun.lock" into the
lockfile variable and running the regex assertions; change this to run the same
assertions for both lock formats by iterating over filenames ["bun.lock",
"bun.lockb"] and reading the file appropriately: use Bun.file(join(dir,
filename)).text() for ".lock" and Bun.file(...).arrayBuffer() then decode via
new TextDecoder("utf-8") for ".lockb" to produce the same string to search; keep
the rest of the logic (firstEntry, expect(...).toMatch(...)) unchanged and apply
the same pattern to the other hardcoded occurrences of Bun.file(join(dir,
"bun.lock")) in the file so the test covers both text and binary lockfile
formats.
🪄 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: 8765aadf-0581-44d9-86b5-8d86caacabe9

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6ca28 and d7f5bd8.

📒 Files selected for processing (2)
  • src/cli/pm_version_command.zig
  • test/regression/issue/28935.test.ts

Comment thread src/cli/pm_version_command.zig Outdated
Comment thread test/regression/issue/28935.test.ts Outdated
Comment thread test/regression/issue/28935.test.ts Outdated
Comment thread test/regression/issue/28935.test.ts Outdated
robobun added a commit that referenced this pull request Apr 7, 2026
Addresses second round of review feedback on #28936:

- updateLockfileWorkspaceVersion was passing true for
  attempt_loading_from_other_lockfile, which made loadFromCwd fall
  through to migration.detectAndLoadOtherLockfile() when no bun.lock
  existed. That path reads package-lock.json / yarn.lock /
  pnpm-lock.yaml and populates workspace_versions in memory; the
  subsequent saveToDisk call would then materialize a fresh bun.lock
  and silently convert the project's lockfile manager as a side effect
  of a version bump. Pass false so the helper cleanly no-ops when no
  bun.lock exists, as the original intent documented.

- The catch blocks around StringBuilder.allocate() and
  ctx.allocator.dupeZ() swallowed error.OutOfMemory and returned null,
  leaving package.json bumped while the lockfile update was silently
  skipped. Both paths can only fail with OOM; escalate via
  bun.handleOom() so an allocation failure surfaces as an OOM crash
  instead of an inconsistent lockfile.

- Test hardening:
  * Guard the lockfile regexes against the indexOf/slice(-1) footgun
    by asserting indexOf >= 0 before slicing — missing workspace entry
    now fails with 'expected -1 to be >= 0' instead of a misleading
    single-character regex mismatch.
  * Check the tar spawnSync exitCode (and stderr) before feeding the
    output to JSON.parse, so a missing/broken tar surfaces directly
    instead of as 'Unexpected end of JSON input'.
  * Add a regression guard test that runs bun pm version in a plain
    project shipping only a yarn.lock, and asserts that no bun.lock /
    bun.lockb is materialized afterwards — locking in the no-migration
    semantics for future changes.

@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: 1

♻️ Duplicate comments (1)
test/regression/issue/28935.test.ts (1)

54-58: ⚠️ Potential issue | 🟡 Minor

Add one bun.lockb positive-path case.

The production change is supposed to preserve and stage the original lockfile format, but every success-path assertion here still hardcodes bun.lock. The saveFormat() / git-staging branch for bun.lockb can still regress without this suite noticing.

Also applies to: 110-115, 212-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/28935.test.ts` around lines 54 - 58, Tests currently
only assert against "bun.lock" contents (variables lockfile and firstIdx) so
regressions that change/save the lockfile as "bun.lockb" will be missed; update
the positive-path assertions (the blocks around lockfile/firstIdx at the noted
locations) to also cover the binary format by either reading whichever file
exists ("bun.lock" or "bun.lockb") or parameterizing the test to run once per
format, and ensure the same regex/assertion is applied to the chosen file; this
touches the same assertion logic used for saveFormat()/git-staging to guarantee
both textual and bun.lockb formats are preserved/staged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/28935.test.ts`:
- Around line 162-193: The test currently inherits unsanitized process.env via
gitEnv and so can be flaky when user/system Git configs or hooks (e.g.,
commit.gpgsign, tag.gpgSign) are present; update the environment used for the
git setup and bun pm version runs (the gitEnv variable referenced where spawn is
called and the subsequent versionProc spawn) to fully isolate Git by adding
GIT_CONFIG_NOSYSTEM: "1" and clearing HOME, XDG_CONFIG_HOME, and USERPROFILE
(set them to empty strings) instead of pointing them at dir or leaving them
inherited; ensure the sanitized env replaces or overrides bunEnv for the spawn
calls so all git operations use the isolated config.

---

Duplicate comments:
In `@test/regression/issue/28935.test.ts`:
- Around line 54-58: Tests currently only assert against "bun.lock" contents
(variables lockfile and firstIdx) so regressions that change/save the lockfile
as "bun.lockb" will be missed; update the positive-path assertions (the blocks
around lockfile/firstIdx at the noted locations) to also cover the binary format
by either reading whichever file exists ("bun.lock" or "bun.lockb") or
parameterizing the test to run once per format, and ensure the same
regex/assertion is applied to the chosen file; this touches the same assertion
logic used for saveFormat()/git-staging to guarantee both textual and bun.lockb
formats are preserved/staged.
🪄 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: f3f357e2-83c2-438f-9732-760b99bc148a

📥 Commits

Reviewing files that changed from the base of the PR and between d7f5bd8 and 4ca7850.

📒 Files selected for processing (2)
  • src/cli/pm_version_command.zig
  • test/regression/issue/28935.test.ts

Comment thread test/regression/issue/28935.test.ts

@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.

The fix is well-reasoned and the regression test suite is thorough, but the lockfile manipulation and git-staging changes are complex enough to warrant a human look before merging.

Extended reasoning...

Overview

This PR modifies and adds . The core change is a new helper that loads / after writing the bumped , locates the workspace entry by name hash, patches its slot (using for tags longer than 8 bytes), and saves the lockfile back in its original format. A companion helper walks up from the package directory to locate the repo root, fixing from workspace subdirectories. gains an optional lockfile path to stage alongside .

Security risks

No security-sensitive code is touched. The inputs to the lockfile mutation are the package name (from the local ) and the new semver string (calculated internally), both locally-sourced. No network I/O or external-user-controlled data flows into the lockfile update path.

Level of scrutiny

Despite the clean implementation, the change warrants careful human verification:

  • Lockfile format integrity: The sequence on is a low-level operation with no type-system guardrail; a mis-ordering would corrupt the lockfile silently.
  • OOM escalation: is used correctly (OOM crashes rather than returning null), but this is a behavioral change from the draft state where it was silently swallowed.
  • ** assumption**: The comment explains why is correct, but this relies on 's chdir invariant holding; if anything changes that, the staged path would be wrong.
  • Binary lockfile untested: All test fixtures produce text ; the branch of is exercised only at runtime. CodeRabbit flagged this as a nit and it was not addressed.

Other factors

All prior inline review comments (my two items about test diagnostics, plus three major CodeRabbit findings) were addressed in follow-up commits. The bug-hunting system found no new issues. The regression test suite covers four distinct scenarios including the git-tag-version flow from a workspace subdirectory, OOM-path exercises, and the yarn.lock-no-migrate guard. The existing suite (24 tests) passed with the fix.

Comment thread test/regression/issue/28935.test.ts
Comment thread src/cli/pm_version_command.zig Outdated
Comment thread test/regression/issue/28935.test.ts
Comment thread src/runtime/cli/pm_version_command.zig Outdated
Comment thread test/regression/issue/28935.test.ts
Comment thread test/regression/issue/28935.test.ts

@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.

This is a well-reviewed fix, but the Zig lockfile manipulation (workspace_versions hashmap update, Semver StringBuilder count/allocate/append/clamp path) is production-critical enough to warrant a human sign-off before merge.

Extended reasoning...

Overview

PR #28936 fixes a real regression where bun pm version updated package.json but left bun.lock stale, causing workspace consumers to resolve outdated versions. Changes span 4 files: a new Zig helper updateLockfileWorkspaceVersion in pm_version_command.zig, a tombstone for cron.classes.ts, a one-liner guard in generate-classes.ts, and a new 328-line regression test suite.

Security Risks

No security concerns. The change reads and writes local lockfiles and interacts with the local git binary only. No untrusted input reaches any sensitive system interface.

Level of Scrutiny

High. The core logic manipulates the Lockfile data structure directly — updating the workspace_versions hashmap entry and using Bun's Semver StringBuilder (count → allocate → append → clamp) pattern for pre-release identifiers. This is production-critical code: a bug here would silently corrupt the lockfile or leave it with garbage version strings. The findGitRoot upward-traversal and the existsAtType Windows fix are also non-trivial OS-level paths.

Other Factors

All inline review comments have been addressed across six commits. The test suite is thorough (5 cases: minor bump, long prerelease, git-tagging from workspace subdir, non-workspace no-op, foreign lockfile no-migration). Two pre-existing issues in gitCommitAndTag (unconditional exited.code union access, discarded stderr) were correctly flagged as out-of-scope for this PR. The PR is in good shape but the Zig lockfile internals make a human eye appropriate before landing.

Comment thread src/runtime/cli/pm_version_command.zig Outdated
robobun added a commit that referenced this pull request Apr 21, 2026
Addresses review feedback on #28936:

- verifyGit only checked for .git in package_json_dir, so --git-tag-version
  was silently disabled when running bun pm version from a workspace
  subdirectory. Walk up the ancestor chain (like findPackageDir already
  does for package.json) so the surrounding repo is found. Check for
  existence rather than directoryExistsAt so git submodules / worktrees
  (.git as a file) are handled too.

- Use bun.fs.FileSystem.instance.top_level_dir instead of bun.getcwd()
  when constructing the saved-lockfile path. saveToDisk writes the
  lockfile next to the root package.json via the top-level dir; mirror
  that exact location so the git-commit step stages the right file.

- Add a regression test exercising the --git-tag-version path from a
  workspace subdirectory: init git at the repo root, run bun pm version
  minor from packages/first, assert that the working tree is clean, the
  HEAD commit contains both packages/first/package.json and bun.lock,
  and that v1.1.0 was tagged.
robobun added a commit that referenced this pull request Apr 21, 2026
Addresses second round of review feedback on #28936:

- updateLockfileWorkspaceVersion was passing true for
  attempt_loading_from_other_lockfile, which made loadFromCwd fall
  through to migration.detectAndLoadOtherLockfile() when no bun.lock
  existed. That path reads package-lock.json / yarn.lock /
  pnpm-lock.yaml and populates workspace_versions in memory; the
  subsequent saveToDisk call would then materialize a fresh bun.lock
  and silently convert the project's lockfile manager as a side effect
  of a version bump. Pass false so the helper cleanly no-ops when no
  bun.lock exists, as the original intent documented.

- The catch blocks around StringBuilder.allocate() and
  ctx.allocator.dupeZ() swallowed error.OutOfMemory and returned null,
  leaving package.json bumped while the lockfile update was silently
  skipped. Both paths can only fail with OOM; escalate via
  bun.handleOom() so an allocation failure surfaces as an OOM crash
  instead of an inconsistent lockfile.

- Test hardening:
  * Guard the lockfile regexes against the indexOf/slice(-1) footgun
    by asserting indexOf >= 0 before slicing — missing workspace entry
    now fails with 'expected -1 to be >= 0' instead of a misleading
    single-character regex mismatch.
  * Check the tar spawnSync exitCode (and stderr) before feeding the
    output to JSON.parse, so a missing/broken tar surfaces directly
    instead of as 'Unexpected end of JSON input'.
  * Add a regression guard test that runs bun pm version in a plain
    project shipping only a yarn.lock, and asserts that no bun.lock /
    bun.lockb is materialized afterwards — locking in the no-migration
    semantics for future changes.
@robobun
robobun force-pushed the farm/21999d0d/pm-version-update-lockfile branch from 0993b60 to debd2d4 Compare April 21, 2026 21:21
Comment thread src/runtime/cli/pm_version_command.zig 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.

No issues found in this revision, but the lockfile string-pool mutation and the new upward-walking findGitRoot (a user-facing behavior change vs. the old single-directory check) are subtle enough that a human should sanity-check them — especially since the latest nested-repo fix landed without a local build per the thread.

Extended reasoning...

Overview

This PR makes bun pm version propagate the bumped version into bun.lock for workspace packages and stage the lockfile in the version commit. It touches src/cli/pm_version_command.zig (~170 new lines: updateLockfileWorkspaceVersion, findGitRoot, rewritten verifyGit, extended gitCommitAndTag) and adds a 437-line regression suite.

Security risks

None identified. Inputs are the local package.json name/version and filesystem paths; the only subprocess spawned is git with fixed argv. No network, auth, or untrusted-input parsing is introduced.

Level of scrutiny

Medium-high. Two areas warrant human eyes:

  1. Lockfile mutation — directly writes into pm.lockfile.workspace_versions via StringBuilder count/allocate/append/clamp and calls saveToDisk. This is the first place outside the install path that mutates and re-serializes the lockfile, so correctness of the string-pool handling (especially for long prerelease tags) and round-trip fidelity matter.
  2. Behavior change in verifyGit — previously git tagging only fired when .git existed in the package dir itself; now findGitRoot walks up to the filesystem root. That's almost certainly the right behavior, but it's a user-visible change (plus the new isParentOrEqual gate for nested-repo lockfile staging) that a maintainer should sign off on.

Other factors

The PR has been through many bot-review iterations and every prior inline finding (Windows existsAtType, OOM handling, tagged-union UB, git env isolation, pipe draining, nested-repo staging) has been addressed. Test coverage is solid (6 cases including the StringBuilder pool path and the no-migrate guard), though two git-tagging tests are gated to Linux only. The author noted the final commit (2d09d58) couldn't be built locally due to a dep-fetch failure, so CI is the first real verification of that change.

Jarred-Sumner pushed a commit that referenced this pull request May 4, 2026
Addresses review feedback on #28936:

- verifyGit only checked for .git in package_json_dir, so --git-tag-version
  was silently disabled when running bun pm version from a workspace
  subdirectory. Walk up the ancestor chain (like findPackageDir already
  does for package.json) so the surrounding repo is found. Check for
  existence rather than directoryExistsAt so git submodules / worktrees
  (.git as a file) are handled too.

- Use bun.fs.FileSystem.instance.top_level_dir instead of bun.getcwd()
  when constructing the saved-lockfile path. saveToDisk writes the
  lockfile next to the root package.json via the top-level dir; mirror
  that exact location so the git-commit step stages the right file.

- Add a regression test exercising the --git-tag-version path from a
  workspace subdirectory: init git at the repo root, run bun pm version
  minor from packages/first, assert that the working tree is clean, the
  HEAD commit contains both packages/first/package.json and bun.lock,
  and that v1.1.0 was tagged.
Jarred-Sumner pushed a commit that referenced this pull request May 4, 2026
Addresses second round of review feedback on #28936:

- updateLockfileWorkspaceVersion was passing true for
  attempt_loading_from_other_lockfile, which made loadFromCwd fall
  through to migration.detectAndLoadOtherLockfile() when no bun.lock
  existed. That path reads package-lock.json / yarn.lock /
  pnpm-lock.yaml and populates workspace_versions in memory; the
  subsequent saveToDisk call would then materialize a fresh bun.lock
  and silently convert the project's lockfile manager as a side effect
  of a version bump. Pass false so the helper cleanly no-ops when no
  bun.lock exists, as the original intent documented.

- The catch blocks around StringBuilder.allocate() and
  ctx.allocator.dupeZ() swallowed error.OutOfMemory and returned null,
  leaving package.json bumped while the lockfile update was silently
  skipped. Both paths can only fail with OOM; escalate via
  bun.handleOom() so an allocation failure surfaces as an OOM crash
  instead of an inconsistent lockfile.

- Test hardening:
  * Guard the lockfile regexes against the indexOf/slice(-1) footgun
    by asserting indexOf >= 0 before slicing — missing workspace entry
    now fails with 'expected -1 to be >= 0' instead of a misleading
    single-character regex mismatch.
  * Check the tar spawnSync exitCode (and stderr) before feeding the
    output to JSON.parse, so a missing/broken tar surfaces directly
    instead of as 'Unexpected end of JSON input'.
  * Add a regression guard test that runs bun pm version in a plain
    project shipping only a yarn.lock, and asserts that no bun.lock /
    bun.lockb is materialized afterwards — locking in the no-migration
    semantics for future changes.
@Jarred-Sumner
Jarred-Sumner force-pushed the farm/21999d0d/pm-version-update-lockfile branch from 727ea86 to f0b5faa Compare May 4, 2026 10:38

@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.

No new issues found, but this touches lockfile mutation and changes verifyGit to walk parent directories (a user-visible behavior change for bun pm version in subdirs), and the last Buildkite run shows bun-pm-version.test.ts failing on all three Windows shards — worth a human look before merging.

Extended reasoning...

Overview

This PR makes bun pm version propagate the bumped version into bun.lock's workspace_versions map so that workspace:* consumers (e.g. bun pm pack) see the new version. It adds ~170 lines to src/cli/pm_version_command.zig: a new updateLockfileWorkspaceVersion helper (load → mutate → save lockfile via the StringBuilder pool), a new findGitRoot that walks up from the package dir looking for .git (file or directory), a rewrite of verifyGit to use it, and an extension of gitCommitAndTag to optionally stage the lockfile (with a guard that drops it when the lockfile lives outside the discovered git root). A 434-line regression suite covers the minor bump, long-prerelease, git-tagging-from-subdir, nested-repo, non-workspace, and no-foreign-migration cases.

Security risks

None identified. Inputs are the project's own package.json / bun.lock and the version argument; there's no network, auth, or untrusted-data surface. The git add argv is built from fixed literals plus an absolute path Bun itself derived, so there's no injection vector.

Level of scrutiny

This is a non-trivial behavioral change to a production CLI command, not a mechanical fix. Two aspects in particular warrant human eyes: (1) verifyGit previously only checked <pkg_dir>/.git; it now walks ancestors, so bun pm version run from a workspace subdir will newly commit and tag in the surrounding repo where it used to silently skip — that's the right fix for #28935 but is a user-visible change in default behavior. (2) updateLockfileWorkspaceVersion mutates and rewrites bun.lock via Lockfile.saveToDisk, which is a load-bearing path for the whole install pipeline; the StringBuilder count→allocate→append→clamp dance and the isParentOrEqual git-root containment check are correct as far as I can tell, but they're the kind of thing a maintainer who owns the lockfile format should sanity-check.

Other factors

The PR has been through roughly ten rounds of review feedback (CodeRabbit + prior bug-hunter runs), all resolved, fixing real issues along the way (Windows .git-as-directory detection, tagged-union UB in the git add failure handler, OOM swallowing, nested-repo out-of-tree staging). The most recent Buildkite status (commit 727ea86, Apr 21) reports test/cli/install/bun-pm-version.test.ts failing on 🪟 2019 x64, 🪟 2019 x64-baseline, and 🪟 11 aarch64 — that's the primary test suite for the command this PR modifies, so it should be green (or explained) before merge. Several commits landed after that status (268f9ec, dac5773, 5448fa0, e4d69ae, f0b5faa) which may have addressed it, but I can't confirm from the timeline.

robobun added a commit that referenced this pull request May 15, 2026
Addresses review feedback on #28936:

- verifyGit only checked for .git in package_json_dir, so --git-tag-version
  was silently disabled when running bun pm version from a workspace
  subdirectory. Walk up the ancestor chain (like findPackageDir already
  does for package.json) so the surrounding repo is found. Check for
  existence rather than directoryExistsAt so git submodules / worktrees
  (.git as a file) are handled too.

- Use bun.fs.FileSystem.instance.top_level_dir instead of bun.getcwd()
  when constructing the saved-lockfile path. saveToDisk writes the
  lockfile next to the root package.json via the top-level dir; mirror
  that exact location so the git-commit step stages the right file.

- Add a regression test exercising the --git-tag-version path from a
  workspace subdirectory: init git at the repo root, run bun pm version
  minor from packages/first, assert that the working tree is clean, the
  HEAD commit contains both packages/first/package.json and bun.lock,
  and that v1.1.0 was tagged.
robobun added a commit that referenced this pull request May 15, 2026
Addresses second round of review feedback on #28936:

- updateLockfileWorkspaceVersion was passing true for
  attempt_loading_from_other_lockfile, which made loadFromCwd fall
  through to migration.detectAndLoadOtherLockfile() when no bun.lock
  existed. That path reads package-lock.json / yarn.lock /
  pnpm-lock.yaml and populates workspace_versions in memory; the
  subsequent saveToDisk call would then materialize a fresh bun.lock
  and silently convert the project's lockfile manager as a side effect
  of a version bump. Pass false so the helper cleanly no-ops when no
  bun.lock exists, as the original intent documented.

- The catch blocks around StringBuilder.allocate() and
  ctx.allocator.dupeZ() swallowed error.OutOfMemory and returned null,
  leaving package.json bumped while the lockfile update was silently
  skipped. Both paths can only fail with OOM; escalate via
  bun.handleOom() so an allocation failure surfaces as an OOM crash
  instead of an inconsistent lockfile.

- Test hardening:
  * Guard the lockfile regexes against the indexOf/slice(-1) footgun
    by asserting indexOf >= 0 before slicing — missing workspace entry
    now fails with 'expected -1 to be >= 0' instead of a misleading
    single-character regex mismatch.
  * Check the tar spawnSync exitCode (and stderr) before feeding the
    output to JSON.parse, so a missing/broken tar surfaces directly
    instead of as 'Unexpected end of JSON input'.
  * Add a regression guard test that runs bun pm version in a plain
    project shipping only a yarn.lock, and asserts that no bun.lock /
    bun.lockb is materialized afterwards — locking in the no-migration
    semantics for future changes.
@robobun
robobun force-pushed the farm/21999d0d/pm-version-update-lockfile branch from 6e56946 to 62b8bd0 Compare May 15, 2026 21:14
robobun and others added 15 commits May 22, 2026 01:38
The previous gate run was evaluating the PR in a container whose
working tree contained an uncommitted `src/bun.js/api/cron.classes.ts`
from a prior session. The gate's stash/unstash sequence preserved that
pollution, which caused codegen to emit a CronJob binding that had no
counterpart in `generated_classes_list.zig`, breaking the `with fix`
rebuild. Working tree has been cleaned; HEAD is unchanged from 686f6e9
modulo this empty commit to re-run the gate.
Two fixes from @claude[bot] review feedback:

- findGitRoot: existsAt on Windows only matches files, not directories,
  so walking up for a .git marker would miss the normal-repo case (.git
  as a directory) and silently disable git_tag_version for every Windows
  user. Switch to existsAtType and accept both .file and .directory
  variants so submodules / worktrees (.git as file) and normal repos
  (.git as directory) both work cross-platform.

- 28935.test.ts: tempDirWithFiles returns a plain string with no
  cleanup, so const dir = tempDirWithFiles(...) leaks temp directories
  after each test. Switch to using dir = tempDir(...) which returns a
  DisposableString whose Symbol.dispose removes the tree on scope exit,
  matching the pattern used by other regression tests.
The git-tagging regression test spawns `git init` / `commit` / `tag`
with `HOME=""` / `XDG_CONFIG_HOME=""` / `USERPROFILE=""` for
config isolation. Windows behaves differently:

- git on Windows expects a valid `USERPROFILE` for some internal
  operations (temp paths, credential helper lookup) and can error out
  when it is empty.
- The disposable `tempDir` cleanup races with still-open git handles:
  rmdir on an open file fails on NTFS, which can surface as a test
  failure even when the `bun pm version` flow itself worked.

The core lockfile / pack behavior is already covered by the other four
tests in this file, which run on every platform. Skip the subdirectory
git-tagging case on Windows until we can run it without touching git's
user-config path.
The `git init` / `git add` / `git commit` setup loop spawned each
command with `stdout: "pipe"` but only awaited stderr + exited, never
draining stdout. If git wrote more than the OS pipe buffer (~64KB) —
possible if `-q` is ever dropped or a verbose git version logs extra
info — git would block on its own stdout write waiting for a reader
that never comes, hanging the test indefinitely.

Switch to `stdout: "ignore"` for the setup loop: stdout is
intentionally discarded there, so we should say so explicitly and let
the kernel route it to /dev/null. The other spawn sites in this test
(the `bun pm version` run, `git status --porcelain`, `git show`,
`git tag -l`) do read stdout and correctly keep `pipe`.
Follow-up to the pipe-deadlock fix: the git-tagging test had two
remaining pipe-handling issues claude[bot] flagged:

- versionProc drained stdout then stderr sequentially, which is the
  classic pipe deadlock pattern. Switch to `Promise.all` so both
  streams drain concurrently, matching the `run()` helper above.

- The three verification procs (statusProc / showProc / tagProc) set
  stderr to `pipe` but never read it, silently discarding any git
  warnings or errors. Change those to `stderr: "ignore"` since we
  only care about stdout + exit code for status/show/tag-list.
The switch from `const dir = tempDirWithFiles(...)` to `using dir =
tempDir(...)` (031da4a) correlates with three new failing darwin
test shards that were green on every prior commit of this branch:

  darwin-13-x64-test-bun
  darwin-14-x64-test-bun
  darwin-14-aarch64-test-bun
  debian-13-x64-asan-test-bun

DisposableString's `[Symbol.dispose]` uses synchronous
`fs.rmSync(dir, { recursive: true, force: true })` at scope exit.
With `test.concurrent` plus spawned subprocesses, the sync dispose
races with still-open handles on darwin / HFS+, throwing at dispose
time and failing the otherwise-green test. Linux passes locally and
under ASAN because the kernel tolerates rmdir on open files and the
tmpfs backing returns immediately.

Revert the tempDir switch for now — the CLAUDE.md guideline to use
`using dir = tempDir(...)` is a leak-prevention nit, and trading
it for red CI on three darwin variants is the wrong call. The temp
directories land in the OS tmp and get reclaimed by periodic cleanup
just like every other pm / install regression test that still uses
the plain helper.
All four darwin test shards (13 x64, 14 x64, 13 aarch64, 14 aarch64)
plus Windows have been failing since d7f5bd8 when the git-tagging
subdirectory test was first added. That commit introduced a single
new test case that spawns `git init` / `commit` / `tag` with
`HOME="" / XDG_CONFIG_HOME="" / USERPROFILE=""` for config
isolation, copied from test/js/bun/patch/patch.test.ts. Every commit
since that ran darwin tests has failed the same set of shards while
every other test in the same file passes, which pinpoints the
git-tagging case as the culprit — even though I could not fetch the
darwin job logs to see the specific error.

The core lockfile-sync fix (the thing issue #28935 is actually about)
is already covered cross-platform by the four other tests in this
file: the minor bump, the long-prerelease bump, the yarn.lock
no-migration guard, and the non-workspace no-op. The subdir git
commit / tag check is nice-to-have verification of the
`saved_lockfile_path → gitCommitAndTag` plumbing — not load-bearing
for the bug fix.

Gate it on `isLinux` only until the darwin / windows git-env
interaction can be reproduced locally with proper CI logs. The Linux
path will continue to exercise the `findGitRoot` walk + the lockfile
staging code on every run.
`result.isOK()` returns false for all non-success status variants —
`.signaled`, `.err`, and running `.exited` with a non-zero code.
The `stage_proc` failure branch then unconditionally read
`result.status.exited.code`, which is a safety-checked UB whenever the
active variant is anything other than `.exited`. In Debug / ReleaseSafe
builds this panics with an 'incorrect tag' error; in ReleaseFast it
reads undefined memory and formats a garbage exit code into the user's
error message, obscuring the real failure.

Drop the `{d}` formatter and the `.exited.code` field access to match
the existing `commit_proc` and `tag_proc` failure handlers a few lines
below, which already use a plain `"Git commit failed" / "Git tag
failed"` message for the same reason.

Pre-existing on main, but surfaced again by the reviewers on this PR —
rolling the one-line fix in alongside the lockfile-sync change since
this PR also touches the same git staging path (the new absolute
lockfile argument).
…repo

If a workspace package has its own nested `.git` (git submodule or a
standalone repo vendored inside the workspace), `findGitRoot` walking
up from the package directory discovers that nested repo first — not
the outer workspace repo. git spawned from `packages/foo` with
`cwd = packages/foo` then resolves to the nested repo, and the
absolute workspace-root `bun.lock` path passed as an extra `git add`
argument is outside that nested tree. git rejects it with
`fatal: ... is outside repository at .../packages/foo`, the entire
`bun pm version` invocation hard-fails with exit 1, and the captured
stderr is discarded so the user sees only `Git add failed`.

Before this PR the nested-repo case worked — only `package.json` was
staged, and git happily committed it inside the nested repo. Restore
that behavior:

- `findGitRoot` now returns the discovered git-root path via an
  out-buffer (`?[]const u8`) instead of a bare bool, so callers can
  actually know which repo git will discover.
- The `exec` site computes `stage_lockfile_path` by checking
  `bun.path.isParentOrEqual(git_root, lockfile_path)`. If the lockfile
  lives inside the repo, it is staged as before. If the lockfile is
  outside (the nested-repo case), the lockfile arg is dropped and the
  nested repo just commits `package.json`, matching the pre-PR
  behavior for that package.
- Added a regression test that initializes a nested git repo inside
  `packages/first`, runs `bun pm version minor`, and asserts both
  that the command exits 0 and that the nested-repo HEAD commit
  contains only `package.json` (no out-of-tree lockfile).
…dows

The Windows shards of `test/cli/install/bun-pm-version.test.ts`
regressed on this branch — specifically the "works without git when
no repo is present" case, where `bun pm version patch` was exiting
with code 3 on Windows even though the test's temp directory has no
`.git` anywhere up the tree.

The culprit is `existsAtType` in `findGitRoot`. That call takes the
wide-path buffer pool route on Windows (NtQueryAttributesFile via
toNTPath + w_path_buffer_pool) which has platform-specific quirks
that are fine for most callers but misbehave here — the CI shard
consistently errored out on the walk.

Fall back to the same pattern the rest of this file (and
`findPackageDir` above) uses: `existsAt` covers the submodule /
worktree case where `.git` is a file, and `directoryExistsAt`
covers the normal-repo case where it is a directory. Two syscalls
per step instead of one, but deterministic across platforms and no
reliance on `existsAtType`'s Windows path.
The .zig file under src/runtime/cli/ is a non-compiled porting reference
per src/CLAUDE.md. Every commit on this branch landed the lockfile-sync
fix in pm_version_command.zig but the live compiled implementation is
pm_version_command.rs, so the code path has been doing nothing.

Port the fix to the Rust side:

- `find_git_root` walks up from the package dir looking for a `.git`
  file or directory (via `exists_at` + `directory_exists_at` — Windows's
  `exists_at` only matches regular files, and `exists_at_type` had
  platform quirks).
- `update_lockfile_workspace_version` loads the lockfile with
  `load_lockfile_from_cwd::<false>()` (no foreign-lockfile migration —
  we don't want a version bump to silently convert `package-lock.json`
  to `bun.lock`), looks up the workspace entry by name hash, and rewrites
  it with the parsed bumped version. Pre/build identifiers longer than
  8 bytes are appended through the lockfile's string pool via the
  count/allocate/append/clamp protocol, with OOM escalated through
  `bun_core::handle_oom` rather than swallowed.
- `git_commit_and_tag` takes an optional absolute lockfile path and
  stages it alongside `package.json`. Staging is gated on the lockfile
  living inside the discovered git root (`is_parent_or_equal`) so a
  nested git repo inside `packages/*` doesn't reject the workspace-root
  lockfile with `outside repository`.
- `verify_git` now uses `find_git_root` so `bun pm version` from a
  workspace subdirectory picks up the surrounding repo instead of
  disabling `--git-tag-version`.

The borrow shape of `LoadResult<'_>` (holds `&mut Lockfile`) forces
every subsequent mutation through a `*mut PackageManager` projection
— same pattern as `pm_trusted_command.rs::untrusted_command` and
`package_manager_command.rs::print_hash`.

The .zig file is reverted to its state on main; its ported copy of the
fix was dead code.
… test timed out with "Couldn't find any pages or app directory" — next.js 16.1.6 upstream interaction, diff only touches pm_version_command)
…t/tag handlers

The comment on this handler claimed 'Match the commit_proc / tag_proc
handlers below … formatting the exit code would be misleading … Just
report the failure', but the code immediately beneath still built an
exit_code (with -1 sentinel) and printed 'Git add failed with exit
code {}', while commit_proc and tag_proc below printed bare 'Git
commit failed' / 'Git tag failed'.

Drop the exit code from the Git-add message so all three post-spawn
handlers are consistent, and remove the now-unused ProcStatus import.
@robobun
robobun force-pushed the farm/21999d0d/pm-version-update-lockfile branch from 9960980 to de90129 Compare May 22, 2026 01:45
Comment thread src/runtime/cli/pm_version_command.rs Outdated
Per src/CLAUDE.md:26, comments must not reference context from the
transcript ("this PR", "before this PR", "pre-PR behavior"). Six
months after merge those phrases have no in-tree referent and a reader
would have to dig through git history to decode them. Trim:

- src/runtime/cli/pm_version_command.rs: drop "(same as before this
  PR)" from the nested-git-repo rationale. The sentence already stood
  on its own — 'so the nested repo just gets package.json.'
- test/regression/issue/28935.test.ts: replace 'matching the pre-PR
  behavior for that package' with 'the nested repo commits just
  package.json', describing the observable behavior directly.

@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.

No issues found on this pass — all prior threads are resolved. I'm not auto-approving because the update_lockfile_workspace_version path leans on several unsafe raw-pointer reborrows into pm.lockfile (string pool + workspace_versions + save_to_disk simultaneously), and the upward .git walk is a user-visible behavioral change; both feel worth a quick human look before merge.

Extended reasoning...

Overview

This PR fixes #28935 by making bun pm version propagate the bumped version into bun.lock's workspace_versions map, so sibling workspaces depending via workspace:* resolve the new version on the next bun pm pack / install. It touches src/runtime/cli/pm_version_command.rs (~250 lines added: find_git_root, update_lockfile_workspace_version, an extra lockfile_path parameter threaded through git_commit_and_tag, and a nested-repo guard) and adds a 6-test regression suite at test/regression/issue/28935.test.ts.

Security risks

None identified. Git argv is built from fixed slices plus an internally-derived absolute path (no shell, no user-controlled interpolation). No auth, crypto, network, or permission surfaces are touched.

Level of scrutiny

Medium-high. The change is well-tested and the bug-hunting system found nothing new, but two areas warrant human judgment rather than bot sign-off:

  • unsafe lockfile mutation (pm_version_command.rs:436-512): the borrowck workaround takes *mut PackageManager / *mut Lockfile / *mut Semver::Version and relies on documented-in-comments disjointness between workspace_versions, the string pool, and what save_to_disk reads from LoadResult. The pattern is cited as matching pm_trusted_command.rs, but the invariants are subtle enough that a maintainer familiar with bun_install::Lockfile internals should confirm them.
  • Behavioral change to git detection: verify_git previously checked only <pkg_dir>/.git; it now walks upward and accepts .git as either a file or directory. This is correct for the workspace-subdir case the PR targets, but it broadens when git tagging fires — worth a maintainer ack.

Other factors

The PR has been through ~15 commits of bot-driven iteration including a mid-flight Zig→Rust port; every prior CodeRabbit and claude thread is resolved. Two git-path tests are gated skipIf(!isLinux) due to darwin/windows CI flakiness that the author hasn't yet root-caused (noted in the test comments). CI status on the latest commit (f8a9f98) is the remaining open question.

…nted_unsafe_blocks, drop_non_drop)

Three deny-level clippy lints landed upstream (PR #31116) and trip on
update_lockfile_workspace_version:

- ref_as_ptr at :465 — 'e as *mut Semver::Version' on a '&mut' produced
  by get_ptr_mut is the exact pattern clippy wants via from_mut; switch
  to std::ptr::from_mut::<Semver::Version>(e).
- undocumented_unsafe_blocks at :521 — the tail 'unsafe { &(*pm_raw).options }'
  had no SAFETY comment. Add one (pm_raw is the singleton; options is
  POD disjoint from lockfile).
- drop_non_drop at :525 — LoadResult<'_> isn't Drop, so drop() is a
  no-op that only extends lifetime. Replace with a comment stating the
  lexical borrow ends at the function boundary.
Comment thread src/runtime/cli/pm_version_command.rs
Comment thread src/runtime/cli/pm_version_command.rs Outdated
robobun and others added 2 commits May 22, 2026 03:12
…ment

Two issues from code review:

**gitignored bun.lock (regression fix)**

`git add` refuses explicitly-named ignored paths with
"The following paths are ignored by one of your .gitignore files: …"
and exits 1. When a workspace has `bun.lock` in `.gitignore` (a
deliberate pattern for library-style monorepos that regenerate
lockfiles per-environment), the new explicit lockfile argument made
`bun pm version` hard-fail the whole bump with a bare "Git add
failed". Before this PR only `package.json` was staged so the same
setup worked.

Add `is_path_ignored_by_git` that runs `git check-ignore -q -- <path>`:
exit 0 means ignored, anything else (1 / 128 / spawn error) is treated
as "not ignored". Wire it alongside the existing `is_parent_or_equal`
nested-repo guard so both "git can't accept the path" conditions drop
the lockfile arg from the `git add` argv.

Add a regression test that gitignores `bun.lock`, inits a git repo,
runs `bun pm version minor` from a workspace subdir, and asserts the
bump succeeds (exit 0, no "Git add failed" on stderr) while only
`package.json` lands in the HEAD commit.

**tombstone comment**

8d0543a dropped a `drop(load_result)` call to satisfy
`clippy::drop_non_drop` and replaced it with a two-line comment
explaining the removed call. That violates `src/CLAUDE.md:25` ("NEVER
add comments to deleted code blocks") — the phrase "no explicit drop
needed" only makes sense to a reader who saw the deleted line. Remove
the comment; the surrounding code doesn't need it.
Comment thread src/runtime/cli/pm_version_command.rs Outdated
The `is_path_ignored_by_git` doc comment asserted that treating a
non-zero `git check-ignore` exit as "not ignored" would let the
caller surface git's actual error. That's wrong: the `stage_proc`
failure handler prints a bare "Git add failed" and discards the
captured stderr — wiring the echo is an explicit follow-up, not in
this PR. Drop the parenthetical so the comment describes what the
code actually does.

@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.

No issues found in this pass — the last nit (inaccurate parenthetical) was addressed in 1c0b75d. Deferring to a human reviewer given the unsafe raw-pointer projections into pm.lockfile (workspace_versions / string_builder / save_to_disk) and the behavioral change to verify_git's repo discovery, both of which warrant a look from someone familiar with the bun_install::Lockfile aliasing invariants.

Extended reasoning...

Overview

This PR makes bun pm version propagate workspace package version bumps into bun.lock so that workspace:* consumers (e.g. bun pm pack) resolve the new version. It touches src/runtime/cli/pm_version_command.rs (~270 added lines: new find_git_root, update_lockfile_workspace_version, is_path_ignored_by_git; modified verify_git, git_commit_and_tag, exec) and adds a 543-line regression suite at test/regression/issue/28935.test.ts with 7 test cases.

Security risks

None identified. The new git check-ignore / git add argv is built from an internally-computed absolute path (top_level_dir() + save_format.filename()), not user input, so there's no injection surface. No auth, crypto, or permissions code is touched.

Level of scrutiny

This warrants human review rather than auto-approval. The core of update_lockfile_workspace_version uses three unsafe blocks with raw-pointer re-projection (pm_raw: *mut PackageManager, entry_ptr: *mut Semver::Version) to hold a LoadResult<'_> borrow of pm.lockfile while simultaneously mutating workspace_versions, the string pool, and calling save_to_disk. Each block carries a SAFETY comment asserting field disjointness, but those invariants depend on bun_install::Lockfile internals that a maintainer should confirm. Separately, verify_git changes from a single <pkg_dir>/.git check to an upward walk — a deliberate behavior change that broadens when git tagging fires.

Other factors

The PR has a long iteration history (Zig→Rust port mid-review, ~15 rounds of inline fixes for Windows existsAt semantics, nested-repo staging, gitignored lockfiles, pipe handling, OOM escalation, comment hygiene). All prior inline threads are now resolved; my last nit was addressed in the HEAD commit. CI shows two failures (node-http-backpressure.test.ts SIGKILL, test-timers-user-call.js illegal instruction) that appear unrelated to this change. Test coverage is thorough for the happy path and the edge cases discovered during review, though three of the git-tagging tests are gated skipIf(!isLinux).

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closing this PR because it has been inactive for more than 90 days.

@github-actions github-actions Bot closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bun pm version does not update bun.lock in workspaces

1 participant