Skip to content
pilots
Dashboard
All pages

Runners for GitHub Actions

Change runs-on to pilots and a GitHub Actions job runs on pilots. Each job gets a machine of its own, made from your team's runner image the moment the job is queued and removed when it ends. Nothing waits warm for a job, so a team pays for the seconds its jobs run and for nothing in between.

Setting it up

  1. Install the pilots GitHub App on the account that owns the repository, choosing the repository. pilot runner enable opens the install page when it is missing.
  2. Enable the repository for your team. You must administer it on GitHub, because its jobs carry its secrets and anyone on the team can open a shell in a runner.
  3. Set runs-on: pilots in a workflow and push.
pilot runner enable acme/shop           # one repository
pilot runner enable acme                # every repository of an account
pilot runner enable acme/shop --max 20  # how many jobs run at the same time
pilot runner disable acme/shop
.github/workflows/ci.yml
jobs:
  test:
    runs-on: pilots            # or pilots-4cpu-16gb
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

The first enable also builds the team's runner image. Jobs queued while it builds wait for it and start as soon as it is ready. The commit shows a pending status saying so.

Sizes

Label What the job gets
pilots two vCPUs and eight gigabytes of memory, the shape GitHub gives a private repository
pilots-<N>cpu-<M>gb any size you name, for example pilots-4cpu-16gb

A job may also name self-hosted, linux and x64. A runner registers with exactly the labels its job asked for, so it never takes a job meant for runners of your own.

An image is made per size. The first job of a new size waits while that size is made from the image you already have, and every later job of that size starts from it directly.

What is installed

Ubuntu 24.04 with the Actions runner, Node 24, Bun, Go, Python 3 with pip and venv, git and git-lfs, the GitHub CLI, the usual build tools, Docker, and the Playwright browsers. The job runs as the user runner, which has passwordless sudo, so a step can install anything else it needs.

The image is a template in your team, rebuilt when you ask. A fresh build reinstalls every tool at its current version.

pilot runner ls       # links, images, and the jobs running now
pilot runner usage    # minutes this month, per repository
pilot runner image --size 4cpu-16gb
pilot runner image --fresh             # reinstall every tool

What it costs

A runner is an ordinary machine in your team, billed per second for the time it exists, at the same price as any other (billing and usage). Runner time is labelled with its repository, so pilot runner usage and the dashboard's Runners page show minutes per repository.

How many at once

A team runs ten jobs at the same time unless it sets another number with --max. Jobs beyond that wait in GitHub's queue and start as earlier ones end. It is your setting, there to make a wide matrix a choice rather than a surprise on the bill.

When a job cannot start

A job that asks for pilots and cannot run here says why on its commit, as a status named pilots runners: a label pilots does not provide, a repository that is not enabled, an image still being built, or a team whose machines are on hold. A job would otherwise wait in the queue for a day with nothing saying why.

Where it stops

  • No KVM inside a runner, so nothing that needs nested virtualisation runs there. Tests that boot Firecracker, QEMU with acceleration or an Android emulator belong on a bare-metal machine.
  • No GPU, and no arm64: runners are x86-64.
  • One job per runner. A runner is removed when its job ends, so nothing is shared between jobs except what a step caches itself.