Build and test only what changed in GitHub Actions
Speed up GitHub Actions and cut runner minutes by scoping 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.
How StarSling worksAI agents open the PR
StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.
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.12
# The job needs `permissions: { contents: read, actions: read }`
# (plus `pull-requests: read` if you run a merge queue): nx-set-shas calls the
# Actions API to find the last successful run, and a permissions block sets every
# scope you don't list to none.
- uses: actions/checkout@v7
with:
fetch-depth: 0 # full history so the base commit is reachable
- uses: nrwl/nx-set-shas@afb73a62d26e41464e9254689e1fd6122ee683c1 # v5.0.1
id: shas
- run: pnpm install --frozen-lockfile
- run: |
# THE fail-safe, and it has to key off noPreviousBuild - NOT off an empty
# NX_BASE. nx-set-shas never leaves NX_BASE empty: when it cannot find a
# successful run to diff against it warns, silently falls back to HEAD~1,
# and carries on. nx affected then scopes to a single commit and the check
# goes green having tested a subset of a multi-commit PR. noPreviousBuild is
# the only signal that says so, so branch on it and run everything.
if [ "${{ steps.shas.outputs.noPreviousBuild }}" = "true" ]; 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 appSeen in the wild
Adobe, discord.js, and Nx use affected-project selection so a small PR does not rebuild and retest the whole monorepo.
- name: Set SHAs
uses: nrwl/nx-set-shas@310288c04d90696f9f1bc27c5e3caea6642b53d4 # v5.0.0
# ...
pnpm nx affected --targets=lint,test,build,e2e,e2e-ci,format-native,lint-native,gradle:build-ci,vale,run &non-adjacent lines joined by # ...
- name: Tests (PR)
if: ${{ github.event_name != 'push' }}
run: pnpm exec turbo run test --filter="...[origin/${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}]" --concurrency=4 fetch-depth: 0
- uses: moonrepo/setup-toolchain@v0.6.2
with:
auto-install: true
- run: pnpm install --frozen-lockfile
- run: moon ci 2>&1 | tee moon-ci.log; exit ${PIPESTATUS[0]}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.
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 (resolve the base with the repo's graph tool or `git merge-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.
Ground these changes in the upstream docs before you edit: https://turborepo.dev/docs/crafting-your-repository/running-tasks. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.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.
Go further
One fix, all of them, or forever.
You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.
Fix this one thing
Copy the prompt above
Hand OPT70 to your coding agent and fix it in your repo today.
Fix everything, once
Install the ci-score skill
One prompt grades your whole workflow config against all 11 CI Score checks, this one included, and hands your agent a ranked fix for every gap it finds. Open source, runs locally. It grades configuration, not speed.
Keep it fixed, forever
Install the StarSling GitHub App
Connect GitHub and the fixes stay applied as your CI evolves, with agents that keep inspecting your workflows and opening optimization PRs you review.
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
Speed up GitHub Actions tests by splitting 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 with paths and paths-ignore in GitHub Actions
Add
pathsorpaths-ignoreto expensive workflows so a docs-only or unrelated change doesn't trigger the full test suite.
More ways to improve GitHub Actions
If you do not know why a run is slow yet, start with the diagnostic guide. Let an agent find which of these applies: /ci-speedup. If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement. To find exploitable workflow paths before attackers do, ci-secure checks the ten critical GitHub Actions attack vectors and lets you choose which fixes to apply.
- GitHub Actions too slow
- Fast GitHub Actions
- GitHub Actions alternatives
- GitHub Actions pricing
- Self-hosted GitHub Actions runners
- Docker CI on GitHub Actions
- Install the free /ci-score skill to improve your GitHub Actions setup
- Install the /ci-secure skill to close critical attack vectors in GitHub Actions
- Browse GitHub Actions agent skills
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.
Should I pin nx-set-shas at v4 or v5?
Use v5 on current GitHub-hosted runners and self-hosted runners that support Node 24 actions. As of July 21, 2026, v5.0.1 is the current release; v5 moves the action runtime from Node 20 to Node 24 while preserving the base, head, and noPreviousBuild outputs used by this workflow. If a legacy self-hosted runner cannot run Node 24 actions, keep v4 only as a temporary fallback while you upgrade it. Pin the chosen release's full commit SHA instead of a mutable major-version tag, and keep the full-run fallback keyed on noPreviousBuild.
Sources
Last updated 2026-08-19