Freesbe

Blog

We moved CI to a MacBook Air

By Freesbe3 min read

One day in October, Freesbe’s repository ran 266 CI jobs. That’s what happens when several AI coding agents work in parallel, each pushing to its own branch, each pull request running format, typecheck, tests, the web build, a desktop package, an Android build and, sometimes, a full screenshot run.

Hosted CI minutes on a private repository are billed. That one day used more than 3,000 minutes, well past the monthly free allowance, and then GitHub stopped starting jobs until the billing was sorted out. CI had become the most expensive and least reliable part of shipping.

First: stop running what doesn’t need to run

Before buying or building anything, we cut the waste:

  • The Android build runs only when something that affects it changed: the phone app, the shared packages, the lockfile or the CI config. It was the biggest single job, about 8 minutes, and most pull requests never touch the phone app.
  • A new push cancels the previous run on the same pull request. Nobody needs results for a commit that’s already been replaced. Runs on main are never cancelled.
  • Screenshots run once a day, not on every pull request.
  • Branches aren’t updated just to rerun CI. A pull request whose checks are green and that doesn’t conflict gets merged as it is.

That roughly halved the minutes. The rest moved to our own machines.

Then: self-hosted runners, by weight

GitHub Actions can send jobs to machines you run yourself. We split jobs into two weights, each with its own runner label:

  • Light: format, typecheck, tests, the web build, the desktop package. A few minutes each, modest memory.
  • Heavy: the Android build and the screenshot suite. Lots of memory, the Android SDK, a real browser.

Today the pool is a MacBook Air with an M-series chip, running three runners (two light, one heavy), plus a small Debian virtual machine with three light ones. The Mac turned out to be the best build machine we have: fast single-core performance, lots of memory bandwidth, silent, and already sitting on a desk.

Which machines run a job is one repository variable. If every self-hosted runner disappeared tomorrow, clearing that variable sends everything back to hosted runners, with no workflow edits.

What stays on hosted runners

Deploys. The deploy job holds the token that can change production, and we don’t want that token on a laptop, ever. So deploys run on GitHub’s own runners, in a protected environment, and nothing else does.

Our rule of thumb: self-host the work that’s expensive and harmless, and keep the work that’s cheap and dangerous on someone else’s locked-down machine.

The outage that taught us about idle timeouts

For a while the pool also included six runners on a Windows PC, inside the Windows Subsystem for Linux (WSL). One afternoon, CI simply stopped. Jobs sat in the queue, the runners showed as offline, and nothing on the PC looked wrong.

The cause was a WSL default we hadn’t thought about: when no interactive session is using the Linux VM for a while, Windows shuts it down to save memory, background services and all. Our runners were background services. The fix is one line in .wslconfig (vmIdleTimeout=-1), and the lesson is broader: a build machine that something else can put to sleep isn’t a build machine. We’ve since retired that PC from the pool.

What we turned down

  • Paying for more hosted minutes. The simplest answer, and the right one for many teams. For us the bill grew with every agent we added.
  • A Kubernetes runner fleet. Autoscaling runners are great at scale, and a lot of infrastructure for a small team.
  • Fewer checks. Never. The answer to expensive CI is to run it smarter, not to test less.

What we’d tell another team

Measure before you move. Most of our savings came from not running jobs, not from running them somewhere cheaper. Then self-host by job weight, keep deploy credentials off your own machines, and make the fallback to hosted runners one switch away.

Try all of it free with up to 10 people.

No card, no time limit. Invite your team and see what sticks.