| | |

pnpm 13 📦 CI/CD Integration with pnpm install –frozen-lockfile

⏱️ Estimated read: 10 min | |

A lockfile that is committed but not enforced is a suggestion, not a contract. The --frozen-lockfile flag is the mechanism that turns the lockfile into an enforcement tool: it installs the exact versions recorded in the lockfile and fails if the lockfile would need to change. In pnpm, this flag is not just recommended for CI—it is the default behavior when CI=true is detected. Since version 11, pnpm also fails on lockfiles written by a newer major version, preventing silent rewrites that could mask incompatibilities .

Key point: When pnpm detects a CI environment, it automatically switches to frozen-lockfile mode. The --frozen-lockfile flag makes this behavior explicit and independent of environment detection. The flag fails the build if package.json and pnpm-lock.yaml are out of sync, forcing the developer to commit the lockfile change before the pipeline can proceed.


Why frozen lockfiles and CI-specific behavior matter

The silent drift problem. In a local environment, running pnpm install after adding a dependency without updating the lockfile resolves the new dependency and rewrites the lockfile. This is convenient for development but dangerous in CI: the build succeeds with a dependency tree that was never reviewed, committed, or tested. The --frozen-lockfile flag closes this gap by making the install a verification step rather than a resolution step .

The CI auto-detection problem. pnpm does not require the flag to be passed explicitly in CI. When the CI environment variable is set, pnpm automatically enables frozen-lockfile mode. This means a CI pipeline that runs a bare pnpm install is already protected. The flag exists to make the behavior explicit and to ensure it applies in environments where CI may not be set correctly, such as certain container runtimes or self-hosted runners .

The lockfile version incompatibility problem. Since pnpm 11, CI installs fail if the lockfile was written by a newer pnpm major version. This prevents a CI runner with an older pnpm version from silently rewriting a lockfile that was generated by a newer version, which could introduce subtle resolution differences. The fix is to ensure the CI pnpm version matches the version used to generate the lockfile .

The metadata cache cost problem. In CI, the dominant cost of an install (once the store is warm) is verifying the lockfile against supply-chain policies. pnpm 11.22.0 introduced pnpm cache path to print the metadata cache directory. Caching this directory alongside the store allows CI jobs to skip re-verifying an unchanged lockfile, significantly reducing install time .

The trade-off. A frozen lockfile install is strict and deterministic. It fails when the lockfile is out of sync, which is the correct behavior for CI but can be frustrating when a developer forgets to commit the lockfile. The error message identifies the mismatch, and the fix is always to run pnpm install locally and commit the result.


a. The –frozen-lockfile flag and its alternatives

The --frozen-lockfile flag tells pnpm to install exactly what the lockfile specifies and to fail if the lockfile does not match package.json. It is the CI equivalent of npm ci .

# Frozen install: fail if lockfile would change
pnpm install --frozen-lockfile

# In CI, this is the default behavior
# No flag needed if CI=true is set
pnpm install

The flag has two related siblings. --no-frozen-lockfile disables the behavior, which is useful only in workflows that intentionally regenerate the lockfile, such as a dependency update bot. --fix-lockfile attempts to repair a lockfile that has become inconsistent, such as one left by a badly merged pull request, instead of failing outright .

The --prefer-offline flag is sometimes confused with frozen-lockfile. It skips the staleness check for cached metadata but still fetches missing data from the server. It does not verify the lockfile consistency and should not be used as a substitute for --frozen-lockfile in CI .


b. Installing pnpm in CI without Corepack

The pnpm documentation now recommends against Corepack for CI because it installs a JavaScript shim that requires Node.js to start before pnpm itself starts, adding overhead to every invocation. The standalone install script is the preferred method, and it has two advantages: it needs no Node.js (pnpm can install Node.js itself), and it follows the version declared in package.json‘s packageManager field automatically .

# Standalone install in CI
curl -fsSL https://get.pnpm.io/install.sh | sh -

# Declare PNPM_HOME and PATH (CI jobs do not reload shell profiles)
export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$PNPM_HOME/bin:$PATH"

# If the build scripts call node directly, install Node.js via pnpm
pnpm runtime set node lts -g

The packageManager field in package.json is the version contract. When pnpm starts and detects this field, it switches to the specified version. The CI pipeline does not need to pin a version in two places—the manifest is the source of truth .

For GitHub Actions, pnpm/action-setup@v4 is the official installer. It must run before actions/setup-node@v4 if the setup-node action is used with cache: 'pnpm', because the cache detection looks for the pnpm binary .


c. Caching the store and metadata cache

pnpm’s global store is the primary cache target. The store location is printed by pnpm store path. Caching this directory across CI runs eliminates the need to download packages that were already fetched in previous runs .

# GitHub Actions example
- name: Get pnpm store directory
  id: pnpm-cache
  run: echo "store=$(pnpm store path)" >> $GITHUB_OUTPUT

- uses: actions/cache@v4
  with:
    path: ${{ steps.pnpm-cache.outputs.store }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-store-

The cache key includes the lockfile hash. When the lockfile changes, the cache is invalidated and a fresh store is populated. The restore-keys fallback allows a partial cache restore even when the exact key does not match .

Since pnpm 11.22.0, the metadata cache can also be cached. The directory is printed by pnpm cache path. It contains the lockfile verification log, which allows CI to skip re-verification when the lockfile is unchanged .

- name: Get pnpm metadata cache
  id: pnpm-meta
  run: echo "cache=$(pnpm cache path)" >> $GITHUB_OUTPUT

- uses: actions/cache@v4
  with:
    path: ${{ steps.pnpm-meta.outputs.cache }}
    key: ${{ runner.os }}-pnpm-meta-${{ hashFiles('**/pnpm-lock.yaml') }}

The pnpm documentation notes that caching the store is not always faster than a fresh install from npm’s CDN. GitHub Actions cache restore has overhead, and for small dependency trees, a direct install may be faster. The decision depends on the project’s dependency count and the CI runner’s network speed .


Complete Example Session

# ============================================
# PART 1: INSTALL PNPM IN CI
# ============================================

curl -fsSL https://get.pnpm.io/install.sh | sh -
export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$PNPM_HOME/bin:$PATH"
# ============================================
# PART 2: VERIFY PNPM VERSION
# ============================================

pnpm --version
# Output: 12.x.x (or the version from packageManager)
# ============================================
# PART 3: GET STORE PATH
# ============================================

pnpm store path
# Output: /home/runner/.local/share/pnpm/store/v10
# ============================================
# PART 4: RUN FROZEN INSTALL
# ============================================

pnpm install --frozen-lockfile
# Fails if package.json and pnpm-lock.yaml disagree.
# Succeeds if the lockfile is consistent.
# ============================================
# PART 5: OBSERVE CI AUTO-DETECTION
# ============================================

CI=true pnpm install
# Same behavior as --frozen-lockfile.
# ============================================
# PART 6: CACHE THE STORE (GITHUB ACTIONS)
# ============================================

- name: Cache pnpm store
  uses: actions/cache@v4
  with:
    path: $(pnpm store path)
    key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
# ============================================
# PART 7: CACHE THE METADATA CACHE
# ============================================

- name: Cache pnpm metadata
  uses: actions/cache@v4
  with:
    path: $(pnpm cache path)
    key: pnpm-meta-${{ hashFiles('**/pnpm-lock.yaml') }}
# ============================================
# PART 8: HANDLE LOCKFILE MISMATCH
# ============================================

# If --frozen-lockfile fails:
pnpm install
git add pnpm-lock.yaml
git commit -m "Update lockfile"
git push
# The next CI run succeeds.
# ============================================
# PART 9: REPAIR BROKEN LOCKFILE
# ============================================

pnpm install --fix-lockfile
# Repairs a lockfile with missing dependency entries.

The nine parts trace the CI workflow: installing pnpm, verifying the version, getting the store path, running a frozen install, observing CI auto-detection, caching the store, caching the metadata cache, handling a mismatch, and repairing a broken lockfile.


Quick Reference

Frozen Lockfile Flags

FlagEffect
--frozen-lockfileFail if lockfile would change
--no-frozen-lockfileAllow lockfile updates
--fix-lockfileRepair inconsistent lockfile

CI Environment Variables

VariableEffect
CI=trueEnables frozen-lockfile automatically
PNPM_HOMEInstall directory for standalone pnpm

Cache Paths

CommandPurpose
pnpm store pathContent-addressable store location
pnpm cache pathMetadata cache location

GitHub Actions Order

StepAction
1pnpm/action-setup@v4 (or standalone script)
2actions/setup-node@v4 with cache: 'pnpm' (optional)
3pnpm install --frozen-lockfile

Best Practices

✅ Do This:

pnpm install --frozen-lockfile
<!-- Explicit flag for clarity in CI -->              # ✅
pnpm cache path
<!-- Cache metadata for faster verification -->       # ✅
pnpm install --fix-lockfile
<!-- Repair inconsistent lockfile instead of failing --> # ✅

❌ Don’t Do This:

# Rely on CI auto-detection alone
<!-- Explicit flag is safer in containers -->         # ❌
# Cache node_modules instead of the pnpm store
<!-- pnpm uses symlinks; the store is the content --> # ❌
# Use --no-frozen-lockfile in CI
<!-- Allows unreviewed dependency resolution -->      # ❌

Common Pitfalls

PitfallWhy It HappensFix
Build fails with frozen lockfileLockfile out of syncRun pnpm install locally and commit
Lockfile version errorCI pnpm newer/older than lockfileMatch pnpm versions in CI and local
Cache restore slower than installSmall dependency treeDisable store caching if slower
pnpm: command not foundPNPM_HOME not in PATHExport PATH in CI step
Metadata cache not usedNot cachedAdd pnpm cache path to cache step

Real-World Examples

1. Frozen Install

pnpm install --frozen-lockfile

2. CI Auto-Detection

CI=true pnpm install

3. Get Store Path

pnpm store path

4. Get Metadata Cache Path

pnpm cache path

5. Standalone Install

curl -fsSL https://get.pnpm.io/install.sh | sh -

6. GitHub Actions Setup

- uses: pnpm/action-setup@v4

7. Cache Key

key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}

8. Fix Lockfile

pnpm install --fix-lockfile

9. Disable Frozen

pnpm install --no-frozen-lockfile

10. Verify Install

pnpm why <package>

Visual

CI Install Decision

┌──────────────────────────────────────────────┐
│  pnpm install in CI                          │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  CI=true detected?                           │
│  ├─ YES → Frozen lockfile mode               │
│  └─ NO  → Normal install (allow changes)     │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Lockfile matches package.json?              │
│  ├─ YES → Install exact versions             │
│  └─ NO  → FAIL                               │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Store and metadata cache warm?              │
│  ├─ YES → Fast install                       │
│  └─ NO  → Download packages                  │
└──────────────────────────────────────────────┘

Cache Architecture

┌──────────────────────────────────────────────┐
│  STORE (pnpm store path)                     │
│  ├── Package files (content-addressable)     │
│  ├── Hard-linked into projects               │
│  └── Primary cache target                    │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  METADATA CACHE (pnpm cache path)            │
│  ├── Registry metadata                       │
│  ├── Lockfile verification log               │
│  └── Secondary cache target                  │
└──────────────────────────────────────────────┘

Summary

ItemValue
Frozen flag--frozen-lockfile
CI auto-detectionCI=true enables frozen mode
Install scriptcurl -fsSL https://get.pnpm.io/install.sh | sh -
Store pathpnpm store path
Metadata cache pathpnpm cache path
GitHub Actions installerpnpm/action-setup@v4
Lockfile repairpnpm install --fix-lockfile
Lockfile version checkFails if lockfile written by newer major

Key takeaways:

  • --frozen-lockfile makes CI a verification step. The lockfile must already contain every resolution. A mismatch fails the build instead of being silently repaired .
  • pnpm enables frozen-lockfile automatically when CI=true. The flag exists to make the behavior explicit and to ensure it applies in environments where CI may not be set .
  • The standalone install script is preferred over Corepack. It avoids the Node.js shim overhead and follows the version declared in package.json‘s packageManager field .
  • The store and metadata cache are separate cache targets. The store contains package files. The metadata cache contains registry metadata and the lockfile verification log. Both can be cached to reduce install time .
  • Lockfile version mismatches fail in CI since pnpm 11. If the CI pnpm version is newer or older than the version that wrote the lockfile, the install fails instead of silently rewriting .

Remember: A CI pipeline that installs dependencies without a frozen lockfile is not verifying anything. It is resolving a fresh dependency tree on every run, and the result may not match what was tested. The --frozen-lockfile flag is the line between a build that lies and a build that verifies. For pnpm, the flag is enforced by default in CI, but passing it explicitly documents the intent.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!