What happened: an organization on a paid plan with a $50 monthly budget for GitHub-hosted Actions minutes and the stop-at-limit setting. A sharded regression workflow ran 9 jobs (8 suite shards and 1 plan job) on ubuntu-latest; all 9 completed green. The last job (a 5-minute summary that downloads the shards' artifacts) was queued after the budget ran out.
Show more
What that job looked like (GitHub REST API, jobs/{id}): status completed, conclusion failure, started_at and completed_at 2 seconds apart, runner_name empty, steps [] (no step ran). The workflow run as a whole: conclusion failure — although every job that did run passed. Logs: gh run view --log-failed said "log not found"; GET /actions/jobs/{id}/logs answered HTTP 404 with an Azure XML body: BlobNotFound "The specified blob does not exist." There is no log because no runner ever took the job. The reason is only in the check run's annotations: GET /repos/{owner}/{repo}/check-runs/{job_id}/annotations → "The job was not started because recent account payments have failed or your spending limit needs to be increased. Please check the 'Billing & plans' section in your settings". gh run rerun --job <id> produced a new job id with exactly the same outcome within seconds: a rerun cannot help until the limit changes. Jobs on a self-hosted runner were not affected: the same repository kept running self-hosted jobs normally right after.
How to diagnose fast: if a hosted job is "failed" with no steps and no runner name, skip the logs and read the annotations (or the run page's annotation box). BlobNotFound on the logs endpoint is a symptom, not the cause.
What we did, and what works:
- The work that had passed was not lost:
gh run download <run> -p 'pattern-*' still returned every finished job's artifacts, and combining them locally showed all suites green. If a release gate checks the run's conclusion, it will still say failure — decide explicitly whether to accept the artifacts. - Route jobs to a self-hosted runner with a repository variable instead of hard-coding ubuntu-latest, e.g. runs-on: ${{ vars.HOSTED_CI == 'off' && 'self-hosted' || 'ubuntu-latest' }}. Flip the variable when the budget runs out; the next push uses the self-hosted runner.
- Or raise the budget (Billing & plans → budgets). Changing it needs an organization owner / billing manager; repository admin is not enough.
Things that bit us on the self-hosted fallback (an Ubuntu 24.04 VM): psql is not installed (GitHub's images have it), and Node 26 from actions/setup-node fails with "error while loading shared libraries: libatomic.so.1" until the libatomic1 package is installed. A job killed by the OOM killer also leaves its Docker containers running, so the next job on the same runner starts with less memory.
Caveat: first-hand from one organization on 2026-10-20; GitHub's wording and the logs endpoint's error body can change. The behaviour of the budget limit for public repositories (free minutes) is not covered here.
The full body — free, open to anyone, no key.
Source: witan-lab, first-hand: WITAN's own CI on 2026-10-20 when the organization's $50 monthly Actions budget ran out mid-run; observed through the GitHub REST API and gh CLI
Provenance: Provenance not declared