Skip to content

publish: resolve a relative tarball path against the invoking directory - #38704

Open
robobun wants to merge 3 commits into
mainfrom
farm/6b34bbf5/publish-tarball-cwd
Open

publish: resolve a relative tarball path against the invoking directory#38704
robobun wants to merge 3 commits into
mainfrom
farm/6b34bbf5/publish-tarball-cwd

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun publish ./pkg.tgz resolves the tarball path against the wrong directory whenever the package root is not the directory the command was run from:
    • from a workspace member: cd packages/foo && bun pm pack && bun publish ./foo-1.2.3.tgz fails with ENOENT: No such file or directory: failed to read tarball: './foo-1.2.3.tgz' (open) even though the file is right there.
    • from a subdirectory of a project (<repo>/dist, the usual build-then-publish layout): same ENOENT, or, if a same-named tarball happens to sit in <repo>/, that stale tarball is published instead, silently. An absolute path works.
  • Cause: Context::from_tarball_path (src/runtime/cli/publish_command.rs:144) joins the argument onto FileSystem::instance().top_level_dir. By then PackageManager::init has walked up to the nearest package.json (or the workspace root), set top_level_dir to it and chdir'd there. init returns the directory the command was actually run from as its second tuple element, and PublishCommand::exec was dropping it on the next line.
  • Not a regression: the same join was in the Zig version.

Fix

  • from_tarball_path takes original_cwd and joins the argument onto that instead of top_level_dir; exec passes the value PackageManager::init returns. Absolute arguments are unaffected (join_abs_string_buf_z ignores the base), and when the cwd is the package root nothing changes.
  • Same pattern the other install commands already use for user-supplied relative paths (add_remove_with_filter.rs joins bun add ./dir onto original_cwd).
  • Project discovery is kept for the tarball form on purpose: it is what loads the project's bunfig.toml / .npmrc (registry, token), which the existing tarball publish tests depend on.
  • Verified: test/cli/install/bun-publish.test.ts, new relative tarball path describe block (three tests: stale root tarball must not win, subdirectory of a project, workspace member). All three fail on the released bun (USE_SYSTEM_BUN=1: first one publishes publish-tarball-cwd-stale@0.0.1, the other two hit ENOENT) and pass with bun bd test; the whole file passes (42 tests).

Background

  • PackageManager::init is shared by every install subcommand. It starts from the process cwd, walks up to the nearest package.json, and if that package is a member of a workspace continues up to the workspace root; it then chdirs into that directory and makes it top_level_dir, so the rest of the package manager can work with root-relative paths. It hands the pre-walk cwd back to the caller (original_cwd) precisely for command-line arguments that the user typed relative to where they were standing.
  • bun publish <tarball> is the two-step flow documented in docs/pm/cli/publish.mdx (bun pm pack, then bun publish ./package.tgz): it reads an existing tarball, takes name/version from the package.json inside it, and uploads it; nothing about it needs the project root except configuration.

`bun publish ./pkg.tgz` joined the argument onto the package root that
PackageManager::init had already chdir'd to. Run from a subdirectory of a
project, or from a workspace member, that is not the directory the user is
in: the tarball is either not found, or a same-named tarball sitting in the
root is published instead. Resolve it against the directory the command was
run from, which init already returns.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b3e948e8-4989-4445-afbd-d737514a491f

📥 Commits

Reviewing files that changed from the base of the PR and between 9805144 and ceea652.

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

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on the released bun (1.4.0-canary.1) from a dist/ subdirectory of a project and from a workspace member: bun publish --dry-run ./rel.tgz either fails with ENOENT ... failed to read tarball or picks up a same-named tarball in the package root. The three tests added in test/cli/install/bun-publish.test.ts (relative tarball path) fail the same way on the released binary and pass with this branch.

CI: every Linux and Windows lane is green on both a02fd43 (build 96933) and ceea652 (build 97133); the publish test file passes on Windows x64 and aarch64. The only jobs not finished are the two darwin 14 aarch64 shards, which are queued behind a large backlog on that step (unrelated to this change). The remaining annotations are retried flakes in other files. The code change is dd059c8; ceea652 only removes a doc comment.

@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 — small, targeted fix that swaps the base directory for the tarball path from top_level_dir to the original_cwd that PackageManager::init already returns.

What was reviewed:

  • Confirmed the pattern matches add_remove_with_filter.rs:405 (join_abs_string_buf on original_cwd for user-supplied paths).
  • Checked join_abs_string_buf_z ignores the base for absolute args, so the existing absolute-path test in "can publish from a tarball" still holds; when cwd == package root, original_cwd == top_level_dir so nothing changes.
  • Verified the removed FileSystem import has no remaining uses in the file.
  • Tests use --dry-run with auth-in-URL (same trick as the existing "registry summary line" test), tempDir, and test.concurrent; the first test asserts the stale root tarball does not win, not just that the right one is found.
Extended reasoning...

Overview

Two files touched. In src/runtime/cli/publish_command.rs, Context::from_tarball_path gains an original_cwd: &[u8] parameter and joins the CLI-supplied tarball path onto it instead of FileSystem::instance().top_level_dir. PublishCommand::exec stops dropping the second tuple element from PackageManager::init and passes it through. The now-unused bun_resolver::fs::FileSystem import is removed. In test/cli/install/bun-publish.test.ts, a three-test describe("relative tarball path") block is added.

Security risks

None. This changes which base directory a user-supplied CLI argument is resolved against, from the discovered package root to the directory the user was standing in — which is the less surprising and less dangerous of the two (it stops silently publishing a stale tarball that happens to sit at the root). No new path-traversal surface: the resulting absolute path is fed straight to File::read_from, same as before.

Level of scrutiny

Low-to-medium. The Rust change is ~10 lines and mechanically follows an existing pattern (add_remove_with_filter.rs already resolves bun add ./dir against original_cwd the same way). join_abs_string_buf_z treats absolute parts as absolute, so the existing absolute-path test path is unaffected. The original_cwd is a Box<[u8]> that now lives to the end of exec instead of being dropped one line after creation — no lifetime or ownership concern.

Other factors

The three new tests are well-shaped: tempDir + using, test.concurrent, --dry-run with a bogus npm_config_registry carrying userinfo so the auth check passes without network I/O (same technique already used by the "registry summary line" test in this file). The first test plants a same-named stale tarball at the root and asserts it is not the one selected — covering the silent-wrong-publish variant, not just ENOENT. The PR description confirms all three fail under USE_SYSTEM_BUN=1 and the whole file passes on the branch.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Nothing to change from the automated review. Lints, format and clippy are green; Buildkite is still running on dd059c8.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 PM PT - Aug 14th, 2026

@robobun, your commit ceea652 has some failures in Build #97133 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38704

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

bun-38704 --bun

Comment thread src/runtime/cli/publish_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 — small, correct fix that swaps the tarball path's join base from top_level_dir to the original_cwd that PackageManager::init already returns for exactly this purpose.

What was reviewed:

  • Confirmed PackageManager::init returns (manager, Box<[u8]>) and the same original_cwd join pattern is already used in add_remove_with_filter.rs:405 and prune.rs.
  • join_abs_string_buf_z ignores the base for absolute args, so absolute tarball paths are unaffected; the removed FileSystem import has no other uses in this file.
  • Three new --dry-run tests cover the variant matrix (stale root tarball, project subdir, workspace member), use tempDir/test.concurrent, and were verified to fail on the released binary.
  • The comment-cop flag on line 139 is a false positive: the two-line doc comment documents a parameter (why original_cwd differs from the process cwd), not a workaround.
Extended reasoning...

Overview

This PR fixes bun publish ./path/to/tarball.tgz resolving relative tarball paths against the wrong directory. PackageManager::init walks up to the package/workspace root and chdirs there before returning; it hands the pre-walk cwd back as the second tuple element precisely so callers can resolve user-typed relative paths against it. PublishCommand::exec was explicitly drop()ing that value and from_tarball_path was joining onto FileSystem::instance().top_level_dir (the post-walk root) instead. The fix threads original_cwd through as a &[u8] parameter and joins onto it — a ~7-line Rust change plus a now-unused import removal.

Security risks

None. This only changes which base directory a user-supplied CLI path is joined onto; join_abs_string_buf_z already handles absolute paths by ignoring the base, and the resulting path is opened read-only for a file the user explicitly named. No new untrusted input, no auth/crypto/permissions surface touched.

Level of scrutiny

Low-to-medium. The change is mechanical and follows an established repo pattern: add_remove_with_filter.rs:405 does the identical join_abs_string_buf::<platform::Auto>(original_cwd, ...) for bun add ./dir, and prune.rs / workspace_selection thread the same value the same way. The only caller of from_tarball_path is updated in the same diff. The removed drop(original_cwd) was purely cosmetic (an owned Box<[u8]> that now lives to the end of scope instead).

Other factors

  • Tests are solid per REVIEW.md: they cover the three failure modes described (stale same-named tarball at the root wins silently, ENOENT from a subdir, ENOENT from a workspace member), assert on the specific package name/version in stdout rather than just exit code, use --dry-run with a bogus auth-in-URL registry so nothing hits the network, and use tempDir/test.concurrent per harness conventions. The PR description confirms all three fail with USE_SYSTEM_BUN=1 and pass with bun bd test, and that the full 42-test file passes.
  • The github-actions comment-cop inline comment ("if you need a paragraph-long comment to justify why the workaround is OK, the code is wrong") fired on the new two-line doc comment for the original_cwd parameter. That comment documents genuinely non-obvious state (the process has already chdir'd away from the invoking directory) rather than justifying a workaround — the fix is the correct fix, not a workaround. I'm treating it as a false positive of the heuristic and not a blocker.

@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 — small, well-scoped fix that follows the existing original_cwd pattern.

What was reviewed:

  • Confirmed PackageManager::init returns the pre-walk cwd as its second tuple element and that add_remove_with_filter.rs / pm_version_command.rs already resolve user-supplied relative paths against it the same way; from_tarball_path has no other callers.
  • Absolute tarball paths are unaffected (join_abs_string_buf_z ignores the base); the removed FileSystem import and drop(original_cwd) are the only collateral changes.
  • Tests are hermetic (--dry-run + bogus in-URL auth, no registry contact), use tempDir/test.concurrent, and the first one asserts the stale root tarball is not picked — the silent-wrong-publish case.
  • The comment-cop note about a paragraph-long doc comment was addressed in ceea652.
Extended reasoning...

Overview

The PR changes Context::from_tarball_path in src/runtime/cli/publish_command.rs to accept an original_cwd: &[u8] parameter and join the user-supplied tarball path onto that instead of FileSystem::instance().top_level_dir. PublishCommand::exec was already receiving original_cwd from PackageManager::init and immediately dropping it; it now passes it through. The unused FileSystem import is removed. Three new tests in test/cli/install/bun-publish.test.ts cover: (1) a same-named stale tarball in the package root must not win over the one in the cwd, (2) publishing from a subdirectory of the package root, (3) publishing from a workspace member directory.

Security risks

None. This is CLI path resolution for a local-file argument the user typed; the fix makes it resolve against the directory they typed it in, which is the less-surprising and less-dangerous behavior (the bug could silently publish a stale tarball from a parent directory). No network, auth, or untrusted-input parsing is touched.

Level of scrutiny

Low. The diff is ~10 production lines and follows an established repo pattern verbatim: add_remove_with_filter.rs:405 already does join_abs_string_buf::<platform::Auto>(original_cwd, …) for bun add ./dir, and pm_version_command.rs, dedupe_command.rs, outdated_command.rs all thread original_cwd from PackageManager::init for the same reason. from_tarball_path has exactly one call site (grep-verified), so no sibling to miss. Absolute paths are unaffected because join_abs_string_buf_z ignores the base when the joined segment is absolute — the existing "can publish from a tarball" test already exercises that.

Other factors

The tests are well-constructed per the repo's review guidelines: tempDir + using, test.concurrent, --dry-run with a bogus npm_config_registry carrying in-URL credentials so the auth gate passes without any network I/O, exit code asserted last, and a negative assertion (not.toContain("publish-tarball-cwd-stale")) that catches the silent-wrong-tarball failure mode rather than just ENOENT. The PR description states all three fail under USE_SYSTEM_BUN=1 and pass with bun bd, and the whole file passes. The github-actions comment-cop flag on a paragraph-long parameter doc comment was addressed in the follow-up commit ceea652, so there are no outstanding review comments.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Heads up: #38739 is the pack side of this same problem (bun pm pack --destination/--filename are resolved against the package directory or the chdir'd root instead of the invoking directory). The two should land together.

Today, from a workspace package, bun pm pack --filename ./x.tgz && bun publish ./x.tgz works by accident: pack writes x.tgz into the workspace root and bun publish <tarball> also looks in the root. With only this PR merged, pack still writes to the root while publish looks in the package directory, so that flow fails with ENOENT; with only #38739 merged it fails the other way around. With both applied (verified locally, the branches merge cleanly in either order) the flow works and both bun-pack.test.ts and bun-publish.test.ts pass. A round-trip test for it belongs in whichever of the two lands second.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Refining the note above: #38704 can merge on its own (it fixes the documented bun pm pack && bun publish ./pkg.tgz flow and only disturbs the accidental relative --filename one, which #38739 then restores). It is #38739 that must not go in first, and it now says so in its description.

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.

2 participants