Q19 of 38 · CI/CD & DevOps

How would you structure a monorepo's CI to only test changed packages?

CI/CD & DevOpsMidci-cdmonorepoturboreponxselective-testing

Short answer

Short answer: Compute the affected set from the diff against the base branch — Nx, Turborepo, and Bazel all expose a 'what's affected' command. Run tests for affected packages and their dependents only. Run the full suite nightly to catch missed deps and cross-package regressions.

Detail

Monorepos can grow to thousands of packages; testing all of them on every PR is wasteful (and slow). Selective testing is the answer, but it has to be correct — missing a dependent risks shipping bugs.

Tooling that supports affected-set computation:

  • Nx (nx affected:test) — tracks the dependency graph, computes packages whose source or deps changed.
  • Turborepo (turbo run test --filter=...[origin/main]) — similar, plus content-hash-based caching for skipping work entirely.
  • Bazel (bazel test //... with --config=changed) — heavyweight but most precise. Used at Google, Uber.
  • Pants, Lerna — older / less common.

The dependency graph is the source of truth.

  • Modify package A → test A.
  • Modify A, and B depends on A → test A and B.
  • Modify the root tsconfig → test everything (or treat as nightly only).

The CI shape:

- run: turbo run test --filter='...[origin/main]'  # only affected

Pitfalls to guard against:

  • Implicit deps — package A imports from B at runtime via a string path, but the dep graph doesn't see it. The affected-set misses B's tests.
  • Shared config — eslint config, tsconfig changes affect everyone. Treat config files as "test all."
  • Build-time codegen — generated types from a schema package; consumers should rebuild + retest if the schema changes.

Mitigations:

  • Nightly full-suite — catches cases the affected-set missed.
  • Strict dep-graph rules — lint rules that enforce explicit imports, ban string-path requires.
  • Cache invalidation in tools — Turbo and Nx hash inputs and skip when nothing changed; this is independent of "affected-set" but hugely speeds things up.

Reporting: in PR comments, show what was tested vs. skipped: "Tested: api, web. Skipped: cli, mobile (no changes)." Devs see what's covered.

// EXAMPLE

.github/workflows/monorepo-ci.yml

name: Monorepo CI
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }  # need history for diff
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci
      - run: npx turbo run lint test build \
                  --filter='...[origin/main]'

// WHAT INTERVIEWERS LOOK FOR

Naming a tool (Nx/Turbo/Bazel), the dependency-graph mental model, the implicit-dep risk and nightly-full-suite mitigation.

// COMMON PITFALL

Trusting the affected-set 100% and never running the full suite. Implicit deps eventually escape and break main; full-suite nightly catches what selective missed.