Skip to content

fix(install): write the root workspace lockfile under sharedWorkspaceLockfile=false - #882

Merged
jdx merged 5 commits into
aubepkg:mainfrom
rubnogueira:fix/root-workspace-lockfile
Jun 13, 2026
Merged

jdx merged 5 commits into
aubepkg:mainfrom
rubnogueira:fix/root-workspace-lockfile

Conversation

@rubnogueira

@rubnogueira rubnogueira commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

In a workspace with sharedWorkspaceLockfile: false, aube wrote a per-project pnpm-lock.yaml for every member except the workspace root — so the root's own pnpm-lock.yaml (the one holding the root package's own dependencies) silently disappeared after install. pnpm, under the same setting, writes a lockfile for every project including the root. This restores that behavior.

Root cause

write_per_project_lockfiles explicitly skipped the . importer:

if importer_path == "." {
    // The root manifest gets no per-project lockfile ...
    continue;
}

so the root project never got its lockfile written under the per-project layout.

Fix

crates/aube/src/commands/install/workspace.rs:

  • Remove the .-skip; the root importer is now written like any other project.
  • Resolve the root importer's directory to the workspace root itself (. → workspace_root); members continue to nest under their relative path.
  • A dependency-free root with no graph importer entry is still skipped naturally via the existing subset_to_importer(...) -> None guard.
  • Existing-format preservation is unchanged: a root already on pnpm-lock.yaml keeps getting pnpm-lock.yaml (no surprise aube-lock.yaml); a root with no lockfile uses the workspace fallback kind.
  • Docstring updated to describe the root-inclusive behavior.

Verification

  • New regression test writes_root_importer_lockfile_alongside_members asserts the root pnpm-lock.yaml is written (with its own importer, format preserved) alongside members. Existing per-project tests still pass.
  • End-to-end on the real binary with sharedWorkspaceLockfile: false: both ./pnpm-lock.yaml (root importer ., root's own deps) and ./packages/member/pnpm-lock.yaml are written, format preserved, with no aube-lock.yaml produced anywhere.

Test plan

  • cargo test -p aube per_project_lockfile_tests
  • cargo clippy --tests -p aube clean, cargo fmt --check clean
  • Manual: in a sharedWorkspaceLockfile: false workspace, run aube install
    and confirm the root pnpm-lock.yaml plus each member's lockfile are all
    present and in the correct format

Summary by CodeRabbit

  • Bug Fixes

    • Per-project lockfile writing now correctly writes a root lockfile when the root is a project and skips it when the root is config-only.
  • New Features

    • Filtered installs now only rewrite lockfiles for the selected project(s), avoiding unrelated members or the workspace root.
  • Tests

    • Updated and added regression tests for root vs config-only behavior and scoped (filtered) install writes.
  • Documentation

    • Clarified per-importer lockfile behavior and root/importer handling.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6334acda-374c-4fb8-a2ef-cbee6d9b9a6f

📥 Commits

Reviewing files that changed from the base of the PR and between 9546980 and 5d841da.

📒 Files selected for processing (4)
  • crates/aube/src/commands/install/mod.rs
  • crates/aube/src/commands/install/resolve.rs
  • crates/aube/src/commands/install/workspace.rs
  • test/workspace.bats
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/workspace.bats

📝 Walkthrough

Walkthrough

write_per_project_lockfiles now processes the workspace root importer (.) when the workspace root contains a package.json, computes on-disk pkg_dir per importer (workspace_root for . or workspace_root.join(importer_path)), preserves existing lockfile kinds, and includes regression and bats test updates for these cases.

Changes

Root importer inclusion with format preservation

Layer / File(s) Summary
write_per_project_lockfiles doc and logic
crates/aube/src/commands/install/workspace.rs
Document sharedWorkspaceLockfile=false behavior, remove unconditional skip for ., skip . only if workspace_root/package.json is missing, and map . → workspace_root, others → workspace_root.join(importer_path); preserve existing lockfile kind or use fallback when absent.
Thread selection through run & resolve
crates/aube/src/commands/install/mod.rs, crates/aube/src/commands/install/resolve.rs
Compute per_project_write_selection once for the install and pass it (as an Option<&BTreeSet>) into lockfile-only and full-install write paths so filtered installs only rewrite selected importers.
per_project_write_selection helper
crates/aube/src/commands/install/workspace.rs
New helper returns None for unfiltered installs or a BTreeSet of importer keys for filtered installs (including . when applicable).
Regression tests for root importer behavior
crates/aube/src/commands/install/workspace.rs
Add tests asserting: when root is a project, its pnpm-lock.yaml is rewritten at workspace root and no adjacent aube-lock.yaml is created; when root is config-only, no root lockfile is written while members still receive lockfiles.
Bats test updates for multi-lockfile workspaces
test/pnpm_savecatalog.bats, test/pnpm_update.bats, test/workspace.bats
Adjust shell tests and comments to expect a workspace-root lockfile when the root is a project under sharedWorkspaceLockfile=false, clarify config-only root behavior, and add a --filter test asserting only the selected member's lockfile is written.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • jdx/aube#880: Both PRs modify write_per_project_lockfiles logic to correctly handle sharedWorkspaceLockfile=false by preserving each importer's existing lockfile format and avoiding forced default overwrites.

Poem

🐰 The root no longer hides away,
Dot hops forward—here to stay.
Each lockfile keeps its kind,
Tests prove the paths aligned,
Hooray, the workspace springs to play.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing the root workspace lockfile writing behavior under sharedWorkspaceLockfile=false, which is the core bug being addressed across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

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


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

@greptile-apps

greptile-apps Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Under sharedWorkspaceLockfile=false, the workspace root's per-project lockfile was silently dropped because write_per_project_lockfiles unconditionally skipped the . importer. This PR restores pnpm-parity by writing the root lockfile when the root is a project (has a package.json), while keeping config-only roots skipped, and adds a per_project_write_selection gate so aube install --filter writes only the selected importers' lockfiles.

  • workspace.rs: Replaces the hard .-skip with a package.json-presence guard and a new per_project_write_selection helper; uses importer_project_dir for path normalization on the root importer.
  • mod.rs / resolve.rs: Threads per_project_write_selection through both the main install path and the --lockfile-only short-circuit so both paths scope writes identically.
  • Tests: Three new unit tests cover root-is-a-project, config-only-root, and --filter-scoped writes; e2e bats tests updated to match the corrected assertions.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to the sharedWorkspaceLockfile=false write path and restores behavior that matches pnpm's own output.

The fix correctly handles all three cases (root project, config-only root, filtered install), the ordering of the two new guards is intentional and correct, and the new code introduces no unwraps at public API boundaries. Both the unit tests and the updated e2e bats tests give good behavioral coverage for the changed path.

No files require special attention.

Important Files Changed

Filename Overview
crates/aube/src/commands/install/workspace.rs Core fix: removes the hard .-skip for the root importer, adds a package.json presence guard for config-only roots, and introduces per_project_write_selection to scope writes under --filter. Logic is sound, edge cases (config-only root, filtered installs, format preservation) are all handled. Three new unit tests cover the new behavior precisely.
crates/aube/src/commands/install/mod.rs Wires the new per_project_write_selection into both the streaming-install write and the --lockfile-only short-circuit path so both code paths stay identically scoped.
crates/aube/src/commands/install/resolve.rs Adds per_project_write_selection to the LockfileOnlyInput struct and passes it through to write_per_project_lockfiles, keeping the lockfile-only path in sync with the main install path.
test/workspace.bats Updates existing per-project lockfile tests to expect the root lockfile (now that root is a project in those setups), adds a new e2e regression for --filter scoping, and verifies root lockfile contents are member-dep-free.
test/pnpm_savecatalog.bats Updates the assertion from ! -e aube-lock.yaml to assert_file_exists aube-lock.yaml, matching the corrected behavior where the root project gets its own lockfile.
test/pnpm_update.bats Refines the comment to clarify why no root lockfile is expected: the workspace root in that test is config-only (no package.json), so the skip is deliberate — no logic change.

Reviews (3): Last reviewed commit: "Merge branch 'main' into fix/root-worksp..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/aube/src/commands/install/workspace.rs (2)

482-518: ⚡ Quick win

Consider covering the root-importer case in test/workspace.bats too.

This unit test proves write_per_project_lockfiles, but the regression was in the full sharedWorkspaceLockfile=false install flow and the existing bats coverage in the provided context still only exercises member-only layouts. One CLI-level root-importer test would lock in the fallback-kind plumbing end to end.

🤖 Prompt for 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.

In `@crates/aube/src/commands/install/workspace.rs` around lines 482 - 518, Add a
CLI-level bats test that exercises the sharedWorkspaceLockfile=false install
flow for a workspace that has a root importer (mirroring the unit test
writes_root_importer_lockfile_alongside_members). The test should invoke the
real install command, create a workspace with a root package plus a member
package, ensure the root already has a pnpm-lock.yaml (so the runner preserves
its format), run the install, and assert that the root pnpm-lock.yaml is written
with the root importer present and that the member receives an aube-lock.yaml;
this verifies the end-to-end behavior of write_per_project_lockfiles and the
fallback LockfileKind::Aube in the CLI path.

296-304: ⚡ Quick win

Reuse importer_project_dir() here.

This reimplements the same "."/member mapping but drops the helper's lexical normalization for parent-relative importer keys (for example ../sibling). Calling the existing helper keeps root handling and off-root workspace members consistent in one place.

♻️ Proposed change
-        let pkg_dir = if importer_path == "." {
-            workspace_root.to_path_buf()
-        } else {
-            workspace_root.join(importer_path)
-        };
+        let pkg_dir = importer_project_dir(workspace_root, importer_path);
🤖 Prompt for 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.

In `@crates/aube/src/commands/install/workspace.rs` around lines 296 - 304, The
code computes pkg_dir manually from workspace_root and importer_path; replace
that block with a call to the existing helper importer_project_dir(...) so
importer_project_dir(workspace_root, importer_path) is assigned to pkg_dir,
preserving the helper's lexical normalization for parent-relative importer keys
(e.g., "../sibling") and keeping root (".") handling consistent.
🤖 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.

Nitpick comments:
In `@crates/aube/src/commands/install/workspace.rs`:
- Around line 482-518: Add a CLI-level bats test that exercises the
sharedWorkspaceLockfile=false install flow for a workspace that has a root
importer (mirroring the unit test
writes_root_importer_lockfile_alongside_members). The test should invoke the
real install command, create a workspace with a root package plus a member
package, ensure the root already has a pnpm-lock.yaml (so the runner preserves
its format), run the install, and assert that the root pnpm-lock.yaml is written
with the root importer present and that the member receives an aube-lock.yaml;
this verifies the end-to-end behavior of write_per_project_lockfiles and the
fallback LockfileKind::Aube in the CLI path.
- Around line 296-304: The code computes pkg_dir manually from workspace_root
and importer_path; replace that block with a call to the existing helper
importer_project_dir(...) so importer_project_dir(workspace_root, importer_path)
is assigned to pkg_dir, preserving the helper's lexical normalization for
parent-relative importer keys (e.g., "../sibling") and keeping root (".")
handling consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 19b7c0d1-bdf7-47d0-b6a3-e7a14d0c850c

📥 Commits

Reviewing files that changed from the base of the PR and between bfcd123 and 91e6291.

📒 Files selected for processing (1)
  • crates/aube/src/commands/install/workspace.rs

@rubnogueira
rubnogueira force-pushed the fix/root-workspace-lockfile branch from 91e6291 to 6bb7882 Compare June 13, 2026 19:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/aube/src/commands/install/workspace.rs (1)

311-317: ⚡ Quick win

Prefer the existing importer_project_dir helper for path computation.

The inline directory computation duplicates logic from the importer_project_dir helper (line 153) but omits normalize_lexical for non-root importers. When workspace packages use parent-relative glob patterns (../**), importer paths can contain .. segments that should be collapsed for correct path operations. The helper already handles this case (line 167).

♻️ Proposed refactor
-        // The root importer (`.`) writes its lockfile at the workspace
-        // root itself; members nest under their relative path.
-        let pkg_dir = if importer_path == "." {
-            workspace_root.to_path_buf()
-        } else {
-            workspace_root.join(importer_path)
-        };
+        // The root importer (`.`) writes its lockfile at the workspace
+        // root itself; members nest under their relative path.
+        let pkg_dir = importer_project_dir(workspace_root, importer_path);
🤖 Prompt for 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.

In `@crates/aube/src/commands/install/workspace.rs` around lines 311 - 317, The
inline computation of pkg_dir duplicates logic and misses normalize_lexical for
non-root importers; replace the conditional that derives pkg_dir (which checks
importer_path == ".") with a call to the existing helper importer_project_dir
passing workspace_root and importer_path (or the helper's expected parameter
order), so the path normalization (including normalize_lexical) is applied for
both root and non-root importers; ensure you remove the old if/else block and
use the helper wherever pkg_dir is computed.
🤖 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.

Nitpick comments:
In `@crates/aube/src/commands/install/workspace.rs`:
- Around line 311-317: The inline computation of pkg_dir duplicates logic and
misses normalize_lexical for non-root importers; replace the conditional that
derives pkg_dir (which checks importer_path == ".") with a call to the existing
helper importer_project_dir passing workspace_root and importer_path (or the
helper's expected parameter order), so the path normalization (including
normalize_lexical) is applied for both root and non-root importers; ensure you
remove the old if/else block and use the helper wherever pkg_dir is computed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d4c3ffda-6cd0-4c53-81a3-d78a571d7900

📥 Commits

Reviewing files that changed from the base of the PR and between 91e6291 and 9546980.

📒 Files selected for processing (4)
  • crates/aube/src/commands/install/workspace.rs
  • test/pnpm_savecatalog.bats
  • test/pnpm_update.bats
  • test/workspace.bats
✅ Files skipped from review due to trivial changes (1)
  • test/pnpm_update.bats

jdx commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

I checked this against real pnpm behavior with Corepack pnpm v11.6.0, and the main behavior here does appear to match pnpm:

  • with sharedWorkspaceLockfile: false and a root package.json, pnpm writes a root pnpm-lock.yaml
  • if the root has no dependencies, pnpm still writes a root lockfile with an empty . importer
  • if the root is config-only (pnpm-workspace.yaml but no root package.json), pnpm does not write a root lockfile
  • members still get their own per-project lockfiles

So the existing test expectation changes from “no root lockfile” to “root lockfile exists” look correct for fixtures where the workspace root is itself a project.

One edge I think is worth covering before merge: filtered workspace installs. In a quick pnpm check, pnpm --filter lib install in a sharedWorkspaceLockfile=false workspace with a root package.json wrote only packages/lib/pnpm-lock.yaml, not the root lockfile. Since aube’s per-project write path iterates manifests, this PR could potentially create/rewrite the root lockfile during filtered commands unless the graph/manifests have already been scoped appropriately. A Bats regression test for aube --filter <member> install under this setting would make that explicit.

I also agree with the existing review note to use importer_project_dir(workspace_root, importer_path) for pkg_dir; the manual workspace_root.join(importer_path) path loses the helper’s lexical normalization for parent-relative workspace importers like ../sibling.

This comment was generated by Codex.

rubnogueira and others added 2 commits June 13, 2026 22:36
…kfile=false

A scoped `aube install --filter <pkg>` rewrote the root and every
unrelated member's per-project lockfile, diverging from pnpm which
touches only the selected project(s). Derive the write selection from
the same selector the link pass uses, and reuse `importer_project_dir`
for pkg_dir so parent-relative importer keys stay normalized.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jdx
jdx merged commit 976fbb3 into aubepkg:main Jun 13, 2026
17 checks passed
@greptile-apps greptile-apps Bot mentioned this pull request Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants