fix(deps-status): detect lockfile-only changes#12106
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughTwo bugs are fixed in ChangesLockfile Modification Detection Fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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. Comment |
PR Summary by Qodofix(deps-status): detect lockfile-only changes in optimistic install check WalkthroughsDescription• Fix checkDepsStatus falsely returning upToDate: true when only pnpm-lock.yaml changed (no manifest modifications), causing pnpm install --optimisticRepeatInstall to skip validation. • Fix checkDepsStatus hardcoding pnpm-lock.yaml path instead of using the branch-specific lockfile name when useGitBranchLockfile is enabled. • Optimize getCurrentBranch to read .git/HEAD directly instead of spawning a git symbolic-ref subprocess, and propagate cwd through the entire lockfile-name resolution chain. • Merge the conflict-detection and lockfile-modification stat passes into a single scanWantedLockfiles loop to reduce redundant synchronous I/O on the fast path. Diagramgraph TD
A(["checkDepsStatus"]) --> B["scanWantedLockfiles"]
A --> C["getWantedLockfileName"]
C --> D["getCurrentBranch"]
D --> E["readBranchFromHeadFile"]
D -->|fallback| F["git symbolic-ref"]
B --> G[("pnpm-lock.yaml / pnpm-lock.branch.yaml")]
A --> H["readWantedLockfile"]
A --> I["workspaceState"]
subgraph Legend
direction LR
_svc(["Function"]) ~~~ _db[("File/State")] ~~~ _ext["External Process"]
end
High-Level AssessmentThe PR's approach is optimal. Checking lockfile mtimes in the same synchronous stat loop that already checks for merge conflicts (merged into File ChangesEnhancement (3)
Bug fix (1)
Tests (4)
Documentation (1)
Other (2)
|
There was a problem hiding this comment.
Pull request overview
Fixes optimisticRepeatInstall incorrectly short-circuiting pnpm install as “Already up to date” when only pnpm-lock.yaml changed by ensuring wanted-lockfile mtimes are considered before the optimistic fast path in @pnpm/deps.status.
Changes:
- Detect wanted lockfile modifications (or missing lockfiles) via mtime checks before returning
upToDate: true. - When a wanted lockfile changed, validate dependency status for all projects (not only manifest-modified ones).
- Add a regression test and a patch changeset for
@pnpm/deps.statusandpnpm.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| deps/status/src/checkDepsStatus.ts | Adds wanted-lockfile mtime detection to prevent the optimistic fast path from skipping lockfile validation. |
| deps/status/test/checkDepsStatus.test.ts | Adds a regression test intended to cover “lockfile-only changed” behavior. |
| .changeset/fix-optimistic-repeat-install-lockfile.md | Patch changeset documenting the lockfile-only change fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
deps/status/src/checkDepsStatus.ts (1)
267-284: ⚡ Quick winReuse the already-computed lockfile dirs.
getWantedLockfileDirs(...)is invoked here with the exact same arguments as in thefindConflictedLockfileDircall at Line 172. Compute it once and reuse to avoid the duplicated literal drifting out of sync. The newlockfilesModifiedgating itself looks correct.♻️ Hoist the shared
lockfileDirsOutside the changed segment, at Lines 172-178:
- const conflictedLockfileDir = findConflictedLockfileDir(getWantedLockfileDirs({ - allProjects, - lockfileDir, - rootProjectManifestDir, - sharedWorkspaceLockfile, - workspaceDir, - }), workspaceState.lastValidatedTimestamp) + const lockfileDirs = getWantedLockfileDirs({ + allProjects, + lockfileDir, + rootProjectManifestDir, + sharedWorkspaceLockfile, + workspaceDir, + }) + const conflictedLockfileDir = findConflictedLockfileDir(lockfileDirs, workspaceState.lastValidatedTimestamp)Then within the changed segment:
- const lockfileDirs = getWantedLockfileDirs({ - allProjects, - lockfileDir, - rootProjectManifestDir, - sharedWorkspaceLockfile, - workspaceDir, - }) const lockfilesModified = lockfileDirs.some((wantedLockfileDir) => { const wantedLockfileStats = safeStatSync(path.join(wantedLockfileDir, WANTED_LOCKFILE)) return wantedLockfileStats == null || wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp })🤖 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 `@deps/status/src/checkDepsStatus.ts` around lines 267 - 284, Duplicate call to getWantedLockfileDirs: compute lockfileDirs once and reuse it instead of calling getWantedLockfileDirs(...) twice; hoist the single invocation into a shared variable used by both the earlier findConflictedLockfileDir(...) logic and the later lockfilesModified computation (the variable name lockfileDirs, and the functions getWantedLockfileDirs and findConflictedLockfileDir are the anchors to change), then use that lockfileDirs when building lockfilesModified and when passing into any conflict-checking code so the arguments stay in sync.
🤖 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 `@deps/status/src/checkDepsStatus.ts`:
- Around line 267-284: Duplicate call to getWantedLockfileDirs: compute
lockfileDirs once and reuse it instead of calling getWantedLockfileDirs(...)
twice; hoist the single invocation into a shared variable used by both the
earlier findConflictedLockfileDir(...) logic and the later lockfilesModified
computation (the variable name lockfileDirs, and the functions
getWantedLockfileDirs and findConflictedLockfileDir are the anchors to change),
then use that lockfileDirs when building lockfilesModified and when passing into
any conflict-checking code so the arguments stay in sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f5904492-c1b5-4832-93ca-40f4540a19e0
📒 Files selected for processing (3)
.changeset/fix-optimistic-repeat-install-lockfile.mddeps/status/src/checkDepsStatus.tsdeps/status/test/checkDepsStatus.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)
Files:
deps/status/src/checkDepsStatus.tsdeps/status/test/checkDepsStatus.test.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use util.types.isNativeError() instead of instanceof Error for error type checking in Jest tests
Files:
deps/status/test/checkDepsStatus.test.ts
🧠 Learnings (2)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.
Applied to files:
deps/status/src/checkDepsStatus.tsdeps/status/test/checkDepsStatus.test.ts
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.
Applied to files:
.changeset/fix-optimistic-repeat-install-lockfile.md
🔇 Additional comments (3)
deps/status/src/checkDepsStatus.ts (1)
351-352: LGTM!deps/status/test/checkDepsStatus.test.ts (1)
492-575: LGTM!.changeset/fix-optimistic-repeat-install-lockfile.md (1)
1-6: LGTM!
188bad2 to
12e1e4a
Compare
Code Review by Qodo
1. Missing lockfile bypasses check
|
12e1e4a to
c45b7b5
Compare
|
Code review by qodo was updated up to the latest commit c45b7b5 |
|
Code review by qodo was updated up to the latest commit d10b1b8 |
There was a problem hiding this comment.
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 `@deps/status/src/checkDepsStatus.ts`:
- Around line 677-699: In the scanWantedLockfiles function, the call to
wantedLockfileHasMergeConflictsSync is only passing lockfileDir but is missing
the wantedLockfileName parameter. This causes the function to always check the
hardcoded pnpm-lock.yaml file regardless of the actual lockfile name being
scanned. Update the wantedLockfileHasMergeConflictsSync call to pass both
lockfileDir and wantedLockfileName as arguments so it can correctly check merge
conflicts in the dynamic lockfile name (such as pnpm-lock.<branch>.yaml when
useGitBranchLockfile is enabled).
🪄 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: CHILL
Plan: Pro Plus
Run ID: 185ba52a-45c0-4814-a5b9-6f31ece4c188
📒 Files selected for processing (6)
.changeset/fix-optimistic-repeat-install-lockfile.mdcspell.jsondeps/status/src/checkDepsStatus.tsdeps/status/test/checkDepsStatus.test.tseslint.config.mjsinstalling/commands/src/installDeps.ts
✅ Files skipped from review due to trivial changes (3)
- eslint.config.mjs
- .changeset/fix-optimistic-repeat-install-lockfile.md
- cspell.json
🚧 Files skipped from review as they are similar to previous changes (1)
- deps/status/test/checkDepsStatus.test.ts
0c38f98 to
d10b1b8
Compare
d10b1b8 to
dd40790
Compare
1 similar comment
afa9b84 to
966ad4c
Compare
… assertions - Hoist getWantedLockfileDirs into a shared lockfileDirs variable used by both findConflictedLockfileDir and lockfilesModified computation - Fix lockfile-only-change test to exercise the assertLockfilesEqual mismatch path instead of the lockfile-not-found path - Assert the actual issue message instead of readWantedLockfile call count - Update e2e test expectations for the updated debug messages
bench-work-env/ contains local benchmark clones (gitignored) that triggered false-positive eslint parser errors and wasted cspell scans.
checkDepsStatus hardcoded pnpm-lock.yaml for all stat and readWantedLockfile calls, which broke when useGitBranchLockfile is enabled: the wanted lockfile lives at pnpm-lock.<branch>.yaml, so stats on pnpm-lock.yaml returned ENOENT and could cascade into RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND. Compute the wanted lockfile name once via getWantedLockfileName and thread it through scanWantedLockfiles, missingWantedLockfileStandIn, all stat calls, and all readWantedLockfile calls. Also merge the conflict-detection and lockfilesModified stat passes into a single scanWantedLockfiles loop to avoid redundant synchronous I/O on the optimistic fast-path.
getCurrentBranch now reads .git/HEAD directly instead of spawning 'git symbolic-ref --short HEAD'. This eliminates a process spawn on every verify-deps-before-run check in git-branch lockfile mode. Also adds cwd support throughout the branch resolution chain: - getCurrentBranch accepts cwd (already existed, now used by the file reader) - getWantedLockfileName accepts and forwards cwd - checkDepsStatus passes workspaceDir as cwd so the branch is resolved in the repo that owns the lockfile, not process.cwd() The file-based reader handles the standard .git/HEAD layout and git worktrees (.git as a file with gitdir pointer), falling back to the git command only when .git/HEAD can't be read.
…changes Merge-conflict detection now reads the resolved wanted lockfile name instead of always reading pnpm-lock.yaml, so conflicts in pnpm-lock.<branch>.yaml are detected. With mergeGitBranchLockfiles enabled, every pnpm-lock.*.yaml is scanned for modifications and conflicts, matching what readWantedLockfile merges. Also harden the detached-HEAD git-utils test against a global commit.gpgsign setting and add gitdir to the cspell dictionary.
9279fb1 to
ab484b8
Compare
|
Code review by qodo was updated up to the latest commit ab484b8 |
scanWantedLockfiles ignored ENOENT, so a missing wanted lockfile did not block the optimistic early-exit and checkDepsStatus could report upToDate=true without validating lockfile presence. This was inconsistent with the slow path, which throws RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND for missing git-branch and per-project lockfiles. Track missing lockfiles in the scan and only skip the full check when the current lockfile can stand in for a missing shared workspace lockfile; otherwise fall through so the existing checks report the missing lockfile.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
deps/status/src/checkDepsStatus.ts (1)
338-342:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMerged branch-lockfile updates can still bypass current-vs-wanted equality checks.
assertLockfilesEqualis gated bywantedLockfileStatsonly. WithmergeGitBranchLockfiles, anotherpnpm-lock.*.yamlcan change, setlockfilesModified, and still skip equality validation, leading to stale installed deps being reported up-to-date.Suggested fix
- if (wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp) { + if (lockfilesModified || wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp) { const currentLockfile = await readCurrentLockfile(path.join(workspaceDir, 'node_modules/.pnpm'), { ignoreIncompatible: false }) const wantedLockfile = (await wantedLockfilePromise) ?? throwLockfileNotFound(workspaceDir) assertLockfilesEqual(currentLockfile, wantedLockfile, workspaceDir) }- if (wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp) { + if (lockfilesModified || wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp) { const currentLockfile = await readCurrentLockfile(path.join(wantedLockfileDir, 'node_modules/.pnpm'), { ignoreIncompatible: false }) const wantedLockfile = (await wantedLockfilePromise) ?? throwLockfileNotFound(wantedLockfileDir) assertLockfilesEqual(currentLockfile, wantedLockfile, wantedLockfileDir) }Also applies to: 358-362, 391-392
🤖 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 `@deps/status/src/checkDepsStatus.ts` around lines 338 - 342, The condition gating the assertLockfilesEqual call only checks if wantedLockfileStats.mtime is newer than the lastValidatedTimestamp, but it does not account for changes to branch lockfiles merged via mergeGitBranchLockfiles. Update the condition that gates the assertLockfilesEqual invocation to also check the lockfilesModified flag, so that equality validation is triggered whenever either the wanted lockfile or any merged branch lockfiles have been modified. This same fix should be applied at the other affected locations mentioned in the comment (around lines 358-362 and 391-392) where similar guard conditions exist.
🤖 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 `@deps/status/src/checkDepsStatus.ts`:
- Around line 702-704: The catch block in checkDepsStatus.ts currently treats a
missing lockfile (ENOENT error) by simply continuing execution, allowing the
fast path to return up-to-date status when a wanted lockfile has disappeared.
Instead of continuing on ENOENT errors, you should treat missing wanted
lockfiles as a modification signal that indicates dependencies have changed.
Change the error handling logic to either break out of the current control flow
or set a flag that prevents the fast path from returning up-to-date status,
ensuring that a missing lockfile is properly detected as a modification rather
than silently ignored.
- Around line 721-723: The catch block in the area around branchLockfileNames
assignment is swallowing all errors indiscriminately and setting
branchLockfileNames to an empty list, which masks important errors like
permission or IO failures. Instead of a blanket catch, check the error code
property to determine if it is an ENOENT error (file not found). Only set
branchLockfileNames to an empty list if the error is specifically ENOENT, and
re-throw or handle other errors appropriately so that permission and IO failures
are not silently converted to an empty list.
---
Outside diff comments:
In `@deps/status/src/checkDepsStatus.ts`:
- Around line 338-342: The condition gating the assertLockfilesEqual call only
checks if wantedLockfileStats.mtime is newer than the lastValidatedTimestamp,
but it does not account for changes to branch lockfiles merged via
mergeGitBranchLockfiles. Update the condition that gates the
assertLockfilesEqual invocation to also check the lockfilesModified flag, so
that equality validation is triggered whenever either the wanted lockfile or any
merged branch lockfiles have been modified. This same fix should be applied at
the other affected locations mentioned in the comment (around lines 358-362 and
391-392) where similar guard conditions exist.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 4c7315de-c1aa-40c1-9f6a-f08064fd506e
📒 Files selected for processing (15)
.changeset/fix-optimistic-repeat-install-lockfile.mdcspell.jsondeps/status/src/checkDepsStatus.tsdeps/status/test/checkDepsStatus.test.tseslint.config.mjsinstalling/commands/src/installDeps.tslockfile/fs/src/gitBranchLockfile.tslockfile/fs/src/index.tslockfile/fs/src/lockfileName.tslockfile/fs/src/read.tslockfile/fs/test/gitBranchLockfile.test.tslockfile/fs/test/lockfileName.test.tsnetwork/git-utils/src/index.tsnetwork/git-utils/test/index.test.tspnpm/test/verifyDepsBeforeRun/multiProjectWorkspace.ts
✅ Files skipped from review due to trivial changes (3)
- eslint.config.mjs
- cspell.json
- pnpm/test/verifyDepsBeforeRun/multiProjectWorkspace.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- installing/commands/src/installDeps.ts
- lockfile/fs/src/lockfileName.ts
- lockfile/fs/test/lockfileName.test.ts
- network/git-utils/test/index.test.ts
- .changeset/fix-optimistic-repeat-install-lockfile.md
- network/git-utils/src/index.ts
gitBranchLockfileNames only swallows ENOENT now, so permission/IO errors no longer hide lockfile changes by returning an empty list. readBranchFromHeadFile only reads .git when it is a regular file; a FIFO/device .git in an attacker-controlled working directory no longer risks a synchronous-read hang before falling back to git symbolic-ref.
|
Code review by qodo was updated up to the latest commit 389e0a0 |
…at install Port of the pnpm-side fix for pnpm#12100. The optimistic fast path exited on manifest mtimes alone, so a lockfile-only change (a git checkout or stash-restore of just pnpm-lock.yaml, or an external rewrite) was skipped and reported as up to date. check_optimistic_repeat_install now probes the wanted lockfile mtime (wanted_lockfile_modified) before the manifest-mtime exit, and validates every project rather than just the modified ones when the lockfile changed (mirroring upstream's projectsToCheck = lockfilesModified ? all : modified). The git-branch-lockfile parts of the upstream change are not ported: useGitBranchLockfile/mergeGitBranchLockfiles are not implemented in pacquet (listed in pnpm_default_parity NOT_PORTED).
|
Code review by qodo was updated up to the latest commit 2ff7568 |
The matcher left its dots unescaped, so files like pnpm-lockXmainYyaml could be treated as branch lockfiles and, with mergeGitBranchLockfiles, deleted by cleanGitBranchLockfiles. Require literal dots and a non-empty branch segment (pnpm-lock.<branch>.yaml).
|
Code review by qodo was updated up to the latest commit 3733a29 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs (1)
1438-1458: ⚡ Quick winAdd the workspace-mode variant for this regression.
This covers the single-project branch, but the production change also altered the workspace-state path via
is_workspace_install. A second assertion withcontent_check_decision(..., true, ...)would catch regressions in the workspace-specific lockfile comparison/refresh behavior that originally drives the optimistic fast path. As per coding guidelines, pacquet should match the TypeScript pnpm behavior for the same feature.Suggested additional test
#[test] fn returns_skipped_when_only_the_lockfile_changed() { let (dir, config) = setup_content_check_project(); // Rewrite only the wanted lockfile; package.json keeps its original // (pre-state) mtime, so `modifiedProjects` is empty. fs::write(dir.path().join(Lockfile::FILE_NAME), FOO_LOCKFILE.replace("1.0.0", "1.0.1")) .unwrap(); let manifest = PackageManifest::from_path(dir.path().join("package.json")).unwrap(); let decision = content_check_decision(&dir, config, false, &[(dir.path().to_path_buf(), &manifest)]); assert!( matches!(decision, Decision::Skipped { reason } if reason.contains("not up to date")), "expected Skipped(outdated deps), got {decision:?}", ); } + +#[test] +fn returns_skipped_in_workspace_when_only_the_lockfile_changed() { + let (dir, config) = setup_content_check_project(); + + fs::write(dir.path().join(Lockfile::FILE_NAME), FOO_LOCKFILE.replace("1.0.0", "1.0.1")) + .unwrap(); + let manifest = PackageManifest::from_path(dir.path().join("package.json")).unwrap(); + + let decision = + content_check_decision(&dir, config, true, &[(dir.path().to_path_buf(), &manifest)]); + assert!( + matches!(decision, Decision::Skipped { reason } if reason.contains("not up to date")), + "expected Skipped(outdated deps), got {decision:?}", + ); +}🤖 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 `@pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs` around lines 1438 - 1458, The test `returns_skipped_when_only_the_lockfile_changed` currently only tests the single-project branch by passing `false` as the fourth parameter to `content_check_decision`. To catch regressions in the workspace-specific lockfile comparison behavior that the production change also affected, add another assertion in this test that calls `content_check_decision` with `true` for the is_workspace_install parameter, using the same setup and expecting the same Decision::Skipped result. This ensures both the single-project and workspace-mode paths correctly handle the lockfile-only change scenario.Source: Coding guidelines
🤖 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 `@pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs`:
- Around line 1438-1458: The test
`returns_skipped_when_only_the_lockfile_changed` currently only tests the
single-project branch by passing `false` as the fourth parameter to
`content_check_decision`. To catch regressions in the workspace-specific
lockfile comparison behavior that the production change also affected, add
another assertion in this test that calls `content_check_decision` with `true`
for the is_workspace_install parameter, using the same setup and expecting the
same Decision::Skipped result. This ensures both the single-project and
workspace-mode paths correctly handle the lockfile-only change scenario.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3cbdadcb-8558-44e9-9aed-1c4eb7c08c36
📒 Files selected for processing (4)
lockfile/fs/src/gitBranchLockfile.tslockfile/fs/test/gitBranchLockfile.test.tspacquet/crates/package-manager/src/optimistic_repeat_install.rspacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- lockfile/fs/test/gitBranchLockfile.test.ts
- lockfile/fs/src/gitBranchLockfile.ts
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #12106 +/- ##
=======================================
Coverage 88.03% 88.03%
=======================================
Files 309 309
Lines 41595 41603 +8
=======================================
+ Hits 36618 36626 +8
Misses 4977 4977 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.2068015799400005,
"stddev": 0.12660037776181793,
"median": 4.2469908706399995,
"user": 4.00662152,
"system": 3.45263848,
"min": 3.99276941964,
"max": 4.39943427164,
"times": [
4.24419913764,
3.99276941964,
4.39943427164,
4.24978260364,
4.27474068564,
4.14512459664,
4.04781994264,
4.25815464264,
4.12587908564,
4.33011141364
]
},
{
"command": "pacquet@main",
"mean": 4.11496992304,
"stddev": 0.11394487431845444,
"median": 4.08321478264,
"user": 3.9862212199999996,
"system": 3.4149873800000003,
"min": 4.00768085664,
"max": 4.36347633264,
"times": [
4.09568471464,
4.0315439706400005,
4.06926533064,
4.00768085664,
4.12978361264,
4.36347633264,
4.26027170564,
4.01077953664,
4.07074485064,
4.11046831964
]
},
{
"command": "pnpr@HEAD",
"mean": 2.12257302214,
"stddev": 0.13923620656474747,
"median": 2.0698269821400004,
"user": 2.7265339199999996,
"system": 2.8879116799999993,
"min": 1.96951352064,
"max": 2.33906274464,
"times": [
1.9840867886400002,
2.32323601464,
2.0971854906400003,
2.33906274464,
2.2570684236400003,
1.96951352064,
2.04246847364,
2.15853474764,
2.01894933264,
2.03562468464
]
},
{
"command": "pnpr@main",
"mean": 2.1099714160400005,
"stddev": 0.1397411402439283,
"median": 2.04487801564,
"user": 2.69520062,
"system": 2.8784868800000005,
"min": 1.9746937316400002,
"max": 2.2984918806400003,
"times": [
2.2984918806400003,
2.0385787626400003,
1.9938508086400002,
1.9855400256400002,
1.9746937316400002,
2.28820759064,
2.2841821326400003,
1.9884307586400003,
2.19656120064,
2.05117726864
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6268701072,
"stddev": 0.00585899458235506,
"median": 0.6272391964,
"user": 0.37025768000000003,
"system": 1.3204679000000001,
"min": 0.6184690829,
"max": 0.6340010719,
"times": [
0.6210669759,
0.6315836919,
0.6184690829,
0.6273806049,
0.6196204099,
0.6270977879,
0.6239098999,
0.6340010719,
0.6328341439,
0.6327374029
]
},
{
"command": "pacquet@main",
"mean": 0.6392451951000001,
"stddev": 0.053130425709497324,
"median": 0.6208392444,
"user": 0.36920687999999996,
"system": 1.3177352999999998,
"min": 0.6036416629,
"max": 0.7846217539,
"times": [
0.6352824339,
0.6167864129,
0.6036416629,
0.6088070519000001,
0.6177582389,
0.6239202499000001,
0.6356864919,
0.6525847329,
0.7846217539,
0.6133629219
]
},
{
"command": "pnpr@HEAD",
"mean": 0.7555108369,
"stddev": 0.0813492298138147,
"median": 0.7290268579000001,
"user": 0.39334848,
"system": 1.4018450999999998,
"min": 0.7101418339000001,
"max": 0.9832631349000001,
"times": [
0.7221489639,
0.7298316729000001,
0.7101418339000001,
0.7243429199,
0.7164397369000001,
0.7352519459,
0.7282220429,
0.9832631349000001,
0.7430154959,
0.7624506219
]
},
{
"command": "pnpr@main",
"mean": 0.6802888256999999,
"stddev": 0.027975218507984255,
"median": 0.6668677004,
"user": 0.36892198,
"system": 1.3460768,
"min": 0.6615417209,
"max": 0.7357765609,
"times": [
0.7357765609,
0.6660801669,
0.6665321179,
0.6732514809,
0.6615417209,
0.6630571259,
0.7292640229,
0.6770832799000001,
0.6630984979,
0.6672032829
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.195742727319999,
"stddev": 0.05573527387948484,
"median": 4.1812712117199995,
"user": 3.7838997599999997,
"system": 3.27660944,
"min": 4.116894963219999,
"max": 4.270944429219999,
"times": [
4.17350707422,
4.25745177322,
4.16947560422,
4.2353993472199996,
4.116894963219999,
4.270944429219999,
4.17131484822,
4.25271443822,
4.189035349219999,
4.120689446219999
]
},
{
"command": "pacquet@main",
"mean": 4.20060211822,
"stddev": 0.05352127824792671,
"median": 4.20400981022,
"user": 3.785425959999999,
"system": 3.24147354,
"min": 4.107325214219999,
"max": 4.269897127219999,
"times": [
4.20145769222,
4.269897127219999,
4.263741115219999,
4.20800798222,
4.167949482219999,
4.107325214219999,
4.26108435022,
4.206561928219999,
4.15053573122,
4.16946055922
]
},
{
"command": "pnpr@HEAD",
"mean": 2.14823458312,
"stddev": 0.12608172493084785,
"median": 2.12831812722,
"user": 2.5382696599999997,
"system": 2.7923077399999996,
"min": 1.95825376822,
"max": 2.35569152422,
"times": [
2.22759058422,
2.23895819922,
2.35569152422,
2.03342057922,
2.28133683622,
2.16680275622,
1.95825376822,
2.04313377322,
2.08983349822,
2.08732431222
]
},
{
"command": "pnpr@main",
"mean": 2.18082318772,
"stddev": 0.16706870429061743,
"median": 2.11932708172,
"user": 2.52133406,
"system": 2.7850267399999997,
"min": 1.9559307272200002,
"max": 2.38668029422,
"times": [
2.10090269222,
2.37351511722,
2.32282870422,
1.9719772132200002,
1.9559307272200002,
2.09177854422,
2.36691446222,
2.13775147122,
2.09995265122,
2.38668029422
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.35712774344,
"stddev": 0.013003345500635739,
"median": 1.35388333124,
"user": 1.30144032,
"system": 1.7510767999999999,
"min": 1.34457298224,
"max": 1.38780582524,
"times": [
1.3540717052400002,
1.34457298224,
1.36717108424,
1.3457248722400001,
1.35088946724,
1.3618218752400002,
1.35369495724,
1.3467650802400002,
1.38780582524,
1.35875958524
]
},
{
"command": "pacquet@main",
"mean": 1.33115138964,
"stddev": 0.019374178906692133,
"median": 1.32512809274,
"user": 1.30007282,
"system": 1.6929802999999999,
"min": 1.30937674024,
"max": 1.37120641124,
"times": [
1.32112826024,
1.32406507524,
1.33067212624,
1.37120641124,
1.34872859224,
1.3129476022400002,
1.30937674024,
1.3489589812400002,
1.3261911102400001,
1.3182389972400002
]
},
{
"command": "pnpr@HEAD",
"mean": 0.64336450244,
"stddev": 0.010650035348341148,
"median": 0.64115780224,
"user": 0.31954432,
"system": 1.2785704999999998,
"min": 0.63134110724,
"max": 0.66214474724,
"times": [
0.63818198724,
0.63134110724,
0.63384078424,
0.63631594724,
0.6441336172400001,
0.6337939032400001,
0.6538486302400001,
0.66214474724,
0.64429701024,
0.65574729024
]
},
{
"command": "pnpr@main",
"mean": 0.6274798992399999,
"stddev": 0.008626259708595423,
"median": 0.62717502674,
"user": 0.31282301999999995,
"system": 1.2638658999999997,
"min": 0.61228166024,
"max": 0.64018867624,
"times": [
0.63665926424,
0.62575134024,
0.62057491524,
0.62859871324,
0.64018867624,
0.6219703332400001,
0.62155442824,
0.63315492624,
0.6340647352400001,
0.61228166024
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.9894347521599998,
"stddev": 0.022036977798116614,
"median": 2.99613173886,
"user": 1.7477455799999997,
"system": 1.9747909599999995,
"min": 2.9549604543599997,
"max": 3.01908381236,
"times": [
2.99760284636,
3.00470291336,
3.00809346636,
2.9549604543599997,
2.99466063136,
2.9587588023599998,
2.96836118836,
3.01908381236,
2.9840643723599998,
3.00405903436
]
},
{
"command": "pacquet@main",
"mean": 3.01365820006,
"stddev": 0.08372877020197195,
"median": 2.9963512993599997,
"user": 1.7509932799999999,
"system": 1.9856248600000002,
"min": 2.91038330336,
"max": 3.21711716936,
"times": [
2.98296636836,
2.98277599936,
3.01906941836,
2.94494299636,
2.91038330336,
3.00356914736,
2.98913345136,
3.21711716936,
3.07433295436,
3.0122911923599998
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6433779280599999,
"stddev": 0.005335681799046514,
"median": 0.64403160386,
"user": 0.31695858,
"system": 1.29794226,
"min": 0.63453259036,
"max": 0.64954864436,
"times": [
0.64099772436,
0.64352721036,
0.64954864436,
0.64453599736,
0.63453259036,
0.64006275836,
0.64934405836,
0.64826432936,
0.63622773036,
0.64673823736
]
},
{
"command": "pnpr@main",
"mean": 0.65167317936,
"stddev": 0.02754042448942165,
"median": 0.6433271593600001,
"user": 0.32025437999999995,
"system": 1.27515696,
"min": 0.6347577613600001,
"max": 0.72815488636,
"times": [
0.64512976336,
0.65777607736,
0.64184425536,
0.63858130936,
0.6347577613600001,
0.64366178736,
0.64299253136,
0.63939559136,
0.64443783036,
0.72815488636
]
}
]
} |
|
| Branch | pr/12106 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,195.74 ms(+0.52%)Baseline: 4,173.86 ms | 5,008.63 ms (83.77%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,989.43 ms(-0.02%)Baseline: 2,989.89 ms | 3,587.87 ms (83.32%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,357.13 ms(+3.29%)Baseline: 1,313.92 ms | 1,576.70 ms (86.07%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,206.80 ms(+4.66%)Baseline: 4,019.41 ms | 4,823.29 ms (87.22%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 626.87 ms(+1.51%)Baseline: 617.52 ms | 741.03 ms (84.59%) |
|
| Branch | pr/12106 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,148.23 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 643.38 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 643.36 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,122.57 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 755.51 ms |
Summary
Fixes
pnpm installwithoptimisticRepeatInstallincorrectly returningAlready up to datewhenpnpm-lock.yamlchanged but project manifests did not.Fixes #12100.
Root Cause
checkDepsStatusused modified manifest mtimes as the only signal for whether it needed to validate dependency status. If no manifest was newer thanworkspaceState.lastValidatedTimestamp, it returnedupToDate: truebefore checking whether the wanted lockfile had changed.That skipped lockfile validation for workflows like:
git checkout HEAD~1 -- pnpm-lock.yamlpnpm-lock.yamlfrom a stashChanges
@pnpm/deps.statusandpnpm.Validation
pnpm --filter @pnpm/deps.status run compilepnpm --filter @pnpm/deps.status test test/checkDepsStatus.test.ts -t "only the lockfile changed"Push hooks also passed TypeScript build, pnpm compile, spellcheck, metadata lint, and eslint. Existing skipped-test warnings were reported.
Written by an agent (opencode, gpt-5.5).
Summary by CodeRabbit
pnpm-lock.yamlchanges.mergeGitBranchLockfilessupport for branch/merge lockfile workflows..git/HEAD.