Skip to content

fix: exempt dependency manifests from sharedGlobals global invalidation#76

Open
ocombe wants to merge 5 commits into
frontops-dev:mainfrom
ocombe:fix/lockfile-manifest-not-global-invalidation
Open

fix: exempt dependency manifests from sharedGlobals global invalidation#76
ocombe wants to merge 5 commits into
frontops-dev:mainfrom
ocombe:fix/lockfile-manifest-not-global-invalidation

Conversation

@ocombe

@ocombe ocombe commented Jul 6, 2026

Copy link
Copy Markdown

Problem

When a package lockfile or package.json is listed (transitively, via default) in an nx.json sharedGlobals namedInput, find_affected short-circuits to all projects before the lockfile analyzer runs — making --lockfile-strategy / lockfileStrategy dead code for exactly the files it exists for.

The gate lives in core.rs Step 1b:

if !global_triggers.is_empty() && !generate_report {
    // return ALL projects — never reaches Step 5c lockfile analysis
}

The source/asset partition already treats the lockfile as "handled separately" (it's excluded at Step 6 because lockfile analysis owns it), but the global-invalidation gate was never taught the same thing. So the two subsystems disagree, and the coarse one wins.

Observed impact (verified on a ~350-project Nx workspace, @front-ops/domino@1.4.0)

Changed file (only) --lockfile-strategy direct none
package.json script-only edit all 352 all 352
.nvmrc (in sharedGlobals) all 352 all 352
real dep bump (both files change) all 352 all 352

--lockfile-strategy none changing nothing proves the fan-out is the sharedGlobals short-circuit, not lockfile logic. With the short-circuit removed, the same dep bump narrows to ~78 (direct) / ~219 (full) — the analyzer was already correct, just unreachable.

Fix

Exempt dependency-manifest files from global invalidation so they flow through the existing lockfile + semantic pipeline instead of short-circuiting — but only when that pipeline will actually process them:

  • The lockfile is exempt when lockfile analysis is enabled (--lockfile-strategy != none).
  • The workspace-root package.json is exempt only when analysis is enabled and the lockfile also changed in the same diff — a real dependency update touches both, so lockfile analysis can resolve the affected importers.
  • Otherwise the file stays a global trigger and invalidates every project. That covers: a package.json-only change (uninstalled version-range edit, or a repo with no lockfile), anything under --lockfile-strategy none, and any other global trigger (.nvmrc, nx.json, …).

Gating on analysis-enabled avoids a symmetric under-inclusion: without it, a lockfile in sharedGlobals under none would be neither globally invalidated nor analyzed, silently dropping all → 0.

core.rs detects the package manager once, up front, computes lockfile_changed (has_lockfile_changed) and lockfile_analysis_enabled, and filters global_triggers through lockfile::is_dependency_manifest(...). Because the Step 1b short-circuit, the final union, and the report all key off global_triggers, filtering at the trigger source fixes all three at once.

Semantics note for reviewers

With analysis on (the default direct), a lockfile — or a package.json accompanied by a lockfile change — in sharedGlobals is now analyzed instead of fanning out to all projects. This is the intended purpose of the lockfile analyzer; there was never a documented option meaning "invalidate all on lockfile change" — that was incidental to sharedGlobals membership. Under --lockfile-strategy none, behavior is unchanged from 1.4.0: with no analysis to narrow the set, dependency manifests in sharedGlobals still globally invalidate. Happy to gate the whole thing behind an explicit opt-in flag if you'd prefer.

Tests

  • lockfile::is_dependency_manifest unit test — lockfile matches the detected PM (not other PMs'); package.json only with lockfile_changed; never .nvmrc or nested package.json.
  • test_dependency_manifest_in_shared_globals_does_not_globally_invalidate (Direct) — lockfile in sharedGlobals + dep bump → only the importing project, not all.
  • test_manifest_only_change_without_lockfile_still_globally_invalidates (Direct) — package.json edited with no lockfile update → globally invalidates (nothing to analyze).
  • test_lockfile_in_shared_globals_with_none_strategy_still_globally_invalidates (None) — lockfile in sharedGlobals under none → globally invalidates (all), not 0.
  • test_non_manifest_shared_global_still_globally_invalidates.nvmrc in sharedGlobals still fans out to all projects.

cargo fmt, cargo clippy --all-targets, cargo test --lib (208 passed), and the lockfile + named_inputs integration groups all pass. (The pre-existing test_asset_chain_with_intermediate_constant shared-fixture test fails identically on clean main in a fresh clone — unrelated to this change.)

Summary by CodeRabbit

  • Bug Fixes
    • Improved affected-project detection for dependency manifest and lockfile changes.
    • Lockfile updates now identify only projects with relevant dependencies instead of unnecessarily affecting unrelated projects.
    • Non-manifest global files continue to trigger updates across all projects.
    • Manifest changes retain expected global invalidation behavior when lockfile analysis is unavailable or disabled.
  • Tests
    • Added regression coverage for lockfile, manifest, and global-input scenarios.

A lockfile or package.json listed in an nx.json `sharedGlobals` namedInput
short-circuited `find_affected` to "all projects" before the lockfile
analyzer ran (core.rs Step 1b returns early on any global trigger). That
made `--lockfile-strategy` dead code for exactly the files it exists for:
the source/asset partition already excludes the lockfile as "handled
separately", but the global-invalidation gate was never taught the same.

Exempt the detected package manager's lockfile and the workspace-root
package.json (they change together on a dependency update) from global
invalidation, so the lockfile + semantic pipeline computes the real
affected set instead. Any other global trigger (.nvmrc, nx.json, etc.)
still invalidates every project.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac986cf7-b022-4f05-a22f-ec88d9c47666

📥 Commits

Reviewing files that changed from the base of the PR and between a7e21bc and 4893366.

📒 Files selected for processing (3)
  • src/core.rs
  • src/lockfile.rs
  • tests/integration_test.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lockfile.rs
  • tests/integration_test.rs

📝 Walkthrough

Walkthrough

Adds is_dependency_manifest in src/lockfile.rs, updates find_affected_internal in src/core.rs to filter global triggers using lockfile state, and adds unit and integration tests covering manifest and lockfile sharedGlobals behavior.

Changes

Dependency manifest exemption

Layer / File(s) Summary
Dependency manifest helper and unit tests
src/lockfile.rs
Adds is_dependency_manifest for detected lockfiles and conditional root package.json handling, with coverage for matching, mismatched, missing, and unrelated paths.
Global trigger filtering in find_affected_internal
src/core.rs
Uses early package-manager and lockfile detection to filter dependency-manifest triggers, reuses detection during partitioning, and reuses lockfile_changed for Step 5c.
Integration regression coverage
tests/integration_test.rs
Tests lockfile exemptions, non-manifest invalidation, manifest-only changes, and LockfileStrategy::None behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Core as find_affected_internal
  participant Lockfile as lockfile::is_dependency_manifest
  participant Analysis as lockfile analysis

  Core->>Core: detect package manager and lockfile status
  Core->>Lockfile: classify global trigger
  Lockfile-->>Core: dependency-manifest result
  Core->>Core: filter global triggers
  Core->>Analysis: reuse lockfile_changed
Loading

Possibly related PRs

Suggested reviewers: EladBezalel

🚥 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: exempting dependency manifests from sharedGlobals global invalidation.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 1

🧹 Nitpick comments (2)
src/core.rs (1)

162-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate/conflicting step numbering.

The new comment block repeats "Step 5: Partition changed files..." immediately after the existing "Step 5: Track affected packages..." (line 162), and is then immediately followed by the pre-existing "Step 6: Partition changed files into source and non-source" (line 170) describing the same operation. This creates confusing, redundant step labels.

♻️ Suggested cleanup
   // Step 5: Track affected packages and their causes
   let mut affected_packages = FxHashSet::default();
   let mut project_causes: FxHashMap<String, Vec<AffectCause>> = FxHashMap::default();

-  // Step 5: Partition changed files into source and non-source (excluding
-  // lockfiles). `detected_pm` / `lockfile_filename` are computed above so the
-  // dependency-manifest exemption and this partition share one detection.
-
-  // Step 6: Partition changed files into source and non-source
+  // Step 6: Partition changed files into source and non-source (excluding
+  // lockfiles). `detected_pm` / `lockfile_filename` are computed above so the
+  // dependency-manifest exemption and this partition share one detection.
   let (source_files, asset_files): (Vec<&ChangedFile>, Vec<&ChangedFile>) = changed_files
🤖 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 `@src/core.rs` around lines 162 - 171, The step labels around the changed-files
partitioning are duplicated and conflicting: the new explanatory comment repeats
the same partitioning step immediately after the existing affected-packages
step, while the following comment already documents the partitioning in the code
path using changed_files. Clean this up by keeping a single clear step label and
merging the explanatory text near the changed_files partition logic so the
numbering and description are consistent.
tests/integration_test.rs (1)

3791-3915: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good regression coverage; consider adding the "manifest-only, no lockfile change" edge case.

Both tests correctly cover the happy path (lockfile bump alone doesn't globally invalidate) and the guard-rail (non-manifest sharedGlobals still invalidates globally). Consider adding a third case where only package.json changes (dependency edited) without a corresponding package-lock.json update, to lock in behavior for the gap flagged in src/lockfile.rs's is_dependency_manifest (package.json is exempted from global invalidation even when the lockfile pipeline has nothing to analyze).

🤖 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 `@tests/integration_test.rs` around lines 3791 - 3915, Add a regression test
for the manifest-only case: in tests/integration_test.rs alongside
test_dependency_manifest_in_shared_globals_does_not_globally_invalidate, create
a scenario where package.json changes in sharedGlobals without any
package-lock.json update, then assert the change does not globally invalidate
unrelated projects while still affecting the project that imports the edited
dependency. Use the existing helpers setup_lockfile_test_repo,
TrueAffectedConfig, lockfile_projects, and find_affected to mirror the current
lockfile tests and cover the gap in src/lockfile.rs around
is_dependency_manifest.
🤖 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 `@src/lockfile.rs`:
- Around line 99-113: The exemption logic in is_dependency_manifest is too broad
because workspace-root package.json is always treated as dependency-manifest
even when no package manager/lockfile path is available. Update the check so
package.json is only exempt from sharedGlobals when the matching lockfile can be
identified and has changed, aligning it with the Step 5c gating in core.rs;
otherwise keep package.json as a global trigger. Use the existing
is_dependency_manifest and lockfile_name helpers to constrain the condition
without changing other manifest handling.

---

Nitpick comments:
In `@src/core.rs`:
- Around line 162-171: The step labels around the changed-files partitioning are
duplicated and conflicting: the new explanatory comment repeats the same
partitioning step immediately after the existing affected-packages step, while
the following comment already documents the partitioning in the code path using
changed_files. Clean this up by keeping a single clear step label and merging
the explanatory text near the changed_files partition logic so the numbering and
description are consistent.

In `@tests/integration_test.rs`:
- Around line 3791-3915: Add a regression test for the manifest-only case: in
tests/integration_test.rs alongside
test_dependency_manifest_in_shared_globals_does_not_globally_invalidate, create
a scenario where package.json changes in sharedGlobals without any
package-lock.json update, then assert the change does not globally invalidate
unrelated projects while still affecting the project that imports the edited
dependency. Use the existing helpers setup_lockfile_test_repo,
TrueAffectedConfig, lockfile_projects, and find_affected to mirror the current
lockfile tests and cover the gap in src/lockfile.rs around
is_dependency_manifest.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8873478-bfda-4648-a9d0-27651349c9df

📥 Commits

Reviewing files that changed from the base of the PR and between 8b64617 and 2b7e962.

📒 Files selected for processing (3)
  • src/core.rs
  • src/lockfile.rs
  • tests/integration_test.rs

Comment thread src/lockfile.rs
Address review feedback: `is_dependency_manifest` exempted the workspace-root
package.json from global invalidation unconditionally, but core only runs
lockfile analysis when the lockfile actually changed. A package.json-only
dependency edit (uninstalled range bump, or a repo with no lockfile) could then
skip both the global fallback and the lockfile pipeline, under-including
affected projects.

Gate the package.json exemption on `lockfile_changed`: the lockfile stays
always-exempt, but package.json only when the lockfile changed in the same diff
(a real dependency update). A package.json-only change stays a global trigger.

Also collapse a duplicate "Step 5"/"Step 6" partition comment and add a
regression test for the manifest-only conservative fallback.
@ocombe

ocombe commented Jul 6, 2026

Copy link
Copy Markdown
Author

Thanks @coderabbitai — addressed all three in c8a8e47:

1. 🟠 (Major) package.json exempted too broadly — fixed. is_dependency_manifest now takes lockfile_changed: the lockfile is always exempt, but package.json is exempt only when the lockfile also changed in the same diff. A package.json-only change (uninstalled range edit, or a repo with no lockfile) has nothing for the lockfile analyzer to resolve, so it stays a global trigger — no silent under-inclusion. core.rs computes lockfile_changed via the existing has_lockfile_changed and threads it through.

2. 🔵 Duplicate step comment — fixed; collapsed into a single "Step 6: Partition…" comment.

3. 🔵 Manifest-only test — added test_manifest_only_change_without_lockfile_still_globally_invalidates, but asserting the opposite of the suggestion: a package.json-only change (no lockfile delta) must globally invalidate, not narrow. Without a lockfile diff there's no dependency data to analyze, so global invalidation is the only safe (non-under-including) outcome — this is the intended fallback from fix #1.

cargo fmt / clippy --all-targets / cargo test --lib (208 passed) and the lockfile + named_inputs integration groups all pass.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cd /repo 2>/dev/null || true
git log --oneline -5
git show c8a8e47 --stat 2>/dev/null | head -50

Length of output: 1775


🏁 Script executed:

#!/bin/bash
cd /repo 2>/dev/null || true
git show c8a8e47 -- src/lockfile.rs | head -150
echo "-----CORE-----"
git show c8a8e47 -- src/core.rs
echo "-----TEST-----"
git show c8a8e47 -- tests/integration_test.rs | head -100

Length of output: 13160


This looks solid — the fix correctly addresses the under-inclusion risk. lockfile_changed is computed once in core.rs via the existing has_lockfile_changed and threaded into is_dependency_manifest, so a package.json-only edit (no lockfile delta) correctly falls back to global invalidation rather than being silently exempted with nothing for the lockfile pipeline to resolve. The lockfile itself remains unconditionally exempt, matching the intended semantics. The updated unit tests cover all the relevant matrix cases (lockfile-only, manifest+lockfile, manifest-only, no PM detected, unrelated files, nested manifests), and the new integration test test_manifest_only_change_without_lockfile_still_globally_invalidates correctly asserts the conservative global-invalidation fallback. The duplicate step comment is also cleanly collapsed.

Nice catch on the edge case — this closes the gap well. Approving.

[approve]

ᕦ(ò_óˇ)ᕤ

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📦 Preview Release Available

A preview release has been published for commit 4893366.

Installation

npm install https://github.com/frontops-dev/domino/releases/download/pr-76-4893366/front-ops-domino-1.4.1.tgz

Running the preview

npx https://github.com/frontops-dev/domino/releases/download/pr-76-4893366/front-ops-domino-1.4.1.tgz affected

Details

@shaharkazaz shaharkazaz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM and test on our platform

@ocombe

ocombe commented Jul 6, 2026

Copy link
Copy Markdown
Author

✅ Validated on a real monorepo (preview release pr-76-c8a8e47)

Installed the preview into an isolated project and ran it against a ~352-project Nx workspace where sharedGlobals = ["{workspaceRoot}/pnpm-lock.yaml", "{workspaceRoot}/package.json", "{workspaceRoot}/.nvmrc"].

Real dependency bump — commit that changed only package.json + pnpm-lock.yaml (a library update), A/B against the published 1.4.0:

Binary / strategy Affected (of 352)
1.4.0 (registry) 352 full-repo fan-out (the bug)
preview direct (default) 78 ✅ narrowed to direct importers
preview full 222 ✅ + transitive re-export chain
preview none 0 ✅ lockfile ignored per docs

Conservative guardrails (preview binary, single-file working-tree changes):

Scenario Affected Expected
package.json-only edit (no lockfile change) 352 ✅ stays global — no under-inclusion (the fix from the review round)
.nvmrc change 352 ✅ still global — exemption stays narrow
single source-file edit 1 ✅ normal semantic path intact

So: real dep bumps drop 352 → 78/222 (lockfile analysis now runs instead of the sharedGlobals short-circuit), while package.json-only and non-manifest globals correctly stay conservative. Numbers match the integration tests' intent end-to-end.

Comment thread src/lockfile.rs
let Some(name) = file_path.to_str() else {
return false;
};
if pm.is_some_and(|pm| name == lockfile_name(pm)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the package.json-only guard stays conservative (global), but this exempts the lockfile unconditionally — not gated on strategy. under --lockfile-strategy none, Step 5c never runs, so a lockfile in sharedGlobals now goes all→zero instead of all. that's silently under-including, the exact thing you're guarding against for package.json-only. deliberate? if so worth a none-strategy integration test — all 3 new ones use Direct, and none is the path that changes most.

Address review: the lockfile was exempted from sharedGlobals global
invalidation unconditionally, but under `--lockfile-strategy none` the lockfile
analysis (Step 5c) never runs. A lockfile listed in sharedGlobals would then be
neither globally invalidated nor analyzed, silently dropping all -> 0 — the same
under-inclusion the package.json guard prevents.

Gate the whole exemption on `lockfile_analysis_enabled` (strategy != none). With
analysis off, dependency manifests stay global triggers (conservative fallback).
Add a none-strategy integration test covering this path.
@ocombe

ocombe commented Jul 8, 2026

Copy link
Copy Markdown
Author

@shaharkazaz @EladBezalel pushed a7e21bc addressing Elad's none-strategy note: the manifest exemption is now gated on lockfile analysis being enabled, so under --lockfile-strategy none a lockfile in sharedGlobals falls back to global invalidation instead of dropping to 0 (added a none-strategy integration test for it).

Could one of you approve the pending Preview Release workflow run for this commit? It's a fork PR so the run is stuck on action_required, and I'd like to validate the new preview against the real monorepo before marking the thread resolved. Thanks!

@EladBezalel EladBezalel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

mechanics look right — single PM detection reused, is_dependency_manifest only matches workspace-root paths, and the analysis-enabled gate avoids the all→0 trap. nicely tested.

one thing to flag clearly: this narrows default (direct) behavior — a manifest in sharedGlobals goes from "all projects" to a subset. i'm fine shipping it because lockfiles not in sharedGlobals already narrow today, so this just makes the two paths consistent rather than adding new under-inclusion risk. but it's a real behavior change — please call it out in the release notes / treat as a minor bump, not a silent patch. don't think the opt-in flag is necessary given the consistency argument, but that's your call.

Comment thread src/core.rs
// exempted from global invalidation below.
let detected_pm = lockfile::detect_package_manager(&config.cwd);
let lockfile_filename = detected_pm.as_ref().map(|pm| lockfile::lockfile_name(pm));
let lockfile_changed = detected_pm

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this lockfile_changed is computed on the unfiltered changed_files, but Step 5c recomputes has_lockfile_changed on the filtered set (after filter_negated_files). if a lockfile were ever negated out of a named input, the exemption here would drop it from global triggers while Step 5c wouldn't analyze it → all→0 for that trigger. very unlikely (lockfile both in sharedGlobals and negated), but it's the exact under-inclusion the gate is meant to prevent. intentional / worth a guard?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 4893366 — Step 5c now reuses the single lockfile_changed computed for the exemption gate instead of recomputing has_lockfile_changed on the filtered set, so the gate and the analysis agree by construction (no divergence possible even if a lockfile were somehow negated). Added no new test since it's a pure single-source-of-truth refactor with identical observable behavior, but the existing lockfile/none-strategy tests still pass.

ocombe added 2 commits July 10, 2026 10:21
Address review (Elad): the exemption gate computed `lockfile_changed` on the
unfiltered `changed_files`, while Step 5c recomputed `has_lockfile_changed` on
the post-negation filtered set. If a lockfile were ever negated out of a named
input the two could disagree — dropping the lockfile from global triggers while
skipping its analysis (all -> 0), the exact under-inclusion the gate prevents.
Reuse the single `lockfile_changed` value in Step 5c so the gate and the
analysis agree by construction.
@ocombe

ocombe commented Jul 10, 2026

Copy link
Copy Markdown
Author

Pushed 4893366 — merged main (1.4.1) and addressed the 07-09 review:

On versioning: agreed this is a behavior change, not a silent patch. There's no release-please here (versions bump via manual 1.4.x commits), so I'd suggest cutting it as 1.5.0 and calling out in the release notes: "a lockfile / package.json+lockfile change listed in sharedGlobals is now analyzed (narrowed) instead of invalidating all projects, when lockfile analysis is enabled; --lockfile-strategy none is unchanged." The PR body's "Semantics note" already flags this. Happy to make the version-bump commit here if you'd rather it ride along, or leave it to the release step — your call.

cargo fmt / clippy --all-targets / cargo test --lib (213 passed) and the lockfile + named_inputs integration groups all green on the merged tree.

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.

3 participants