Skip to content

perf: cache pnpm's lockfile verification results - #30

Open
zkochan wants to merge 6 commits into
mainfrom
cache-lockfile-verification
Open

perf: cache pnpm's lockfile verification results#30
zkochan wants to merge 6 commits into
mainfrom
cache-lockfile-verification

Conversation

@zkochan

@zkochan zkochan commented Aug 13, 2026

Copy link
Copy Markdown
Member

Why

pnpm v11 and newer verify every lockfile entry against the configured supply-chain policies (minimumReleaseAge, trustPolicy, …) and memoize the verdict in <cacheDir>/lockfile-verified.jsonl. cache: true caches only pnpm store path, so every job starts with that verdict missing and re-checks the whole lockfile against the registry.

Measured on typescript-eslint's repository (2058 lockfile entries), where the store cache was warm:

? Verifying lockfile against supply-chain policies (2058 entries)...
✓ Lockfile passes supply-chain policies (2058 entries in 16.6s)
Done in 17.6s using pnpm v12.0.0-rc.4

On Windows it was 40.1s of a 42.4s install. Locally, the same install with the verdict already recorded takes 1.5s instead of 13.5s — the log itself is under a kilobyte.

What

  • The verdict is restored before pnpm install and saved in the post step, under its own key, pnpm-lockfile-verified-<OS>-<arch>-<lockfile hash>.
  • No prefix fallback on restore: the verdict is only valid for the exact lockfile it was recorded for, so an older entry could never be reused. (pnpm re-verifies anyway if it does not trust a record, so a stale one is safe, just useless.)
  • Saving runs before pnpm store prune, which deletes the log along with the store's other derived state.
  • The cache directory is read from pnpm config get cacheDir, falling back to pnpm's per-platform default — pnpm config get reports settings, not defaults, and prints undefined when the setting is unset.
  • Every failure in this path is a warning, never a failed build: the worst case is that the next job re-verifies.

Tests

A new job installs with a supply-chain policy configured on ubuntu, macOS and Windows, then asserts that pnpm wrote lockfile-verified.jsonl exactly where the action looks for it — that is the part of this change most likely to drift, since pnpm resolves cacheDir per platform and does not print it.

Follow-ups (not in this PR)

  • pnpm config get cacheDir cannot report the effective default, so the action mirrors pnpm's platform logic. A pnpm cache path command would remove the duplication.
  • The TypeScript CLI's pnpm store prune deletes lockfile-verified.jsonl; the Rust CLI's does not. Worth reconciling — the log is derived from the lockfile and the policies, not from the store.

Written by an agent (Claude Code, claude-opus-5).

Summary by CodeRabbit

  • New Features

    • Added caching for pnpm lockfile verification results alongside the pnpm store.
    • Cache entries are keyed by the lockfile, operating system, and architecture.
    • Added cross-platform support and verification for Linux, macOS, and Windows.
    • Verification caches are restored and saved automatically during workflow execution.
  • Documentation

    • Updated action and usage documentation to explain lockfile verification caching, cache keys, and behavior.

pnpm v11 and newer verify every lockfile entry against the configured
supply-chain policies (`minimumReleaseAge`, `trustPolicy`, ...) and memoize
the verdict in `<cacheDir>/lockfile-verified.jsonl`. The action cached only
the store, so every job started with that verdict missing and re-checked the
whole lockfile against the registry — on typescript-eslint's repository,
16.6s of a 17.6s install on Linux and 40.1s of 42.4s on Windows.

The verdict depends on the lockfile content and the policies, never on the
runner, so it is cached under its own key alongside the store cache and
restored without prefix fallback: an entry recorded for a different lockfile
could never be reused. Saving happens before `pnpm store prune`, which drops
the log along with the store's other derived state.

Anything that goes wrong here only costs the next job the re-verification, so
failures are reported as warnings instead of failing the build.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@zkochan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 941fb746-0389-4005-bfe3-319bcba20a09

📥 Commits

Reviewing files that changed from the base of the PR and between ec0bd77 and 7f46262.

⛔ Files ignored due to path filters (1)
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (7)
  • .github/workflows/test.yaml
  • README.md
  • action.yml
  • src/cache-restore/index.ts
  • src/cache-restore/run.ts
  • src/index.ts
  • src/lockfile-verification-cache/index.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b44aea7-2848-4743-a1d5-ea75822c7dca

📥 Commits

Reviewing files that changed from the base of the PR and between 72087f7 and ec0bd77.

📒 Files selected for processing (2)
  • src/index.ts
  • src/lockfile-verification-cache/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: zkochan
Repo: pnpm/setup PR: 1
File: src/cache-save/index.ts:10-11
Timestamp: 2026-05-11T16:24:38.150Z
Learning: In `src/cache-save/index.ts` (pnpm/setup), the `catch (error) { setFailed((error as Error).message) }` pattern is intentional and should not be flagged. The code is kept verbatim from `pnpm/action-setup` to simplify future syncs, and `actions/cache.saveCache` only throws `Error` instances per the documented toolkit contract. Do not suggest adding `instanceof Error` narrowing here.
Learnt from: zkochan
Repo: pnpm/setup PR: 25
File: action.yml:36-39
Timestamp: 2026-08-09T14:55:42.374Z
Learning: For the pnpm/setup action, the README “Context-aware global shims” section is the authoritative documentation for `PNPM_CONFIG_GLOBAL_SHIMS` workflow override behavior. Keep `action.yml` concise and avoid repeating the detailed condition that workflow-provided `PNPM_CONFIG_GLOBAL_SHIMS` or `pnpm_config_global_shims` values are preserved.
📚 Learning: 2026-05-11T16:24:38.150Z
Learnt from: zkochan
Repo: pnpm/setup PR: 1
File: src/cache-save/index.ts:10-11
Timestamp: 2026-05-11T16:24:38.150Z
Learning: In `src/cache-save/index.ts` (pnpm/setup), the `catch (error) { setFailed((error as Error).message) }` pattern is intentional and should not be flagged. The code is kept verbatim from `pnpm/action-setup` to simplify future syncs, and `actions/cache.saveCache` only throws `Error` instances per the documented toolkit contract. Do not suggest adding `instanceof Error` narrowing here.

Applied to files:

  • src/lockfile-verification-cache/index.ts
📚 Learning: 2026-08-09T14:55:39.968Z
Learnt from: zkochan
Repo: pnpm/setup PR: 25
File: src/install-runtime/index.ts:72-81
Timestamp: 2026-08-09T14:55:39.968Z
Learning: In `src/install-runtime/index.ts`, `keepInstalledRuntimeAuthoritative` must use a truthiness check for `PNPM_CONFIG_GLOBAL_SHIMS` and `pnpm_config_global_shims`. pnpm's `load_global_shims_setting` ignores empty environment values, so empty values must be treated as unset and the action must export `PNPM_CONFIG_GLOBAL_SHIMS` to disable the installed runtime's context-aware shim.

Applied to files:

  • src/lockfile-verification-cache/index.ts
📚 Learning: 2026-05-11T16:19:49.450Z
Learnt from: zkochan
Repo: pnpm/setup PR: 1
File: src/cache-restore/run.ts:35-35
Timestamp: 2026-05-11T16:19:49.450Z
Learning: When using `actions/exec` (`getExecOutput` / `exec`), it is valid for the `commandLine` option to include both the command and its arguments in a single string (e.g., `getExecOutput('pnpm store path --silent')`). The library tokenizes `commandLine` internally (via `argStringToArray()`), so this behaves like passing an equivalent command + args array (e.g., `getExecOutput('pnpm', ['store','path','--silent'])`). In code reviews, do not flag this as incorrect—this matches documented behavior and a production-tested pattern.

Applied to files:

  • src/lockfile-verification-cache/index.ts
🔇 Additional comments (1)
src/lockfile-verification-cache/index.ts (1)

10-12: LGTM!


📝 Walkthrough

Walkthrough

The action now caches pnpm lockfile verification results with the pnpm store. It restores and saves verification data by lockfile hash, resolves platform-specific cache paths, normalizes Windows paths, and validates the workflow on Linux, macOS, and Windows.

Changes

Lockfile verification cache

Layer / File(s) Summary
Verification cache implementation
src/lockfile-verification-cache/index.ts
Adds lockfile-specific restore and save operations. Resolves configured or platform-specific pnpm cache directories.
Cache restore and post-processing integration
src/cache-restore/run.ts, src/index.ts, src/windows-path/index.ts
Restores verification data with the dependency hash. Saves it before store pruning. Moves Windows path normalization into a dedicated module.
Platform validation and cache documentation
.github/workflows/test.yaml, action.yml, README.md
Adds cross-platform verification-cache checks. Documents that the cache includes pnpm store data and lockfile verification results.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: ⚪ Minimal · up to ec0bd

The change adds best-effort caching for pnpm lockfile verification results without introducing a supplied merge-blocking correctness or production risk; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Workflow
  participant SetupAction
  participant PnpmCache
  Workflow->>SetupAction: Run with caching enabled
  SetupAction->>PnpmCache: Restore store and verification cache
  SetupAction->>PnpmCache: Save verification data during post-processing
  Workflow->>PnpmCache: Verify lockfile-verified.jsonl
Loading

Possibly related PRs

  • pnpm/setup#1: Extends the existing cache implementation and related documentation.
  • pnpm/setup#14: Both changes modify cache restoration in src/cache-restore/run.ts.
  • pnpm/setup#22: Refactors the Windows cache-path normalization helper.

Poem

A rabbit hops through cached files,
With lockfile hashes in tidy piles.
Across platforms, checks appear,
The pnpm paths are clean and clear.
“Save the proof!” the bunny cheers.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: caching pnpm lockfile verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cache-lockfile-verification

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

perf: cache pnpm's lockfile verification results across CI runs

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds a second GitHub Actions cache for pnpm's lockfile supply-chain verification log
 (lockfile-verified.jsonl), keyed on OS, arch and lockfile hash.
• Restores the verdict before pnpm install and saves it before pnpm store prune, which would
 otherwise delete it.
• Resolves pnpm's cacheDir via pnpm config get cacheDir, falling back to pnpm's per-platform
 defaults when unset.
• Extracts the Windows extended-length path fix into a shared windows-path module reused by both
 caches.
• Treats every failure in the new caching path as a non-fatal warning.
• Adds a new CI job across ubuntu/macOS/Windows that configures a supply-chain policy and asserts
 the verification log lands where the action expects it.
• Updates README and action.yml docs to describe the new caching behavior.
Diagram

graph TD
  A["cache-restore/run.ts"] --> B["lockfile-verification-cache"]
  B --> C["pnpm config get cacheDir"] --> D[(lockfile-verified.jsonl)]
  B --> E[[GitHub Actions Cache]]
  F["index.ts runPost"] --> G["saveVerificationCache"] --> E
  F --> H["pnpm-store-prune"] --> D
  B --> I["windows-path"]
  subgraph Legend
    direction LR
    _mod([Module]) ~~~ _db[(Cache file)] ~~~ _ext{{External service}}
  end
Loading
High-Level Assessment

Caching the verification log as a separate, precisely-scoped cache entry (rather than folding it into the store cache or widening the store cache's restore-key fallback) is the correct approach: the verdict is only valid for an exact lockfile match, so allowing prefix-fallback reuse (as the store cache does) would be unsafe, and combining it with the store cache would force unnecessary invalidation of the much larger store whenever only the policy config changes. Deriving cacheDir via pnpm config get with a hardcoded per-platform fallback is a reasonable stopgap given pnpm currently offers no command that reports the effective default; the PR explicitly flags this duplication as a known follow-up pending a pnpm cache path command upstream.

Files changed (7) +185 / -23

Enhancement (3) +109 / -21
index.tsNew module caching pnpm's lockfile verification log +95/-0

New module caching pnpm's lockfile verification log

• Introduces restoreVerificationCache and saveVerificationCache, which read/write GitHub Actions Cache entries for pnpm's lockfile-verified.jsonl, keyed by OS/arch/lockfile hash with no restore-key fallback. Resolves pnpm's cacheDir via 'pnpm config get cacheDir', falling back to a hardcoded per-platform default when unset, and treats all failures as warnings.

src/lockfile-verification-cache/index.ts

run.tsWire lockfile verification cache restore into the store cache flow +10/-21

Wire lockfile verification cache restore into the store cache flow

• Splits runRestoreCache into runRestoreStoreCache plus a call to restoreVerificationCache, both keyed on the same lockfile hash, and imports removeWindowsExtendedPathPrefix from the new windows-path module instead of defining it locally.

src/cache-restore/run.ts

index.tsSave verification cache before pnpm store prune in post step +4/-0

Save verification cache before pnpm store prune in post step

• Calls saveVerificationCache() at the start of runPost, ahead of pruneStore and saveCache, since pnpm store prune deletes the verification log along with other derived store state.

src/index.ts

Refactor (1) +19 / -0
index.tsExtract Windows extended-path stripping into shared module +19/-0

Extract Windows extended-path stripping into shared module

• Moves removeWindowsExtendedPathPrefix out of cache-restore/run.ts into its own module so it can be reused by the new lockfile-verification-cache code.

src/windows-path/index.ts

Tests (1) +43 / -0
test.yamlAdd cross-platform CI job asserting the verification log location +43/-0

Add cross-platform CI job asserting the verification log location

• Adds a matrix job (ubuntu/macOS/Windows) that appends a minimumReleaseAge policy to pnpm-workspace.yaml, runs the action with cache: true, and asserts pnpm wrote lockfile-verified.jsonl at the path the action expects on each OS.

.github/workflows/test.yaml

Documentation (2) +14 / -2
README.mdDocument lockfile verification caching +10/-1

Document lockfile verification caching

• Updates the cache input description and adds a section explaining that caching now covers both the pnpm store and the lockfile verification results, and why the latter matters on large repositories.

README.md

action.ymlUpdate cache input description to mention verification results +4/-1

Update cache input description to mention verification results

• Expands the 'cache' input's description to state that it also caches pnpm's lockfile verification results against supply-chain policies, keyed on the lockfile hash.

action.yml

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Reviews (6): Last reviewed commit: "feat: check the verification log before ..." | Re-trigger Greptile

Comment thread src/index.ts Outdated
The module header explained the whole feature where naming the file's purpose
is enough, and the ordering comment described `pnpm store prune` deleting the
log without saying which versions do — pnpm/pnpm#13893 stops deleting it.
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 13, 2026
The log is under a kilobyte and pnpm writes it on every install, not only
where supply-chain policies are configured: the integrity and tarball-URL
checks are unconditional. A job that starts without it re-checks every
lockfile entry against the registry — on a ~2000-entry lockfile with a warm
store, 13.5s vs 1.5s with `minimumReleaseAge` and `trustPolicy` configured,
and still 6.7s vs 1.6s with no policies at all.

Tying that to the `cache` input made the common case slow for no saving worth
counting, so the log is now restored and saved on its own key whether or not
the store is cached. `cache` goes back to meaning what its name says.
@greptile-apps
greptile-apps Bot dismissed their stale review August 13, 2026 14:57

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 13, 2026
Saving in the post step left the whole job between the install and the upload.
Anything running in that window — the job's tests, its build, a dependency's
own install scripts — can rewrite the log on disk, and the job's own cache
write would then publish a record claiming some other lockfile passed
verification, for every later job to restore and trust. No cache credentials
needed: the attacker rides the write the job performs anyway.

The log is complete the moment the install finishes, so it is uploaded there.
The post step still covers a job that installs in a step of its own, where
that is the first point the log is known to be final; the save is idempotent
across the two, and the process-local flags exist because main and post do not
share state within a run.
@greptile-apps
greptile-apps Bot dismissed their stale review August 13, 2026 15:03

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 13, 2026
The previous commit listed a dependency's own scripts among the things that
run after the install, which is where they do not run: pnpm executes them
during the install, ahead of the upload, so they stay inside the window rather
than being closed out of it. What keeps that narrow is that pnpm refuses to
run them at all — `ERR_PNPM_IGNORED_BUILDS` — unless the repository
allow-lists the package, and such a package can already run code in the job.
@greptile-apps
greptile-apps Bot dismissed their stale review August 13, 2026 15:09

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 13, 2026
Moving the upload to just after the install left one window open: pnpm runs a
package's lifecycle scripts during the install, so an allow-listed dependency
can still append a record claiming some other lockfile passed verification, and
the upload would publish it. Writing pnpm's own record after those scripts
would not help — the log is appended to, so the forged record survives whatever
pnpm writes next to it.

What does distinguish the two is shape: an install appends its own verdict and
leaves earlier records untouched. So the log is uploaded only when every record
that predated the install is still there, and no more records were added than
there were installs. Both failure modes cost a re-verification in the next job
and nothing else, which is also the price of pnpm compacting the log past a
thousand records — rare enough in CI, where a job restores at most one record.
@greptile-apps
greptile-apps Bot dismissed their stale review August 13, 2026 15:14

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant