Skip to content

pack/publish: warn and continue when the lockfile is unreadable - #36260

Closed
robobun wants to merge 5 commits into
mainfrom
farm/8b9e5371/pack-unreadable-lockfile
Closed

robobun wants to merge 5 commits into
mainfrom
farm/8b9e5371/pack-unreadable-lockfile

Conversation

@robobun

@robobun robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

bun pm pack and bun publish eagerly parse bun.lock/bun.lockb before packing. Any LoadStep::{ParseFile,ReadFile,Migrating} failure previously called Global::crash(): exit 1, no tarball, even for a package with no workspace:/catalog: specifiers (the only thing the lockfile is consulted for). npm pack never reads a lockfile.

Realistic triggers: an empty bun.lock from a truncated write, a mid-merge lockfile with git conflict markers, or a corrupt/foreign bun.lockb.

Repro

mkdir /tmp/lockdemo && cd /tmp/lockdemo
printf '{"name":"lockdemo","version":"1.0.0"}' > package.json
echo 'module.exports=1' > index.js

: > bun.lock
bun pm pack
# before: error: Missing lockfile version / failed to parse lockfile: InvalidLockfile -> exit 1, no tarball
# after:  warn: failed to parse bun.lock: InvalidLockfile, continuing without it -> exit 0, tarball written

Same for bun.lock with <<<<<<< HEAD markers (ParserError) and corrupt bun.lockb (InvalidLockfile).

Fix

In both pack_command.rs and publish_command.rs, on any LoadResult::Err (other than ENOENT, which already mapped to None): emit warn: failed to <step> <path>: <err>, continuing without it, reset the manager log (so the stale parse diagnostics don't leak into later log.print() calls), and proceed with lockfile = None. This matches bun install's behaviour, which prints the parse error and then warn: Ignoring lockfile before re-resolving.

edit_root_package_json already errors with Failed to resolve workspace version for "<name>" ... Run \bun install`/catalogs require a lockfilewhen resolution actually needs the lockfile and it'sNone`, so a package that does depend on it still fails with a clear message.

The lazy-load optimization (skip load_from_cwd entirely when the manifest has no workspace:/catalog: spec) would require moving the lockfile load after the package.json parse inside pack(); left as a follow-up since graceful handling alone fixes the user-facing bug.

Verification

bun-pack.test.ts gained an unreadable lockfile block:

  • empty bun.lock, bun.lock with git conflict markers, corrupt bun.lockb → pack succeeds, tarball contents correct, stderr has warn: + continuing without it, no error:
  • empty bun.lock + "pkg1": "workspace:*" → still exits 1 with Failed to resolve workspace version for "pkg1" ... Run \bun install``
  • bun publish --dry-run with empty bun.lock → packing completes (Total files: 2)

All 5 new tests fail on main (failed to parse lockfile → exit 1), all pass with this change. All 81 tests in bun-pack.test.ts pass.


[review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 6 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 (4bf8f539b)

test/cli/install/bun-pack.test.ts:
(pass) basic [192.89ms]
93 |         stdin: "ignore",
94 |         env: bunEnv,
95 |       });
96 |       const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]);
97 | 
98 |       expect(err).toContain("warn:");
                       ^
error: expect(received).toContain(expected)

Expected to contain: "warn:"
Received: "error: Missing lockfile version\n    at bun.lock:1:1\nerror: failed to parse lockfile: InvalidLockfile\n"

      at <anonymous> (/workspace/bun/test/cli/install/bun-pack.test.ts:98:19)
(fail) unreadable lockfile > packs with empty bun.lock [153.94ms]
93 |         stdin: "ignore",
94 |         env: bunEnv,
95 |       });
96 |       const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]);
97 | 
98 |       expect(err).toContain("warn:");
                       ^
error: expect(received).toContain(expected)

Expected to contain: "warn:"
Received: "6 | <<<<<<< HEAD\n  
... (truncated)

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

test/cli/install/bun-pack.test.ts:
(pass) basic [5.94ms]
(pass) unreadable lockfile > packs with empty bun.lock [5.05ms]
(pass) unreadable lockfile > packs with bun.lock with git conflict markers [5.01ms]
(pass) unreadable lockfile > packs with corrupt bun.lockb [4.39ms]
(pass) unreadable lockfile > --silent suppresses the warning [4.17ms]
(pass) unreadable lockfile > still errors when a workspace version must be resolved [4.17ms]
(pass) unreadable lockfile > bun publish --dry-run packs with corrupt lockfile [3.51ms]
(pass) in subdirectory [7.96ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [9.00ms]
(pass) package.json names and versions > missing name [2.32ms]
(pass) package.json names and versions > missing version [1.99ms]
(pass) package.json names and versions > missing name and version [2.60ms]
(pass) package.json names and versions > empty name [2.11ms]
(pass) package.json names and versions > empty version [1.96ms]
(pass) package.json names and versions > empty name and version [2.02ms]
(pass) package.json names and versions > missing [1.84ms]
(pass) package.js
... (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 (4bf8f539b)

test/cli/install/bun-pack.test.ts:
(pass) basic [166.59ms]
(pass) unreadable lockfile > packs with empty bun.lock [161.63ms]
(pass) unreadable lockfile > packs with bun.lock with git conflict markers [147.34ms]
(pass) unreadable lockfile > packs with corrupt bun.lockb [148.95ms]
(pass) unreadable lockfile > --silent suppresses the warning [161.38ms]
(pass) unreadable lockfile > still errors when a workspace version must be resolved [156.30ms]
(pass) unreadable lockfile > bun publish --dry-run packs with corrupt lockfile [148.71ms]
(pass) in subdirectory [315.90ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [437.05ms]
(pass) package.json names and versions > missing name [134.39ms]
(pass) package.json names and versions > missing version [126.33ms]
(pass) package.json names and versions > missing name and version [143.34ms]
(pass) package.json names and versions > empty name [132.86ms]
(pass) package.json names an
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 678ms (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    |  70 ++++++++++----------
 src/runtime/cli/publish_command.rs |  32 ++-------
 test/cli/install/bun-pack.test.ts  | 132 +++++++++++++++++++++++++++++++++++++
 3 files changed, 174 insertions(+), 60 deletions(-)

gate history · 3 passed · 0 rejected · iteration 0

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

`bun pm pack` and `bun publish` eagerly parse bun.lock/bun.lockb and
aborted via Global::crash() on any parse/read/migrate failure, even
though the lockfile is only consulted to resolve workspace:^/~/* and
catalog: specifiers. For a package with neither, a truncated,
merge-conflicted, or corrupt lockfile blocked packing entirely.

Treat every lockfile load failure as `lockfile = None` with a warning,
matching `bun install` ("Ignoring lockfile"). The existing
"Failed to resolve workspace version ... Run `bun install`" /
"catalogs require a lockfile" errors in `edit_root_package_json`
already cover the only case that genuinely needs the lockfile.
@coderabbitai

coderabbitai Bot commented Jul 28, 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: 17 minutes

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: 054b3c6a-21b0-43e7-9029-414f8536cb12

📥 Commits

Reviewing files that changed from the base of the PR and between e532ad9 and 4bf8f53.

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

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

@robobun

robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is green; CI red is unrelated flake.

Build #84414 finished with 195/196 jobs passed and 1 canceled (the :eyes: monitoring step). bun-pack.test.ts (all 82 tests, including the 6 new ones) passed on every lane. cargo clippy, Format, Source lints, Lint JavaScript all pass.

Annotated failures are all tagged [flaky] by the harness and none touch pack/publish/install: napi.test.ts, proxy-stress-protocol.test.ts, res.sendFile.test.ts, bun-serve-date.test.ts, socket-retention.test.ts, streams-leak.test.ts, request-smuggling.test.ts, etc. The one [pre-existing] entry is an Azure agent-create error for the win-aarch64 lane (which then passed on retry).

Ready for review/merge.

@robobun

robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:42 PM PT - Jul 28th, 2026

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


🧪   To try this PR locally:

bunx bun-pr 36260

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

bun-36260 --bun

Comment thread src/runtime/cli/pack_command.rs Outdated
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.

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

  • 🟡 src/runtime/cli/publish_command.rs:480-502 — This ~20-line LoadResult::Err → warn-and-continue block is byte-identical to the one in pack_command.rs:230-252 (only pm_log(manager_ptr).reset() vs log.reset() differs). Per REVIEW.md — "the second time a multi-line block appears in your diff, extract a named helper and use it at EVERY parallel site" — consider extracting a helper (e.g. on the LoadResult err type, or alongside report_lockfile_load_error) so future wording/--silent changes stay atomic across pack and publish.

    Extended reasoning...

    What

    The LoadResult::Err(cause) arm in publish_command.rs:480-502 and pack_command.rs:230-252 are near-identical copies of one another. Both were rewritten in this PR to implement the warn-and-continue behavior, and both appear in the diff.

    Side-by-side

    pack_command.rs:230-252:

    LoadResult::Err(cause) => 'err: {
        if matches!(cause.step, LoadStep::OpenFile)
            && cause.value == bun_install::Error::Sys(bun_errno::SystemErrno::ENOENT)
        {
            break 'err None;
        }
        // A lockfile is only needed to resolve `workspace:^` / ...
        let step = match cause.step {
            LoadStep::OpenFile => "open",
            LoadStep::ParseFile => "parse",
            LoadStep::ReadFile => "read",
            LoadStep::Migrating => "migrate",
        };
        Output::warn(format_args!(
            "failed to {} {}: {}, continuing without it",
            step,
            bstr::BStr::new(cause.lockfile_path.as_bytes()),
            cause.value.name(),
        ));
        pm_log(manager_ptr).reset();
        None
    }

    publish_command.rs:480-502 is the same except the second-to-last line is log.reset();. That difference is trivially unified by passing &mut bun_ast::Log to a helper.

    Why this matters

    REVIEW.md's code-style section is explicit:

    The second time a multi-line block appears in your diff, extract a named helper and use it at EVERY parallel site.

    This is exactly that case — two parallel sites in the same PR with a copy-pasted ~20-line block (comment included). The pre-PR code was also duplicated across these two files, so this isn't a regression the PR introduced; but the rule targets what appears in the diff, and this PR is the natural moment to consolidate since both blocks were rewritten anyway.

    Concretely, any follow-up change — e.g. gating the warning on !silent/log-level, changing the wording, or adding another LoadStep variant — must now be made in two places, and it's easy to miss one (the pre-PR blocks had already diverged in format_args! vs tuple-arg style).

    Suggested shape

    Something like:

    pub(crate) fn warn_lockfile_unreadable(cause: &LoadResultErr, log: &mut bun_ast::Log) {
        let step = match cause.step {
            LoadStep::OpenFile => "open",
            LoadStep::ParseFile => "parse",
            LoadStep::ReadFile => "read",
            LoadStep::Migrating => "migrate",
        };
        Output::warn(format_args!(
            "failed to {} {}: {}, continuing without it",
            step,
            bstr::BStr::new(cause.lockfile_path.as_bytes()),
            cause.value.name(),
        ));
        log.reset();
    }

    placed alongside the existing lockfile-load-error reporting in bun_install (or as a method on the err type), with both call sites collapsing to the ENOENT guard + warn_lockfile_unreadable(&cause, log); None. The LoadStep → verb match in particular is a natural impl LoadStep { fn verb(&self) -> &'static str }.

    Severity

    Nit — style/maintainability only. The PR is functionally correct as-is; extraction is a small cleanup, not blocking.

Comment thread src/runtime/cli/pack_command.rs Outdated
…--silent

Shared between pack and publish. Mirrors report_lockfile_load_error:
the pinpoint parse diagnostics from the manager log are printed before
reset() so the user still sees where the lockfile is broken, and the
whole block is gated on LogLevel::Silent.

@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/runtime/cli/pack_command.rs:229-249 — This ~20-line LoadResult::Err warn-and-continue block is byte-for-byte duplicated at publish_command.rs:479-498 (only pm_log(manager_ptr).reset() vs log.reset() differs — and those resolve to the same log). REVIEW.md → Code style: "The second time a multi-line block appears in your diff, extract a named helper and use it at EVERY parallel site." Since this PR is rewriting both copies anyway, consider extracting a shared helper (or extending report_lockfile_load_error in install_with_manager.rs, which the PR description already says this mirrors) rather than maintaining two copies. Orthogonal to the comment above about dropped diagnostics / --silent — even after fixing those in both places, the duplication remains.

    Extended reasoning...

    What the finding is

    The LoadResult::Err(cause) => 'err: { ... } arm added in pack_command.rs:229-249 is near-identical to the one added in publish_command.rs:479-498. Diffing the two blocks side by side:

    pack_command.rs publish_command.rs
    if matches!(cause.step, LoadStep::OpenFile) && cause.value == ... { break 'err None; } identical
    let step = match cause.step { OpenFile => "open", ParseFile => "parse", ReadFile => "read", Migrating => "migrate" }; identical
    Output::warn(format_args!("failed to {} {}: {}, continuing without it", step, bstr::BStr::new(cause.lockfile_path.as_bytes()), cause.value.name())); identical
    pm_log(manager_ptr).reset(); log.reset();
    None identical

    The single differing line resolves to the same object — pm_log(manager_ptr) is unsafe { &mut *(*manager_ptr).log } and log in publish_command.rs is manager.log_mut(), both the manager's log.

    Why this is flagged

    REVIEW.md's Code style & idioms reviewers enforce section states:

    Simplest honest shape; deduplicate within your own diff. … The second time a multi-line block appears in your diff, extract a named helper and use it at EVERY parallel site. If your fix makes two functions byte-identical, delete one.

    Both blocks are new in this diff — the PR rewrites both call sites — so the rule applies directly. It's true that the pre-PR code was also duplicated between these two files, but since the PR is actively touching both copies anyway, extracting a shared helper is in-scope for this change, not "file-wide standardization riding a focused bugfix."

    Not a duplicate of the existing review comment

    There is already a claude[bot] comment on pack_command.rs:251 about this block, but it addresses a behavioral divergence: the log is .reset() without printing first (discarding pinpoint parse diagnostics), and Output::warn isn't gated on --silent. That comment ends with "Same applies to the identical block in publish_command.rs" — acknowledging the duplication in passing to say the same fix is needed there, but not asking for the blocks to be unified. Even after applying that comment's suggestions to both sites separately, you'd still have two copies of the (now larger) block. This finding is the deduplication ask, orthogonal to that one.

    Step-by-step: why a helper fits naturally here

    1. The PR description says the new behavior "matches bun install's behaviour" and cites report_lockfile_load_error in src/install/PackageManager/install_with_manager.rs:1368-1407 as the reference.
    2. That function already has almost the right signature: fn report_lockfile_load_error(manager: &mut PackageManager, cause: &lockfile::LoadResultErr, log_level: Options::LogLevel). It gates on Silent, prints the manager log, resets it, and emits the warning — exactly the behavior the other review comment asks pack/publish to adopt.
    3. Both new call sites have a &mut PackageManager (via manager_ptr), a LoadResultErr (cause), and access to manager.options.log_level — everything the existing helper needs.
    4. The one piece the existing helper doesn't do is the ENOENT-on-OpenFile → None early-out, but that's a two-line guard the caller can keep, or a small fn warn_and_ignore_lockfile_error(manager, cause) -> Option<&Lockfile> in bun_install::lockfile (or on LoadResult) could wrap both.

    Concretely, both sites could collapse to something like:

    LoadResult::Err(cause) => {
        bun_install::lockfile::warn_unreadable(manager, &cause); // prints log, warns, resets — gated on Silent
        None
    }

    with the ENOENT short-circuit either inside the helper or kept as the two-line guard before it.

    Impact and severity

    This is a maintainability/code-quality concern — nothing breaks if merged as-is. But the two copies will drift (they already differ trivially in how they name the log), and any follow-up fix — including the one the existing comment asks for — has to be applied twice. Nit severity per the rubric: merging without this doesn't cause incorrect behavior, a crash, or a regression; it's a repository-enforced style rule that makes the follow-up work cleaner.

@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Both duplication findings above reviewed an older commit range; addressed in b9ed762. warn_lockfile_unreadable(cause, log, log_level) is the single shared helper now, called from both pack_command.rs and publish_command.rs. It prints the manager log's parse diagnostics before reset() and is gated on LogLevel::Silent, mirroring report_lockfile_load_error.

@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 earlier feedback (print parse diagnostics before log.reset(), gate on --silent, dedupe into a shared helper) is fully addressed in warn_lockfile_unreadable.

What was reviewed:

  • warn_lockfile_unreadable mirrors report_lockfile_load_error: prints the log, flushes, then unconditionally resets; LoadStep match is exhaustive.
  • ENOENT-on-open still short-circuits to None in both callers; the workspace/catalog path still hard-fails via edit_root_package_json (covered by the new negative test).
  • --silent test asserts stderr is empty; diagnostic assertions confirm the log is printed (not just the summary warn line).
Extended reasoning...

Overview

Changes bun pm pack and bun publish to warn-and-continue when bun.lock/bun.lockb fails to load, instead of Global::crash(). A new pub(crate) fn warn_lockfile_unreadable in pack_command.rs is called from both commands' LoadResult::Err arms; it emits the warning, prints any queued parse diagnostics from the manager log, flushes, and resets the log — all gated on log_level != Silent. The ENOENT-on-OpenFile case still maps to None silently as before. Six new tests in bun-pack.test.ts cover empty/merge-conflict/corrupt-binary lockfiles, --silent, the workspace-still-errors negative case, and bun publish --dry-run.

Security risks

None. This is CLI diagnostic-output handling on a local file the user controls; no parsing of untrusted data changed, no new I/O or network paths.

Level of scrutiny

Low-to-medium. The code change is small (~35 net lines of Rust, replacing ~60), follows the existing report_lockfile_load_error pattern from install_with_manager.rs, and the LoadStep match is exhaustive against the enum's four variants. The behavior change (crash → warn+continue) is a strict relaxation that matches how bun install already treats the same failure and how npm pack behaves; the safety net — hard-failing when the lockfile is actually needed for workspace:/catalog: resolution — is preserved and explicitly tested.

Other factors

  • My prior review on this PR flagged two issues (diagnostics discarded before print; --silent not honored). Both were addressed in b9ed762: the helper now prints the log before reset(), gates on Silent, and the block was deduplicated across pack/publish. The comment-cop bot's note about long comments was also addressed. All inline threads are resolved.
  • Test evidence in the PR body shows all 6 new tests fail on main and pass on the PR under both debug-ASAN and release; the full 82-test file passes.
  • log.reset() runs unconditionally (outside the Silent gate), so stale parse errors can't leak into a later log.print() even under --silent.
  • The bun publish --dry-run test intentionally omits an exit-code assertion (auth check runs before the dry-run early-return), scoping the assertion to "packing completed" — which is the behavior under test.
  • No CODEOWNERS covers these paths.

@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 concerns from my earlier pass are all addressed in b9ed762.

What was reviewed:

  • warn_lockfile_unreadable now prints the manager log's parse diagnostics before reset() and gates on LogLevel::Silent; both are locked in by the new tests (diagnostic substring assertions and the --silent → err === "" test).
  • Verified the workspace:* + corrupt-lockfile path still errors via the existing edit_root_package_json guard, and that LoadStep::OpenFile+ENOENT still short-circuits to None in both callers.
  • Helper is shared between pack and publish; no duplicated logic remains.
Extended reasoning...

Overview

Changes bun pm pack and bun publish to warn-and-continue (instead of Global::crash()) when bun.lock/bun.lockb exists but is unreadable/unparseable. A new pub(crate) fn warn_lockfile_unreadable in pack_command.rs handles the diagnostic (prints the parser log, emits a warn: line, resets the log, honors --silent) and is called from both pack_command.rs::exec_with_manager and publish_command.rs::from_workspace. Six new tests in bun-pack.test.ts cover empty/merge-conflict/corrupt-binary lockfiles, --silent, the workspace: still-errors path, and bun publish --dry-run.

Security risks

None. The lockfile is only consulted for workspace:/catalog: version resolution during pack; falling back to None is already a supported state (LoadResult::NotFound), and edit_root_package_json still hard-errors when a workspace version actually needs it. No new inputs are trusted.

Level of scrutiny

Moderate. This is a user-facing behavior change (crash → warn+continue) but a strict relaxation on an edge-case path, and it aligns with both npm pack (never reads a lockfile) and bun install's existing report_lockfile_load_error warn-and-ignore behavior. The core logic change is ~15 lines with the rest being deduplication of two identical error-handling blocks into one helper.

Other factors

My earlier review flagged two divergences from report_lockfile_load_error (diagnostics discarded before printing; --silent not honored). Both were fixed in b9ed762 and are now enforced by tests — the parametrized cases assert the pinpoint parser diagnostic (Missing lockfile version, Expected string but found "<<<<<<<") appears in stderr, and a dedicated --silent test asserts empty stderr. The negative-contract test (still errors when a workspace version must be resolved) confirms packages that actually need the lockfile still fail with a clear message. Evidence in the PR description shows all 82 tests in bun-pack.test.ts pass on both debug+ASAN and release, and the new tests fail on main. All prior review threads are resolved.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Related: #38813 removes the lockfile read from pack and publish entirely (workspace versions and catalogs come from the package.json files instead), which makes the unreadable-lockfile case this PR handles a non-issue if it lands.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #38813, which removes the lockfile load this PR makes tolerant: pack and publish no longer read bun.lock or bun.lockb at all, so there is nothing left to warn about.

Checked against a build of #38813 with the scenarios from this PR (empty bun.lock, bun.lock with conflict markers, corrupt bun.lockb, bun publish): a package without workspace specs and one with workspace:* both pack with nothing on stderr, and publish sends the manifest. Tests for those cases are now part of #38813 (bun-pack.test.ts, bun-publish.test.ts).

@robobun robobun closed this Aug 15, 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.

2 participants