Collectivenix fleet

30 machines · 4 platform classes · 1 registry

System

CI runners

Collective's own repositories build on self-hosted GitHub Actions runners that Collective declares. The devboxes and the Mac each run a pool of ephemeral runners for the owner's repositories, and a handful of hard-won mechanisms keep the pool from quietly disappearing.

The default pool

A machine opts in with one line, and gets an ephemeral runner for each repository in the default pool, 17 at this revision: Collective itself and its first-party projects.

git.hub.actions.enableDefaultRunners = true;

On one devbox that evaluates to 27 runner services, all ephemeral. Every runner carries self-hosted, the OS and architecture, nix, the platform and the machine's hostname, so a job can ask for any Nix builder or pin itself to one machine. Runners labelled kvm also get /dev/kvm for VM tests.

runs-on: [self-hosted, nix, nixos, x86_64-linux]

Project sites, this one included, are published by a shared reusable workflow that runs on these runners and pushes static files to gh-pages, so publishing spends no hosted minutes and no artifact storage.

Ephemeral by default

The life of an ephemeral runnerA loop line with five stations. Wait for a permit: a host-wide first-in first-out gate allows one registration per interval, sixty seconds by default, and a waiting runner keeps its place for up to thirty minutes. Register: mint a short-lived registration token from the stored access token. Listen: connect to GitHub and wait for a job. Run one job in a private work directory. Exit and clean only that runner's own directory; the supervisor starts it again and the loop returns to the gate. Beside the loop, a health timer reads the service journal and the runner's diagnostic log: a runner that says it is connected while GitHub rejects its session is marked failed, and a runner left stopped by an interrupted switch is started again. wait for permit FIFO gate, 60 s apart register from the PAT listen connected run one job private work dir exit, clean up its own directory ephemeral one job per registration health timer reads journal + Runner _diag log session rejected while “Connected to GitHub” → unit marked failed unit left stopped by an interrupted switch → started again visible in systemctl --failed within six minutes
One job per registration.A runner that crashed or was misconfigured simply comes back as a fresh registration, so the pool heals without manual restarts.

Each runner registers, takes exactly one job and exits; its supervisor starts it again. Persistent runners can still be declared per runner, but the pool-wide default is ephemeral.

The registration gate

Restarting dozens of ephemeral runners at once would race for the GitHub API and can exhaust the account's rate limit. Every registration on a host passes one gate:

Failures that look healthy

The two worst runner failures leave every process apparently fine. A health timer reads both the service journal and the runner's own diagnostic log, since some errors only reach the latter.

SymptomCauseHandling
The listener runs and logs "Connected to GitHub", but no jobs arriveGitHub rejects its session: a name collision or invalidated credentialsThe health unit fails, visible in systemctl --failed within six minutes
The whole pool is gone after a rebuild; units show Result=success, zero restartsA switch stops changed units and starts them at the end; an interrupted switch never does. Restart= only covers units that exit on their ownThe timer starts any stopped runner unit again
On macOS: last exit code = 78: EX_CONFIG, "service is in penalty box"launchd throttled a runner that kept exiting faster than its minimum runtimeA minimum-lifetime guard, and recovery by bootout and bootstrap from the plist. kickstart would wait forever for a spawn launchd has decided not to perform. A running listener is never touched
On macOS: actions/checkout hangs on git versionGit's locale lookup blocked inside the system preferences serviceA bounded Git preflight before the runner starts

Every one of these helpers is a shell or Python file that the module includes verbatim and a test runs directly, so the deployed behaviour and the tested behaviour cannot drift. On macOS, the runner logs that tell these failures apart are preserved across restarts, because an outage investigation once found every one of them already gone.

Isolation

A flake check evaluates a real devbox's runner services and asserts their sandbox, so a refactor cannot loosen it silently:

Windows runners from WSL

A runner marked wslTunnel on a WSL devbox is not a Linux service at all. It is installed natively on the Windows host underneath, from inside WSL:

  1. Mint a registration token

    From the stored access token, as the WSL user who can drive Windows interop. The system runner user cannot.

  2. Hand over through a private file

    The token goes into a temporary file that PowerShell reads through the \\wsl$ share; the file is deleted on exit.

  3. Install idempotently

    Download the runner only if absent and register only if there is no local state. --replace is used only for a stale record of the same name on GitHub.

  4. Start without a console

    A windowless launcher in the user's Startup folder keeps the runner and its children off the desktop, and no service identity change is needed.

Windows binaries are referenced by absolute path, since NixOS-WSL does not put them on PATH for services.