The deploy had been dead all night. Not a code error, not a failed test: a GitHub Actions billing block, silent from inside the repo and only visible on GitHub’s org settings page. Fixing it took one commit, and it wasn’t a code change.

The false trail

Two overnight triage passes had already misdiagnosed it. A ci-alert/triage session at 12:26 AM read the failing deploy log, saw BlobNotFound: the specified blob does not exist, and called it a Cloudflare Pages storage or cache problem, a stale build artifact blob expired out from under the deploy step. A second pass 20 minutes later landed on the same theory with slightly different wording: Azure/CDN blob storage error, source unclear. A third pass at 2:26 AM and a fourth at 2:42 AM repeated the same conclusion. Four separate triage runs, four confident-sounding root causes pointing at blob storage, and none of them were it.

BlobNotFound was a real error, but it was downstream. It’s what a Pages deploy step looks like when the workflow run that was supposed to produce the artifact never actually executed, because GitHub had already refused to schedule it. The triage agents were reading the last log line the runner managed to emit before the job got starved, not the actual failure. There was no separate blob storage subsystem to debug. The job that would have written the blob never got a runner.

The same failure mode had shown up hours earlier on a different repo the same day: a client billing platform we run CI for also hit a dead queue, traced that morning to the shared free CI allowance running out. Two repos, same account, same day, same starvation. That repetition is what made the actual cause legible: this wasn’t a Cloudflare quirk, it was one number running out.

What the quota actually is

GitHub’s free tier for Actions minutes is allocated per plan, and a personal free account has one shared pool of minutes across every repository that account owns. It doesn’t matter how unrelated the repos are. dxdev.com’s static site build and an internal CRM’s CI suite were drawing from the same account level allowance, and once other repos on that account had burned through it for the billing period, every subsequent workflow run queues against a quota that’s already at zero. GitHub doesn’t fail loud with a “quota exceeded” status on the commit. It just doesn’t run the job, or runs it and starves it partway through, and whatever step was mid-flight when the throttle hit is what shows up in the log as the error, in this case a blob lookup with nothing to find.

There’s no per-repo override on a free personal account. You can’t ring-fence one project’s Actions minutes from the rest of your account. The three options were: upgrade to a paid plan and buy more minutes, self-host a runner so the jobs stop drawing from GitHub’s pool at all, or move the repo somewhere the allowance resets. Buying minutes fixes it for the one account, permanently coupling every future side project’s CI health to whatever else that account happens to be running. Self-hosting a runner is real infrastructure to babysit for a static site build. Moving the repo into its own GitHub organization gives it a completely separate free Actions allowance, because the allowance is scoped to the plan owner, and an org is its own plan owner, distinct from any personal account.

The fix and the check

The fix was moving the dxdev.com repository into a new dedicated GitHub organization, which gets its own zeroed-out Actions minutes independent of whatever the personal account or any other org had already spent. No code in the repo changed. The workflow YAML was already correct; it had been queuing against a pool with nothing left in it. Once the repo lived under the new org, the next push triggered a workflow run against a fresh allowance, and that deploy succeeded on the first try.

The diagnostic lesson is the gap between “the log shows an error” and “the log shows the failure.” Four automated triage passes correctly transcribed what the log said and still missed the cause, because the log only contains what the starved job managed to emit before it stopped getting scheduled. The tell, in retrospect, wasn’t in that repo’s log at all. It was the second, unrelated repo hitting the identical wall the same day. One instance of BlobNotFound looks like a storage bug. Two unrelated repos going dark on the same account, same day, is a quota.