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
- 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
fiAvoid this
Every PR rebuilds and retests the whole tree, even a one-line change to one package.
- run: turbo run build test # every package, every PR, even a
# one-line change to one appWhy 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.
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?
Confirm your slowest job builds/tests the whole tree regardless of the diff (it's at the top of the PR check-runs list).
Confirm the repo has a real graph to scope against:
turbo.json,nx.json,WORKSPACE,settings.gradle, or a--changed/--onlyChangedrunner.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.
- Parallelization
Shard tests across parallel jobs in GitHub Actions
Split one long test job into parallel shards with a matrix so the suite finishes in a fraction of the wall-clock time.
- Trigger Scope
Filter workflows by path in GitHub Actions
Add
pathsorpaths-ignoreto expensive workflows so a docs-only or unrelated change doesn't trigger the full test suite.
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)
Last updated 2026-07-06
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.