Skip to content

fix: self-update bootstrap to packageManager-pinned version (#233)#256

Merged
zkochan merged 3 commits into
masterfrom
fix/233
May 11, 2026
Merged

fix: self-update bootstrap to packageManager-pinned version (#233)#256
zkochan merged 3 commits into
masterfrom
fix/233

Conversation

@zkochan
Copy link
Copy Markdown
Member

@zkochan zkochan commented May 11, 2026

Summary

  • When package.json pins pnpm via packageManager or devEngines.packageManager.version (exact or range), self-update the bootstrap pnpm to that version up front instead of deferring to pnpm's runtime auto-switch.
  • pnpm store path (the store command sets skipPackageManagerCheck = true in pnpm 11.0.x) doesn't trigger the auto-switch, so the bootstrap reports its own STORE_VERSION while the user's actual pnpm install runs under the pinned version and writes to a different STORE_VERSION. That mismatch is what makes cache: true (and actions/setup-node@v6's cache: pnpm) fail on a cold cache with Path Validation Error: Path(s) specified in the action for caching do(es) not exist.
  • pnpm self-update accepts both exact versions and semver ranges, so we pass either through directly. That makes readTargetVersion total — it always returns a string or throws — so the runtime auto-switch fallback and the pnpm_config_pm_on_fail=download export added for Action fails with devEngines.packageManager.onFail="error" when version input is omitted #252 are no longer reachable and get removed.
  • Drive-by: when pnpm self-update is a no-op (the pinned version resolves to the bootstrap version), PNPM_HOME/bin is not created — fall back to pnpmHome for binDest. Also strip the integrity hash from packageManager: pnpm@<ver>+<sha> before comparing against version: input (pre-existing false-mismatch).

Fixes #233.

Test plan

  • New cache_store_path matrix in test.yaml pins pnpm 10 via packageManager, exact devEngines, and a >=10 <11 range, runs the action with cache: true + run_install, then asserts pnpm store path resolves to a directory that exists on disk — fails on master, passes on this branch.
  • Existing Manifest pin matrix still passes (exact pins now go through self-update; range pins also go through self-update and resolve to whatever satisfies the range, so the >= 9.15.0 assertions still hold).
  • Smoke and Standalone matrices still pass (explicit version: input path is unchanged).

Summary by CodeRabbit

  • Tests

    • Added a CI job to regression-test pnpm cache-store correctness when a different pnpm major is pinned in package.json.
  • Bug Fixes

    • Always perform pnpm self-update and more reliably detect the pnpm binary location after update.
    • Normalize packageManager strings (strip prefixes/suffixes) for accurate version comparisons.
    • Prefer exact semver from dev engine configuration when selecting a pnpm target.

Review Change Stack

When package.json pins pnpm via `packageManager` or an exact
`devEngines.packageManager.version`, self-update the bootstrap up
front. The bootstrap's `pnpm store path` skips pnpm's auto-switch
(the `store` command sets `skipPackageManagerCheck = true`), so it
reports the bootstrap's `STORE_VERSION` while the user's actual
install runs under the pinned version and writes to a different
STORE_VERSION — breaking `cache: true` and `setup-node`'s
`cache: pnpm` on cold caches.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

readTargetVersion now normalizes packageManager (strip pnpm@ and +<integrity>), always returns a pnpm version or throws, runSelfInstaller runs pnpm self-update unconditionally and chooses PNPM_HOME/bin only if it exists, and a new CI job asserts pnpm store path --silent returns an existing directory for pinned pnpm manifests.

Changes

pnpm Cache Path Validation Fix

Layer / File(s) Summary
Target version return
src/install-pnpm/run.ts
readTargetVersion now returns a non-optional string and throws if no pnpm version can be determined.
Normalize packageManager
src/install-pnpm/run.ts
Strip pnpm@ prefix and remove +<integrity> suffix from packageManager before comparisons and returns.
devEngines priority
src/install-pnpm/run.ts
If devEngines.packageManager targets pnpm, its version is returned directly; otherwise the normalized packageManagerVersion is used.
Self-update & bin destination
src/install-pnpm/run.ts
runSelfInstaller unconditionally runs pnpm self-update with the target version and uses PNPM_HOME/bin only if that directory exists, falling back to pnpmHome.
CI: cache_store_path job
.github/workflows/test.yaml
Adds cache_store_path job that matrixes pinned pnpm scenarios, runs the action with caching and --no-frozen-lockfile, and asserts the pnpm store path --silent directory exists.

Sequence Diagram

sequenceDiagram
  participant PackageJSON as package.json
  participant readTargetVersion as readTargetVersion()
  participant runSelfInstaller as runSelfInstaller()
  participant FileSystem as FileSystem
  participant Cache as pnpm store

  PackageJSON->>readTargetVersion: packageManager / devEngines.packageManager
  readTargetVersion-->>runSelfInstaller: resolved target version (string) or throws
  runSelfInstaller->>runSelfInstaller: pnpm self-update <target>
  runSelfInstaller->>FileSystem: check PNPM_HOME/bin exists
  alt PNPM_HOME/bin exists
    FileSystem-->>runSelfInstaller: exists
    runSelfInstaller-->>Cache: set binDest -> PNPM_HOME/bin
  else PNPM_HOME/bin missing
    FileSystem-->>runSelfInstaller: missing
    runSelfInstaller-->>Cache: fallback binDest -> pnpmHome
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • pnpm/action-setup#249: Modifies runSelfInstaller's handling of self-update and the resolved bin destination.
  • pnpm/action-setup#254: Overlaps in installer/version-selection logic and related environment handling.

Poem

🐰 I nibbled hashes, peeled the suffix clean,

I found the target version, no more in-between,
I hopped to PNPM_HOME to see if bin was there,
If missing, I backed up — kept the path square,
Now cache and store sleep safe without a care.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Title check ✅ Passed The title accurately describes the main fix: self-updating the bootstrap pnpm to the version pinned in packageManager, which directly addresses the Path Validation Error issue.
Linked Issues check ✅ Passed The changes implement the core objective from #233: ensuring the action uses the same pnpm version for determining store path that the pinned version uses, fixing the cache path mismatch that caused Path Validation Errors.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the pnpm version mismatch issue: self-update logic, version normalization, and the regression test for cache store path validation.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/233

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.github/workflows/test.yaml (1)

209-216: ⚡ Quick win

Consider adding a test case for packageManager with integrity hash suffix.

The fix includes stripping the integrity hash from packageManager values (e.g., [email protected]+sha512.abc...). This scenario from issue #233 isn't covered in the cache_store_path matrix. Adding a test case would help guard against regressions in the hash-stripping logic.

Suggested additional matrix entry
         include:
           - label: 'packageManager [email protected]'
             manifest: '{"packageManager":"[email protected]","dependencies":{"is-odd":"3.0.1"}}'
+          - label: 'packageManager [email protected] with integrity hash'
+            manifest: '{"packageManager":"[email protected]+sha512.1234567890abcdef","dependencies":{"is-odd":"3.0.1"}}'
           - label: 'devEngines exact [email protected]'
             manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":"10.33.0"}},"dependencies":{"is-odd":"3.0.1"}}'
🤖 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 @.github/workflows/test.yaml around lines 209 - 216, Add a matrix test entry
that covers packageManager values with an integrity-hash suffix so the cache
path hashing/strip logic is exercised; specifically add an include item in the
strategy.matrix (alongside the existing 'packageManager [email protected]' and
'devEngines exact [email protected]') with a manifest where packageManager is set to
a value containing a suffix like "[email protected]+sha512.<hash>" and dependencies
similar to the other entries, and optionally a matching devEngines variant to
validate the stripping logic in both packageManager and devEngines handling.
🤖 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 @.github/workflows/test.yaml:
- Around line 209-216: Add a matrix test entry that covers packageManager values
with an integrity-hash suffix so the cache path hashing/strip logic is
exercised; specifically add an include item in the strategy.matrix (alongside
the existing 'packageManager [email protected]' and 'devEngines exact [email protected]')
with a manifest where packageManager is set to a value containing a suffix like
"[email protected]+sha512.<hash>" and dependencies similar to the other entries, and
optionally a matching devEngines variant to validate the stripping logic in both
packageManager and devEngines handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8971dc09-f3e9-4d12-ba3a-7b4ff2d5d83f

📥 Commits

Reviewing files that changed from the base of the PR and between f61705d and 6f51e58.

⛔ Files ignored due to path filters (1)
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (2)
  • .github/workflows/test.yaml
  • src/install-pnpm/run.ts
📜 Review details
🔇 Additional comments (8)
src/install-pnpm/run.ts (7)

101-106: LGTM!

The filesystem check to determine actual binary location handles the no-op self-update edge case correctly. When the target version equals the bootstrap version, pnpm self-update is a no-op and won't create PNPM_HOME/bin/pnpm, so falling back to pnpmHome is the right behavior.


109-116: LGTM!

The expanded comment clearly explains the rationale for setting pmOnFail=download only in this branch, and the scoping decision to preserve strict onFail behavior when users pass an explicit version: input.


144-149: LGTM!

The integrity hash stripping logic correctly handles both pnpm@<version> and pnpm@<version>+<integrity> formats using .split('+')[0].


151-160: LGTM!

The comparison now correctly uses the normalized packageManagerVersion (without integrity hash) against the version input, preventing false mismatches like 10.33.0 vs 10.33.0+sha512....


162-179: LGTM!

The devEngines priority logic correctly matches pnpm's getWantedPackageManager behavior. The early return of undefined for ranges (lines 177-178) ensures the bootstrap auto-switch path is used, since pnpm self-update requires a specific version.


181-183: LGTM!

The fallback to packageManagerVersion (already stripped of integrity hash) correctly completes the priority chain: devEngines → packageManager → error.


199-202: LGTM!

The regex correctly identifies exact semver versions (including optional prerelease and build metadata) while rejecting range specifiers like >=, ^, ~, and wildcards. This enables the intended behavior of triggering self-update only for exact pins.

.github/workflows/test.yaml (1)

232-241: LGTM!

The test correctly validates the fix for #233 by verifying that the store path reported by pnpm store path --silent actually exists on disk. This directly guards against the "Path Validation Error" that occurs when the cached path doesn't exist.

On Windows pnpm self-update writes `.bin/bin/pnpm` (a JS launcher),
not `.bin/bin/pnpm.exe`, so the previous existsSync probe always
fell back to pnpmHome and the bin_dest output pointed at the
bootstrap pnpm. Check whether the `bin` directory itself exists.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/install-pnpm/run.ts (1)

151-183: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same precedence for mismatch validation and target selection.

Line 152 still validates version: only against packageManager, but Lines 173-183 now treat exact devEngines.packageManager.version as the preferred manifest pin. That means version: can be rejected even when it matches the version this function would otherwise select. Please compare against the effective manifest target instead of packageManagerVersion alone.

Proposed fix
   const packageManagerVersion =
     typeof packageManager === 'string' && packageManager.startsWith('pnpm@')
       ? packageManager.slice('pnpm@'.length).split('+')[0]
       : undefined
+
+  const effectiveManifestVersion =
+    devEngines?.packageManager?.name === 'pnpm'
+      ? (devEngines.packageManager.version != null && isExactSemver(devEngines.packageManager.version)
+          ? devEngines.packageManager.version
+          : undefined)
+      : packageManagerVersion
 
   if (version) {
-    if (packageManagerVersion && packageManagerVersion !== version) {
+    if (effectiveManifestVersion && effectiveManifestVersion !== version) {
       throw new Error(`Multiple versions of pnpm specified:
   - version ${version} in the GitHub Action config with the key "version"
   - version ${packageManager} in the package.json with the key "packageManager"
 Remove one of these versions to avoid version mismatch errors like ERR_PNPM_BAD_PM_VERSION`)
     }
 
     return version
   }
 
-  if (devEngines?.packageManager?.name === 'pnpm') {
-    const v = devEngines.packageManager.version
-    if (v != null && isExactSemver(v)) {
-      return v
-    }
-    return undefined
-  }
-
-  if (packageManagerVersion) {
-    return packageManagerVersion
-  }
+  if (devEngines?.packageManager?.name === 'pnpm') return effectiveManifestVersion
+  if (effectiveManifestVersion) return effectiveManifestVersion
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/install-pnpm/run.ts` around lines 151 - 183, The validation currently
rejects the Action `version` by comparing it only to `packageManagerVersion`,
but the selection logic later prefers an exact
`devEngines.packageManager.version` (checked via
`devEngines?.packageManager?.version` and `isExactSemver`) over
`packageManagerVersion`; update the mismatch check so it compares `version`
against the effective manifest target (the value you would return from the
manifest logic: prefer exact `devEngines.packageManager.version` when present,
otherwise `packageManagerVersion`) instead of `packageManagerVersion`
alone—compute that effective target once (reuse the same logic used in the later
return path) and use it in the `if (packageManagerVersion &&
packageManagerVersion !== version)` validation (or replace that check) to ensure
consistent precedence between validation and selection.
🤖 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.

Outside diff comments:
In `@src/install-pnpm/run.ts`:
- Around line 151-183: The validation currently rejects the Action `version` by
comparing it only to `packageManagerVersion`, but the selection logic later
prefers an exact `devEngines.packageManager.version` (checked via
`devEngines?.packageManager?.version` and `isExactSemver`) over
`packageManagerVersion`; update the mismatch check so it compares `version`
against the effective manifest target (the value you would return from the
manifest logic: prefer exact `devEngines.packageManager.version` when present,
otherwise `packageManagerVersion`) instead of `packageManagerVersion`
alone—compute that effective target once (reuse the same logic used in the later
return path) and use it in the `if (packageManagerVersion &&
packageManagerVersion !== version)` validation (or replace that check) to ensure
consistent precedence between validation and selection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a99954b8-d39d-4ddc-8924-9dccbaabc0a7

📥 Commits

Reviewing files that changed from the base of the PR and between 6f51e58 and 29b3fcd.

⛔ Files ignored due to path filters (1)
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (1)
  • src/install-pnpm/run.ts

`pnpm self-update` resolves semver ranges to a specific version, so
`devEngines.packageManager.version: ">=10 <11"` can also go through
the self-update path. That makes readTargetVersion total — it always
returns a string or throws — so the runtime auto-switch fallback (and
the `pnpm_config_pm_on_fail=download` export from #252) is no longer
reachable and gets removed.

Adds a range case to the cache_store_path matrix.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@src/install-pnpm/run.ts`:
- Around line 150-166: The code returns devEngines.packageManager.version
directly (and falls back to packageManagerVersion) and the comment incorrectly
claims "pnpm self-update" accepts semver ranges; you must either resolve any
semver range to an exact version or validate and fail early. Update the logic
around reading devEngines.packageManager.version and packageManagerVersion (the
value passed to "pnpm self-update") to detect range expressions (using semver
utilities) and if a range is provided resolve it to a specific version (e.g., by
querying the registry or using semver.maxSatisfying against available tags) or
throw a clear error indicating an exact version is required; also update/remove
the incorrect comment about range support for "pnpm self-update".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a9fbcce0-72a5-4e2d-b5f3-c56c0fa0853f

📥 Commits

Reviewing files that changed from the base of the PR and between 29b3fcd and 964ca0d.

⛔ Files ignored due to path filters (1)
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (2)
  • .github/workflows/test.yaml
  • src/install-pnpm/run.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/test.yaml
📜 Review details
🔇 Additional comments (3)
src/install-pnpm/run.ts (3)

87-105: LGTM!

The self-update logic correctly handles both the successful update case (returning PNPM_HOME/bin) and the no-op case where target version equals bootstrap version (falling back to pnpmHome). The comments clearly explain why this fallback is necessary.


132-137: LGTM!

The integrity hash stripping logic correctly handles the pnpm@<version>+<integrity> format. The .split('+')[0] approach safely handles both cases: versions with integrity hashes and plain versions without them.


139-148: LGTM!

The version mismatch check now correctly compares the stripped packageManagerVersion against the action input, preventing false positives from integrity hash suffixes. The error message helpfully shows the original packageManager value for debugging.

Comment thread src/install-pnpm/run.ts
@zkochan zkochan merged commit 739bfe4 into master May 11, 2026
20 checks passed
MarvinZeising added a commit to Staffbase/gha-workflows that referenced this pull request May 17, 2026
The v6.0.0–v6.0.6 self-update bootstrap bug (pnpm/action-setup#256) that
caused ERR_PNPM_BROKEN_LOCKFILE when `packageManager` is set in
package.json has been fixed upstream in v6.0.7 and polished in v6.0.8
(released 2026-05-12). Replace the v4.4.0 workaround with the real fix.

Ref: pnpm/action-setup#256

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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.

v6 causes: Error: Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.

1 participant