Skip to content

install: re-resolve catalog references on plain bun update from the workspace root - #36379

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/f2e10f96/catalog-update-from-root
Jul 31, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/f2e10f96/catalog-update-from-root

Conversation

@robobun

@robobun robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #36304.

Problem

bun update / bun update -r (no --latest) is a no-op for catalog entries when run from the workspace root: a catalog entry ^1.0.0 locked to 1.0.0 stays on 1.0.0 after 1.5.0 is published, while a direct dependency with the identical range moves to 1.5.0. Only --latest bumps catalogs.

# root package.json: workspaces.catalog = { "dep": "^1.0.0" }
# packages/app/package.json: dependencies = { "dep": "catalog:" }
# lockfile: dep@1.0.0, registry now has dep@1.5.0

bun update            # -> "no changes", catalog stays ^1.0.0, dep@1.0.0
bun update -r         # -> "no changes"
bun update -r --latest # -> catalog -> ^1.5.0 (works)

The only existing non---latest test ran update from inside the workspace package, where this already worked.

Cause

get_or_put_resolved_package_with_find_result gates re-resolution on is_root_dependency(dependency_id), i.e. "is this a direct dependency of the package in cwd". A catalog: reference lives in a workspace package, so when running from the root it fails that check, should_update is false, and get_package_id dedupes onto the locked version instead of taking the manifest's best match. With --latest the catalog text is rewritten to latest before install, which trips catalogs_changed and clears the resolution, bypassing the dedupe.

(Separately, -r is not read anywhere in the non-interactive resolve path, so it is equivalent to no flag here; that's #36360 / #33182.)

Fix

In should_update, treat a catalog: reference (dependency.version.tag == Catalog) as eligible for re-resolution regardless of cwd. Catalog definitions are a root-level concept shared across workspaces; re-resolving them matches what #36304 already does for the package.json rewrite in edit_catalogs_after_update.

Tests

New in test/cli/install/catalogs.test.ts: writes a lockfile pinning no-deps@1.0.0 with a catalog range ^1.0.0 (the registry has 1.1.0), runs bun update / bun update -r from the root, asserts the catalog is rewritten to ^1.1.0, node_modules/no-deps is 1.1.0, and the lockfile no longer contains no-deps@1.0.0.

Fail-before (release canary):

(fail) update > update without --latest from root moves catalogs within range (no args)
(fail) update > update without --latest from root moves catalogs within range (-r)
  expected catalog { "no-deps": "^1.1.0" }, received { "no-deps": "^1.0.0" }

With fix: catalogs.test.ts 18/18, bun-update.test.ts 6/6, bun-install-registry.test.ts -t update 16 pass + 1 todo, bun-workspaces.test.ts 63/63, bun-add.test.ts 54/54, bun-lock.test.ts 13/13.


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

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ 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
ninja: Entering directory `/workspace/bun/build/debug'
[1/104] gen ErrorCode+*.h
[2/45] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[3/45] gen cpp.rs (cppbind)
[4/45] gen JSSink.{cpp,h,lut.h,rs}
generated_jssink.rs: 6 sinks, 72 exported symbols
Generating /workspace/bun/build/debug/codegen/JSSink.lut.h from /workspace/bun/build/debug/codegen/JSSink.lut.txt
[5/45] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 239 extern-C blocks audited
[6/45] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (13 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 fields)
Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.
... (truncated)

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

test/cli/install/catalogs.test.ts:
(pass) basic > both catalog and catalogs in top-level [42.17ms]
(pass) basic > both catalog and catalogs in workspaces [16.31ms]
(pass) basic > detect changes (bun.lockb) [23.66ms]
(pass) basic > detect changes (bun.lock) [24.12ms]
(pass) update > --latest updates catalog versions in top-level [18.41ms]
(pass) update > --latest updates catalog versions in workspaces [19.24ms]
(pass) update > --latest updates catalog versions with -r [19.63ms]
(pass) update > --frozen-lockfile passes after --latest updates catalogs [22.89ms]
(pass) update > --latest run from inside a workspace package updates the root catalog [17.98ms]
(pass) update > --latest updates the same package independently per catalog [19.11ms]
(pass) update > --latest --dry-run does not modify any package.json (from root) [18.94ms]
(pass) update > --latest --dry-run does not modify any package.json (from workspace) [16.02ms]
(pass) update > update without --latest from root moves catalogs within range (no args) [7.38ms]
(pass) update > update without --latest from root moves catalogs within range (-r) [6.06ms]
(pass) update > update wi
... (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 (02dc29569)

test/cli/install/catalogs.test.ts:
(pass) basic > both catalog and catalogs in top-level [409.47ms]
(pass) basic > both catalog and catalogs in workspaces [333.92ms]
(pass) basic > detect changes (bun.lockb) [519.80ms]
(pass) basic > detect changes (bun.lock) [509.54ms]
(pass) update > --latest updates catalog versions in top-level [406.88ms]
(pass) update > --latest updates catalog versions in workspaces [372.94ms]
(pass) update > --latest updates catalog versions with -r [378.63ms]
(pass) update > --frozen-lockfile passes after --latest updates catalogs [544.80ms]
(pass) update > --latest run from inside a workspace package updates the root catalog [445.30ms]
(pass) update > --latest updates the same package independently per catalog [390.07ms]
(pass) update > --latest --dry-run does not modify any package.json (from root) [382.96ms]
(pass) update > --latest --dry-run does not modify any package.json (from workspace) [362.05ms]
(pass) update > update without --latest from
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 630ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/82] gen ErrorCode+*.h
[2/37] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[3/37] gen cpp.rs (cppbind)
[4/37] gen JSSink.{cpp,h,lut.h,rs}
generated_jssink.rs: 6 sinks, 72 exported symbols
Generating /workspace/bun/build/release/codegen/JSSink.lut.h from /workspace/bun/build/release/codegen/JSSink.lut.txt
[5/37] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 239 extern-C blocks audited
[6/37] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (13 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 fields)
Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts
  - CronJob (5 fields)
Found 3 classes from /workspac
... (truncated)
diff hotspot
.../PackageManager/PackageManagerEnqueue.rs        |  7 ++-
 test/cli/install/catalogs.test.ts                  | 69 ++++++++++++++++++++++
 2 files changed, 73 insertions(+), 3 deletions(-)

gate history · 3 passed · 0 rejected · iteration 2

evidence per changed file
file                                                 reads  edits  tests
src/install/PackageManager/PackageManagerEnqueue.rs      9      4      0
test/cli/install/catalogs.test.ts                        1      3      0

… workspace root

`bun update` (no --latest) gated re-resolution on `is_root_dependency`,
which only covers direct dependencies of the package in cwd. A `catalog:`
reference is declared in a workspace package, so running from the root
left it deduped onto the locked version even when a newer in-range
version was published; only `--latest` (which rewrites the catalog text
and trips `catalogs_changed`) re-resolved it.

Catalog definitions live in the root package.json, so treat a catalog
reference as eligible for re-resolution regardless of which workspace
the update runs in.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Catalog dependency updates

Layer / File(s) Summary
Catalog update gating and validation
src/install/PackageManager/PackageManagerEnqueue.rs, test/cli/install/catalogs.test.ts
Workspace-root updates now re-resolve catalog: dependencies, with tests covering catalog ranges, workspace references, installed versions, lockfile contents, and command success.

Possibly related PRs

  • oven-sh/bun#36360: Modifies the same workspace update eligibility logic in PackageManagerEnqueue.rs.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main change: catalog references are re-resolved on plain workspace-root updates.
Description check ✅ Passed The description covers what changed and how it was verified, even though it uses custom headings instead of the template's exact labels.

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

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

I didn't find any issues — the fix is a narrow, well-tested widening of should_update for catalog: refs. Deferring to a human because this touches package-manager resolution semantics (catalog refs now bypass the cwd-scoped is_root_dependency gate on every bun update, not just from the root).

What was reviewed:

  • Confirmed dependency.version.tag is still Catalog at this callsite (the resolved catalog version is passed separately as version), so the new predicate identifies the right deps.
  • Checked the change is gated by to_update and the existing update_requests/updating_packages filter, so bun install and targeted bun update <pkg> behavior are unchanged.
  • Test uses the Verdaccio harness, synthesizes a lockfile pinned to 1.0.0 under ^1.0.0, and asserts the result stays in-range at 1.1.0 (not 2.0.0) — fail-before is documented.
Extended reasoning...

Overview

The PR adds one disjunct to the should_update computation in get_or_put_resolved_package_with_find_result (PackageManagerEnqueue.rs): a dependency whose version.tag == Catalog is now eligible for re-resolution during bun update regardless of whether is_root_dependency holds. It adds two parametrized tests in catalogs.test.ts that pin no-deps@1.0.0 in a synthetic lockfile under a ^1.0.0 catalog and assert bun update / bun update -r from the workspace root move it to 1.1.0.

Security risks

None. This is version-resolution bookkeeping inside the package manager; no new inputs are parsed and no trust boundaries change.

Level of scrutiny

Medium-high. The diff is tiny (3 effective lines of logic) and clearly a follow-up to #36304, but it lives in the resolution path that decides which package version gets installed. The predicate is checked before the Catalog tag is replaced with the looked-up version (verified against the 'version block around PackageManagerEnqueue.rs:728), so the tag test is sound. It short-circuits before the is_root_dependency unsafe split, which is fine. It remains gated by this.to_update and by the update_requests.is_empty() || updating_packages.contains(name) filter, so bun install and bun update <specific> are unaffected.

Other factors

The one semantic question worth a human eye: the new disjunct makes every catalog reference re-resolve on bun update, not just those in the cwd package. That's the intent (catalogs are root-scoped), and it mirrors how edit_catalogs_after_update already writes back to the root package.json regardless of cwd — but it is a slight broadening beyond the stated bug (root cwd only). The PR reports the surrounding suites (bun-update, bun-workspaces, bun-add, bun-lock, bun-install-registry -t update) all pass, and the new tests follow harness conventions (VerdaccioRegistry, runUpdate helper, exitCode asserted last, no network). Given the resolution-path stakes I'm deferring rather than approving.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI: catalogs.test.ts passes on all lanes. The only hard failure on build 85539 is the Windows x64 verify-baseline step (HTTP parsing — llhttp SSE4.2 PCMPESTRI > long headers times out and then segfaults under Nehalem emulation), which is unrelated to the install resolver change here and comes from recent main commits merged into this branch. Ready for review.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:41 PM PT - Jul 29th, 2026

❌ @robobun, your commit 02dc295 has 1 failures in Build #85539 (All Failures):

  • ❌ Baseline verification failed on Windows x64 — 1 check(s)
  • The baseline build crashed or failed tests under Nehalem (SSE4.2, no AVX/AVX2/AVX512) emulation (not an unsupported-instruction fault). See the step log for the crash output.

    • SIMD baseline tests

🧪   To try this PR locally:

bunx bun-pr 36379

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

bun-36379 --bun

Comment thread src/install/PackageManager/PackageManagerEnqueue.rs Outdated

@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

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

Inline comments:
In `@test/cli/install/catalogs.test.ts`:
- Around line 431-448: In the runUpdate assertion block, add the exitCode
success check immediately after verifying stderr does not contain an error and
before reading package.json or bun.lock. Keep the existing artifact assertions
unchanged, ensuring command failures are reported before any generated-file
reads occur.
🪄 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: 6551b0bb-c91d-4475-80b0-9a67d36c4499

📥 Commits

Reviewing files that changed from the base of the PR and between 716d736 and fcf2849.

📒 Files selected for processing (2)
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • test/cli/install/catalogs.test.ts

Comment thread test/cli/install/catalogs.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.

I didn't find bugs, but this changes the install resolver's should_update gate — worth a human confirming the semantic widening is what's wanted.

What was reviewed:

  • Traced dependency.version.tag at both call sites of get_or_put_resolved_package_with_find_result — it retains Catalog after the catalog→npm substitution (the resolved version is the separate version param), so the new predicate fires where intended.
  • Checked the update_requests non-empty branch: catalog refs now pass the cwd gate there too, still bounded by updating_packages.contains(name).
  • The predicate makes catalog refs re-resolve from any cwd, not only root — the PR says this is intentional and matches #36304's edit_catalogs_after_update; noting it since the tests only exercise the from-root direction.
Extended reasoning...

Overview

One-condition change to the should_update boolean in get_or_put_resolved_package_with_find_result (src/install/PackageManager/PackageManagerEnqueue.rs): a dependency whose original tag is Catalog now bypasses the is_root_dependency(dependency_id) cwd check during bun update. Two new tests in test/cli/install/catalogs.test.ts seed a lockfile pinned below the catalog range's max and assert bun update / bun update -r from the workspace root move the catalog, node_modules, and lockfile forward.

Security risks

None. No untrusted input parsing, no allocation, no unsafe changes — the added clause reads an enum tag on an already-borrowed &Dependency. The existing unsafe raw-pointer split is unchanged.

Level of scrutiny

Medium-high. The diff is tiny and mechanically obvious, but it sits in the package manager's re-resolution gate, which every bun update hits. I verified at both call sites (lines ~1082 and ~2510) that dependency is the original lockfile Dependency (tag still Catalog) while the resolved npm version is passed separately as version, so the new check reads the right field. The gate is still ANDed with this.to_update, so bun install is unaffected.

Other factors

  • The predicate is symmetric: it also causes catalog refs in other workspace packages to re-resolve when you run bun update from inside one workspace. The PR description argues this is correct (catalogs are root-scoped, and edit_catalogs_after_update from #36304 already rewrites them regardless of cwd), but the new tests only cover from-root; a human should confirm that widening is intended.
  • The comment-cop bot flagged the added 2-line comment; I read it as a false positive (it states an invariant, not a workaround justification), but that's the author's/maintainer's call.
  • Fail-before/pass-after evidence is solid, and the adjacent update/workspace/add/lock suites were run green.

@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 — targeted follow-up to #36304 that widens the should_update gate to include catalog: references, with fail-before/pass-after tests.

What was reviewed:

  • The should_update predicate change: catalog check short-circuits before is_root_dependency, uses dependency.version.tag (the original catalog: tag, not the resolved version param) consistent with the same check at line 728.
  • Confirmed the third clause (update_requests.is_empty() || updating_packages.contains(name)) still gates targeted updates, so bun update <pkg> from root won't re-resolve unrelated catalog entries.
  • New test writes a pre-pinned lockfile and asserts catalog rewrite, installed version, workspace catalog: reference preserved, and lockfile contents — matches the file's existing patterns; both bot threads (comment-cop, coderabbit exit-code ordering) are resolved.
Extended reasoning...

Overview

Two files: a 4-line predicate change in src/install/PackageManager/PackageManagerEnqueue.rs (get_or_put_resolved_package_with_find_result) and two new parametrized tests in test/cli/install/catalogs.test.ts. The predicate previously required is_root_dependency(dependency_id) — true only for direct deps of the package in cwd — so a catalog: reference declared in a workspace package failed the gate when bun update was run from the workspace root. The fix ORs in dependency.version.tag == Tag::Catalog, treating catalog references as always eligible (catalogs are defined at the root and shared across workspaces).

Security risks

None. No untrusted-input parsing, no new I/O paths, no allocation or FFI changes. The change only widens a boolean predicate governing whether get_package_id receives None (re-resolve) vs Some(version) (dedupe onto locked) during bun update.

Level of scrutiny

Moderate — this is core package-manager resolution, but the change is narrow: it only takes effect when this.to_update is already true (i.e., during bun update), and only for dependencies whose original tag is Catalog. I checked that dependency.version.tag is the correct field (vs the version parameter, which is the post-catalog-resolution npm range) by comparing against the identical check at PackageManagerEnqueue.rs:728. The catalog check is placed before the unsafe is_root_dependency call, which short-circuits cleanly.

Other factors

  • Fail-before is demonstrated on release canary in the PR body (the two new tests fail with expected ^1.1.0, received ^1.0.0). The ASAN-without-fix build failure in the evidence block is a build-infra artifact, not a test result.
  • The PR body reports the wider install/update suite passing: catalogs.test.ts 18/18, bun-update 6/6, bun-workspaces 63/63, bun-add 54/54, bun-lock 13/13, and CI is green on all lanes for catalogs.test.ts per the robobun status.
  • Both inline review threads are resolved: the comment-cop paragraph-comment flag was addressed by condensing to a single line in 02dc295; coderabbit's exit-code-ordering suggestion was withdrawn after robobun pointed out the setup-written files can't ENOENT and the file's convention (and test/CLAUDE.md) puts exitCode last.
  • The one behavioral edge I considered — running bun update from inside pkg1 now also marks catalog deps declared only in pkg2 as should_update — is consistent with catalogs being root-scoped (the catalog entry itself lives in the root package.json and is shared), and the 63-test workspace suite passes.

@Jarred-Sumner
Jarred-Sumner merged commit f0c4263 into main Jul 31, 2026
53 of 55 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/f2e10f96/catalog-update-from-root branch July 31, 2026 08:23
dylan-conway added a commit that referenced this pull request Aug 3, 2026
…esolver

Address issues found in review of the dependency-resolution rewrite:

- Guard against version-conflict dependency cycles (a@1 depending on a@2
  depending on a@1) by refusing to re-place a package that already sits in
  the ancestor chain, matching npm. Previously such cycles nested nodes
  without bound.
- Treat catalog references as update targets so a bare `bun update` moves
  them too, restoring the rule from #36379 that the rewrite dropped.
- Chain a checkout for every dependency waiting on a git clone, each from
  its own recorded commit, instead of only the clone task's originating
  dependency. Fixes isolated-linker installs with several git deps on one
  repository, and drops the now-unused originating-dependency field.
- Prefetch a node's manifests when its parent finishes deciding rather than
  when the cursor reaches the node: the whole frontier fetches in parallel
  while the parent's slots (including any alias) already exist, so a name a
  sibling slot satisfies is still never requested.
- Skip the resolution passes entirely on an install whose manifest diff
  produced no work, so an in-sync install pays no resolution cost.
- Give peers one conflict policy in every resolution path: a peer whose
  parent scope holds a different same-kind package binds to it with a
  warning instead of installing a second copy. This also stops dist-tag
  peers from warning when the scope already holds the tagged version.
- On a failed tarball download in the callback-free task loop, reset the
  package back to Extract so a resolver still waiting on it re-checks, hits
  the recorded failure, and fails fast instead of polling an extraction
  that will never land.

Adds a resolution-order test: a fake registry that parks each manifest
response and releases them in a controlled order (forward, reversed, and
seeded shuffles), asserting the resolved lockfile is byte-identical across
every order.
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