← Back to Blog

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.

Anas Rhimi
Anas Rhimi August 2026 • 8 min read

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
Hire Me