Diffinery
Actions: GitHub runners
Docs / Actions

GitHub runners

Keep your code on GitHub and run its Actions jobs on Diffinery's hosted runners: install the app, link the account that pays, and set runs-on to diffinery.

You don't have to move your code to use Diffinery's runners. Keep your repositories and workflows on GitHub, install Diffinery's GitHub App, and change a job's runs-on to diffinery. The job then runs on a Diffinery hosted runner (a fresh Ubuntu 24.04 container with 4 vCPUs on Cloudflare) instead of a GitHub-hosted one. Logs, checks, caches, artifacts and GITHUB_TOKEN stay on GitHub, as with any self-hosted runner.

How it works

  1. GitHub tells Diffinery about each queued job of a repository where the app is installed (the workflow_job webhook).
  2. If the job's runs-on asks for a Diffinery runner, Diffinery checks the hosted minutes and hosted jobs of the Diffinery account you linked, then registers a just-in-time runner for that one job in the repository.
  3. Diffinery starts a hosted runner container. It runs GitHub's own runner (actions/runner), which takes the job, runs it and reports to GitHub.
  4. When the job ends, the runner removes itself from GitHub and the container stops. Nothing carries over to the next job.

Because GitHub's own runner runs the job, everything a GitHub workflow does works the same way: uses: actions, service containers, actions/cache, artifacts, OIDC, environments and required reviewers, concurrency and re-runs.

Set it up

You need a Diffinery account, and on GitHub you must be able to install apps on the user or organization (an organization owner, or the owner of a personal account).

  1. Open Settings → GitHub runners.
  2. Under Connect GitHub, pick the Account that pays: yourself, or a Diffinery organization you own or administer. Its hosted minutes pay for the jobs.
  3. Click Install on GitHub. On GitHub, pick the user or organization, and All repositories or the repositories that should use Diffinery runners, then click Install.
  4. GitHub asks you to authorize the app: click Authorize. This shows Diffinery which installations your GitHub account can manage; Diffinery doesn't keep the token.
  5. You're back on Settings → GitHub runners, with the installation listed under the account that pays.

If the app is already installed (for example, someone else installed it), click Link an existing installation instead. After you authorize the app, Diffinery lists the installations your GitHub account can manage. Pick one and the account that pays, and click Link. Linking an installation that is linked to another Diffinery account moves it.

To change which repositories can use Diffinery runners later, click Choose repositories on GitHub next to the installation.

Use it in a workflow

Set runs-on to a Diffinery label:

jobs:
  test:
    runs-on: diffinery          # 4 vCPU, 12 GiB of memory
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test
  lint:
    runs-on: diffinery-small    # 1/2 vCPU, 4 GiB of memory
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint
runs-onRunner
diffinery or diffinery-largelarge: 4 vCPU, 12 GiB of memory, 20 GB of disk
diffinery-mediummedium: 2 vCPU, 8 GiB, 16 GB
diffinery-smallsmall: 1/2 vCPU, 4 GiB, 8 GB
diffinery-tinytiny: 1/4 vCPU, 1 GiB, 4 GB

Rules:

  • A job is for Diffinery when its labels include diffinery or diffinery-<size>, and every other label is one of self-hosted, linux or x64. With several sizes, the largest wins. runs-on: [self-hosted, linux, diffinery] works.
  • Any other label (ubuntu-latest, gpu, arm64, a runner group) leaves the job to GitHub: Diffinery starts no runner for it.
  • Jobs without a Diffinery label keep running on GitHub-hosted runners, so you can move one job at a time.
  • Diffinery doesn't pick a smaller size by itself for GitHub jobs, as it does for jobs of Diffinery repositories: the label sets the size.
  • Matrix legs each get their own runner.

The machine

The container runs the same image as Diffinery's own hosted runners: Ubuntu 24.04 on x64 with Git, Node.js 22, Python 3.12, Docker, the GitHub CLI and /dev/kvm. Differences from a GitHub-hosted runner:

  • The image is much smaller than GitHub's: Go, Java, Ruby, Rust, .NET, browsers and cloud CLIs aren't preinstalled. The actions/setup-* actions install them. See The Ubuntu 24.04 image.
  • Jobs run as root (RUNNER_ALLOW_RUNASROOT=1). sudo needs no password, as on GitHub.
  • The workspace is under /opt/actions-runner/_work; use GITHUB_WORKSPACE rather than a fixed path.
  • runner.environment is self-hosted and the runner's name is diffinery-<id>-<try>.
  • On instances that set EGRESS=web, jobs reach only HTTP, HTTPS and DNS.

What it costs, and limits

GitHub jobs on Diffinery runners count exactly like hosted jobs of the linked account's own repositories (see Limits):

  • Their running time counts against the account's hosted minutes for the month, from the moment the container starts until it stops.
  • They count against the account's hosted jobs at once. Further jobs wait, oldest first, and start as running ones finish. Settings → GitHub runners shows them as waiting, with the reason.
  • When the account has used its hosted minutes for the month, new jobs aren't started: they show as failed with the reason, and stay queued on GitHub until GitHub gives up on them (after 24 hours) or you cancel them. Switch runs-on back to ubuntu-latest to run them on GitHub.
  • A job runs for at most 6 hours.

GitHub doesn't charge minutes for jobs on self-hosted runners, which is what Diffinery runners are to GitHub.

See what ran

Settings → GitHub runners lists the last 30 runners of each account you manage, newest first, with the job (linked to its log on GitHub), the size, how long it ran and a status:

StatusMeaning
waitingThe job waits for a hosted slot of the account, or for GitHub to register its runner again.
startingThe runner is registered and its container is starting, or it waits for GitHub to hand it the job.
runningGitHub handed the runner a job.
doneThe container stopped. The GitHub job's conclusion is shown under it.
skippedThe job ended before its runner started (cancelled, or run by a runner that isn't one of this account's Diffinery runners), or the installation was suspended or unlinked.
failedNo runner was started: the reason is shown, such as a kill switch, minutes used up, or GitHub refusing to register a runner.

GitHub hands a runner any queued job of the repository whose labels it has, so a runner may run a different queued job with the same labels instead of the one it was started for. Each queued job still gets one runner: the runner's row then shows the job it ran, and the job it was started for waits for the next runner, in its place in line, with its own labels and size.

Security

  • Each job gets a new container and a new just-in-time runner, registered in that one repository for that one job. Nothing of one job, not even the runner's credentials, is there for the next.
  • The just-in-time configuration is the runner's credential. Diffinery passes it only to the container's environment, removes it from the job's environment, and never stores or logs it.
  • The app asks for these repository permissions: Administration: Read and write, which GitHub requires to register and remove a repository's self-hosted runners; Actions: Read and Metadata: Read. It never reads your code.
  • Only a person signed in with a passkey can link or unlink an installation, and only for themselves or an organization they own or administer; GitHub must confirm they can manage the installation. Linking and unlinking are in the audit log as github_runners.link and github_runners.unlink.
  • Public repositories: anyone who opens a pull request can change a workflow, and a workflow that runs on diffinery uses the linked account's minutes. Under the repository's Settings → Actions → General, require approval for fork pull request workflows from outside contributors (for example Require approval for all external contributors), as GitHub advises for any self-hosted runner.
  • A site administrator's kill switches apply: suspending an account's Actions, or pausing hosted compute, fails the waiting jobs and stops the running runners.
  • Unlink on Settings → GitHub runners stops new runners for the installation. Jobs that wait get none; runners already started finish their jobs.
  • Uninstalling the app on GitHub (in the user's or organization's settings, under GitHub Apps or Applications → Installed GitHub Apps) unlinks it too.
  • Suspending the app on GitHub starts no runners until you unsuspend it.

Troubleshooting

The job stays queued on GitHub with "Waiting for a runner to pick up this job". Check, in this order:

  1. The job's labels: every one must be diffinery, diffinery-<size>, self-hosted, linux or x64.
  2. The repository is among the installation's repositories (Choose repositories on GitHub).
  3. The installation is linked: it is listed on Settings → GitHub runners.
  4. The job's row on Settings → GitHub runners and its message.

"Could not register a runner on GitHub: ... Resource not accessible by integration". The app lacks the Administration: Read and write permission on the repository. An owner of the GitHub account reviews and accepts the app's new permissions in the installation's settings on GitHub (Choose repositories on GitHub opens them).

"No job took this runner within 10 minutes". The job it was started for was cancelled, or ran on another runner with the same labels, and GitHub had no other job for it. This also happens, rarely, when a job is cancelled just as the runner started for it takes a different queued job: Diffinery then starts one more runner for the cancelled job. Diffinery stopped the runner and removed it from GitHub; nothing to do.

"The runner stopped before it took a job (exit code N); starting another". GitHub's runner exited before a job started, for example because GitHub was unreachable. Diffinery tries up to 3 runners for a job.

Settings → GitHub runners says "This Diffinery has no GitHub App". The instance's administrator hasn't set the app up. See Configuration.