Skip to content

fix(deps-status): detect lockfile-only changes#12106

Merged
zkochan merged 13 commits into
pnpm:mainfrom
aqeelat:fix/detect-lockfile-changes-optimistic-install
Jun 16, 2026
Merged

fix(deps-status): detect lockfile-only changes#12106
zkochan merged 13 commits into
pnpm:mainfrom
aqeelat:fix/detect-lockfile-changes-optimistic-install

Conversation

@aqeelat

@aqeelat aqeelat commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes pnpm install with optimisticRepeatInstall incorrectly returning Already up to date when pnpm-lock.yaml changed but project manifests did not.

Fixes #12100.

Root Cause

checkDepsStatus used modified manifest mtimes as the only signal for whether it needed to validate dependency status. If no manifest was newer than workspaceState.lastValidatedTimestamp, it returned upToDate: true before checking whether the wanted lockfile had changed.

That skipped lockfile validation for workflows like:

  • git checkout HEAD~1 -- pnpm-lock.yaml
  • restoring only pnpm-lock.yaml from a stash
  • external tools rewriting the lockfile without touching manifests

Changes

  • Check wanted lockfile mtimes before taking the optimistic fast path.
  • If any wanted lockfile is missing or newer than the workspace state timestamp, validate all projects instead of only modified manifests.
  • Add a regression test proving a lockfile-only change does not skip wanted-lockfile validation.
  • Add a patch changeset for @pnpm/deps.status and pnpm.

Validation

  • pnpm --filter @pnpm/deps.status run compile
  • pnpm --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

  • Bug Fixes
    • Fixed optimistic repeat installs incorrectly reporting “Already up to date” when only pnpm-lock.yaml changes.
    • Improved dependency status checks to use the correct wanted (git-branch/merged) lockfile, including better behavior for missing or merge-conflicted wanted lockfiles.
  • New Features
    • Added mergeGitBranchLockfiles support for branch/merge lockfile workflows.
  • Tests
    • Expanded coverage for wanted lockfile selection, optimistic fast-path behavior, merge-conflict detection, and git branch resolution from .git/HEAD.
  • Chores
    • Updated spell/lint ignore rules and adjusted test assertions to match revised CLI wording.

Copilot AI review requested due to automatic review settings June 1, 2026 13:35
@aqeelat aqeelat requested a review from zkochan as a code owner June 1, 2026 13:35
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Two bugs are fixed in checkDepsStatus: (1) pnpm install with optimisticRepeatInstall now detects when only pnpm-lock.yaml was modified and no longer falsely reports Already up to date; (2) when useGitBranchLockfile is enabled, the correct branch-specific lockfile name is stat'd instead of always using pnpm-lock.yaml. The mergeGitBranchLockfiles option is threaded through the relevant types and call sites. Git branch detection is optimized to read .git/HEAD directly rather than spawning a subprocess.

Changes

Lockfile Modification Detection Fix

Layer / File(s) Summary
Git branch detection via .git/HEAD filesystem read
network/git-utils/src/index.ts, network/git-utils/test/index.test.ts
getCurrentBranch now attempts to read the current branch name directly from .git/HEAD (including handling gitdir: references for worktrees/submodules) before falling back to spawning git symbolic-ref. A new readBranchFromHeadFile helper parses the HEAD file and returns the branch name, detached-state null, or undefined for unreadable/unsupported layouts. Tests verify correct branch name extraction, detached HEAD detection, and non-git-repo handling.
Lockfile name resolution with workspace directory support
lockfile/fs/src/lockfileName.ts, lockfile/fs/test/lockfileName.test.ts
GetWantedLockfileNameOptions gains optional cwd field and getWantedLockfileName forwards cwd to getCurrentBranch so git-branch lockfile name resolution occurs in the correct repository context instead of always using process.cwd(). Tests verify the cwd is properly forwarded to getCurrentBranch.
Synchronous git-branch lockfile scanning infrastructure
lockfile/fs/src/gitBranchLockfile.ts, lockfile/fs/src/read.ts, lockfile/fs/src/index.ts, lockfile/fs/test/gitBranchLockfile.test.ts
Centralize git-branch lockfile name matching regex into a module-level constant and add getGitBranchLockfileNamesSync function for synchronous directory scans using fs.readdirSync. Update getGitBranchLockfileNames to use fs.promises and the shared regex; enhance wantedLockfileHasMergeConflictsSync to accept optional lockfileName parameter for per-file conflict detection; re-export the new sync helper from lockfile/fs public API.
Type extensions for mergeGitBranchLockfiles option
deps/status/src/checkDepsStatus.ts, installing/commands/src/installDeps.ts
Extend CheckDepsStatusOptions and InstallDepsOptions to include mergeGitBranchLockfiles in their Partial<Pick<Config, ...>> selections, enabling configuration-driven lockfile merge behavior.
CheckDepsStatus lockfile modification detection refactoring
deps/status/src/checkDepsStatus.ts
Refactor checkDepsStatus to compute wanted lockfile directories and names upfront via getWantedLockfileName, replace conflict-only findConflictedLockfileDir with new scanWantedLockfiles routine that detects mtime changes and merge conflicts in a single pass, adjust early-exit condition to return upToDate: true only when both manifests and lockfiles were unmodified (new anyModified flag), and thread lockfileName and merge options through all lockfile stat/read paths (shared-workspace, per-project, and root-workspace contexts). Add gitBranchLockfileNames helper for expanding lockfile names when merging is enabled.
Test infrastructure and comprehensive coverage
deps/status/test/checkDepsStatus.test.ts
Add ProjectId type import for correctly typed lockfile importer keys, extend @pnpm/lockfile.fs Jest mock to expose getWantedLockfileName, introduce checkDepsStatus - lockfile modification test suite covering mtime-based detection returning upToDate: false, git-branch-only scenarios returning upToDate: true, and assertion that workspaceDir is passed as cwd for branch resolution. Add tests for merge-conflict detection in both useGitBranchLockfile and mergeGitBranchLockfiles modes. Update writeConflictedLockfile helper to accept optional lockfile name parameter.
CLI message expectation updates
pnpm/test/verifyDepsBeforeRun/multiProjectWorkspace.ts
Update assertions throughout single dependency and multiple lockfiles test blocks to expect "manifest files or lockfiles" wording in exiting-check, continuing-check, and updating-workspace-state message variants, reflecting the new behavior that both manifest and lockfile changes trigger dependency validation.
Pacquet optimistic repeat install fast-path lockfile detection
pacquet/crates/package-manager/src/optimistic_repeat_install.rs, pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
Extend the optimistic repeat-install fast path to detect pnpm-lock.yaml-only changes via new wanted_lockfile_modified helper that checks lockfile mtime against lastValidatedTimestamp. When only the lockfile is modified (no manifest changes), switch validation from checking only modified-manifest projects to checking all projects. Add regression test verifying Decision::Skipped is returned when only the lockfile changes while manifests are untouched.
Changeset and tooling ignore patterns
.changeset/fix-optimistic-repeat-install-lockfile.md, cspell.json, eslint.config.mjs
Add patch changeset documenting the two fixes (lockfile mtime detection enabling optimisticRepeatInstall to detect lockfile-only changes, and branch-lockfile path resolution using workspace directory and .git/HEAD reading). Exclude bench-work-env/** from cspell and ESLint ignore patterns; add gitdir to cspell words allowlist.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pnpm/pnpm#11605: Both PRs modify deps/status/src/checkDepsStatus.ts to detect merge-conflicted wanted lockfiles using lockfile/fs/src/read.ts conflict-marker helpers to prevent optimisticRepeatInstall from incorrectly skipping lockfile resolution.
  • pnpm/pnpm#11943: The main PR modifies the pacquet optimistic_repeat_install fast-path logic and its tests to change the "UpToDate" decision when only the lockfile changes, which directly builds on this PR's new optimistic repeat-install fast path implementation.
  • pnpm/pnpm#11945: Both PRs modify pacquet/crates/package-manager/src/optimistic_repeat_install.rs's optimistic repeat-install fast-path decision logic around pnpm-lock.yaml existence/mtime handling.

Suggested labels

area: lockfile

Suggested reviewers

  • zkochan

Poem

🐰 Hop, hop — the lockfile changed, but pnpm stayed asleep,
A git checkout swapped the file, with not a peep!
Now we read .git/HEAD direct, no subprocess spawned in vain,
And checkDepsStatus scans the mtime, detects the file's change refrain,
The bunny fixed the fast path, now the branch lockfile's right,
No more false "Already up to date" — installs ring true each night! 🌙

🚥 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 'fix(deps-status): detect lockfile-only changes' accurately and concisely describes the main fix of preventing pnpm from short-circuiting when only the lockfile is modified.
Linked Issues check ✅ Passed The PR fully addresses the linked issue #12100 by detecting wanted lockfile mtime changes, bypassing the early-exit path when lockfiles are modified, and validating all projects when lockfiles change—all key objectives stated in the issue.
Out of Scope Changes check ✅ Passed All changes directly support the core objective of detecting lockfile-only changes. Configuration updates (cspell, eslint) and test fixture updates are necessary supporting changes for the implementation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

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

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 1, 2026

Copy link
Copy Markdown

PR Summary by Qodo

fix(deps-status): detect lockfile-only changes in optimistic install check
🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

Walkthroughs

Description
• 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.
Diagram
graph 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
Loading
High-Level Assessment

The PR's approach is optimal. Checking lockfile mtimes in the same synchronous stat loop that already checks for merge conflicts (merged into scanWantedLockfiles) is the minimal, zero-overhead change needed. Reading .git/HEAD directly instead of spawning a subprocess is the standard fast-path used by many tools (e.g., Vite, Turborepo). Alternatives like inotify/file watchers or storing the lockfile hash in workspace state would add significant complexity for marginal benefit.

Grey Divider

File Changes

Enhancement (3)
lockfileName.ts Add cwd support to getWantedLockfileName and forward it to getCurrentBranch +3/-2

Add cwd support to getWantedLockfileName and forward it to getCurrentBranch

• Adds an optional 'cwd' field to 'GetWantedLockfileNameOptions' and passes it to 'getCurrentBranch'. Simplifies the default parameter to an empty object. This ensures branch resolution uses the workspace directory rather than 'process.cwd()'.

lockfile/fs/src/lockfileName.ts


index.ts Read .git/HEAD directly to resolve branch without spawning git subprocess +43/-0

Read .git/HEAD directly to resolve branch without spawning git subprocess

• Adds a private 'readBranchFromHeadFile' function that synchronously reads '.git/HEAD' (handling both directory and worktree/submodule file layouts) to extract the current branch name. 'getCurrentBranch' tries this fast path first and only falls back to spawning 'git symbolic-ref' if the file cannot be read.

network/git-utils/src/index.ts


installDeps.ts Expose mergeGitBranchLockfiles in InstallDepsOptions +1/-1

Expose mergeGitBranchLockfiles in InstallDepsOptions

• Adds 'mergeGitBranchLockfiles' to the 'Partial<Pick<Config, ...>>' intersection in 'InstallDepsOptions' so it can be passed through to 'checkDepsStatus'.

installing/commands/src/installDeps.ts


Bug fix (1)
checkDepsStatus.ts Include lockfile mtime in fast-path guard and use correct branch lockfile name +50/-22

Include lockfile mtime in fast-path guard and use correct branch lockfile name

• Refactors 'findConflictedLockfileDir' into 'scanWantedLockfiles' which returns both 'conflictedDir' and 'anyModified'. The fast-path early return now requires both zero modified manifests AND no modified lockfiles. All 'stat' and 'readWantedLockfile' calls are updated to use the dynamically resolved 'wantedLockfileName' instead of the hardcoded 'WANTED_LOCKFILE' constant, fixing the 'useGitBranchLockfile' path. 'mergeGitBranchLockfiles' is also forwarded to 'readWantedLockfile' calls.

deps/status/src/checkDepsStatus.ts


Tests (4)
checkDepsStatus.test.ts Add regression tests for lockfile-only change detection and git-branch lockfile path +207/-1

Add regression tests for lockfile-only change detection and git-branch lockfile path

• Adds a new 'checkDepsStatus - lockfile modification' describe block with three tests: one proving a lockfile-only change triggers validation (regression for the bug), one verifying no error when a git-branch lockfile exists but is unmodified, and one asserting 'getWantedLockfileName' is called with the workspace dir as 'cwd'. Also adds 'ProjectId' type import and mocks 'getWantedLockfileName'.

deps/status/test/checkDepsStatus.test.ts


lockfileName.test.ts Test that cwd is forwarded from getWantedLockfileName to getCurrentBranch +6/-0

Test that cwd is forwarded from getWantedLockfileName to getCurrentBranch

• Adds a test asserting that when 'cwd' is passed to 'getWantedLockfileName', it is forwarded to the 'getCurrentBranch' call.

lockfile/fs/test/lockfileName.test.ts


index.test.ts Add tests for .git/HEAD-based branch resolution (normal, detached, non-repo) +28/-0

Add tests for .git/HEAD-based branch resolution (normal, detached, non-repo)

• Adds three new tests covering: reading branch from '.git/HEAD' in a real git repo, returning 'null' for a detached HEAD, and returning 'null' outside a git repository.

network/git-utils/test/index.test.ts


multiProjectWorkspace.ts Update e2e test assertions to match new debug log messages +16/-16

Update e2e test assertions to match new debug log messages

• Updates all string assertions that matched the old ''No manifest files were modified...'' and ''Some manifest files were modified...'' debug messages to match the updated ''...or lockfiles...'' wording.

pnpm/test/verifyDepsBeforeRun/multiProjectWorkspace.ts


Documentation (1)
fix-optimistic-repeat-install-lockfile.md Add patch changeset for deps.status, lockfile.fs, network.git-utils, and pnpm +10/-0

Add patch changeset for deps.status, lockfile.fs, network.git-utils, and pnpm

• Documents the bug fix and the git-branch lockfile path correction as a patch release for the four affected packages.

.changeset/fix-optimistic-repeat-install-lockfile.md


Other (2)
eslint.config.mjs Exclude bench-work-env from ESLint +1/-1

Exclude bench-work-env from ESLint

• Adds 'bench-work-env/**' to the ESLint ignore list to prevent false-positive parser errors from local benchmark clones.

eslint.config.mjs


cspell.json Exclude bench-work-env from cspell +2/-1

Exclude bench-work-env from cspell

• Adds 'bench-work-env/**' to cspell's 'ignorePaths' to avoid wasted scans on gitignored benchmark directories.

cspell.json


Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.status and pnpm.

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.

Comment thread deps/status/test/checkDepsStatus.test.ts Outdated
Comment thread deps/status/test/checkDepsStatus.test.ts Outdated

@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)
deps/status/src/checkDepsStatus.ts (1)

267-284: ⚡ Quick win

Reuse the already-computed lockfile dirs.

getWantedLockfileDirs(...) is invoked here with the exact same arguments as in the findConflictedLockfileDir call at Line 172. Compute it once and reuse to avoid the duplicated literal drifting out of sync. The new lockfilesModified gating itself looks correct.

♻️ Hoist the shared lockfileDirs

Outside 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

📥 Commits

Reviewing files that changed from the base of the PR and between 118e9be and 86bac5a.

📒 Files selected for processing (3)
  • .changeset/fix-optimistic-repeat-install-lockfile.md
  • deps/status/src/checkDepsStatus.ts
  • deps/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.ts
  • deps/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.ts
  • deps/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!

@aqeelat aqeelat force-pushed the fix/detect-lockfile-changes-optimistic-install branch from 188bad2 to 12e1e4a Compare June 16, 2026 16:03
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (9) 📘 Rule violations (0)

Grey Divider


Action required

1. Missing lockfile bypasses check 🐞 Bug ≡ Correctness
Description
scanWantedLockfiles() ignores ENOENT, so a deleted wanted lockfile does not set lockfilesModified
and the workspace fast-path can return upToDate=true without ever validating lockfile presence. This
is inconsistent with later logic that treats missing wanted lockfiles as an error in git-branch
mode, so missing pnpm-lock.<branch>.yaml (or per-project lockfiles) can be silently skipped.
Code

deps/status/src/checkDepsStatus.ts[R686-694]

try {
-      mtime = fs.statSync(path.join(lockfileDir, WANTED_LOCKFILE)).mtime.valueOf()
+      mtime = fs.statSync(path.join(lockfileDir, wantedLockfileName)).mtime.valueOf()
} catch (err: unknown) {
  if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') continue
  throw err
}
-    // If the lockfile hasn't been modified since the last successful install, it can't have
-    // grown conflict markers — skip the read to preserve the optimistic fast-path.
if (mtime <= lastValidatedTimestamp) continue
-    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) return lockfileDir
+    anyModified = true
+    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) {
Evidence
The scan explicitly ignores missing lockfiles, while the early-exit relies on the scan result; later
code shows missing lockfiles are supposed to be handled/errored, but that code is bypassed by the
early return.

deps/status/src/checkDepsStatus.ts[280-291]
deps/status/src/checkDepsStatus.ts[300-323]
deps/status/src/checkDepsStatus.ts[678-700]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scanWantedLockfiles()` currently `continue`s on ENOENT and never reports the wanted lockfile as “modified/missing”. Because the optimistic early-exit is gated on `!lockfilesModified`, `checkDepsStatus()` can return `upToDate: true` even when the wanted lockfile is missing (especially problematic when `useGitBranchLockfile=true`, where later code expects to throw `RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND`).
### Issue Context
The PR’s intent (per description) is to treat missing wanted lockfiles as a signal to avoid the optimistic fast-path. Right now, missing lockfiles are indistinguishable from “unchanged” in the scan.
### Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[678-700]
- deps/status/src/checkDepsStatus.ts[285-291]
- deps/status/src/checkDepsStatus.ts[300-323]
### Suggested fix
- Change `scanWantedLockfiles()` to report missing wanted lockfiles (e.g., return `{ anyMissing: boolean }` or treat ENOENT as `anyModified=true`).
- Update the early-exit condition to also require “no wanted lockfiles missing”.
- Preserve existing behavior for the non-branch stand-in path if desired (i.e., if `!useGitBranchLockfile` and the current-lockfile stand-in is available, you may still return early with `wantedLockfileToRestore`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Wrong git cwd used 🐞 Bug ≡ Correctness
Description
checkDepsStatus() now computes wantedLockfileName via getWantedLockfileName(), which
determines the current git branch using process.cwd() rather than the workspace/lockfile directory
being validated. With useGitBranchLockfile=true and pnpm invoked via --dir (or from outside the
repo), this can make checkDepsStatus() stat/read the wrong lockfile name and incorrectly report
status or throw RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND.
Code

deps/status/src/checkDepsStatus.ts[R190-194]

+  const wantedLockfileName = await getWantedLockfileName({
+    useGitBranchLockfile: opts.useGitBranchLockfile,
+    mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+  })
+  const { conflictedDir: conflictedLockfileDir, anyModified: lockfilesModified } = scanWantedLockfiles(lockfileDirs, workspaceState.lastValidatedTimestamp, wantedLockfileName)
Evidence
checkDepsStatus() now computes wantedLockfileName up-front and uses it for lockfile stat/read
paths under the workspace, but getWantedLockfileName() derives branch names by invoking git
without a cwd, which defaults to process.cwd(). Callers like runDepsStatusCheck carry a logical
dir for the command, but the lockfile-name resolution is not anchored to it, so the wrong repo can
be queried when cwd differs from the workspace being checked.

deps/status/src/checkDepsStatus.ts[183-195]
deps/status/src/checkDepsStatus.ts[299-305]
lockfile/fs/src/lockfileName.ts[1-17]
network/git-utils/src/index.ts[18-26]
exec/commands/src/runDepsStatusCheck.ts[14-26]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`checkDepsStatus()` resolves `wantedLockfileName` using `getWantedLockfileName()`, which shells out to `git` using the default working directory. When pnpm is run with `--dir` (or otherwise checks a workspace different from `process.cwd()`), the branch lookup can target the wrong repo/detached HEAD and compute the wrong lockfile filename.
### Issue Context
This affects `useGitBranchLockfile` mode, where lockfile names are branch-derived (`pnpm-lock.<branch>.yaml`). `checkDepsStatus()` then uses that name to stat/read lockfiles under `workspaceDir` / project dirs.
### Fix Focus Areas
- lockfile/fs/src/lockfileName.ts[1-17]
- deps/status/src/checkDepsStatus.ts[183-195]
- exec/commands/src/runDepsStatusCheck.ts[14-26]
### Suggested fix
1. Extend `GetWantedLockfileNameOptions` to accept an optional `cwd` (or `dir`) and pass it through to `getCurrentBranch({ cwd })`.
2. In `checkDepsStatus()`, call `getWantedLockfileName({ useGitBranchLockfile, mergeGitBranchLockfiles, cwd: workspaceDir ?? lockfileDir ?? rootProjectManifestDir })` so the branch is resolved in the repo that owns the lockfile.
3. Add/adjust a regression test that runs `checkDepsStatus()` with `useGitBranchLockfile=true` and a simulated mismatched `process.cwd()` vs `workspaceDir`, proving the correct lockfile name is used.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Wrong lockfile conflict scan 🐞 Bug ≡ Correctness
Description
scanWantedLockfiles() stats the computed wantedLockfileName but calls
wantedLockfileHasMergeConflictsSync(lockfileDir), which always reads pnpm-lock.yaml, so merge
conflicts in pnpm-lock..yaml are not detected and the intended early error is skipped.
Code

deps/status/src/checkDepsStatus.ts[R691-695]

if (mtime <= lastValidatedTimestamp) continue
-    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) return lockfileDir
+    anyModified = true
+    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) {
+      conflictedDir = lockfileDir
+      break
Evidence
scanWantedLockfiles() is driven by wantedLockfileName, but the merge-conflict helper it calls
always reads pnpm-lock.yaml (WANTED_LOCKFILE), so conflict detection can be applied to the wrong
file when the wanted lockfile name is branch-specific.

deps/status/src/checkDepsStatus.ts[677-699]
lockfile/fs/src/read.ts[87-97]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scanWantedLockfiles()` uses `wantedLockfileName` to decide whether a lockfile is modified, but it checks merge conflicts via `wantedLockfileHasMergeConflictsSync(lockfileDir)`, which reads the hardcoded `pnpm-lock.yaml`. In git-branch-lockfile mode, this misses conflicts in `pnpm-lock.<branch>.yaml`.
## Issue Context
- `wantedLockfileHasMergeConflictsSync()` currently reads `path.join(pkgPath, WANTED_LOCKFILE)`.
- `checkDepsStatus` now computes `wantedLockfileName` and should use it consistently for conflict checks.
## Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[677-697]
- lockfile/fs/src/read.ts[87-97]
### Suggested approach
- Update `wantedLockfileHasMergeConflictsSync` to accept a `lockfileName` (or full path), defaulting to `WANTED_LOCKFILE` for backwards compatibility.
- Call it from `scanWantedLockfiles()` with `wantedLockfileName` so branch-specific lockfiles are checked.
- Add/adjust a unit test that creates a conflicted `pnpm-lock.<branch>.yaml` and asserts `checkDepsStatus` returns the merge-conflict issue.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Merged lockfiles not scanned 🐞 Bug ≡ Correctness
Description
When mergeGitBranchLockfiles=true, readWantedLockfile() can merge multiple pnpm-lock.*.yaml
files, but checkDepsStatus only scans/stats a single wantedLockfileName for modifications, so
changes in merged branch lockfiles can be missed and the optimistic fast-path can incorrectly return
upToDate: true.
Code

deps/status/src/checkDepsStatus.ts[R190-195]

+  const wantedLockfileName = await getWantedLockfileName({
+    useGitBranchLockfile: opts.useGitBranchLockfile,
+    mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+  })
+  const { conflictedDir: conflictedLockfileDir, anyModified: lockfilesModified } = scanWantedLockfiles(lockfileDirs, workspaceState.lastValidatedTimestamp, wantedLockfileName)
if (conflictedLockfileDir != null) {
Evidence
checkDepsStatus uses lockfilesModified from scanWantedLockfiles() to decide whether it can
exit early, but readWantedLockfile() can merge additional git-branch lockfiles whose mtimes are
not included in the scan, so modifications to those files can be ignored by the fast-path gate.

deps/status/src/checkDepsStatus.ts[183-195]
deps/status/src/checkDepsStatus.ts[279-290]
lockfile/fs/src/lockfileName.ts[9-17]
lockfile/fs/src/read.ts[226-243]
lockfile/fs/src/gitBranchLockfile.ts[4-8]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`checkDepsStatus()` decides whether to take the optimistic exit (and whether to validate all projects) based on `lockfilesModified` from `scanWantedLockfiles()`. That scan only stats one filename (`wantedLockfileName`). However, when `mergeGitBranchLockfiles` is enabled, `readWantedLockfile()` may incorporate changes from *multiple* git-branch lockfiles (`pnpm-lock.*.yaml`). Updates to those additional files will not flip `lockfilesModified`, allowing an incorrect early `upToDate: true`.
## Issue Context
- `getWantedLockfileName({ mergeGitBranchLockfiles: true })` returns `pnpm-lock.yaml`.
- `readWantedLockfile()` merges additional git-branch lockfiles when `mergeGitBranchLockfiles` is set.
## Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[183-195]
- deps/status/src/checkDepsStatus.ts[279-290]
- deps/status/src/checkDepsStatus.ts[385-387]
- lockfile/fs/src/read.ts[212-245]
- lockfile/fs/src/gitBranchLockfile.ts[4-8]
### Suggested approach
- Extend `scanWantedLockfiles()` to account for `mergeGitBranchLockfiles`:
- Always stat `wantedLockfileName`.
- If `mergeGitBranchLockfiles` is true, also list `pnpm-lock.*.yaml` via `getGitBranchLockfileNames(lockfileDir)` and include their mtimes (and optionally conflict checks) in the modification decision.
- Thread `mergeGitBranchLockfiles` into `scanWantedLockfiles()` (or pass a callback that yields extra filenames).
- Add a regression test: with `mergeGitBranchLockfiles=true`, touch only a `pnpm-lock.<branch>.yaml` file (leaving `pnpm-lock.yaml` unchanged) and assert `checkDepsStatus` does not take the optimistic exit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Git-branch lockfile regression 🐞 Bug ≡ Correctness
Description
lockfilesModified treats a missing pnpm-lock.yaml as “modified”, but with
useGitBranchLockfile=true that missing file is the steady state, so the optimistic exit is
skipped. In the shared-workspace-lockfile path this then triggers throwLockfileNotFound() when
pnpm-lock.yaml is absent, breaking verify-deps-before-run/optimisticRepeatInstall flows for
branch-lockfile workspaces.
Code

deps/status/src/checkDepsStatus.ts[R277-283]

+    const lockfilesModified = lockfileDirs.some((wantedLockfileDir) => {
+      const wantedLockfileStats = safeStatSync(path.join(wantedLockfileDir, WANTED_LOCKFILE))
+      return wantedLockfileStats == null || wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp
+    })
-    if (modifiedProjects.length === 0) {
-      logger.debug({ msg: 'No manifest files were modified since the last validation. Exiting check.' })
+    if ((modifiedProjects.length === 0) && !lockfilesModified) {
+      logger.debug({ msg: 'No manifest files or lockfiles were modified since the last validation. Exiting check.' })
Evidence
The new lockfilesModified logic returns true when pnpm-lock.yaml is missing, which blocks the
early return and forces the shared-workspace-lockfile path. That path explicitly throws when
pnpm-lock.yaml is missing and useGitBranchLockfile is true, while installer tests show
pnpm-lock.yaml is expected to be absent in git-branch-lockfile workspaces; runDepsStatusCheck
uses this check before running scripts so the regression is user-visible.

deps/status/src/checkDepsStatus.ts[71-77]
deps/status/src/checkDepsStatus.ts[273-290]
deps/status/src/checkDepsStatus.ts[297-320]
deps/status/src/checkDepsStatus.ts[631-635]
installing/deps-installer/test/install/gitBranchLockfile.test.ts[89-157]
exec/commands/src/runDepsStatusCheck.ts[14-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `checkDepsStatus`, the new `lockfilesModified` probe checks only `${wantedLockfileDir}/pnpm-lock.yaml` and treats `ENOENT` as “modified”. For `useGitBranchLockfile=true`, pnpm intentionally writes `pnpm-lock.<branch>.yaml` and may *not* have `pnpm-lock.yaml`, so this change disables the optimistic fast-path and can cascade into a hard `RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND` in the shared-workspace-lockfile branch.
## Issue Context
- `useGitBranchLockfile` is explicitly documented as meaning the wanted lockfile is `pnpm-lock.<branch>.yaml`.
- The installer tests assert `pnpm-lock.yaml` is absent in that mode (including workspaces).
- `runDepsStatusCheck` calls `checkDepsStatus` before running scripts, so this can surface as repeated install prompts/errors or disabling optimistic repeat installs.
## Fix Focus Areas
- Compute the *actual* wanted lockfile name/path when `opts.useGitBranchLockfile` is enabled (and account for `mergeGitBranchLockfiles` semantics if relevant), and use that for:
- the new `lockfilesModified` mtime probe
- the shared-workspace-lockfile `fs.statSync` / missing-lockfile handling (avoid throwing just because `pnpm-lock.yaml` is missing when the branch lockfile exists)
- Pass `useGitBranchLockfile` (and merge option if applicable) through to `readWantedLockfile(...)` so reads match the selected lockfile scheme.
- Add a regression test for `checkDepsStatus` in a workspace with `useGitBranchLockfile=true` where `pnpm-lock.yaml` is absent but `pnpm-lock.<branch>.yaml` exists.
### Fix Focus Areas (code references)
- deps/status/src/checkDepsStatus.ts[273-290]
- deps/status/src/checkDepsStatus.ts[297-336]
- deps/status/src/checkDepsStatus.ts[631-635]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Overbroad lockfile regex 🐞 Bug ☼ Reliability
Description
The git-branch lockfile filename regex does not escape '.' characters, so non-branch files can match
and be treated as branch lockfiles. This can trigger incorrect modification/conflict detection and,
with mergeGitBranchLockfiles enabled, can delete unintended files during cleanGitBranchLockfiles().
Code

lockfile/fs/src/gitBranchLockfile.ts[R4-15]

+// eslint-disable-next-line regexp/no-useless-non-capturing-group
+const GIT_BRANCH_LOCKFILE_NAME = /^pnpm-lock.(?:.*).yaml$/
+
export async function getGitBranchLockfileNames (lockfileDir: string): Promise<string[]> {
-  const files = await fs.readdir(lockfileDir)
-  // eslint-disable-next-line regexp/no-useless-non-capturing-group
-  const gitBranchLockfileNames: string[] = files.filter(file => file.match(/^pnpm-lock.(?:.*).yaml$/))
-  return gitBranchLockfileNames
+  const files = await fsp.readdir(lockfileDir)
+  return files.filter(file => GIT_BRANCH_LOCKFILE_NAME.test(file))
+}
+
+export function getGitBranchLockfileNamesSync (lockfileDir: string): string[] {
+  const files = fs.readdirSync(lockfileDir)
+  return files.filter(file => GIT_BRANCH_LOCKFILE_NAME.test(file))
}
Evidence
The regex is used to decide which files are considered git-branch lockfiles. Because the intended
filename format uses literal dots, an unescaped-dot regex can match unintended filenames; those
matches can then be unlinked during merge cleanup.

lockfile/fs/src/gitBranchLockfile.ts[1-25]
lockfile/fs/src/lockfileName.ts[10-18]
installing/deps-installer/src/install/index.ts[496-505]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GIT_BRANCH_LOCKFILE_NAME` currently uses unescaped `.` and an unescaped `.yaml` segment, making the match overly permissive. This can include unrelated files in `getGitBranchLockfileNames*()` results, which are later used for scanning and cleanup.
## Issue Context
- Branch lockfiles are generated as `pnpm-lock.<branch>.yaml` (literal dots).
- When `mergeGitBranchLockfiles` is enabled, pnpm calls `cleanGitBranchLockfiles(lockfileDir)` which unlinks every matched file.
## Fix Focus Areas
- lockfile/fs/src/gitBranchLockfile.ts[4-15]
- lockfile/fs/src/lockfileName.ts[10-18]
- installing/deps-installer/src/install/index.ts[502-504]
## Proposed fix
Change the regex to require literal dots and a non-empty branch segment, e.g.:
- `const GIT_BRANCH_LOCKFILE_NAME = /^pnpm-lock\..+\.yaml$/`
Optionally add a small unit test that asserts the matcher does *not* match strings like `pnpm-lockxmainyaml` and does match `pnpm-lock.main.yaml`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Unsafe .git synchronous read 🐞 Bug ⛨ Security
Description
getCurrentBranch() now calls readBranchFromHeadFile() which readFileSync()s /.git whenever
it is not a directory, without ensuring it is a regular file. If .git is a FIFO/device/special
file (or unexpectedly huge) in an attacker-controlled working directory, this can block/hang pnpm
(local DoS) before it falls back to spawning git.
Code

network/git-utils/src/index.ts[R69-83]

+function readBranchFromHeadFile (cwd?: string): string | null | undefined {
+  const baseDir = cwd ?? process.cwd()
+  const dotGitPath = path.join(baseDir, '.git')
+  let gitDir: string
+  try {
+    const stat = fs.statSync(dotGitPath)
+    if (stat.isDirectory()) {
+      gitDir = dotGitPath
+    } else {
+      // `.git` is a file — worktree or submodule. It contains `gitdir: <path>`.
+      const content = fs.readFileSync(dotGitPath, 'utf8').trim()
+      const match = content.match(/^gitdir:\s*(.+)/)
+      if (!match) return undefined
+      gitDir = path.isAbsolute(match[1]!) ? match[1]! : path.resolve(baseDir, match[1]!)
+    }
Evidence
The new helper reads .git and HEAD synchronously, and the .git read is gated only by “is not a
directory”, not by “is a regular file”, so special filesystem objects can cause blocking behavior
before fallback logic runs.

network/git-utils/src/index.ts[21-30]
network/git-utils/src/index.ts[69-94]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`readBranchFromHeadFile()` treats any non-directory `.git` path as a text file and synchronously reads it. This can block or be abused for local DoS if `.git` is a FIFO/device/special file (or very large), because `readFileSync()` may hang or consume excessive resources.
### Issue Context
This helper runs on the `getCurrentBranch()` hot path before falling back to `git symbolic-ref`, so the synchronous read happens even when not in a real git repo.
### Fix Focus Areas
- network/git-utils/src/index.ts[69-93]
### Suggested fix
- After `fs.statSync(dotGitPath)`, branch as:
- if `stat.isDirectory()` -> ok
- else if `stat.isFile()` -> ok to `readFileSync()`
- else -> return `undefined` (fall back to `git symbolic-ref`)
- (Optional hardening) add a small size cap before reading (e.g. if `stat.size` is unexpectedly large, return `undefined`).
- Consider applying the same `isFile()` guard (or a try+stat) for `gitDir/HEAD` before reading it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Git spawn on fast-path 🐞 Bug ➹ Performance
Description
With useGitBranchLockfile=true (and mergeGitBranchLockfiles=false), checkDepsStatus() will now
spawn git to resolve wantedLockfileName before it can take the optimistic early-exit, adding
overhead to verify-deps-before-run hot paths even when nothing changed. This can increase latency
for pnpm run/pnpm exec when git-branch lockfiles are enabled.
Code

deps/status/src/checkDepsStatus.ts[R190-195]

+  const wantedLockfileName = await getWantedLockfileName({
+    useGitBranchLockfile: opts.useGitBranchLockfile,
+    mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+  })
+  const { conflictedDir: conflictedLockfileDir, anyModified: lockfilesModified } = scanWantedLockfiles(lockfileDirs, workspaceState.lastValidatedTimestamp, wantedLockfileName)
if (conflictedLockfileDir != null) {
Evidence
The new code resolves wantedLockfileName before the fast-path early return, and in git-branch
lockfile mode that resolution calls git symbolic-ref via execa. Since runDepsStatusCheck() calls
checkDepsStatus() on every run/exec, this adds a git process spawn to that hot path in the
relevant configuration.

deps/status/src/checkDepsStatus.ts[183-195]
deps/status/src/checkDepsStatus.ts[284-290]
lockfile/fs/src/lockfileName.ts[9-16]
network/git-utils/src/index.ts[18-25]
exec/commands/src/runDepsStatusCheck.ts[14-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`checkDepsStatus()` awaits `getWantedLockfileName()` before the `(modifiedProjects.length === 0 && !lockfilesModified)` early return. In git-branch lockfile mode, `getWantedLockfileName()` shells out to `git`, so even fully up-to-date runs pay a process spawn.
### Issue Context
This affects the verify-deps-before-run pathway (`runDepsStatusCheck`), which is invoked for `pnpm run` / `pnpm exec`.
### Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[183-195]
- lockfile/fs/src/lockfileName.ts[1-17]
- network/git-utils/src/index.ts[18-26]
### Suggested fix
- Prefer a non-spawning branch resolution approach for lockfile naming (e.g., read and parse `.git/HEAD`/`packed-refs` when available) or otherwise reduce spawning.
- If adding `cwd` support (see other finding), consider using it to avoid failures and to enable more efficient repo-local branch lookup.
- Document (or gate) the behavior so that `useGitBranchLockfile` users are aware that verify-deps-before-run may consult git.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
9. Duplicate sync lockfile stats 🐞 Bug ➹ Performance
Description
The new lockfilesModified scan performs synchronous statSync calls across lockfileDirs even
though findConflictedLockfileDir() already statSyncs the same paths, adding extra
event-loop-blocking filesystem work on verify-deps-before-run/optimisticRepeatInstall hot paths
(especially when sharedWorkspaceLockfile=false yields many lockfile dirs).
Code

deps/status/src/checkDepsStatus.ts[R277-280]

+    const lockfilesModified = lockfileDirs.some((wantedLockfileDir) => {
+      const wantedLockfileStats = safeStatSync(path.join(wantedLockfileDir, WANTED_LOCKFILE))
+      return wantedLockfileStats != null && wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp
+    })
Evidence
findConflictedLockfileDir() already loops over lockfileDirs and calls fs.statSync() per dir to
get the lockfile mtime; the PR adds a second loop (lockfilesModified) that calls safeStatSync()
(which wraps fs.statSync()) over the same dirs, duplicating synchronous I/O.

deps/status/src/checkDepsStatus.ts[181-195]
deps/status/src/checkDepsStatus.ts[273-288]
deps/status/src/checkDepsStatus.ts[663-676]
deps/status/src/safeStat.ts[15-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_checkDepsStatus()` now stats every wanted lockfile twice on the optimistic path: once in `findConflictedLockfileDir()` (to decide whether to read for conflict markers) and again in the new `lockfilesModified` computation. This introduces additional synchronous filesystem calls that can block the event loop.
### Issue Context
This code runs in common workflows (`verifyDepsBeforeRun`, `optimisticRepeatInstall`) where minimizing synchronous I/O helps keep CLI latency low.
### Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[181-188]
- deps/status/src/checkDepsStatus.ts[273-288]
- deps/status/src/checkDepsStatus.ts[663-676]
### Suggested approach
- Refactor `findConflictedLockfileDir()` to also return whether any lockfile mtime is newer than `lastValidatedTimestamp` (e.g., `{ conflictedDir?: string, anyLockfileModified: boolean }`), or
- Inline a single loop over `lockfileDirs` that computes both:
- `conflictedLockfileDir` (only read file when mtime is newer)
- `lockfilesModified` (based on the same mtime value)
This keeps the behavior but avoids the second sync stat pass.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 3733a29

Results up to commit 2ff7568


🐞 Bugs (9) 📘 Rule violations (0)


Action required
1. Missing lockfile bypasses check 🐞 Bug ≡ Correctness
Description
scanWantedLockfiles() ignores ENOENT, so a deleted wanted lockfile does not set lockfilesModified
and the workspace fast-path can return upToDate=true without ever validating lockfile presence. This
is inconsistent with later logic that treats missing wanted lockfiles as an error in git-branch
mode, so missing pnpm-lock.<branch>.yaml (or per-project lockfiles) can be silently skipped.
Code

deps/status/src/checkDepsStatus.ts[R686-694]

 try {
-      mtime = fs.statSync(path.join(lockfileDir, WANTED_LOCKFILE)).mtime.valueOf()
+      mtime = fs.statSync(path.join(lockfileDir, wantedLockfileName)).mtime.valueOf()
 } catch (err: unknown) {
   if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') continue
   throw err
 }
-    // If the lockfile hasn't been modified since the last successful install, it can't have
-    // grown conflict markers — skip the read to preserve the optimistic fast-path.
 if (mtime <= lastValidatedTimestamp) continue
-    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) return lockfileDir
+    anyModified = true
+    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) {
Evidence
The scan explicitly ignores missing lockfiles, while the early-exit relies on the scan result; later
code shows missing lockfiles are supposed to be handled/errored, but that code is bypassed by the
early return.

deps/status/src/checkDepsStatus.ts[280-291]
deps/status/src/checkDepsStatus.ts[300-323]
deps/status/src/checkDepsStatus.ts[678-700]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scanWantedLockfiles()` currently `continue`s on ENOENT and never reports the wanted lockfile as “modified/missing”. Because the optimistic early-exit is gated on `!lockfilesModified`, `checkDepsStatus()` can return `upToDate: true` even when the wanted lockfile is missing (especially problematic when `useGitBranchLockfile=true`, where later code expects to throw `RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND`).
### Issue Context
The PR’s intent (per description) is to treat missing wanted lockfiles as a signal to avoid the optimistic fast-path. Right now, missing lockfiles are indistinguishable from “unchanged” in the scan.
### Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[678-700]
- deps/status/src/checkDepsStatus.ts[285-291]
- deps/status/src/checkDepsStatus.ts[300-323]
### Suggested fix
- Change `scanWantedLockfiles()` to report missing wanted lockfiles (e.g., return `{ anyMissing: boolean }` or treat ENOENT as `anyModified=true`).
- Update the early-exit condition to also require “no wanted lockfiles missing”.
- Preserve existing behavior for the non-branch stand-in path if desired (i.e., if `!useGitBranchLockfile` and the current-lockfile stand-in is available, you may still return early with `wantedLockfileToRestore`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Wrong git cwd used 🐞 Bug ≡ Correctness
Description
checkDepsStatus() now computes wantedLockfileName via getWantedLockfileName(), which
determines the current git branch using process.cwd() rather than the workspace/lockfile directory
being validated. With useGitBranchLockfile=true and pnpm invoked via --dir (or from outside the
repo), this can make checkDepsStatus() stat/read the wrong lockfile name and incorrectly report
status or throw RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND.
Code

deps/status/src/checkDepsStatus.ts[R190-194]

+  const wantedLockfileName = await getWantedLockfileName({
+    useGitBranchLockfile: opts.useGitBranchLockfile,
+    mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+  })
+  const { conflictedDir: conflictedLockfileDir, anyModified: lockfilesModified } = scanWantedLockfiles(lockfileDirs, workspaceState.lastValidatedTimestamp, wantedLockfileName)
Evidence
checkDepsStatus() now computes wantedLockfileName up-front and uses it for lockfile stat/read
paths under the workspace, but getWantedLockfileName() derives branch names by invoking git
without a cwd, which defaults to process.cwd(). Callers like runDepsStatusCheck carry a logical
dir for the command, but the lockfile-name resolution is not anchored to it, so the wrong repo can
be queried when cwd differs from the workspace being checked.

deps/status/src/checkDepsStatus.ts[183-195]
deps/status/src/checkDepsStatus.ts[299-305]
lockfile/fs/src/lockfileName.ts[1-17]
network/git-utils/src/index.ts[18-26]
exec/commands/src/runDepsStatusCheck.ts[14-26]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`checkDepsStatus()` resolves `wantedLockfileName` using `getWantedLockfileName()`, which shells out to `git` using the default working directory. When pnpm is run with `--dir` (or otherwise checks a workspace different from `process.cwd()`), the branch lookup can target the wrong repo/detached HEAD and compute the wrong lockfile filename.
### Issue Context
This affects `useGitBranchLockfile` mode, where lockfile names are branch-derived (`pnpm-lock.<branch>.yaml`). `checkDepsStatus()` then uses that name to stat/read lockfiles under `workspaceDir` / project dirs.
### Fix Focus Areas
- lockfile/fs/src/lockfileName.ts[1-17]
- deps/status/src/checkDepsStatus.ts[183-195]
- exec/commands/src/runDepsStatusCheck.ts[14-26]
### Suggested fix
1. Extend `GetWantedLockfileNameOptions` to accept an optional `cwd` (or `dir`) and pass it through to `getCurrentBranch({ cwd })`.
2. In `checkDepsStatus()`, call `getWantedLockfileName({ useGitBranchLockfile, mergeGitBranchLockfiles, cwd: workspaceDir ?? lockfileDir ?? rootProjectManifestDir })` so the branch is resolved in the repo that owns the lockfile.
3. Add/adjust a regression test that runs `checkDepsStatus()` with `useGitBranchLockfile=true` and a simulated mismatched `process.cwd()` vs `workspaceDir`, proving the correct lockfile name is used.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Wrong lockfile conflict scan 🐞 Bug ≡ Correctness
Description
scanWantedLockfiles() stats the computed wantedLockfileName but calls
wantedLockfileHasMergeConflictsSync(lockfileDir), which always reads pnpm-lock.yaml, so merge
conflicts in pnpm-lock..yaml are not detected and the intended early error is skipped.
Code

deps/status/src/checkDepsStatus.ts[R691-695]

if (mtime <= lastValidatedTimestamp) continue
-    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) return lockfileDir
+    anyModified = true
+    if (wantedLockfileHasMergeConflictsSync(lockfileDir)) {
+      conflictedDir = lockfileDir
+      break
Evidence
scanWantedLockfiles() is driven by wantedLockfileName, but the merge-conflict helper it calls
always reads pnpm-lock.yaml (WANTED_LOCKFILE), so conflict detection can be applied to the wrong
file when the wanted lockfile name is branch-specific.

deps/status/src/checkDepsStatus.ts[677-699]
lockfile/fs/src/read.ts[87-97]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scanWantedLockfiles()` uses `wantedLockfileName` to decide whether a lockfile is modified, but it checks merge conflicts via `wantedLockfileHasMergeConflictsSync(lockfileDir)`, which reads the hardcoded `pnpm-lock.yaml`. In git-branch-lockfile mode, this misses conflicts in `pnpm-lock.<branch>.yaml`.
## Issue Context
- `wantedLockfileHasMergeConflictsSync()` currently reads `path.join(pkgPath, WANTED_LOCKFILE)`.
- `checkDepsStatus` now computes `wantedLockfileName` and should use it consistently for conflict checks.
## Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[677-697]
- lockfile/fs/src/read.ts[87-97]
### Suggested approach
- Update `wantedLockfileHasMergeConflictsSync` to accept a `lockfileName` (or full path), defaulting to `WANTED_LOCKFILE` for backwards compatibility.
- Call it from `scanWantedLockfiles()` with `wantedLockfileName` so branch-specific lockfiles are checked.
- Add/adjust a unit test that creates a conflicted `pnpm-lock.<branch>.yaml` and asserts `checkDepsStatus` returns the merge-conflict issue.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Merged lockfiles not scanned 🐞 Bug ≡ Correctness
Description
When mergeGitBranchLockfiles=true, readWantedLockfile() can merge multiple pnpm-lock.*.yaml
files, but checkDepsStatus only scans/stats a single wantedLockfileName for modifications, so
changes in merged branch lockfiles can be missed and the optimistic fast-path can incorrectly return
upToDate: true.
Code

deps/status/src/checkDepsStatus.ts[R190-195]

+  const wantedLockfileName = await getWantedLockfileName({
+    useGitBranchLockfile: opts.useGitBranchLockfile,
+    mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+  })
+  const { conflictedDir: conflictedLockfileDir, anyModified: lockfilesModified } = scanWantedLockfiles(lockfileDirs, workspaceState.lastValidatedTimestamp, wantedLockfileName)
if (conflictedLockfileDir != null) {
Evidence
checkDepsStatus uses lockfilesModified from scanWantedLockfiles() to decide whether it can
exit early, but readWantedLockfile() can merge additional git-branch lockfiles whose mtimes are
not included in the scan, so modifications to those files can be ignored by the fast-path gate.

deps/status/src/checkDepsStatus.ts[183-195]
deps/status/src/checkDepsStatus.ts[279-290]
lockfile/fs/src/lockfileName.ts[9-17]
lockfile/fs/src/read.ts[226-243]
lockfile/fs/src/gitBranchLockfile.ts[4-8]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`checkDepsStatus()` decides whether to take the optimistic exit (and whether to validate all projects) based on `lockfilesModified` from `scanWantedLockfiles()`. That scan only stats one filename (`wantedLockfileName`). However, when `mergeGitBranchLockfiles` is enabled, `readWantedLockfile()` may incorporate changes from *multiple* git-branch lockfiles (`pnpm-lock.*.yaml`). Updates to those additional files will not flip `lockfilesModified`, allowing an incorrect early `upToDate: true`.
## Issue Context
- `getWantedLockfileName({ mergeGitBranchLockfiles: true })` returns `pnpm-lock.yaml`.
- `readWantedLockfile()` merges additional git-branch lockfiles when `mergeGitBranchLockfiles` is set.
## Fix Focus Areas
- deps/status/src/checkDepsStatus.ts[183-195]
- deps/status/src/checkDepsStatus.ts[279-290]
- deps/status/src/checkDepsStatus.ts[385-387]
- lockfile/fs/src/read.ts[212-245]
- lockfile/fs/src/gitBranchLockfile.ts[4-8]
### Suggested approach
- Extend `scanWantedLockfiles()` to account for `mergeGitBranchLockfiles`:
- Always stat `wantedLockfileName`.
- If `mergeGitBranchLockfiles` is true, also list `pnpm-lock.*.yaml` via `getGitBranchLockfileNames(lockfileDir)` and include their mtimes (and optionally conflict checks) in the modification decision.
- Thread `mergeGitBranchLockfiles` into `scanWantedLockfiles()` (or pass a callback that yields extra filenames).
- Add a regression test: with `mergeGitBranchLockfiles=true`, touch only a `pnpm-lock.<branch>.yaml` file (leaving `pnpm-lock.yaml` unchanged) and assert `checkDepsStatus` does not take the optimistic exit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Git-branch lockfile regression 🐞 Bug ≡ Correctness
Description
lockfilesModified treats a missing pnpm-lock.yaml as “modified”, but with
useGitBranchLockfile=true that missing file is the steady state, so the optimistic exit is
skipped. In the shared-workspace-lockfile path this then triggers throwLockfileNotFound() when
pnpm-lock.yaml is absent, breaking verify-deps-before-run/optimisticRepeatInstall flows for
branch-lockfile workspaces.
Code

deps/status/src/checkDepsStatus.ts[R277-283]

+    const lockfilesModified = lockfileDirs.some((wantedLockfileDir) => {
+      const wantedLockfileStats = safeStatSync(path.join(wantedLockfileDir, WANTED_LOCKFILE))
+      return wantedLockfileStats == null || wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp
+    })
-    if (modifiedProjects.length === 0) {
-      logger.debug({ msg: 'No manifest files were modified since the last validation. Exiting check.' })
+    if ((modifiedProjects.length === 0) && !lockfilesModified) {
+      logger.debug({ msg: 'No manifest files or lockfiles were modified since the last validation. Exiting check.' })
Evidence
The new lockfilesModified logic returns true when pnpm-lock.yaml is missing, which blocks the
early return and forces the shared-workspace-lockfile path. That path explicitly throws when
pnpm-lock.yaml is missing and useGitBranchLockfile is true, while installer tests show
pnpm-lock.yaml is expected to be absent in git-branch-lockfile workspaces; runDepsStatusCheck
uses this check before running scripts so the regression is user-visible.

deps/status/src/checkDepsStatus.ts[71-77]
deps/status/src/checkDepsStatus.ts[273-290]
deps/status/src/checkDepsStatus.ts[297-320]
deps/status/src/checkDepsStatus.ts[631-635]
installing/deps-installer/test/install/gitBranchLockfile.test.ts[89-157]
exec/commands/src/runDepsStatusCheck.ts[14-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `checkDepsStatus`, the new `lockfilesModified` probe checks only `${wantedLockfileDir}/pnpm-lock.yaml` and treats `ENOENT` as “modified”. For `useGitBranchLockfile=true`, pnpm intentionally writes `pnpm-lock.<branch>.yaml` and may *not* have `pnpm-lock.yaml`, so this change disables the optimistic fast-path and can cascade into a hard `RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND` in the shared-workspace-lockfile branch.
## Issue Context
- `useGitBranchLockfile` is explicitly documented as meaning the wanted lockfile is `pnpm-lock.<branch>.yaml`.
- The installer tests assert `pnpm-lock.yaml` is absent in that mode (including workspaces).
- `runDepsStatusCheck` calls `checkDepsStatus` before running scripts, so this can surface as repeated install prompts/errors or disabling optimistic repeat installs.
## Fix Focus Areas
- Compute the *actual* wanted lockfile name/path when `opts.useGitBranchLockfile` is enabled (and account for `mergeGitBranchLockfiles` semantics if relevant), and use that for:
- the new `lockfilesModified` mtime probe
- the shared-workspace-lockfile `fs.statSync` / missing-lockfile handling (avoid throwing just because `pnpm-lock.yaml` is missing when the branch lockfile exists)
- Pass `useGitBranchLockfile` (and merge option if applicable) through to `readWantedLockfile(...)` so reads match the selected lockfile scheme.
- Add a regression test for `checkDepsStatus` in a workspace with `useGitBranchLockfile=true` where `pnpm-lock.yaml` is absent but `pnpm-lock.<branch>.yaml` exists.
### Fix Focus Areas (code references)
- deps/status/src/checkDepsStatus.ts[273-290]
- deps/status/src/checkDepsStatus.ts[297-336]
- deps/status/src/checkDepsStatus.ts[631-635]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
6. Overbroad lockfile regex 🐞 Bug ☼ Reliability
Description
The git-branch lockfile filename regex does not escape '.' characters, so non-branch files can match
and be treated as branch lockfiles. This can trigger incorrect modification/conflict detection and,
with mergeGitBranchLockfiles enabled, can delete unintended files during cleanGitBranchLockfiles().
Code

lockfile/fs/src/gitBranchLockfile.ts[R4-15]

+// eslint-disable-next-line regexp/no-useless-non-capturing-group
+const GIT_BRANCH_LOCKFILE_NAME = /^pnpm-lock.(?:.*).yaml$/
+
export async function getGitBranchLockfileNames (lockfileDir: string): Promise<string[]> {
-  const files = await fs.readdir(lockfileDir)
-  // eslint-disable-next-line regexp/no-useless-non-capturing-group
-  const gitBranchLockfileNames: string[] = files.filter(file => file.match(/^pnpm-lock.(?:.*).yaml$/))
-  return gitBranchLockfileNames
+  const files = await fsp.readdir(lockfileDir)
+  return files.filter(file => GIT_BRANCH_LOCKFILE_NAME.test(file))
+}
+
+export function getGitBranchLockfileNamesSync (lockfileDir: string): string[] {
+  const files = fs.readdirSync(lockfileDir)
+  return files.filter(file => GIT_BRANCH_LOCKFILE_NAME.test(file))
}
Evidence
The regex is used to decide which files are considered git-branch lockfiles. Because the intended
filename format uses literal dots, an unescaped-dot regex can match unintended filenames; those
matches can then be unlinked during merge cleanup.

lockfile/fs/src/gitBranchLockfile.ts[1-25]
lockfile/fs/src/lockfileName.ts[10-18]
installing/deps-installer/src/install/index.ts[496-505]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GIT_BRANCH_LOCKFILE_NAME` currently uses unescaped `.` and an unescaped `.yaml` segment, making the match overly permissive. This can include unrelated files in `getGitBranchLockfileNames*()` results, which are later used for scanning and cleanup.
## Issue Context
- Branch lockfiles are generated as `pnpm-lock.<branch>.yaml` (literal dots).
- When `mergeGitBranchLockfiles` is enabled, pnpm calls `cleanGitBranchLockfiles(lockfileDir)` which unlinks every matched file.
## Fix Focus Areas
- lockfile/fs/src/gitBranchLockfile.ts[4-15]
- lockfile/fs/src/lockfileName.ts[10-18]
- installing/deps-installer/src/install/index.ts[502-504]
## Proposed fix
Change the regex to require literal dots and a non-empty branch segment, e.g.:
- `const GIT_BRANCH_LOCKFILE_NAME = /^pnpm-lock\..+\.yaml$/`
Optionally add a small unit test that asserts the matcher does *not* match strings like `pnpm-lockxmainyaml` and does match `pnpm-lock.main.yaml`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Unsafe .git synchronous read 🐞 Bug ⛨ Security
Description
getCurrentBranch() now calls readBranchFromHeadFile() which readFileSync()s /.git whenever
it is not a directory, without ensuring it is a regular file. If .git is a FIFO/device/special
file (or unexpectedly huge) in an attacker-controlled working directory, this can block/hang pnpm
(local DoS) before it falls back to spawning git.
Code

network/git-utils/src/index.ts[R69-83]

+function readBranchFromHeadFile (cwd?: string): string | null | undefined {
+  const baseDir = cwd ?? process.cwd()
+  const dotGitPath = path.join(baseDir, '.git')
+  let gitDir: string
+  try {
+    const stat = fs.statSync(dotGitPath)
+    if (stat.isDirectory()) {
+      gitDir = dotGitPath
+    } else {
+      // `.git` is a file — worktree or submodule. It contains `gitdir: <path>`.
+      con...

Comment thread deps/status/src/checkDepsStatus.ts Outdated
@aqeelat aqeelat force-pushed the fix/detect-lockfile-changes-optimistic-install branch from 12e1e4a to c45b7b5 Compare June 16, 2026 16:17
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c45b7b5

Comment thread deps/status/src/checkDepsStatus.ts Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit d10b1b8

Comment thread deps/status/src/checkDepsStatus.ts Outdated
Comment thread deps/status/src/checkDepsStatus.ts

@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

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between c45b7b5 and d10b1b8.

📒 Files selected for processing (6)
  • .changeset/fix-optimistic-repeat-install-lockfile.md
  • cspell.json
  • deps/status/src/checkDepsStatus.ts
  • deps/status/test/checkDepsStatus.test.ts
  • eslint.config.mjs
  • installing/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

Comment thread deps/status/src/checkDepsStatus.ts Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@aqeelat aqeelat closed this Jun 16, 2026
@aqeelat aqeelat reopened this Jun 16, 2026
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@aqeelat aqeelat force-pushed the fix/detect-lockfile-changes-optimistic-install branch from 0c38f98 to d10b1b8 Compare June 16, 2026 17:53
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@aqeelat aqeelat force-pushed the fix/detect-lockfile-changes-optimistic-install branch from d10b1b8 to dd40790 Compare June 16, 2026 17:56
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

1 similar comment
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@aqeelat aqeelat force-pushed the fix/detect-lockfile-changes-optimistic-install branch from afa9b84 to 966ad4c Compare June 16, 2026 18:03
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@aqeelat aqeelat closed this Jun 16, 2026
@aqeelat aqeelat reopened this Jun 16, 2026
aqeelat added 4 commits June 16, 2026 21:36
… 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.
Comment thread deps/status/src/checkDepsStatus.ts Outdated
zkochan added 2 commits June 16, 2026 21:51
…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.
@zkochan zkochan force-pushed the fix/detect-lockfile-changes-optimistic-install branch from 9279fb1 to ab484b8 Compare June 16, 2026 19:53
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ab484b8

Comment thread network/git-utils/src/index.ts
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.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@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

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 win

Merged branch-lockfile updates can still bypass current-vs-wanted equality checks.

assertLockfilesEqual is gated by wantedLockfileStats only. With mergeGitBranchLockfiles, another pnpm-lock.*.yaml can change, set lockfilesModified, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9279fb1 and ab484b8.

📒 Files selected for processing (15)
  • .changeset/fix-optimistic-repeat-install-lockfile.md
  • cspell.json
  • deps/status/src/checkDepsStatus.ts
  • deps/status/test/checkDepsStatus.test.ts
  • eslint.config.mjs
  • installing/commands/src/installDeps.ts
  • lockfile/fs/src/gitBranchLockfile.ts
  • lockfile/fs/src/index.ts
  • lockfile/fs/src/lockfileName.ts
  • lockfile/fs/src/read.ts
  • lockfile/fs/test/gitBranchLockfile.test.ts
  • lockfile/fs/test/lockfileName.test.ts
  • network/git-utils/src/index.ts
  • network/git-utils/test/index.test.ts
  • pnpm/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

Comment thread deps/status/src/checkDepsStatus.ts
Comment thread deps/status/src/checkDepsStatus.ts Outdated
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.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 389e0a0

Comment thread lockfile/fs/src/gitBranchLockfile.ts Outdated
…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).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2ff7568

zkochan added 2 commits June 16, 2026 22:25
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).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3733a29

@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)
pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs (1)

1438-1458: ⚡ Quick win

Add 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 with content_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

📥 Commits

Reviewing files that changed from the base of the PR and between 389e0a0 and 3733a29.

📒 Files selected for processing (4)
  • lockfile/fs/src/gitBranchLockfile.ts
  • lockfile/fs/test/gitBranchLockfile.test.ts
  • pacquet/crates/package-manager/src/optimistic_repeat_install.rs
  • pacquet/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-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.03%. Comparing base (c112b61) to head (3733a29).
⚠️ Report is 7 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zkochan zkochan enabled auto-merge (squash) June 16, 2026 20:59
@github-actions

Copy link
Copy Markdown
Contributor

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.207 ± 0.127 3.993 4.399 1.99 ± 0.15
pacquet@main 4.115 ± 0.114 4.008 4.363 1.95 ± 0.14
pnpr@HEAD 2.123 ± 0.139 1.970 2.339 1.01 ± 0.09
pnpr@main 2.110 ± 0.140 1.975 2.298 1.00
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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 626.9 ± 5.9 618.5 634.0 1.00
pacquet@main 639.2 ± 53.1 603.6 784.6 1.02 ± 0.09
pnpr@HEAD 755.5 ± 81.3 710.1 983.3 1.21 ± 0.13
pnpr@main 680.3 ± 28.0 661.5 735.8 1.09 ± 0.05
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.196 ± 0.056 4.117 4.271 1.95 ± 0.12
pacquet@main 4.201 ± 0.054 4.107 4.270 1.96 ± 0.12
pnpr@HEAD 2.148 ± 0.126 1.958 2.356 1.00
pnpr@main 2.181 ± 0.167 1.956 2.387 1.02 ± 0.10
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.357 ± 0.013 1.345 1.388 2.16 ± 0.04
pacquet@main 1.331 ± 0.019 1.309 1.371 2.12 ± 0.04
pnpr@HEAD 0.643 ± 0.011 0.631 0.662 1.03 ± 0.02
pnpr@main 0.627 ± 0.009 0.612 0.640 1.00
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.989 ± 0.022 2.955 3.019 4.65 ± 0.05
pacquet@main 3.014 ± 0.084 2.910 3.217 4.68 ± 0.14
pnpr@HEAD 0.643 ± 0.005 0.635 0.650 1.00
pnpr@main 0.652 ± 0.028 0.635 0.728 1.01 ± 0.04
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
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12106
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark 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%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12106
Testbedpnpr

⚠️ 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-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,148.23 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
643.38 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
643.36 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,122.57 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
755.51 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan merged commit 61969fb into pnpm:main Jun 16, 2026
26 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pnpm install is a no-op when only pnpm-lock.yaml is changed (e.g., git checkout) — regression from v10

4 participants