pnpm 13 📦 CI/CD Integration with pnpm install –frozen-lockfile
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
| Flag | Effect |
|---|---|
--frozen-lockfile | Fail if lockfile would change |
--no-frozen-lockfile | Allow lockfile updates |
--fix-lockfile | Repair inconsistent lockfile |
CI Environment Variables
| Variable | Effect |
|---|---|
CI=true | Enables frozen-lockfile automatically |
PNPM_HOME | Install directory for standalone pnpm |
Cache Paths
| Command | Purpose |
|---|---|
pnpm store path | Content-addressable store location |
pnpm cache path | Metadata cache location |
GitHub Actions Order
| Step | Action |
|---|---|
| 1 | pnpm/action-setup@v4 (or standalone script) |
| 2 | actions/setup-node@v4 with cache: 'pnpm' (optional) |
| 3 | pnpm 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Build fails with frozen lockfile | Lockfile out of sync | Run pnpm install locally and commit |
| Lockfile version error | CI pnpm newer/older than lockfile | Match pnpm versions in CI and local |
| Cache restore slower than install | Small dependency tree | Disable store caching if slower |
pnpm: command not found | PNPM_HOME not in PATH | Export PATH in CI step |
| Metadata cache not used | Not cached | Add 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
| Item | Value |
|---|---|
| Frozen flag | --frozen-lockfile |
| CI auto-detection | CI=true enables frozen mode |
| Install script | curl -fsSL https://get.pnpm.io/install.sh | sh - |
| Store path | pnpm store path |
| Metadata cache path | pnpm cache path |
| GitHub Actions installer | pnpm/action-setup@v4 |
| Lockfile repair | pnpm install --fix-lockfile |
| Lockfile version check | Fails if lockfile written by newer major |
Key takeaways:
--frozen-lockfilemakes 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 whereCImay 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‘spackageManagerfield . - 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.
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!