---
name: github-actions-monorepo-ci
description: Build a GitHub Actions CI for npm workspaces / pnpm / turbo monorepos that only runs jobs for changed packages, caches builds aggressively, and ships sub-3-minute PR feedback. Use when adding CI to a workspace repo, fixing slow CI, or scaling from one package to many.
category: devops
version: 0.1.0
tags: [github-actions, ci, monorepo, npm, pnpm, turbo]
recommended_npm: ["turbo"]
license: MIT
author: claude-code-skills
---

The default monorepo CI runs everything every time. That's why CI takes 15 minutes. Here's how to keep it under 3.

## Strategy

1. **Detect changed packages.** `turbo run build --filter=...[origin/main]` or `pnpm --filter "...[origin/main]"`.
2. **Cache aggressively.** Turborepo remote cache (free with Vercel) or self-hosted via S3-compatible store.
3. **Parallelize across packages.** Each affected package = one job. Use the matrix strategy.
4. **Reuse setup.** A single setup step with `actions/setup-node` + cache restore covers all jobs.

## Workflow skeleton (Turbo + pnpm)

```yaml
# .github/workflows/ci.yaml
name: ci
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  affected:
    runs-on: ubuntu-latest
    timeout-minutes: 8
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }   # turbo needs history for the diff base

      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Lint affected
        run: pnpm turbo run lint --filter=...[origin/main]

      - name: Type-check affected
        run: pnpm turbo run typecheck --filter=...[origin/main]

      - name: Build affected
        run: pnpm turbo run build --filter=...[origin/main]
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}      # remote cache
          TURBO_TEAM:  ${{ vars.TURBO_TEAM }}

      - name: Test affected
        run: pnpm turbo run test --filter=...[origin/main]
```

## Matrix per package (when needed)

If one package needs special runners (Playwright on macOS, ARM-only build), split out:

```yaml
  e2e:
    needs: affected
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run build --filter=@app/web
      - name: Playwright
        run: pnpm --filter @app/web exec playwright test --shard=${{ matrix.shard }}
```

## Caching beyond Turbo

Turbo's cache covers task outputs. For *install* speed:

```yaml
- uses: actions/setup-node@v4
  with: { node-version: 22, cache: pnpm }
```

Cache the pnpm store automatically. Add a separate cache for Playwright browsers if they balloon install time:

```yaml
- name: Cache Playwright
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: pw-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
```

## Required PR checks

Set in repo settings → Branches → main:
- Status: `affected` must pass.
- "Require branches to be up to date before merging" = ON.
- Squash-merge only.

## Anti-patterns

- ❌ `pnpm install` without `--frozen-lockfile` in CI — generates new lockfiles on PRs. Always frozen.
- ❌ `actions/checkout@v4` without `fetch-depth: 0` when running affected detection — diff base is wrong.
- ❌ Running every package's tests on every PR — defeats monorepo speed.
- ❌ Caching `node_modules` (it's reproducible from the lockfile + store cache) — slows things down.
- ❌ Running CI on draft PRs — burn money on incomplete work. Filter with `if: github.event.pull_request.draft == false`.
- ❌ Hardcoding secrets in workflow files. Use `secrets.*`.
- ❌ Using `--no-verify` to skip pre-commit hooks in CI.

## Quality gates

- PR feedback in < 3 minutes for "touch one package" changes.
- Cache hit rate > 80% in steady state (visible in Turbo dashboard or cache logs).
- No flaky job > 1% failure rate over 100 runs.
- A red PR check actually blocks merge (verify in branch protection rules).
