Fastest CI for Nx Monorepos on GitHub Actions
Last updated
The fastest Nx CI does three things: run only affected projects (nx affected), reuse results through a remote cache, and spread the remaining work over several machines. This guide goes deeper on Nx than the general Nx and Turborepo guide, including the git settings that quietly break affected runs.
Step 1: affected-only runs
nx affected compares two commits and runs tasks only for projects that changed or depend on something that did. It needs a base and a head. The nrwl/nx-set-shas action works them out for you.
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
filter: tree:0
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- uses: nrwl/nx-set-shas@v5
- run: npx nx affected -t lint test build
Per the nx-set-shas README (checked October 2026), the action exports NX_BASE and NX_HEAD for the job and also exposes base and head as step outputs. On a pull request the base is the merge base with main. On a push to main it is the last commit whose workflow run succeeded, so a red commit does not hide its changes from the next run. If it cannot find one, it falls back to HEAD~1 unless you set error-on-no-successful-workflow: true.
The fetch-depth requirement
actions/checkout fetches one commit by default. With a single commit there is no base to compare, and nx affected either errors or treats everything as affected. The nx-set-shas README says it needs all branches and commits so Nx has a base to compare against, which is why the workflow uses fetch-depth: 0.
filter: tree:0 makes it a blobless-style partial clone, which keeps the full history cheap: you fetch commit metadata up front and file contents on demand. For huge repos this is often seconds instead of a minute.
Change the default base
Nx also reads defaultBase in nx.json (usually main). Keep it in sync with your trunk branch name or affected runs compare against the wrong ref.
Step 2: remote cache
Local caching only helps if the same runner sees the same files. On GitHub Actions each job starts clean, so you need a remote cache to reuse task outputs between runs, branches and teammates.
The Nx docs (checked October 2026) connect a workspace with npx nx-cloud onboard connect-workspace, which writes nxCloudId to nx.json. For CI authentication they describe read-only and read-write access tokens in their CI access tokens guide. Use a read-only token for pull requests from forks and read-write for trusted branches, so an untrusted PR cannot poison the cache.
Confirm hits in the log. A cached task prints [local cache] or [remote cache] and replays the output instead of running the command. A summary at the end reports how many tasks were read from cache.
Without Nx Cloud, you can use other remote cache implementations through Nx's custom task runner and cache plugins, or cache the .nx/cache directory with actions/cache. The Actions-cache version is the cheapest to try, with two limits: PR caches are scoped, and the repo's 10 GB cap applies. See cache limits.
- uses: actions/cache@v6
with:
path: .nx/cache
key: nx-${{ runner.os }}-${{ github.sha }}
restore-keys: |
nx-${{ runner.os }}-
Step 3: distribute the tasks
After affected and caching, a large PR can still be slow. Nx can distribute tasks across agents. The Nx CI intro (checked October 2026) configures that in .nx/ci-config.yaml and starts agents in the workflow before install:
# .nx/ci-config.yaml
dte:
distribute-on: 3 linux-large-js
lifecycle:
stop-after:
- build
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
filter: tree:0
- run: npx nx-cloud start-workers
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- uses: nrwl/nx-set-shas@v5
- run: npx nx affected -t lint test build
Starting workers first lets them boot while your main job installs dependencies. Only tasks run through nx are cached and distributed, so make sure npm test wraps nx test rather than calling the test runner directly.
If you would rather stay on plain GitHub Actions, shard yourself with a matrix. Nx supports splitting by project, and for single large test suites you can shard the runner itself. See test sharding in GitHub Actions.
Putting it together
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
actions: read
contents: read
jobs:
main:
runs-on: [self-hosted, Linux, compiler-ci] # or ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
filter: tree:0
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- uses: nrwl/nx-set-shas@v5
- run: npx nx affected -t lint test build --parallel=4
--parallel controls how many tasks run at once on the one machine. More cores let you raise it. The runs-on line is the only compiler.dev-specific part: it moves the job to an 8 vCPU machine at $0.012 per minute, and you change it back to undo. See pricing.
Mistakes that turn affected into everything
- Shallow checkout. The most common one. Use
fetch-depth: 0. - Touching a root file. Changing
package-lock.json,nx.jsonor a shared tsconfig makes everything affected. Nx'snamedInputslet you narrow what each target depends on. - Base is a red commit. Using
HEAD~1after a failed run hides failures. nx-set-shas picks the last green one. - Non-Nx scripts. Tasks outside
nxskip cache and affected logic. - A cache that never saves. Make sure a trusted branch writes it.
For slow installs, see npm caching or pnpm caching. For other monorepo tools, see Turborepo and Bazel.
FAQ
What does nx affected actually compare?
The files changed between NX_BASE and NX_HEAD, mapped through the project graph to projects and their dependents.
Do I need Nx Cloud?
Not for affected runs. You need a remote cache or distributed execution for cross-run reuse; Nx Cloud is the supported path, and actions/cache on .nx/cache is a limited alternative.
Why is nx affected running everything on my PR?
Check fetch depth, check that NX_BASE is not equal to NX_HEAD's ancestor in an unexpected way, and see whether a root config file changed.
Does this work with a merge queue?
nx-set-shas has a use-previous-merge-group-commit input (default true) for merge groups. See GitHub merge queue.
Related
- Fastest CI for Bazel Monorepos on GitHub ActionsFast Bazel CI on GitHub Actions. Target determination for affected builds, remote cache flags, read-only PR uploads, sharding and a complete workflow.
- Fastest CI for pnpm Workspaces on GitHub ActionsFast CI for pnpm workspaces on GitHub Actions. Changed-package filters, store caching, fetch-depth, matrix sharding and a full workflow you can copy.
- Fastest CI for Turborepo Monorepos on GitHub ActionsSpeed up Turborepo CI on GitHub Actions. --filter for changed packages, remote cache with TURBO_TOKEN, fetch-depth, sharding and a complete workflow.
- Monorepo CI With Nx or Turborepo: Affected BuildsRun only what changed in a monorepo on GitHub Actions. Nx affected, Turborepo filters, remote caching, path filters and the checkout settings that matter.