Yarn 14 ๐งถ CI/CD Integration and Immutable Installs
A CI pipeline that installs dependencies without immutable installs is a pipeline that lies. It resolves new versions silently, rewrites the lockfile if needed, and produces a build that may not match what developers tested locally. Yarn Berry changes this default: when it detects a CI environment, it automatically enables immutable installs. A lockfile that drifts from package.json fails the build instead of being quietly repaired. The exam tests the flags, the environment variable, and the cache paths that make CI both fast and correct.
Key point: yarn install --immutable fails if the lockfile would need to change. Yarn enables this automatically when CI=true is detected. For Zero-Installs repositories, --immutable-cache goes further, failing if the committed cache would be modified. The two flags together make the build a verification step, not a resolution step.
Why immutable installs exist
The silent drift problem. In a development environment, running yarn install after adding a dependency without updating the lockfile resolves the new dependency and rewrites the lockfile. This is convenient locally, but in CI it is dangerous: the build succeeds with a dependency tree that was never reviewed, committed, or tested. The --immutable flag closes this gap. If the lockfile does not already contain the resolution, the install fails with an error code (YN0028), forcing the developer to commit the lockfile change .
The CI auto-detection problem. Yarn does not require the --immutable flag in CI. The enableImmutableInstalls setting defaults to true when the CI environment variable is set. The flag exists to make the behavior explicit and independent of environment detection, which matters for containers and self-hosted runners where CI may not be set correctly . Passing --immutable explicitly ensures the same behavior regardless of the runtime.
The cache integrity problem. For Zero-Installs repositories, the cache is committed to Git. The --immutable-cache flag aborts if the install would add to or modify the cache folder. Without it, a package missing from the committed cache is fetched from the registry and the workflow passes, defeating the purpose of committing the cache in the first place .
The checksum problem. The checksumBehavior setting defaults to throw when a checksum does not match. This catches cases where a package archive in the cache has been modified, either accidentally or maliciously. For repositories that accept external pull requests, this is the line of defense against cache tampering .
The caching problem. A CI runner starts with an empty cache. For non-Zero-Installs projects, the global Yarn cache must be restored from a previous run to avoid downloading every package. The cache key must be based on yarn.lock (and .yarnrc.yml if the linker or cache mode changed), because the contents of the cache depend on those files . For Zero-Installs repositories, the cache is already in the checkoutโno CI cache step is needed .
a. The immutable flags and their behavior
The --immutable flag is the primary CI safeguard. It makes yarn install a verification operation: the lockfile must already contain every resolution, and the cache must already contain every package. If not, the install fails.
# CI install: fail if lockfile would change
yarn install --immutable
# Zero-Installs CI: also fail if cache would change
yarn install --immutable --immutable-cache
The enableImmutableInstalls setting in .yarnrc.yml controls the default. When set to true, the flag is implied even outside CI. When false, the flag is not implied even in CI, which is appropriate only for workflows that intentionally regenerate the lockfile .
# .yarnrc.yml
enableImmutableInstalls: true
The --immutable-cache flag is specific to Zero-Installs. It prevents the install from fetching a missing package and writing it into .yarn/cache. A repository that commits the cache but does not use this flag can silently diverge: the committed cache is incomplete, but CI fetches the missing packages from the registry anyway .
b. GitHub Actions caching for Yarn Berry
For projects that do not commit the cache (i.e., enableGlobalCache: true), the CI cache step must restore the global Yarn cache folder. The folder path is discovered with yarn config get cacheFolder, which works on every platform and Yarn version, and remains correct if someone changes enableGlobalCache in a pull request .
- name: Get Yarn cache folder
id: yarn-cache-dir
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock', '**/.yarnrc.yml') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install --immutable
The key hashes both yarn.lock and .yarnrc.yml because the linker and cache mode settings determine what the restored directory contains. A change to nodeLinker or enableGlobalCache changes the contents of the cache folder, and an old cache entry would no longer match the expected state .
For repositories with native dependencies, the .yarn/unplugged folder and the state files (.yarn/build-state.yml, .yarn/install-state.gz) should also be cached. The build-state file records which packages have had their postinstall scripts run successfully. Without it, Yarn re-runs every native build on every CI run, which is both slow and a source of transient failures .
path: |
${{ steps.yarn-cache-dir.outputs.dir }}
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
For Zero-Installs repositories, the cache step is unnecessary. The .yarn/cache folder is already present after actions/checkout, and the install proceeds without network access .
c. Environment variables and hardened mode
The CI environment variable is set by default on GitHub Actions, GitLab CI, and most CI platforms. Yarn uses it to enable immutable installs automatically. If the platform does not set it, the variable must be set explicitly.
env:
CI: true
The YARN_ENABLE_IMMUTABLE_INSTALLS environment variable overrides the .yarnrc.yml setting. This is used in the rare case where a workflow intentionally regenerates the lockfile (e.g., a dependency update bot). The value 0 disables immutable installs for that step only .
- name: Regenerate lockfile
run: yarn install
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: 0
Hardened mode (enableHardenedMode) is automatically enabled for pull requests from public forks on GitHub. It adds additional checks for malicious changes in the dependency tree. For workflows that run on push (not pull requests), hardened mode must be enabled explicitly with YARN_ENABLE_HARDENED_MODE=1 .
- run: yarn install --immutable
env:
YARN_ENABLE_HARDENED_MODE: 1
Complete Example Session
# ============================================
# PART 1: BASIC IMMUTABLE INSTALL
# ============================================
yarn install --immutable
# Fails if yarn.lock would need to change.
# Succeeds if the lockfile is already correct.
# ============================================
# PART 2: ZERO-INSTALLS IMMUTABLE CACHE
# ============================================
yarn install --immutable --immutable-cache
# Also fails if .yarn/cache would need to change.
# ============================================
# PART 3: GITHUB ACTIONS CACHE SETUP
# ============================================
- name: Get Yarn cache folder
id: yarn-cache-dir
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock', '**/.yarnrc.yml') }}
# ============================================
# PART 4: INSTALL WITH CACHE
# ============================================
- run: yarn install --immutable
# The cache is restored before this step.
# ============================================
# PART 5: CACHE NATIVE STATE
# ============================================
path: |
${{ steps.yarn-cache-dir.outputs.dir }}
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
# ============================================
# PART 6: OVERRIDE FOR LOCKFILE REGENERATION
# ============================================
- run: yarn install
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: 0
# Only for workflows that intentionally update the lockfile.
# ============================================
# PART 7: HARDENED MODE FOR PUSH EVENTS
# ============================================
- run: yarn install --immutable
env:
YARN_ENABLE_HARDENED_MODE: 1
# ============================================
# PART 8: VERIFY ON A FRESH CLONE
# ============================================
git clone <repo> test-ci
cd test-ci
yarn install --immutable --immutable-cache
# Should succeed without network for Zero-Installs.
The eight parts trace the CI configuration: basic immutable install, Zero-Installs cache validation, GitHub Actions cache setup, install with cache, caching native state, overriding for lockfile regeneration, hardened mode, and verification on a fresh clone.
Quick Reference
Immutable Flags
| Flag | Effect |
|---|---|
--immutable | Fail if lockfile would change |
--immutable-cache | Fail if cache would change (Zero-Installs) |
YARN_ENABLE_IMMUTABLE_INSTALLS=0 | Disable immutable for this step |
CI Environment Variables
| Variable | Purpose |
|---|---|
CI=true | Enables immutable installs automatically |
YARN_ENABLE_IMMUTABLE_INSTALLS | Overrides .yarnrc.yml setting |
YARN_ENABLE_HARDENED_MODE | Explicitly enable hardened mode |
Cache Paths
| Path | When to Cache |
|---|---|
$(yarn config get cacheFolder) | All non-Zero-Installs projects |
.yarn/unplugged | Projects with native dependencies |
.yarn/build-state.yml | Projects with postinstall scripts |
.yarn/install-state.gz | All Yarn Berry projects |
Best Practices
โ Do This:
yarn install --immutable
<!-- Always pass the flag explicitly in CI --> # โ
key: ${{ hashFiles('**/yarn.lock', '**/.yarnrc.yml') }}
<!-- Include .yarnrc.yml in the cache key --> # โ
path: |
.yarn/unplugged
.yarn/build-state.yml
<!-- Cache native build state for faster runs --> # โ
โ Don’t Do This:
# Rely on CI auto-detection alone
<!-- Explicit flag is safer in containers --> # โ
# Cache only .yarn/cache for non-Zero-Installs
<!-- The global cache is the correct path --> # โ
# Disable immutable installs globally
<!-- Only for specific lockfile-regeneration steps --> # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Build fails with YN0028 | Lockfile out of sync | Commit the lockfile change |
| Cache step restores wrong files | Key does not include .yarnrc.yml | Add .yarnrc.yml to hashFiles |
| Native build re-runs every time | build-state.yml not cached | Add it to the cache path |
| Immutable fails in scaffolded app | Lockfile not yet generated | Set YARN_ENABLE_IMMUTABLE_INSTALLS=0 for that step |
| Zero-Installs cache incomplete | Missing --immutable-cache | Add the flag to the install |
Real-World Examples
1. Basic Immutable Install
yarn install --immutable
2. Zero-Installs Validation
yarn install --immutable --immutable-cache
3. Get Cache Folder
yarn config get cacheFolder
4. Cache Key
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock', '**/.yarnrc.yml') }}
5. Cache Native State
path: |
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
6. Disable for Regeneration
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: 0
7. Hardened Mode
env:
YARN_ENABLE_HARDENED_MODE: 1
8. CI Variable
env:
CI: true
9. Verify Locally
yarn install --immutable
10. Check Cache Size
du -sh $(yarn config get cacheFolder)
Visual
Immutable Install Decision
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ yarn install --immutable โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Lockfile resolves every dependency? โ
โ โโ YES โ Proceed โ
โ โโ NO โ FAIL (YN0028) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ --immutable-cache set? โ
โ โโ YES โ Cache has every package? โ
โ โ โโ YES โ Proceed โ
โ โ โโ NO โ FAIL โ
โ โโ NO โ Fetch missing packages โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Install completes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Every gate must pass before the install succeeds.
Zero-Installs vs Cached CI
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Zero-Installs โ
โ โโโ .yarn/cache in checkout โ
โ โโโ No CI cache step needed โ
โ โโโ Use --immutable-cache โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Cached CI โ
โ โโโ Global cache restored via actions/cache โ
โ โโโ Key based on yarn.lock + .yarnrc.yml โ
โ โโโ Use --immutable โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The cache location determines the CI strategy.
Summary
| Item | Value |
|---|---|
| Immutable flag | --immutable |
| Zero-Installs flag | --immutable-cache |
| Auto-detection | CI=true enables immutable |
| Override | YARN_ENABLE_IMMUTABLE_INSTALLS=0 |
| Cache folder | yarn config get cacheFolder |
| Cache key | yarn.lock + .yarnrc.yml |
| Native cache paths | .yarn/unplugged, build-state.yml |
| Hardened mode | Auto for public PRs; YARN_ENABLE_HARDENED_MODE=1 |
Key takeaways:
- Immutable installs make CI a verification step. The lockfile must already contain every resolution. A drift fails the build instead of being silently repaired .
- Yarn enables immutable installs when
CI=true. But passing the flag explicitly ensures the same behavior in containers and self-hosted runners where the variable may be missing . --immutable-cacheis for Zero-Installs. It prevents the install from fetching missing packages and writing them into the committed cache .- The cache key must include
.yarnrc.yml. The linker and cache mode settings determine the cache contents. A key based only onyarn.lockmisses this dependency . - Native build state should be cached. The
.yarn/build-state.ymlfile records which postinstall scripts succeeded. Without it, every CI run rebuilds native dependencies . - Hardened mode is automatic for public PRs. For push events, it must be enabled explicitly with
YARN_ENABLE_HARDENED_MODE=1.
Remember: A CI pipeline that installs dependencies without immutable installs is not verifying anything. It is resolving a fresh dependency tree on every run, and the result may not match what was tested. The --immutable flag is the line between a build that lies and a build that verifies. For Zero-Installs, --immutable-cache extends that verification to the committed packages.
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!