Skip to content

fix(install): repair member installs under sharedWorkspaceLockfile=false - #891

Merged
jdx merged 10 commits into
aubepkg:mainfrom
rubnogueira:fix/member-install-detection
Jun 14, 2026
Merged

jdx merged 10 commits into
aubepkg:mainfrom
rubnogueira:fix/member-install-detection

Conversation

@rubnogueira

@rubnogueira rubnogueira commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes several issues hitting workspace members under sharedWorkspaceLockfile=false (per-project lockfiles), where aube install from inside a member behaved incorrectly.

  • Warm-path freshness: the freshness check anchored on a single root lockfile, so every member install fell through to a full resolve/relink even when nothing changed. Member lockfile hashes are now tracked and checked.
  • Settings-only nested pnpm-workspace.yaml: a member carrying a settings-only workspace yaml (no packages:) is now treated as member config, not a standalone workspace, so cd member && aube install resolves to the parent root and its workspace: siblings stay linked.
  • Member relink on warm path: the warm install path now merges per-project member lockfile graphs, so a member whose node_modules was deleted is correctly re-linked instead of reporting "Already up to date".
  • packageExtensions on git/tarball/dir deps: extensions were only applied on the registry resolution path. They're now applied to non-registry packages too, so an extension injecting a sibling dependency (e.g. a LoopBack connector a git-sourced package require()s at runtime) is resolved and linked. The name@* selector match is also hardened for non-semver (git) versions.

Scoping a member install (no code change — pnpm parity)

A plain aube install from anywhere in a workspace installs all importers, because recursiveInstall defaults to true (matching pnpm). To install just one service and the workspace siblings it depends on, use a filter with the trailing ...:

aube install --filter '<service>...'

This installs <service> + its workspace deps (and their transitive deps) while leaving unrelated members untouched (no node_modules, no lockfile written). Without ..., only the named member is installed (sibling symlinked but its own deps not populated) — exactly pnpm's documented foo vs foo... semantics.

Test plan

  • test/shared_workspace_lockfile_warm_path.bats — warm-path / member relink cases
  • test/git_deps.bats — packageExtensions injected into a git package
  • test/filter.bats — --filter <member>... scoping under sharedWorkspaceLockfile=false
  • cargo test (aube-resolver unit tests for packageExtensions flat-map + wildcard)
  • cargo build / cargo clippy / cargo fmt --check

Upgrade note

Run aube install --force once at the monorepo root to refresh lockfiles with the packageExtensions-injected dependencies; afterwards plain aube install (and --filter '<service>...') stay on the warm path.

Summary by CodeRabbit

  • New Features

    • Package extensions now apply to non-registry dependencies (including git, remote tarballs, and directory sources).
  • Improvements

    • Workspace root discovery is more accurate for member-declaring vs settings-only pnpm-workspace.yaml, and it stops correctly on invalid YAML.
    • Warm-path reuse for sharedWorkspaceLockfile=false now correctly merges per-member lockfile graphs so installs don’t skip required work.
    • Recursive installs from the node-gyp bootstrap avoid accidentally targeting an outer workspace.
  • Tests

    • Expanded Bats/regression coverage for package extension injection, filter-scoped installs, and warm-path invalidation.

rubnogueira and others added 4 commits June 14, 2026 00:06
Under sharedWorkspaceLockfile=false a workspace writes one lockfile per
member and no shared root lockfile. The warm path anchored its freshness
decision on a single root lockfile, so a config-only root always read
"no lockfile found" and fell through to the full resolve/fetch/delta
pipeline on every aube install (and every aube run auto-install check),
re-walking the whole graph and visibly re-linking even when nothing
changed.

Record each member's lockfile fingerprint in the install state and, when
no root lockfile is present, verify the per-member lockfiles instead of
bailing. Re-enumerating members on each check keeps added, removed, and
edited members busting the warm path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jun 14, 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: c24fdf83-4326-40d9-a8c8-fb02b6981fc3

📥 Commits

Reviewing files that changed from the base of the PR and between 2181cee and 6c1c478.

📒 Files selected for processing (1)
  • crates/aube-manifest/src/workspace/config.rs

📝 Walkthrough

Walkthrough

This PR implements two independent features: (1) packageExtensions now apply to non-registry packages (git, remote-tarball, directory) via a new apply_package_extensions_to_deps helper and a *-wildcard selector fix; (2) sharedWorkspaceLockfile=false workspaces gain full warm-path support through WorkspaceYamlKind classification, per-member lockfile fingerprinting in InstallState, merge_member_lockfile_graphs for lockfile-reuse, and updated workspace-root discovery that skips settings-only YAMLs.

Changes

packageExtensions for non-registry packages

Layer / File(s) Summary
apply_package_extensions_to_deps and wildcard selector matching
crates/aube-resolver/src/package_ext.rs, crates/aube-resolver/src/lib.rs, crates/aube-resolver/src/tests.rs
Adds normalize_range import, introduces apply_package_extensions_to_deps for flat name -> range dep maps, and updates package_selector_matches to treat normalized * as an unconditional match for non-semver versions. Re-exports from lib.rs. Adds three unit tests.
Driver wiring and integration test
crates/aube-resolver/src/resolve/driver.rs, test/git_deps.bats, crates/aube-lockfile/src/pnpm/tests.rs, crates/aube-store/src/lib.rs
Wires apply_package_extensions_to_deps call into the non-registry local-source resolution path in the driver. Adds a Bats test asserting a git+file:// dependency injected via packageExtensions is resolved in the lockfile and linked in the virtual store. Updates test fixture URLs.

sharedWorkspaceLockfile=false warm path and workspace root discovery

Layer / File(s) Summary
WorkspaceYamlKind enum and classification function
crates/aube-manifest/src/workspace/config.rs, crates/aube-manifest/src/workspace.rs
Adds WorkspaceYamlKind (Absent, SettingsOnly, DeclaresMembers, Invalid) and workspace_yaml_kind(project_dir), refactors WorkspaceConfig::load to use a load_present helper, re-exports both symbols, and adds unit tests for all classification cases plus a regression test asserting classification does not cache config state.
Workspace root discovery skips settings-only YAMLs
crates/aube/src/dirs.rs
Rewrites find_workspace_root_uncached to walk past settings-only workspace YAMLs toward a member-declaring ancestor, falling back to the nearest settings-only root if none is found. Adds three unit tests for the new behavior.
InstallState member lockfile fingerprinting and warm-path helpers
crates/aube/src/state.rs
Adds member_lockfile_hashes/member_lockfile_meta fields to InstallState and FreshnessState. Introduces collect_member_lockfile_state and member_lockfiles_stale to track and detect member lockfile changes. Extends warm-path stale checking with layout-transition handling. Updates InstallLayoutState::from_graph to track direct symlinks for all importers. Updates and adds tests.
merge_member_lockfile_graphs and install wiring
crates/aube/src/commands/install/workspace.rs, crates/aube/src/commands/install/mod.rs
Introduces merge_member_lockfile_graphs to reconstruct missing member importer entries from per-project lockfiles and wires the call into the lockfile-reuse path when shared_workspace_lockfile=false. Adds two unit tests for reconstruction and idempotency.
Integration tests for warm path, bootstrap, and --filter with sharedWorkspaceLockfile=false
test/shared_workspace_lockfile_warm_path.bats, test/filter.bats, crates/aube/src/commands/install/node_gyp_bootstrap.rs
Adds a full Bats suite covering warm-path short-circuit, settings-only nested YAML resolution, node_modules deletion relinking, member add/remove busting, and a sharedWorkspaceLockfile=true control. Adds two --filter tests for scoped installs. Updates bootstrap marker YAML to be non-empty with a packages: declaration to halt workspace discovery at the tool directory.

Sequence Diagram

sequenceDiagram
    participant cli as aube install
    participant check as check_needs_install_inner
    participant mlf as member_lockfiles_stale
    participant merge as merge_member_lockfile_graphs
    participant linker as Linker

    cli->>check: check freshness (FreshnessState)
    check->>check: compare lockfile hash (layout transition handled)
    check->>mlf: compare per-member lockfile hashes + mtime
    mlf-->>check: stale reason / None
    check-->>cli: NeedsInstall or AlreadyUpToDate

    alt NeedsInstall and sharedWorkspaceLockfile=false
        cli->>merge: merge member per-project lockfiles into graph
        merge-->>cli: graph with all importer entries
        cli->>linker: link with full importer graph
    end

    cli->>cli: write_state (collect_member_lockfile_state)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 Hopping through the lockfile trees,
Where settings-only YAMLs tease —
"Not a root!" the rabbit cried,
And found the workspace, warm inside.
Git deps get their extensions too,
Fresh paths for me, and warm for you! 🌿

🚥 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 pull request title accurately describes the main fix: repairing member installations under the sharedWorkspaceLockfile=false configuration option.
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 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes four bugs affecting workspace members under sharedWorkspaceLockfile=false: warm-path freshness was anchored only on the root lockfile (missing per-member hash checks), a settings-only pnpm-workspace.yaml in a member directory was treated as a standalone workspace root, the warm install path never re-linked members whose node_modules was deleted, and packageExtensions were silently dropped for git/tarball/directory dependencies.

  • state.rs adds per-member lockfile hashing and decouples the member freshness check from the root lockfile branch so a workspace root that is itself a package no longer masks member lockfile drift.
  • workspace/config.rs + dirs.rs introduce WorkspaceYamlKind and update find_workspace_root to walk past settings-only yamls to the real members-declaring root; find_workspace_yaml_root (used by aube add catalog resolution) is not yet updated and still stops at the first yaml found.
  • workspace.rs adds merge_member_lockfile_graphs so the warm/fresh linker path reconstructs missing member importers from their own per-project lockfiles, and write_per_project_lockfiles with format-preserving per-member writes scoped by an optional filter selection.
  • package_ext.rs + resolve/driver.rs apply packageExtensions to non-registry packages and short-circuit name@* selectors for non-semver version strings.

Confidence Score: 5/5

Safe to merge; the four targeted bug fixes are well-reasoned, each has dedicated unit and/or bats integration tests, and no new correctness gaps were introduced in the changed code paths.

All four fix areas are covered by new tests and the logic is sound. The find_workspace_yaml_root gap affecting aube add catalog resolution is a pre-existing issue already flagged before this PR and not made worse here. No new regressions were found in the reviewed code.

crates/aube/src/dirs.rs — find_workspace_yaml_root still uses workspace_yaml_existing and will stop at a settings-only member yaml, so aube add from such a member still resolves catalogs against the member directory instead of the real workspace root.

Important Files Changed

Filename Overview
crates/aube/src/state.rs Adds per-member lockfile hashing to both InstallState and FreshnessState, decouples the member freshness check from the root lockfile branch, and expands InstallLayoutState::from_graph to track all importers so a deleted member node_modules correctly busts the warm path.
crates/aube-manifest/src/workspace/config.rs Refactors load_uncached → load_present returning Option to distinguish absent yamls from settings-only ones; adds WorkspaceYamlKind enum and workspace_yaml_kind function that deliberately skips the typed cache so mid-process writes are never shadowed.
crates/aube/src/dirs.rs find_workspace_root_uncached updated to use workspace_yaml_kind so settings-only member yamls no longer stop the ancestor walk; find_workspace_yaml_root is unchanged and still stops at the first yaml it finds, leaving aube add catalog resolution anchoring on the member directory.
crates/aube/src/commands/install/workspace.rs Adds merge_member_lockfile_graphs, per_project_write_selection, and write_per_project_lockfiles with format-preserving per-member writes scoped by an optional filter selection; all new functions have dedicated unit tests.
crates/aube-resolver/src/package_ext.rs Adds apply_package_extensions_to_deps for the non-registry resolve path and hardens package_selector_matches to short-circuit name@* selectors for non-semver version strings.
crates/aube-resolver/src/resolve/driver.rs Applies apply_package_extensions_to_deps to the non-registry resolution path after attach_integrity_to_git_source, correctly injecting extension dependencies into git/tarball/dir packages.
crates/aube-lockfile/src/pnpm/tests.rs Updates test fixture URLs from a PruvoNet tarball to an astro-hosted tarball — mechanical test data refresh, no logic change.
crates/aube-resolver/src/tests.rs Adds regression tests for wildcard-selector matching on non-semver versions and for apply_package_extensions_to_deps inject-then-authoritative behaviour.

Reviews (4): Last reviewed commit: "fix(workspace): stop root discovery from..." | 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.

Actionable comments posted: 2

🤖 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 `@crates/aube-manifest/src/workspace/config.rs`:
- Around line 816-819: The current implementation collapses unparseable or
unreadable nested pnpm-workspace.yaml files into
WorkspaceYamlKind::SettingsOnly, allowing discovery to continue walking up the
tree. Instead of collapsing the error case at the Err(_) arm in the match
expression, create or use a distinct WorkspaceYamlKind variant that explicitly
represents an invalid or broken YAML state. This distinct state should signal to
stop discovery at that location rather than silently continuing to resolve to a
parent workspace, ensuring parsing and I/O errors are properly surfaced instead
of hidden.

In `@crates/aube/src/state.rs`:
- Around line 325-347: In the `if let Some(path) = lockfile_path` branch, the
code only verifies the root lockfile hash but does not check if member lockfiles
have drifted. In a `sharedWorkspaceLockfile=false` workspace that has both a
root `package.json` and per-member lockfiles, you must also verify member
lockfiles are up-to-date by calling `member_lockfiles_stale(project_dir,
&state)` in addition to checking the root lockfile hash. If the root lockfile
hasn't changed but a member lockfile has drifted, the check will incorrectly
allow the warm path to proceed. Add a check for `member_lockfiles_stale` so that
any member lockfile changes are detected even when the root lockfile remains
unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3471f7e0-4ad9-43d9-93df-5e8e96dae67a

📥 Commits

Reviewing files that changed from the base of the PR and between c0f6b22 and c4f89a6.

📒 Files selected for processing (15)
  • crates/aube-lockfile/src/pnpm/tests.rs
  • crates/aube-manifest/src/workspace.rs
  • crates/aube-manifest/src/workspace/config.rs
  • crates/aube-resolver/src/lib.rs
  • crates/aube-resolver/src/package_ext.rs
  • crates/aube-resolver/src/resolve/driver.rs
  • crates/aube-resolver/src/tests.rs
  • crates/aube-store/src/lib.rs
  • crates/aube/src/commands/install/mod.rs
  • crates/aube/src/commands/install/workspace.rs
  • crates/aube/src/dirs.rs
  • crates/aube/src/state.rs
  • test/filter.bats
  • test/git_deps.bats
  • test/shared_workspace_lockfile_warm_path.bats

Comment thread crates/aube/src/state.rs Resolved
Comment thread crates/aube/src/state.rs Resolved
rubnogueira and others added 5 commits June 14, 2026 15:04
Add WorkspaceYamlKind::Invalid for a present-but-broken workspace yaml.
find_workspace_root now anchors on the member's own directory instead of
silently escaping to an enclosing workspace, so the parse/IO error
surfaces when the config is loaded rather than being hidden.

Also drop the absent-yaml negative cache so a workspace yaml created
mid-process (e.g. by `aube add`) isn't shadowed by a cached default.

Co-authored-by: Cursor <cursoragent@cursor.com>
…lockfile

Under sharedWorkspaceLockfile=false a workspace root that is itself a
package carries its own per-project lockfile, so the warm-path check only
compared the root hash and skipped member lockfiles entirely. Decouple
the member-lockfile staleness check from that branch so member
add/remove/edit still busts the warm path.

Co-authored-by: Cursor <cursoragent@cursor.com>
… a package

Co-authored-by: Cursor <cursoragent@cursor.com>
workspace_yaml_kind warmed the typed config cache while classifying each
ancestor during root discovery. A command that writes a workspace yaml
mid-process (patch-commit, approve-builds, catalog edits) and then
re-reads via WorkspaceConfig::load got the pre-write config back: patch-
commit recorded a patch but the in-process reinstall reused a cached
config without patchedDependencies and silently skipped applying it.
Classify without warming the cache; WorkspaceConfig::load still memoizes
for its own callers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jdx
jdx merged commit f8f06a3 into aubepkg:main Jun 14, 2026
17 checks passed
@cursor cursor Bot mentioned this pull request Jun 14, 2026
@rubnogueira
rubnogueira deleted the fix/member-install-detection branch June 15, 2026 10:39
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