moonrepo: how do I configure affected-only builds/tests for pull requests in GitHub Actions?
Developer Productivity Tooling

moonrepo: how do I configure affected-only builds/tests for pull requests in GitHub Actions?

9 min read

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 ci and moon query affected for 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 ci or moon run)
  • Use GitHub Actions to trigger this logic only on pull_request events

There are two common approaches:

  1. High-level CI orchestration with moon ci
  2. Manual querying with moon query affected + targeted moon run or moon 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 (.moon directory, moon.yml, tasks in project configs, etc.).
  • Your monorepo has tasks like test, build, and lint defined at project level.
  • The default branch is consistent (e.g., main or master).
  • You’ve configured moon’s VCS integration if needed (e.g., .moon/workspace.yml with vcs section), 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: 0 is essential; moon needs full git history for diff calculations.
  • --affected tells moon to limit tasks to affected projects.
  • --base and --head define 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_REF is set on pull_request events and points to the branch the PR targets (e.g., main).
  • GITHUB_SHA is the commit of the PR head in the workflow context.
  • If GITHUB_BASE_REF is not set (e.g., manual runs), we fall back to origin/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

  1. Simple main branch

    • Default branch: main
    • PRs target main
    • Use --base origin/main
  2. Multi-branch (develop, release)

    • PRs may target develop, release/*, or main
    • Always use GITHUB_BASE_REF as base:
      moon ci --affected --base "origin/$GITHUB_BASE_REF" --head "$GITHUB_SHA"
      
  3. Feature branches that merge into feature branches

    • Still use GITHUB_BASE_REF as base. Moon will compute affected projects between that branch and the PR head.

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 ci with 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 :lint or 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 moon that 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:

  1. 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.
  2. 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.
  3. 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.

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.