Moon.build Testing Guide: Task Orchestration and Affected Task Detection

Moon.build Testing Guide: Task Orchestration and Affected Task Detection

Moon.build (Moonrepo) is a task runner and monorepo management tool that tracks project dependencies, detects which projects are affected by a change, and runs only the tests that matter — drastically cutting CI time in large codebases.

Key Takeaways

  1. Moon uses a dependency graph to determine which projects are affected by every change.
  2. Affected task detection means you never re-run tests for code that hasn't changed.
  3. Tasks are defined in moon.yml per project and inherit from a workspace-level toolchain config.
  4. Moon's action graph provides a visual and programmatic view of what will run and in what order.
  5. Remote caching (via Moonbase or custom S3) eliminates redundant work across all CI runners.

Why Task Orchestration Matters in Monorepos

Running all tests on every pull request is a mistake that compounds with every new package you add. A monorepo with 30 packages and a naive test:all script will run thousands of test files on a change to a single utility function. Engineers wait. Feedback loops stretch. Flaky tests that live in unrelated packages start failing the builds you care about.

Moon.build solves this with a precise dependency graph. When you change packages/utils, Moon knows which other packages import it, and it runs only those affected packages' tests — nothing else.

Setting Up Moon in a Monorepo

Start by installing Moon globally or as a project dependency:

npm install --save-dev @moonrepo/cli
# or use the standalone installer
curl -fsSL https://moonrepo.dev/install/moon.sh <span class="hljs-pipe">| bash

Initialize a workspace:

moon init

This creates .moon/workspace.yml at the repository root:

# .moon/workspace.yml
vcs:
  manager: git
  defaultBranch: main

projects:
  - packages/*
  - apps/*

node:
  version: '20.11.0'
  packageManager: pnpm
  packageManagerVersion: '8.15.0'

Defining Tasks in moon.yml

Each project gets a moon.yml file that declares its tasks:

# packages/utils/moon.yml
language: typescript

tasks:
  build:
    command: tsc --build
    inputs:
      - src/**/*
      - tsconfig.json
    outputs:
      - dist

  test:
    command: vitest run
    inputs:
      - src/**/*
      - tests/**/*
      - vitest.config.ts
    deps:
      - build

The inputs field is critical. Moon uses it to compute a hash of everything that could affect this task's output. If none of the inputs changed since the last run, Moon restores the result from cache and skips execution entirely.

Affected Task Detection

The core of Moon's value proposition is --affected. When you run:

moon run :test --affected

Moon compares the current workspace state to a base revision (default: the main branch) and walks the dependency graph to find which projects have changed — directly or transitively. Only those projects' test tasks execute.

You can configure the affected comparison strategy:

# .moon/workspace.yml
vcs:
  defaultBranch: main
  remoteCandidates:
    - origin
    - upstream

For CI, you typically pass the base and head commits explicitly:

moon run :test --affected --base <span class="hljs-variable">$GITHUB_BASE_REF --<span class="hljs-built_in">head <span class="hljs-variable">$GITHUB_SHA

The Action Graph

Before executing anything, Moon builds an action graph — a directed acyclic graph (DAG) of every task that needs to run, respecting inter-project dependencies. You can inspect it:

moon action-graph :test --affected

This outputs a visual graph showing the execution order. If app-web depends on ui-components which depends on utils, Moon will:

  1. Build and test utils
  2. Build and test ui-components (only after utils is done)
  3. Build and test app-web (only after ui-components is done)

Tasks at the same level of the graph run in parallel automatically.

Task Inheritance with toolchain.yml

Moon lets you define task defaults at the workspace level to avoid copy-pasting the same task config into every project:

# .moon/toolchain.yml
node:
  version: '20.11.0'

typescript:
  createMissingConfig: true
  rootConfigFileName: tsconfig.json
  rootOptionsConfigFileName: tsconfig.options.json

And in .moon/tasks.yml, you can define inherited tasks:

# .moon/tasks.yml
tasks:
  test:
    command: vitest run
    inputs:
      - src/**/*
      - tests/**/*
      - '*.config.ts'
    options:
      runInCI: true
      cache: true

Each project can extend this definition and override only what it needs:

# apps/dashboard/moon.yml
language: typescript

tasks:
  test:
    args: '--reporter=junit --outputFile=test-results.xml'

Running Tests Across the Workspace

Run a specific task across all projects:

# Run test task in all projects
moon run :<span class="hljs-built_in">test

<span class="hljs-comment"># Run test task only in affected projects
moon run :<span class="hljs-built_in">test --affected

<span class="hljs-comment"># Run test task in a specific project
moon run utils:<span class="hljs-built_in">test

<span class="hljs-comment"># Run multiple tasks
moon run :lint :<span class="hljs-built_in">test :build --affected

Tokens and Input/Output Contracts

Moon uses tokens to make task definitions reusable and self-documenting:

tasks:
  test:
    command: vitest run --coverage
    inputs:
      - '@globs(sources)'    # resolved from language defaults
      - '@globs(tests)'
    outputs:
      - coverage
    options:
      cache: true
      outputStyle: stream

The @globs(sources) and @globs(tests) tokens expand based on the language setting, saving you from specifying src/**/* and tests/**/* in every project.

Integrating with GitHub Actions

A typical CI setup for Moon looks like this:

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # required for Moon's affected detection

      - uses: moonrepo/setup-moon-action@v1

      - uses: pnpm/action-setup@v3
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Run affected tests
        run: moon run :test --affected --base ${{ github.base_ref }} --head ${{ github.sha }}

The fetch-depth: 0 is important — Moon needs full git history to compute which files changed relative to the base branch.

Caching Strategies

Moon caches task outputs locally by default. Cache artifacts are stored in .moon/cache/ and keyed by a hash of all inputs. When the hash matches, Moon skips execution and restores outputs immediately.

For CI, local caching only helps within the same runner. Remote caching via Moonbase or a compatible S3-backed cache server extends this across all runners and all branches:

# .moon/workspace.yml
runner:
  cacheLifetime: '7 days'
  inheritColorsForPipedTasks: true

moonbase:
  host: 'https://moonbase.moonrepo.app'

We will cover remote caching and parallelism configuration in depth in a separate post.

Debugging Test Failures

When a test fails in CI but passes locally, Moon's deterministic inputs model is your friend. Run:

moon query hash utils:<span class="hljs-built_in">test

This prints the exact hash Moon computed for the task. If it differs between local and CI, something in the inputs list is environment-dependent. Common culprits:

  • Timestamps in generated files not listed in .gitignore
  • Node version mismatch (check .moon/toolchain.yml)
  • Environment variables included in inputs unintentionally

End-to-End Testing with HelpMeTest

Unit and integration tests handle your business logic, but your monorepo likely produces one or more web applications that need browser-level testing. HelpMeTest lets you write end-to-end tests in plain English — no Playwright boilerplate, no selector gymnastics. Describe the user journey, and HelpMeTest runs it against your deployed app on every CI run. It integrates naturally into the same CI pipeline where Moon handles your unit tests, giving you complete coverage across the stack.

Summary

Moon.build brings discipline to monorepo testing by making task orchestration explicit and deterministic. The dependency graph tells Moon exactly what to build and test for any given change. Affected task detection cuts CI time dramatically as your monorepo grows. And the layered configuration model — workspace defaults that each project can extend — keeps your moon.yml files lean and consistent. Start with moon run :test --affected in your next PR and watch your CI times drop.

Read more