Taylor Addison

CI fleet

Actions Runners

Many self-hosted GitHub Actions runners on one Apple Silicon Mac, and the page that says whether any of them is broken.

GitHub-hosted macOS minutes bill at ten times the rate against a private account's included allowance, and one iOS repo exhausting that allowance blocks Actions account-wide — taking the cheap Ubuntu jobs in unrelated repos down with it. A fleet on hardware you own removes that shared fate. The cost is owning the host's health: idle listeners are nearly free, two simultaneous Xcode builds are not.

The dashboard exists because GitHub has no cross-repo Actions view, and half of the question — is that runner even alive — is not answerable from GitHub at all. One Node process with no npm dependencies and no build step polls GitHub and the machine, keeps everything it learns in SQLite forever, and serves one page: Fleet, Runs, Analytics, Lint, Alerts, Capacity, Hosts and Control.

0
runtime dependencies, no build step
6
kinds of drift between launchd and GitHub
123
fields checked against PyYAML, zero mismatches
10×
what hosted macOS bills against the allowance
GitHub above; one Mac running the runner LaunchAgents, the job hooks and local probes alongside the fleetd daemon, whose collector and server share an in-memory snapshot backed by SQLite; below it a browser dashboard reading over SSE and acting over a bearer-token API, and a second Mac's agent reporting inbound over a heartbeat.
One process does both jobs: the collector and the server share a snapshot in memory, so the live view never polls. Everything runs under launchd — which is also how it gets a GitHub token.
The Fleet tab: every registered runner online, zero drift, memory pressure normal, and a grid of runner cards grouped by project, every one idle.
The Fleet tab on a live host, with repository and host names replaced. Green means launchd, GitHub and this machine agree; the drift rules exist for the six ways they can silently stop agreeing.
The Analytics tab: runs, success rate, CI time, hosted-macOS allowance saved, peak concurrency, never-scheduled runs, and a table classifying why jobs failed and whose problem each cause is.
Thirty days of a real fleet, names replaced. The failure table says whose problem each cause is — a broken test, a starved runner and an account-level block are the same conclusion in a runs table and nothing alike in practice.

What it proves

Three things worth reporting

The most useful thing it knows is when two systems disagree

Drift is the set of states where launchd and GitHub disagree about a runner, and each one is silent from whichever side you happen to be looking at. A runner registered on GitHub with no LaunchAgent queues jobs forever; a listener GitHub calls offline looks fine locally. One rule exists because no runner plist sets KeepAlive — a crashed service is never revived by launchd, and the only symptom is one repo's jobs queuing while every other repo looks healthy.

The numbers exclude what would make them wrong

Cancelled runs are out of every duration percentile, because a cancelled run's duration measures how long until something killed it. Jobs that never reached a runner are out of concurrency, because they present as day-long intervals overlapping everything. The host tile leads with the kernel's memory-pressure level and the swap-in rate, not swap used — on macOS that is an accumulator, not a gauge, and it stays high long after nothing needs it.

Alerts fire on transitions, and the control plane has no shell

A rule evaluated every fifteen seconds that notified whenever it was true would send 240 notifications an hour for one dead runner. Each condition opens once and closes once, with a sustain window and a storm guard. Every action the daemon can take is execFile with an argv array against an entity it already discovered — a runner name containing a shell command is a name that fails to match a known runner.

JavaScriptNodeSQLitelaunchdGitHub ActionsmacOSSSEYAML

More open source

Fleet Runner