Optimizing GitHub Actions Caching for Monorepo CI/CD Pipelines
How I built a reliable WhatsApp AI shopping assistant for Clickmothercare that survives hallucinated products, silent save failures, and multi-agent handoff bugs.
The Monorepo CI Bottleneck
Monorepos offer incredible advantages for code sharing, but they can cripple CI/CD pipelines if dependencies aren't cached efficiently. Running npm install or pip install across a massive repository for every PR leads to wasted compute and frustrated developers. The solution lies in granular, hash-based caching strategies in GitHub Actions.
Implementing Granular Dependency Caching (Node.js Example)
Instead of caching the entire node_modules directory, which can be massive and prone to OS-level incompatibilities, we cache the global npm/yarn cache directory. We derive the cache key directly from the lockfile hash, ensuring cache invalidation occurs immediately when dependencies change.
name: Monorepo CI Pipeline
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Get npm cache directory
id: npm-cache-dir
# Extracts the exact path to the npm cache dynamically
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Cache npm dependencies
uses: actions/cache@v4
id: npm-cache # Used to check for cache hits later
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
# The key incorporates the OS, Node version, and the hash of the package-lock.json
# If the lockfile changes, the hash changes, and a new cache is created.
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# Restore keys provide fallback options if an exact match isn't found,
# speeding up resolution for minor dependency updates.
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
# If we got a cache hit, npm ci will use the cache, significantly reducing network IO
run: npm ci
- name: Run Build (Nx / Lerna / Turborepo)
run: npx turbo run build --filter=...
The critical element here is hashFiles('**/package-lock.json'). In a monorepo workspace environment, this hashes all lockfiles across the repository. For even more granular optimization, you can isolate jobs per workspace package and hash only the lockfile relevant to that specific package.
Is your AI agent's infrastructure secure and reliable?
Book a Free 15-Min Technical Audit