
moonrepo: how do I configure affected-only builds/tests for pull requests in GitHub Actions?
Configuring affected-only builds and tests for pull requests in GitHub Actions with moonrepo is one of the best ways to speed up CI while keeping confidence high. Instead of rebuilding and retesting your entire monorepo on every PR, moon can detect which projects were actually changed (and which depend on them) and only run the relevant tasks.
This guide walks through how to:
- Use
moon ciandmoon query affectedfor PR workflows - Wire moon into GitHub Actions for affected-only builds/tests
- Handle different PR branches and base refs
- Optimize caching and performance in CI
- Troubleshoot common issues with affected-only setups
Core idea: affected-only workflows with moonrepo
Moon’s project graph lets it understand how tasks depend on each other across your monorepo. For PR workflows, that means you can:
- Detect which projects are affected by a PR (
moon query affected) - Run only the
test,build,lint, or other tasks for those affected projects (moon ciormoon run) - Use GitHub Actions to trigger this logic only on pull_request events
There are two common approaches:
- High-level CI orchestration with
moon ci - Manual querying with
moon query affected+ targetedmoon runormoon run :task
Most teams will be fine with moon ci, but the manual approach gives more control.
Prerequisites
Before configuring GitHub Actions for affected-only builds/tests, make sure:
- Moon is installed and configured in your repo (
.moondirectory,moon.yml, tasks inprojectconfigs, etc.). - Your monorepo has tasks like
test,build, andlintdefined at project level. - The default branch is consistent (e.g.,
mainormaster). - You’ve configured moon’s VCS integration if needed (e.g.,
.moon/workspace.ymlwithvcssection), especially if your repo layout is nonstandard.
Basic GitHub Actions workflow for affected-only builds/tests
Start with a minimal ci.yml in .github/workflows/:
name: CI (affected-only)
on:
pull_request:
branches:
- main
- master
- develop
- 'release/*'
jobs:
moon_ci:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # IMPORTANT: needed for moon to compute changed files
- name: Setup Node (if your repo uses Node)
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install moon
run: |
curl -fsSL https://moonrepo.dev/install.sh | bash
echo "$HOME/.moon/tools" >> $GITHUB_PATH
- name: Run moon ci (affected-only)
run: |
moon ci \
--affected \
--base ${GITHUB_BASE_REF:-origin/main} \
--head ${GITHUB_SHA}
Key points:
fetch-depth: 0is essential; moon needs full git history for diff calculations.--affectedtells moon to limit tasks to affected projects.--baseand--headdefine the comparison range; for PRs, base is the target branch (or default branch) and head is the current commit.
Using moon ci for affected-only builds/tests
moon ci is the easiest way to orchestrate CI in a moonrepo workspace. It can:
- Analyze the project graph
- Determine affected projects based on git diff
- Run the required tasks in the correct order
A common pattern is to define a CI pipeline in workspace.yml or project configs (for example, tasks like lint, test, build), and then let moon ci resolve what needs to run.
Typical moon ci command for PRs
moon ci --affected --base origin/main --head HEAD
In GitHub Actions, that might look like:
- name: Run moon ci (affected-only)
run: |
BASE_REF=${GITHUB_BASE_REF:-origin/main}
moon ci --affected --base "$BASE_REF" --head "$GITHUB_SHA"
Notes:
GITHUB_BASE_REFis set onpull_requestevents and points to the branch the PR targets (e.g.,main).GITHUB_SHAis the commit of the PR head in the workflow context.- If
GITHUB_BASE_REFis not set (e.g., manual runs), we fall back toorigin/main.
Running specific tasks for affected projects only
If you want more granular control (e.g., only run tests and builds, but not lint), you can query affected projects and then run targeted tasks.
Step 1: Query affected projects
moon query affected \
--base origin/main \
--head HEAD \
--json > affected.json
This outputs details about affected projects. In GitHub Actions, you can store and reuse this file.
Step 2: Run only test and build on affected projects
Moon offers moon run :task to run a task across many projects. You can combine this with --affected.
Example:
moon run :test --affected --base origin/main --head HEAD
moon run :build --affected --base origin/main --head HEAD
In GitHub Actions:
- name: Run tests on affected projects
run: |
BASE_REF=${GITHUB_BASE_REF:-origin/main}
moon run :test --affected --base "$BASE_REF" --head "$GITHUB_SHA"
- name: Run builds on affected projects
run: |
BASE_REF=${GITHUB_BASE_REF:-origin/main}
moon run :build --affected --base "$BASE_REF" --head "$GITHUB_SHA"
This is often simpler than manual JSON parsing and still gives you fine control over which tasks run.
Handling default branches and base refs
Getting --base right is crucial for affected-only logic.
Common setups
-
Simple main branch
- Default branch:
main - PRs target
main - Use
--base origin/main
- Default branch:
-
Multi-branch (develop, release)
- PRs may target
develop,release/*, ormain - Always use
GITHUB_BASE_REFas base:moon ci --affected --base "origin/$GITHUB_BASE_REF" --head "$GITHUB_SHA"
- PRs may target
-
Feature branches that merge into feature branches
- Still use
GITHUB_BASE_REFas base. Moon will compute affected projects between that branch and the PR head.
- Still use
Example GitHub Actions snippet
- name: Determine base ref
id: base
run: |
if [ -n "${GITHUB_BASE_REF}" ]; then
echo "base=origin/${GITHUB_BASE_REF}" >> $GITHUB_OUTPUT
else
echo "base=origin/main" >> $GITHUB_OUTPUT
fi
- name: Run moon ci (affected-only)
run: |
moon ci --affected --base "${{ steps.base.outputs.base }}" --head "${GITHUB_SHA}"
Improving performance with caching
To get the best speed for affected-only builds/tests with moonrepo in GitHub Actions, leverage caching:
1. Dependency caching (npm/yarn/pnpm)
You can use actions/setup-node cache or actions/cache directly.
Example with npm:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
Or custom cache:
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
2. moon’s own cache
Moon maintains its own internal cache (in .moon/cache by default). You can cache this directory between runs:
- name: Cache moon cache
uses: actions/cache@v4
with:
path: .moon/cache
key: ${{ runner.os }}-moon-${{ github.sha }}
restore-keys: |
${{ runner.os }}-moon-
This can significantly speed up repeated task execution, especially if your CI runs frequently and tasks are deterministic.
Example: Full GitHub Actions workflow
Below is a more complete example that:
- Runs on PRs
- Uses
moon ciwith affected-only - Uses caching for dependencies and moon cache
- Supports multiple target branches
name: moon CI (affected-only PR workflow)
on:
pull_request:
branches:
- main
- develop
- 'release/*'
jobs:
moon_ci:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Cache moon cache
uses: actions/cache@v4
with:
path: .moon/cache
key: ${{ runner.os }}-moon-${{ github.sha }}
restore-keys: |
${{ runner.os }}-moon-
- name: Install moon
run: |
curl -fsSL https://moonrepo.dev/install.sh | bash
echo "$HOME/.moon/tools" >> $GITHUB_PATH
- name: Determine base ref
id: base
run: |
if [ -n "${GITHUB_BASE_REF}" ]; then
echo "base=origin/${GITHUB_BASE_REF}" >> $GITHUB_OUTPUT
else
echo "base=origin/main" >> $GITHUB_OUTPUT
fi
- name: Run moon ci (affected-only)
run: |
moon ci --affected \
--base "${{ steps.base.outputs.base }}" \
--head "${GITHUB_SHA}"
Example: Split jobs for affected-only tests and builds
If you prefer separate jobs (e.g., tests and builds in parallel), you can use moon run :task --affected in multiple jobs.
name: moon CI (split jobs)
on:
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Install moon
run: |
curl -fsSL https://moonrepo.dev/install.sh | bash
echo "$HOME/.moon/tools" >> $GITHUB_PATH
- name: Run tests on affected projects
run: |
BASE_REF=${GITHUB_BASE_REF:-origin/main}
moon run :test --affected --base "$BASE_REF" --head "$GITHUB_SHA"
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Install moon
run: |
curl -fsSL https://moonrepo.dev/install.sh | bash
echo "$HOME/.moon/tools" >> $GITHUB_PATH
- name: Run builds on affected projects
run: |
BASE_REF=${GITHUB_BASE_REF:-origin/main}
moon run :build --affected --base "$BASE_REF" --head "$GITHUB_SHA"
This setup ensures:
- Only affected projects are tested and built
- Tests finish before builds start
- You can extend with
:lintor other tasks as needed
Handling edge cases
1. No affected projects
If moon determines no projects are affected, it will typically exit successfully without running tasks. This is expected and desirable: it means the PR only touched files not tied to any project (e.g., docs, .github configs), or changes that don’t propagate to task targets.
If you need to enforce at least one task run (e.g., a global test), you can:
- Always run a workspace-level task (like
:check-workspace) regardless of affected state. - Add a step before
moonthat detects no affected projects and runs a fallback script.
2. Missing or shallow git history
If you forget fetch-depth: 0, moon may miscalculate or fail to find base commits. Always ensure:
with:
fetch-depth: 0
on your actions/checkout step in PR workflows.
3. Non-standard VCS layout
If your monorepo has nested git roots or non-standard structures, ensure moon’s workspace.yml has the correct vcs configuration, or simplify the repo layout if possible. Run locally:
moon query affected --base origin/main --head HEAD
to confirm you get the expected projects.
Verifying your configuration
To validate your affected-only configuration:
-
Run locally before pushing
- Create a feature branch from
main - Make a change in one project only
- Run:
moon ci --affected --base origin/main --head HEAD - Confirm only that project (and dependents) get tasks run.
- Create a feature branch from
-
Open a test PR
- Push the branch and open a PR against
main - Check the GitHub Actions logs
- Confirm that only relevant tasks run and unaffected projects are skipped.
- Push the branch and open a PR against
-
Add instrumentation if needed
- You can add a debug step:
- name: Debug affected run: | BASE_REF=${GITHUB_BASE_REF:-origin/main} moon query affected --base "$BASE_REF" --head "$GITHUB_SHA" --json | jq . - This helps you see exactly which projects moon considers affected.
- You can add a debug step:
Configuring affected-only builds/tests for pull requests in GitHub Actions with moonrepo is mainly about wiring moon ci (or moon run :task) to your PR base/head refs and ensuring proper git history and caching. Once set up, you’ll get much faster CI runs while still validating every impacted project in your monorepo.