Moonrepo CI Configuration: Caching, Parallelism, and Remote Caching

Moonrepo CI Configuration: Caching, Parallelism, and Remote Caching

Moonrepo's CI performance comes from three compounding optimizations: content-addressed local caching, parallel task execution across dependency levels, and remote caching shared across all runners and branches. Together, they can reduce CI times by 70–90% in large monorepos.

Key Takeaways

  1. Moon's cache key is a hash of all declared inputs — if nothing changed, the task is skipped instantly.
  2. Tasks at the same dependency level run in parallel automatically; no explicit configuration needed.
  3. Remote caching (Moonbase or S3) shares cached results across all CI runners and all developers.
  4. The --concurrency flag controls how many tasks run simultaneously, tunable per runner size.
  5. Cache lifetime and invalidation rules are configurable per task and at the workspace level.

The Three Layers of Moonrepo Speed

Most CI slowdowns in monorepos have the same root cause: running everything every time. Moonrepo addresses this at three levels:

  1. Local caching — skip tasks whose inputs haven't changed since the last run on this machine/runner
  2. Parallelism — run independent tasks simultaneously rather than one at a time
  3. Remote caching — share cached results across all machines so no task runs twice anywhere

Understanding how each layer works lets you configure them precisely for your team's needs.

Local Caching: Content-Addressed Task Results

Moon stores task results in .moon/cache/ keyed by a hash of all declared inputs. When you run a task, Moon first computes this hash and checks whether a matching cache entry exists. If it does, Moon restores the outputs and marks the task as a cache hit.

The inputs hash includes:

  • All files matching the inputs glob patterns in moon.yml
  • The task command and arguments
  • Environment variables listed in env (if any)
  • The tool version (Node.js, Rust, etc.) from toolchain config
  • Moon's own version

Configure caching behavior per task:

# packages/api/moon.yml
tasks:
  test:
    command: vitest run
    inputs:
      - src/**/*
      - tests/**/*
      - vitest.config.ts
      - package.json
    outputs:
      - coverage
      - test-results.xml
    options:
      cache: true
      cacheLifetime: '12 hours'

  typecheck:
    command: tsc --noEmit
    inputs:
      - src/**/*
      - tsconfig.json
    options:
      cache: true
      cacheLifetime: '24 hours'

The outputs field tells Moon what to restore when a cache hit occurs. Outputs are archived and stored alongside the hash in the cache directory.

Workspace-Level Cache Configuration

Set cache defaults in .moon/workspace.yml:

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

vcs:
  manager: git
  defaultBranch: main
  remoteCandidates:
    - origin
    - upstream

The cacheLifetime setting determines how long a cache entry remains valid regardless of input changes. This is a safety net — if your inputs list is incomplete, the cache will still invalidate after this period.

Parallelism: The Dependency Graph as an Execution Plan

Moon doesn't just skip unchanged tasks — it also executes independent tasks simultaneously. The dependency graph determines what can run in parallel.

Given this project structure:

apps/
  web/          → depends on: ui-components, utils
  mobile/       → depends on: ui-components, utils
packages/
  ui-components/ → depends on: utils
  utils/         → no dependencies

Moon's execution plan for moon run :test looks like:

Wave Tasks (run in parallel)
1 utils:test
2 ui-components:test
3 web:test, mobile:test

Wave 3 runs web:test and mobile:test simultaneously because they have no dependencies on each other.

Controlling Concurrency

By default, Moon uses the number of CPU cores as the concurrency limit. Override this with the --concurrency flag:

# Run at most 4 tasks simultaneously
moon run :<span class="hljs-built_in">test --concurrency 4

<span class="hljs-comment"># Run all tasks as fast as possible (no limit)
moon run :<span class="hljs-built_in">test --concurrency 0

For small CI runners (2 vCPU), setting --concurrency 2 avoids thrashing. For large runners (16+ vCPU), increasing concurrency reduces total wall time significantly.

Task-Level Parallelism Options

Some tasks are inherently sequential (database migrations, for example). Mark them as non-parallel:

tasks:
  migrate:
    command: 'db:migrate'
    options:
      runInCI: true
      cache: false
      # Prevent this task from running alongside other tasks
      mutex: 'database'

The mutex option groups tasks that must not run concurrently, even if they appear at the same level of the dependency graph.

Remote Caching with Moonbase

Local caching only helps within a single machine. When you have multiple CI runners or multiple developers, each machine starts cold. Remote caching shares the cache across all of them.

Moonbase is Moon's official remote cache server. Sign up at moonrepo.app and configure it:

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

# Remote caching via Moonbase
moonbase:
  host: 'https://moonbase.moonrepo.app'

Set the authentication token as an environment variable in CI:

export MOONBASE_SECRET_KEY=your_secret_key

With remote caching enabled, the workflow becomes:

  1. Runner A builds and tests utils:test — result is uploaded to Moonbase
  2. Runner B starts a parallel job — it downloads the cached result from Moonbase and skips execution
  3. Developer pulls main and runs moon run utils:test — same cache hit, instant result

Self-Hosted Remote Caching with S3

If Moonbase doesn't fit your infrastructure, Moon supports any S3-compatible object store as a remote cache backend. Use moon-cache-server or configure the S3 backend directly:

# .moon/workspace.yml
runner:
  archivableTargets:
    - 'packages/**:build'
    - 'packages/**:test'

Then point your CI to a custom cache server that implements Moon's remote cache protocol. Several open-source implementations exist for AWS S3, Google Cloud Storage, and MinIO.

Full CI Pipeline Example

Here is a complete GitHub Actions workflow using all three optimization layers:

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

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      affected: ${{ steps.affected.outputs.result }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: moonrepo/setup-moon-action@v1
        with:
          auto-install: true

  test:
    needs: setup
    runs-on: ubuntu-latest
    env:
      MOONBASE_SECRET_KEY: ${{ secrets.MOONBASE_SECRET_KEY }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

      - uses: pnpm/action-setup@v3

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

      - run: pnpm install --frozen-lockfile

      - name: Run affected tests
        run: |
          moon run :lint :typecheck :test \
            --affected \
            --base ${{ github.base_ref }} \
            --head ${{ github.sha }} \
            --concurrency 4

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: '**/test-results.xml'

Matrix Builds with Affected Detection

For very large monorepos, you can combine Moon's affected detection with GitHub Actions matrix builds to parallelize across multiple runners:

jobs:
  determine-affected:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.matrix.outputs.result }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: moonrepo/setup-moon-action@v1
      - id: matrix
        run: |
          AFFECTED=$(moon query projects --affected --base ${{ github.base_ref }} --json)
          echo "result=$AFFECTED" >> $GITHUB_OUTPUT

  test:
    needs: determine-affected
    runs-on: ubuntu-latest
    strategy:
      matrix:
        project: ${{ fromJson(needs.determine-affected.outputs.matrix) }}
    steps:
      - uses: actions/checkout@v4
      - uses: moonrepo/setup-moon-action@v1
      - run: pnpm install --frozen-lockfile
      - run: moon run ${{ matrix.project }}:test

This pattern assigns each affected project to its own runner, providing maximum parallelism at the cost of more runner-minutes.

Measuring Cache Effectiveness

Track cache hit rates to understand where time is being saved:

# View cache statistics for the last run
moon run :<span class="hljs-built_in">test --affected 2>&1 <span class="hljs-pipe">| grep -E <span class="hljs-string">"(cache hit|cache miss|skipped)"

Moon's output annotates each task with its cache status:

▸ utils:test (cache hit, skipped)
▸ ui-components:test (cache miss, running)
▸ web:test (cache miss, running)

A healthy CI pipeline should show 60–80% cache hits on PRs that touch a small portion of the codebase.

Common Pitfalls

Missing inputs cause stale cache. If a file that affects your tests is not listed in inputs, Moon won't know to invalidate the cache when it changes. Be comprehensive with your input globs, and include config files like jest.config.ts, vitest.config.ts, and tsconfig.json.

Environment variables leaking into cache keys. If your test command reads environment variables that differ between developers (like HOME or USER), Moon's hash may differ even when code hasn't changed. Explicitly list env vars in the task config to make them part of the hash — or exclude them.

Large output artifacts slowing remote cache. If your build outputs are hundreds of megabytes, uploading and downloading from Moonbase can take longer than just running the task. Be selective about which tasks have remote caching enabled.

Adding End-to-End Test Coverage

Your CI pipeline handles unit tests, type checking, and linting through Moon. For browser-level end-to-end tests against your deployed apps, HelpMeTest runs tests written in plain English — no Selenium, no Playwright configuration. Drop HelpMeTest into the same CI workflow and cover the user-facing layer that unit tests can't reach.

Summary

Moonrepo's CI performance gains come from stacking three complementary strategies: content-addressed local caching eliminates redundant work within a runner, automatic parallelism collapses independent tasks into concurrent waves, and remote caching extends both benefits across every machine and every developer. Configure your inputs lists carefully, set appropriate cache lifetimes, and wire up Moonbase for remote caching — and your CI times will drop dramatically as your monorepo grows.

Read more