GitHub Actions Tutorial — Monorepo Caching
Monorepo Caching
Monorepos with multiple packages need cache strategies that avoid cache thrashing and keep each package’s dependencies isolated. Here are patterns for the most common monorepo tools.
The Problem
In a monorepo:
my-monorepo/
├── packages/
│ ├── frontend/
│ │ └── package-lock.json
│ ├── backend/
│ │ └── package-lock.json
│ └── shared/
│ └── package-lock.json
└── package-lock.json # root
A single cache key based on the root lockfile misses changes in sub-packages. A key based on all lockfiles changes too often.
Strategy 1 — Per-Package Caching
Cache each package separately:
jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: packages/frontend/node_modules
key: fe-${{ runner.os }}-${{ hashFiles('packages/frontend/package-lock.json') }}
restore-keys: fe-${{ runner.os }}-
- run: cd packages/frontend && npm ci
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: packages/backend/node_modules
key: be-${{ runner.os }}-${{ hashFiles('packages/backend/package-lock.json') }}
restore-keys: be-${{ runner.os }}-
- run: cd packages/backend && npm ci
Pros: Precise cache invalidation per package. Cons: More cache entries; more verbose workflow.
Strategy 2 — Combined Lock File Hash
Hash all lockfiles together:
steps:
- uses: actions/cache@v4
with:
path: |
**/node_modules
key: mono-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: mono-${{ runner.os }}-
Pros: Simple, one cache call. Cons: Any lockfile change invalidates the entire cache.
Strategy 3 — npm/Yarn/pnpm Workspace Cache
For workspace-aware package managers:
pnpm (recommended for monorepos)
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm' # built-in pnpm cache support
- run: pnpm install --frozen-lockfile
Yarn (Berry/v3+)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
- run: yarn install --immutable
Strategy 4 — Turborepo Cache
Turborepo has its own build cache:
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-
turbo-${{ runner.os }}-
- run: npx turbo run build test --cache-dir=.turbo
Turborepo caches build outputs per task, so unchanged packages skip entirely.
Cache Size Tips for Monorepos
| Tip | Impact |
|---|---|
| Use pnpm (shared store) | Smaller cache than npm |
Cache the download cache, not node_modules |
Safer, less storage |
| Exclude dev dependencies in production builds | Smaller cache |
| Use Turborepo remote caching | Shared across developers |
Common Mistakes
| Mistake | Fix |
|---|---|
| Cache thrashes on every commit | Use lockfile hash, not commit SHA in cache key |
| Caching too broadly | Cache per-package or use workspace-aware tools |
| Missing packages after restore | Run install after cache restore — cache supplements, not replaces |
| Cache exceeds 10 GB | Audit cache sizes with gh cache list; prune unused entries |
Quick Reference
| Strategy | Best For |
|---|---|
| Per-package | Independent packages with separate lockfiles |
| Combined hash | Small monorepos, few packages |
| Workspace manager | pnpm/Yarn workspaces |
| Turborepo | Large monorepos with build caching |
← Previous: Cache Strategies
Next: Practice Exercises →