Best practice · Parallelization

Build and test only what changed in GitHub Actions

Scope your slowest build/test job to only the packages a PR actually changed, using your monorepo tool's affected mode, with a mandatory full-run fallback so a resolution error never silently skips work.

Do this

In a workspace whose tooling knows the dependency graph (Turborepo, Nx, Bazel, Gradle, or a --changed-capable test runner), the long-pole job runs only the targets affected by the diff against the merge base. Crucially, it fails safe: if the tool can't resolve the graph or returns an empty set on a real diff, it runs the full build/suite instead of passing green having run nothing.123

Affected-only, diffed against the merge base
- uses: actions/checkout@v4
  with:
    fetch-depth: 0                     # full history so the base commit is reachable
- uses: nrwl/nx-set-shas@v4            # resolves NX_BASE / NX_HEAD robustly
- run: |
    # Fail safe: if the base didn't resolve, run the FULL build/test
    # instead of silently running nothing.
    if [ -z "$NX_BASE" ]; then
      npx nx run-many -t build test
    else
      npx nx affected -t build test --base=$NX_BASE --head=$NX_HEAD
    fi

Avoid this

Every PR rebuilds and retests the whole tree, even a one-line change to one package.

Rebuild and retest the whole tree every PR
- run: turbo run build test   # every package, every PR, even a
                                #   one-line change to one app

Why it matters

In a large monorepo, rebuilding and retesting everything on every PR is the biggest source of redundant CI work. But this is the most dangerous lever in the catalog: scoping trades correctness headroom for speed. An incomplete dependency graph can mark a truly-affected target 'unaffected' and skip it, and a green check that ran fewer tests is indistinguishable from a real speedup, until a bug ships. Adopt it only with the guardrails below.

When to use

Use it when

A monorepo whose build tool (Turborepo/Nx/Bazel/Gradle) or test runner genuinely models the dependency graph, where the long-pole job rebuilds far more than any single PR touches.

Be careful when

Never scope without a full-run fallback on resolution error, a build-error exit that's distinct from a test failure, and a parallel scoped-vs-full shadow period before cutover. Always keep the full build/suite on the merge queue and main so the trunk is validated end to end. If your tooling doesn't model the graph, this isn't actionable.

Verify on your repo

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.

Prompt for your coding agent
Inspect this repo's .github/workflows for a job that rebuilds and retests the whole tree on every PR. Confirm the repo has a real dependency graph to scope against (`turbo.json`, `nx.json`, `WORKSPACE`, `settings.gradle`, or a `--changed`-capable runner), then scope the long-pole job to only the targets affected by the diff against the merge base (use `nrwl/nx-set-shas` or `git merge-base` to resolve the base). This is a correctness-sensitive change: it MUST fail safe, so if the base does not resolve or the scoper returns an empty set on a real diff, run the full build and test instead of passing green having run nothing, and keep the full run on the merge queue and `main`. Show me the diff and open a PR rather than applying it blindly, and do not cut over without a scoped-vs-full shadow period.
Prefer to check by hand?
  1. Confirm your slowest job builds/tests the whole tree regardless of the diff (it's at the top of the PR check-runs list).

  2. Confirm the repo has a real graph to scope against: turbo.json, nx.json, WORKSPACE, settings.gradle, or a --changed/--onlyChanged runner.

  3. Verify the guardrail: does the job fall back to a full run when the scoper errors or returns empty on a non-trivial diff? If not, it's unsafe.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

FAQ

Why diff against the merge base instead of HEAD~1?

HEAD~1 only sees the last commit, so a multi-commit PR would skip targets changed in earlier commits. Diff against the merge base, the commit where your branch forked from the base, not the current tip of the base branch (which may have moved on since). nrwl/nx-set-shas computes it for you; by hand it's git merge-base origin/<base_ref> HEAD.

How do I roll this out without missing test coverage?

Run the scoped job in parallel with the existing full job for a week or two of real PRs, comparing pass/fail and coverage on every one, including at least one PR that touches a shared base package. Cut over only after they match, and keep the full run on the merge queue and main.

Sources

1nrwl/nx-set-shas (opens in new tab)

2Nx · affected (opens in new tab)

3Turborepo · running tasks (opens in new tab)

Last updated 2026-07-06

Get started

Test what the diff changed, not the whole repo.

One line to install. Faster runs on day one, and agents that open reviewable PRs to keep your pipeline following practices like this one.