Skip to documentation
MMirageDocs

Documentation

Route jobs

Configure workflows

Configure a local-infrastructure route and GitHub-hosted fallback in your existing workflow.

Your workflow’s runs-on selects a job’s execution route. This guide configures local infrastructure with GitHub-hosted fallback while keeping your build and test steps. Other jobs in the workflow can use different providers.

For existing workflows, a setup pull request is the recommended path. Mirage writes the routing for each job you select and keeps its current runner as the fallback. Its fallback reruns only the affected job, and a manual rerun tries your machines again. Follow this guide when you want to write the route yourself.

For a direct Cloudflare route, see Cloudflare runners. To compare providers and change a job’s default through Mirage-generated pull requests, use Compare CI providers.

Change runs-on

Start with an existing build or test job. Keep its steps and replace its GitHub-hosted runner label with the expression for the same operating system and architecture. For a job that currently uses ubuntu-24.04:

Linux x64 · replace the job’s runs-on
runs-on: ${{ github.run_attempt == '1' && 'mirage-linux-x64' || 'ubuntu-24.04' }}

On the first attempt, GitHub sends the job to Mirage. If Mirage needs to use fallback, it requests a second attempt of the same workflow; the expression selects GitHub’s runner for that attempt. There is no extra setup job or Mirage action to add.

Merge queues

Mirage supports GitHub merge queues on local infrastructure and Cloudflare runners. Add the merge_group trigger to each workflow that reports a required queue check, keeping its runner labels, routing, and test steps:

Workflow triggers
on:
  pull_request:
  merge_group:
    types: [checks_requested]

GitHub runs these checks against its temporary merge-queue branch. Checkout must use that run's commit; the default checkout behavior does this. An explicit checkout of a PR head or main tests a different commit from the one the queue needs. See GitHub's merge-queue documentation.

Runner labels

Use the Mirage label and GitHub-hosted fallback for the job’s operating system and architecture. Runner labels lists each label with its resources, fallback, and limits.

Linux arm64
runs-on: ${{ github.run_attempt == '1' && 'mirage-linux-arm64' || 'ubuntu-24.04-arm' }}
macOS arm64
runs-on: ${{ github.run_attempt == '1' && 'mirage-macos-arm64' || 'macos-26' }}

Choose the architecture your tools support. On an Apple-silicon Mac, Linux arm64 runs natively and Linux x64 runs more slowly through Rosetta, which must be installed. Native macOS needs an enabled and ready macOS runner profile. The machine’s details page lists each profile and any setup still required.

Tools, secrets, and caches

Mirage’s runner image has its own toolchain; it does not include everything from GitHub’s hosted images. Use setup actions or installation steps to select the versions your project needs, and install your dependencies before building or testing. Runner labels lists what each image includes.

Jobs start in fresh environments. Use checkout to fetch your code, GitHub Actions artifacts to pass files between jobs, and cache actions for dependencies. Keep secrets in GitHub Actions and reference them as you already do. Tools and files installed in your Mac’s user account are not automatically available inside a job.

Caching selects GitHub, Cloudflare, Machine or None in Mirage settings. Existing cache and setup actions keep working without changes to workflow YAML.

Linux runners on Macs support Docker commands, service containers, job containers, and Docker container actions. Cloudflare runners have different limits and do not support those Docker features.

Parallel jobs

Each job requests CPU, memory, and disk. Machines run concurrent Linux jobs while their allocations fit the background work budget. macOS CI and agent VMs share an allowance of at most two macOS guests. A job waits for enough resources, and one that waits longer than the repository’s queue timeout falls back or fails. If you submit several jobs at once, add machines, increase the background work budget, request smaller runners in Repos, or use needs to run jobs in sequence:

Two sequential jobs · jobs section
jobs:
  first:
    runs-on: ${{ github.run_attempt == '1' && 'mirage-linux-arm64' || 'ubuntu-24.04-arm' }}
    steps:
      - run: echo "First job"

  second:
    needs: first
    runs-on: ${{ github.run_attempt == '1' && 'mirage-linux-arm64' || 'ubuntu-24.04-arm' }}
    steps:
      - run: echo "Second job"

Each job still gets a fresh environment. A dependency controls execution order; it does not share a working directory.

How fallback works

If no machine takes the job within the repository’s queue timeout, or a runner has an infrastructure failure, Mirage cancels attempt 1 and requests one rerun of the whole workflow. Attempt 2 uses your configured fallback. GitHub keeps both attempts under the same run ID.

  • A failed test or build is a CI result and does not trigger an infrastructure retry.
  • A manual rerun also increments the attempt, so the expression selects GitHub-hosted runners.
  • Fallback needs Mirage and GitHub to be reachable. A Mirage outage can leave a job queued.
  • GitHub-hosted jobs use your GitHub Actions allowance or billing.

Queue timeout

A job waits in GitHub’s queue until one of your machines can fit its requested resources. Each repository has a queue timeout: 120 minutes unless you change it on the repository’s page in Repos. When a job has waited that long, Mirage stops waiting for a machine:

  • Attempt expression: Mirage cancels the run and reruns the workflow. Attempt 2 runs the job on its GitHub-hosted runner.
  • Setup pull request: Mirage cancels the run and reruns only that job, on the runner it used before.
  • Literal label: the job has no fallback, so it fails. Mirage adds a failed check that says how long the job waited and what to fix, then cancels the run.

GitHub can’t end a single queued job, so Mirage cancels its run. Before it reruns a routed job or fails one, Mirage waits until no other job in the run is running or could still start on your machines, so the cancel doesn’t cut other work short. If a machine takes the job in the meantime, it runs.

While a job waits, its Mirage check says when the timeout ends and what Mirage will do then. If your Mirage operator sets one timeout for every repository, the repository’s page shows it as global.

Run only on your machines

If you want every attempt to target Mirage, use a literal label:

No GitHub-hosted fallback
runs-on: mirage-linux-arm64

This label never selects a GitHub-hosted runner, including on a rerun. While no eligible machine can fit its allocation, the job stays queued. If none takes it within the repository’s queue timeout, Mirage adds a failed check that explains why and cancels the run. To let jobs wait longer, raise the timeout on the repository’s page in Repos. GitHub still cancels a job that has been queued for 24 hours; see GitHub’s Actions limits.

If a machine takes the job and then fails as infrastructure, Mirage cancels the attempt and reruns the whole workflow once. The rerun comes back to your machines. If it also fails as infrastructure, Mirage cancels the run and adds a failed check that explains why. Use the attempt expression above when you want automatic hosted fallback.

When GitHub-hosted runners are blocked

GitHub does not charge for Actions usage on self-hosted runners. It blocks GitHub-hosted runners when an account without a payment method uses up its included minutes, or when a budget set to stop usage runs out. See GitHub’s Actions billing and budgets documentation.

To GitHub, a Mirage job is a self-hosted runner job, so it can still run in that state. Use a literal label rather than the attempt expression:

While GitHub-hosted runners are blocked
runs-on: mirage-linux-arm64

With the attempt expression, a fallback would cancel attempt 1 and rerun the workflow on the blocked GitHub-hosted runners, and a manual rerun would select them too. Restore the expression once GitHub-hosted runners are available again.

Mirage still relies on GitHub to dispatch each job. If GitHub stops starting jobs for your account altogether, Mirage cannot run them either.