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
- Moon uses a dependency graph to determine which projects are affected by every change.
- Affected task detection means you never re-run tests for code that hasn't changed.
- Tasks are defined in moon.yml per project and inherit from a workspace-level toolchain config.
- Moon's action graph provides a visual and programmatic view of what will run and in what order.
- 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">| bashInitialize a workspace:
moon initThis 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:
- buildThe 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 --affectedMoon 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
- upstreamFor 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_SHAThe 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 --affectedThis outputs a visual graph showing the execution order. If app-web depends on ui-components which depends on utils, Moon will:
- Build and test
utils - Build and test
ui-components(only afterutilsis done) - Build and test
app-web(only afterui-componentsis 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.jsonAnd 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: trueEach 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 --affectedTokens 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: streamThe @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">testThis 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.