A Thousand Build Runs a Month — and Not One of Them Costs Anything ⚙️
A correction first. The first version of this article claimed our GitHub build minutes had run out and that this forced an upgrade to a Pro account. That was wrong, in a way a single glance at the documentation would have cleared up: for public repositories, GitHub build minutes are free. Artifacts and caches too. Our repository has been public since the end of May — so we have never paid for a minute, and we never will. Rather than quietly fixing the text, we'd rather rewrite it and tell you what the measurements actually showed. That turns out to be far more interesting than the wrong version.
Guess first, look second. The trigger was an evening when a finished pull request sat in the browser and not a single check fired. No green ticks, just silence. We went for the obvious explanation — "quota exhausted" — and took a Pro account. Everything ran again afterwards, which seemed to confirm the theory. It didn't: the silence had a different cause (a pull request GitHub can't cleanly merge with the main branch at that moment doesn't start any checks). We've since queried the billing data of every single run. The answer, for every run and every operating system: billable time = 0 milliseconds. Supporting a platform with a subscription is perfectly fine — but as an explanation for this repository, it doesn't hold.
What is true: the machinery has grown. Every pull request kicks off a small factory: the whole server is built and its test suite run, linter and code analysis look it over, CodeQL hunts for security holes, and Docker images for the game server, the portal, the WorldHost and the AI backend get produced. A release adds Windows, Linux and macOS builds of the client, plus the WebGL version for the browser game. Since the end of May that adds up to 1,723 build runs, 1,092 of them in July alone — across 131 merged pull requests in four weeks. And yes, the recent surge really does have to do with the AI 😉: the project is developed 100% with AI assistance, and with the bigger subscription simply more happens in parallel. Only the currency isn't money — it's waiting time.
Where the waiting time goes. Measured across eight days: the normal check run (CI) 379 minutes, the CodeQL security analysis 230, the release builds 126, the rest is small change. A single release costs around 110 minutes of compute, split across WebGL (31.6), the Docker image (19.4), macOS (13.7), Linux (13.7), the big test gate (12.1), Windows (10.9) and packaging (7.2). That's the time between "done" and "installable for you".
And now the thing that really is scarce: the cache. So that a build doesn't start from zero every time, GitHub stores intermediate results — and for that you get 10 GB per repository, no matter how public you are. Ours currently holds 5.3 GB across 63 entries. The problem is the distribution: roughly two thirds of it are Docker intermediate layers, re-uploaded on every run — and they crowd out precisely the thing that would help most. That's the real bottleneck, and no subscription in the world buys your way out of it.
The nicest find was an actual bug. When building, Unity creates a huge intermediate folder (Library) whose reuse saves minutes. Measuring revealed: for Windows, Linux and macOS that folder was never saved — the builds dutifully tried to restore it, but nobody ever wrote it. And the Windows build was the only one with a cache key carrying no platform marker. The result: it downloaded 1.7 GB of WebGL intermediate state, which Unity then dutifully threw away before re-importing everything. The log shows it in black and white: 257 seconds of asset import instead of 85 in the warm case. That's nearly nine minutes wasted per release — because one word was missing from a key.
What else turned up. Our test suite runs three times per shipped commit (in the pull request, after merging, and again at release). CodeQL checks three languages on every pull request although it isn't even a required check. A cache for the .NET packages is missing entirely. An emulator for foreign processor architectures gets set up although we only build for one. And 7.4 GB of old build artifacts sit around on the default 90-day retention. None of it is dramatic — together it's a decent chunk of lifetime.
The lesson is the same one our server taught us. Back then we measured that half the occupied memory wasn't going to the game at all, but to Docker and the monitoring. This time it was more embarrassing still: we assumed a bill that never existed, and paid before looking. Measure, don't guess — apparently that sentence holds until you've genuinely internalised it. The clean-up work is planned but not done yet; when it runs, we'll tell you what it bought us. 🚀
Comments