Skip to content

install: print the update row for catalog entries moved by bun update - #38763

Open
robobun wants to merge 2 commits into
mainfrom
farm/27935079/update-catalog-rows
Open

install: print the update row for catalog entries moved by bun update#38763
robobun wants to merge 2 commits into
mainfrom
farm/27935079/update-catalog-rows

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A bare bun update / bun update --latest that moves a root catalog entry rewrites package.json and bun.lock but the summary never prints the ^ name old -> new row for it. Catalog zed: ^1.0.0 consumed by a workspace member, locked at 1.4.0, latest 3.0.0:
    • bun update --latest from the root prints only 1 package installed; with -r the same.
    • Run from the consuming member, or when the root itself has zed: "catalog:", it prints + zed@3.0.0 (a plain install row).
    • With the isolated linker and the new version already in node_modules/.bun, a second run prints Done! Checked 3 packages (no changes) while a semver-major bump was written.
    • The same project with a direct zed: ^1.0.0 prints ^ zed 1.4.0 -> 3.0.0 in every one of those cases, and --dry-run / --lockfile-only already print the row for the catalog case too (update_transitive::print_plan diffs the rows directly), so only the install summary disagreed.
  • Cause: the summary derives update rows from manager.updating_packages (tree_printer.rs, should_print_package_install), keyed by dependency name with the version the name was locked to. edit_catalogs_before_update records catalog entries only in updating_catalogs, and record_updating_package_versions (install_with_manager.rs) skips every row whose version is not Npm/DistTag, so a catalog: row never has an original to compare against. The printer also only walks the rows of the workspaces whose package.json it edited, so an entry consumed elsewhere had no row to print from even if it had been registered.

Fix

  • package_json_editor::record_catalog_originals (called at the end of record_updating_package_versions, i.e. on the loaded lockfile before the differ re-enqueues anything): for every catalog: row whose entry edit_catalogs_before_update recorded, register the dependency name in updating_packages with the row's locked version as the original, the same shape the cwd's own dependency lists produce. The entry is marked catalog_entry and written_back, so the post-install edit_update_entries pass does not rewrite a same-named non-catalog dependency of the cwd from it (verified: a root zed: "latest" next to a catalog zed entry stays "latest" on a plain update). A name the dependency lists already registered keeps their original, so the existing + / ^ output for direct dependencies is unchanged.
  • tree_printer.rs: rows of the walked workspaces now print ^ rows for their catalog: dependencies for free (and stop printing + for them). In the normal single-section summary, print_catalog_entry_updates additionally walks the catalog: rows of names registered this way, sharing the section's per-name dedupe, so an entry consumed only outside the walked set is reported too; the --verbose summary prints one section per workspace, each of which already reports its own catalog: rows, so it skips the walk (sole_section) and a moved entry shows up once, under the workspace that depends on it. Both go through should_print_package_install, which for bun update reports a moved row whether or not its new version had to be installed, so the isolated-store rerun now prints the row as well (the count line stays (no changes), as it does for a direct dependency in the same situation).
  • Entries that do not move print nothing: a registered name whose rows still resolve to the original version compares equal, exactly like a direct dependency that did not move. Checked by hand with an exact catalog entry on a plain update, a two-entry catalog where only one entry moves, a name declared both directly and in the catalog (one row), and --filter <member> (catalogs untouched, no row).
  • The version copy that record_updating_package_versions and update_transitive::register_moved each open-coded becomes PackageUpdateInfo::set_original_version, used by all three registration sites. (semver: record cloned prerelease/build tag offsets against the whole destination buffer #38687 changes the clone_into signature those copies call; whichever lands second adjusts this one helper.)
  • Verified with test/cli/install/catalogs.test.ts (update block): the --latest variants (catalog in workspaces, top-level catalog, -r), the plain update within range (with and without -r), the run from inside a member, plus three new cases: the root itself consuming the entry (one row, no + row), --verbose (exactly one row, under pkg1:) and the isolated-store rerun. All 9 row assertions fail on the unfixed build (null or + no-deps@2.0.0 instead of the row) and pass with the fix; the unmoved a-dep entry is asserted to produce no row. Also ran bun-update.test.ts (156), bun-update-transitive, bun-update-lockfile-sync, bun-add-catalog, lockfile-only (413) and the update subset of bun-install-registry.test.ts: all green.

Background

  • Catalogs: the root package.json's catalog / catalogs maps hold version ranges; a workspace declares "zed": "catalog:" (or catalog:<group>) and resolves through the map. A bare bun update treats the entries like the root's own dependencies: edit_catalogs_before_update points them at latest (with --latest) in memory and remembers each entry in updating_catalogs; after resolving, edit_catalogs_after_update writes the resolved range back into the root package.json.
  • updating_packages: the map a bare update keeps per dependency name, holding the literal being updated and (record_updating_package_versions) the version bun.lock had for it. The install summary walks the rows of the updated workspaces and prints ^ name old -> new for every row whose name is in the map and whose resolution now differs from that recorded version; that check runs before the "was it installed" check, which is why direct dependencies print even when nothing new was linked. update_transitive::register_moved uses the same map, with an empty literal, for packages moved by the transitive half of the update; print_transitive_updates prints those from the set of packages installed this run, which is why this change does not reuse that path for catalogs.
  • Per-name granularity is pre-existing: the map holds one original per name, so a name declared with different ranges in several places prints one row from whichever registration came first. Catalog entries follow the same rule (the dependency lists win over the catalog; within catalogs, the first consuming row wins).

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

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

test/cli/install/catalogs.test.ts:
(pass) basic > both catalog and catalogs in top-level [451.26ms]
(pass) basic > both catalog and catalogs in workspaces [347.20ms]
(pass) basic > detect changes (bun.lockb) [613.92ms]
(pass) basic > detect changes (bun.lock) [522.67ms]
(pass) basic > switching a dependency to a different catalog is detected [674.49ms]
(pass) basic > catalog and catalogs.default may split different packages between them [394.41ms]
355 |       const { out, err, exitCode } = await runUpdate(packageDir, ...flags);
356 |       expect(err).not.toContain("error:");
357 | 
358 |       // The moved entry is reported like a direct dependency of the root, even though only pkg1 depends on it.
359 |       // a-dep still resolves to 1.0.10 (only its literal changes), so it gets no row.
360 |       expect(out.match(/^.*no-deps.*$/gm)).toStrictEqual(["^ no-deps 1.1.0 -> 2.0.0"]);
                                                 ^
error: expect(received).toStrictEqual(expe
... (truncated)

release without fix: 42 FAILED
bun test v1.4.0-canary.1 (b7a043103)

test/cli/install/catalogs.test.ts:
(pass) basic > both catalog and catalogs in top-level [72.52ms]
(pass) basic > both catalog and catalogs in workspaces [27.66ms]
(pass) basic > detect changes (bun.lockb) [61.08ms]
(pass) basic > detect changes (bun.lock) [41.65ms]
1508 |   }
1509 |   if (!options?.allowWarnings) {
1510 |     expect(err).not.toContain("warn:");
1511 |   }
1512 |   if ((options?.savesLockfile ?? true) && !production && !options?.frozenLockfile) {
1513 |     expect(err).toContain("Saved lockfile");
                       ^
error: expect(received).toContain(expected)

Expected to contain: "Saved lockfile"
Received: ""

      at runBunInstall (/workspace/bun/test/harness.ts:1513:17)
      at async <anonymous> (/workspace/bun/test/cli/install/catalogs.test.ts:251:11)
(fail) basic > switching a dependency to a different catalog is detected [17.20ms]
1502 |   expect(stdout).toBeDefined();
1503 |   expect(stderr).toBeDefined();
1504 |   const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
1505 |   expect(err).not.toContain("panic:");
1506 |   if (!options?.allowErrors) {
1507 |     ex
... (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/catalogs.test.ts
bun test v1.4.0 (870f7abbb)

test/cli/install/catalogs.test.ts:
(pass) basic > both catalog and catalogs in top-level [1021.77ms]
(pass) basic > both catalog and catalogs in workspaces [1353.21ms]
(pass) basic > detect changes (bun.lockb) [629.68ms]
(pass) basic > detect changes (bun.lock) [631.48ms]
(pass) basic > switching a dependency to a different catalog is detected [598.92ms]
(pass) basic > catalog and catalogs.default may split different packages between them [394.33ms]
(pass) update > --latest updates catalog versions in top-level [496.25ms]
(pass) update > --latest updates catalog versions in workspaces [464.62ms]
(pass) update > --latest updates catalog versions with -r [574.27ms]
(pass) update > --frozen-lockfile passes after --latest updates catalogs [582.45ms]
(pass) update > --latest run from inside a workspace package updates the root catalog [463.43ms]
(pass) update > --latest reports a catalog entry the root itself depends on once [828.78ms]
(pass) update > --latest --verbose reports 
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1477ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/24] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[2/24] gen cpp.rs (cppbind)
[3/24] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 240 extern-C blocks audited
[4/24] gen JS modules (bundle-modules)
Preprocess modules (9751ms)
Bundle modules (154ms)
Postprocesss modules (262ms)
Bundle Functions (1111ms)
Generate Code (36ms)

[11.34s] Bundled "src/js" for production
  2632 kb
  197 internal modules
  13 native modules
  91 internal functions across 17 files
[4/12] 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_jsc v0.0.0 (/workspace/bun/src/jsc)
�[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�[
... (truncated)
diff hotspot
src/install/PackageManager.rs                      | 13 ++++
 src/install/PackageManager/PackageJSONEditor.rs    | 64 +++++++++++++++--
 src/install/PackageManager/install_with_manager.rs | 23 ++----
 src/install/lockfile/printer/tree_printer.rs       | 69 +++++++++++++++++-
 src/install/update_transitive.rs                   | 12 +---
 test/cli/install/catalogs.test.ts                  | 82 +++++++++++++++++++++-
 6 files changed, 225 insertions(+), 38 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                reads  edits  tests
src/install/PackageManager.rs                           1      1      0
src/install/PackageManager/PackageJSONEditor.rs         5      4      0
src/install/PackageManager/install_with_manager.rs      5      2      0
src/install/lockfile/printer/tree_printer.rs            4     12      0
src/install/update_transitive.rs                        3      2      0
test/cli/install/catalogs.test.ts                       5      7      0

@coderabbitai

coderabbitai Bot commented Aug 15, 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: 15 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: 44ca127b-48d4-4beb-8bb4-2b76527ddeb9

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf3f36 and 870f7ab.

📒 Files selected for processing (6)
  • src/install/PackageManager.rs
  • src/install/PackageManager/PackageJSONEditor.rs
  • src/install/PackageManager/install_with_manager.rs
  • src/install/lockfile/printer/tree_printer.rs
  • src/install/update_transitive.rs
  • test/cli/install/catalogs.test.ts

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

@robobun

robobun commented Aug 15, 2026

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

@robobun, your commit 870f7ab has some failures in Build #97000 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38763

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

bun-38763 --bun

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on a debug build of main at e7460e3 with a local registry serving zed@1.0.0 / 1.4.0 / 3.0.0 and a root catalog zed: ^1.0.0 consumed by one workspace member. Before this change, bun update --latest printed 1 package installed with no row (root), + zed@3.0.0 (from the member, or with the root consuming the catalog itself), nothing with -r, and Done! Checked 3 packages (no changes) on an isolated-linker rerun; the direct-dependency control printed ^ zed 1.4.0 -> 3.0.0 in all of those. With this change every variant prints the ^ row, and --verbose prints it once under the workspace that depends on it.

The row assertions added to test/cli/install/catalogs.test.ts fail on the unfixed build (9 cases) and pass with the fix; bun-update.test.ts, bun-update-transitive, bun-update-lockfile-sync, bun-add-catalog, lockfile-only and the update subset of bun-install-registry are unaffected.

CI for the current revision 870f7ab (build 97000) has finished: 177 of 179 jobs passed (every Linux platform including ASAN, Windows x64 and arm64; the few yellow entries passed on retry) and no test failed anywhere. The remaining two jobs are the macOS arm64 test shards, which expired four times in a row without ever being assigned an agent: the test-darwin queue has been around 300 jobs deep all night and PR builds run at a lower priority than main and merge-queue builds there, so that lane is currently starving every PR. Nothing in this diff is platform-specific, and the merge queue runs those shards at a higher priority, so I am leaving the build as is rather than re-pushing into the same queue; the two jobs can also simply be retried once the backlog clears. Ready for review.

Comment thread src/install/lockfile/printer/tree_printer.rs Outdated
A bare `bun update` (with or without --latest) rewrites the root's catalog
entries and bun.lock, but the install summary never showed a
`^ name from -> to` row for them: the row printer reads
`updating_packages`, which only the cwd's own dependency lists (and the
transitive plan) fill in, and `record_updating_package_versions` skips
`catalog:` rows. Depending on who consumed the entry the output was
nothing, a plain `+ name@new` install row, or `(no changes)` when the new
version was already in the isolated store.

Before the differ runs, register every `catalog:` row of a recorded
catalog entry under its dependency name with the version it was locked
to, marked as a catalog entry and already written back (the entry itself
is written by edit_catalogs_after_update, so the cwd's dependency lists
must not pick the registration up). The summary then prints the walked
importers' catalog rows as update rows, and additionally walks the
`catalog:` rows of names registered this way so an entry consumed only by
a workspace outside the walked set is reported too, whether or not the
new version had to be installed.

The version copy shared by the three registration sites moves into
PackageUpdateInfo::set_original_version.
The verbose summary prints a section per importer, and each section
already prints the ^ rows of its own catalog: dependencies, so walking
the catalog rows from the ungrouped section as well printed a moved
entry twice. Only the non-verbose summary, which has a single section,
walks them.
@robobun
robobun force-pushed the farm/27935079/update-catalog-rows branch from 035a078 to 870f7ab Compare August 15, 2026 00:55

@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. My earlier --verbose double-print note doesn't apply to this revision — print_catalog_entry_updates is gated on sole_section, which is false on both verbose-path calls, and the new --latest --verbose test pins exactly one row under pkg1:.

What was reviewed:

  • record_catalog_originals registration: written_back: true + catalog_entry: true keeps edit_update_entries from rewriting a same-named non-catalog dep (traced the root "latest" case); direct-dep registrations win via found_existing.
  • set_original_version refactor matches the prior register_moved shape (unconditional clone_into); the old install_with_manager variant only differed by skipping a zero-length tag buffer.
  • sole_section threading: verbose passes false (root and per-workspace), non-verbose passes true on the single call — catalog sweep runs exactly where the shared update_dedupe holds.
Extended reasoning...

Overview

Cosmetic fix to bun update's install summary: catalog entries that move now print a ^ name old -> new row like direct dependencies do, instead of nothing (from root), a + install row (from a member), or (no changes) (isolated rerun). Touches five Rust files in src/install/ — a new record_catalog_originals that registers catalog rows in updating_packages alongside direct deps, a catalog_entry flag on PackageUpdateInfo, a print_catalog_entry_updates sweep in the tree printer gated by a new sole_section parameter, and a shared set_original_version helper replacing three open-coded copies. Tests add row assertions to every existing update case and three new tests (root-consumer, --verbose, isolated-store rerun).

Prior review

My earlier inline comment claimed a --verbose double-print. That was a misread: print_catalog_entry_updates only runs when sole_section is true, and both verbose-path calls pass false. In verbose mode each workspace's own update_owners walk prints its catalog: rows (root has none, so only pkg1: prints). The new --latest --verbose test asserts exactly one row under pkg1:, directly covering that scenario. The concern is moot for this revision.

Security risks

None. This changes only what the install summary prints; lockfile writing, package.json write-back, and installation are unaffected. The one write-back interaction — written_back: true on catalog-registered entries — was traced: edit_update_entries (after-install) short-circuits on written_back, so a same-named non-catalog dependency of the cwd is not rewritten from the catalog entry, and a name the cwd's dependency lists registered first skips catalog registration entirely (found_existing).

Level of scrutiny

Medium. The package manager is critical, but the surface here is the summary printer plus a bookkeeping map (updating_packages) whose only other consumer is the post-install package.json write-back, guarded by written_back. The set_original_version consolidation is a straight refactor of two existing shapes into the register_moved one (unconditional clone_into into an owned tag buffer; the old install_with_manager variant only skipped a zero-length allocation).

Other factors

Tests are thorough — 8 row assertions across the existing --latest/plain/-r/from-member cases plus dedicated root-consumer, verbose, and isolated-rerun tests, and a negative assertion that the unmoved a-dep entry produces no row. The PR description confirms all fail on the unfixed build. CI is green on the install suites; the two red lanes are unrelated pre-existing failures per the robobun status.

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