Skip to content

GitHub Actions runners

A Runner is a virtual machine in your Lane that runs GitHub Actions jobs for your repos. af creates the machine; you register it with GitHub using your own GitHub credential, so af never holds a GitHub token.

You decide:

  • repo or organisation;
  • its labels;
  • one job or kept;
  • what your workflow does when the runner is offline.
Terminal window
af runner create ci1 --size M --timeout 900 --json # installed, not registered yet
# Create a registration token with your own GitHub credential and pipe it in.
# af stores it nowhere, and it is never an argument.
gh api -X POST repos/OWNER/REPO/actions/runners/registration-token --jq .token \
| af runner register ci1 --token - --url https://github.com/OWNER/REPO --labels my-label --json

Then in your workflow:

jobs:
build:
runs-on: [self-hosted, my-label]
  • The first create takes a few minutes: about 6 to unpack the image, then about 2 to boot. Give it --timeout 900. If it answers UNREACHABLE or says it did not answer, repeat the same create; it returns the same Runner with "created": false. A repeat may wait up to 11 minutes first.
  • Registration tokens last an hour. If yours fails, af runner register stops with INTERNAL, shows the runner’s own message (never the token) and leaves the Runner unchanged. Get a new token and repeat.
  • Registered already? A second register is NAME_TAKEN. To move a Runner to another repo, delete it, create it again and register it.

An --ephemeral Runner takes one job, then GitHub removes its registration and the machine powers off. Register it with a JIT config:

Terminal window
af runner create once1 --ephemeral --json
gh api -X POST repos/OWNER/REPO/actions/runners/generate-jitconfig \
-f name=once1 -F runner_group_id=1 -f 'labels[]=self-hosted' -f 'labels[]=my-label' --jq .encoded_jit_config \
| af runner register once1 --jit-config - --json

A JIT config has only the labels you name, so name self-hosted too if your workflow asks for it; otherwise the job stays queued. A finished ephemeral Runner still counts against your caps until you af runner delete it.

Ubuntu 24.04, the Actions runner, Node 22, pnpm, git, Python 3, build tools, Java 17, Swift 6.4.0 (at /home/runner/toolchains/swift-6.4.0), the Android SDK (at /home/runner/android-sdk), Playwright’s system libraries, and TZ=Etc/UTC.

Jobs run as the user runner, without sudo. There is no SSH into a Runner: you reach it through GitHub and af runner logs.

--kvm asks for nested virtualisation, for jobs that start virtual machines of their own, such as Android test devices. Ask for it if your job needs it.

A kept Runner has a 50 GB cache disk that holds its pnpm store, Turbo cache, ~/.gradle and Playwright browsers. It survives across jobs, and even when you delete the Runner and create the same name again. It counts against your Lane’s disk cap. af runner delete keeps it unless you pass --with-cache. Ephemeral Runners have none.

S, M and L work as for Boxes, on your Lane’s own cores. --size CI (16 GB) is made for Actions jobs: instead of holding cores of its own, it shares your Lane’s CPUs with your other machines. A Runner keeps the CPUs it was made with; if your admin changes your Lane’s CI setting, delete and re-create it.

Terminal window
af runner list --json # live size, registered, busy, and how much cache it uses
af runner logs ci1 --json # the runner service's last lines (--lines up to 1000)

A Runner that stopped, or an ephemeral one that finished: delete it and create the same name again. The cache stays.

Terminal window
gh api -X POST repos/OWNER/REPO/actions/runners/remove-token --jq .token \
| af runner delete ci1 --confirm ci1 --removal-token - --json

With a removal token, af takes the Runner off GitHub first; if that fails, nothing is deleted. Deregistering cancels a job it is running. Without a token, GitHub drops an offline runner by itself. deregistered: false means it was never registered, or was registered with a JIT config, which GitHub removes on its own.

A job whose label has no online runner waits up to 24 hours on GitHub, then fails (GitHub’s docs). If a workflow must survive your runner being offline, pick the runner in a first job, and fall back to a GitHub-hosted runner when yours is not online.