The Green Check That Ran Nothing: A No-Op CI Workflow That Billed 45% of a Budget

Date:

A failing check gets fixed the same day. A check that passes while doing nothing can bill you every push for months, because a green tick looks exactly like success.

The short answer: A no-op CI workflow is a job that reports green while doing no useful work – it boots a runner, checks out the code, hits an early exit 0 or a skipped step, and stops. It still costs a full billed minute every time it runs. In one eight-repo private estate, a single no-op governance workflow was 45% of a month’s GitHub Actions bill – about 807 of 1,806 billed minutes over 22 days – and enforced nothing, because on a free plan a CI check cannot even gate a merge. The fix is to find it with the usage report, confirm it protects no one, and retire it (recoverably); the harder question it exposes is how to raise the ceiling once the remaining jobs are real work.
A bar chart of billed GitHub Actions minutes by workflow over 1 to 22 September. The largest bar, flagged in red, is a no-op governance workflow at 807 billed minutes that only ran exit 0 and enforced nothing; the architecture test suite billed 490 minutes of real work; the PR base guard billed 264 for a single string comparison; project CI billed 245 for lint and figures. The no-op alone was 45 percent of the 1,806-minute bill.
  • The result: one workflow removed, ~45% of the monthly Actions bill recovered, zero coverage lost.
  • Who: a single-developer estate of eight private repositories on the GitHub Free tier.
  • Timeframe: an audit of 1-22 September 2026, acting on a budget already 90% consumed.

Background: an audit that started with the wrong suspect

The trigger was a warning that 90% of a 2,000-minute monthly allowance was already gone with a third of the month left. The reflex was to hunt for a slow build. That reflex is almost always wrong on a CI budget – GitHub Actions bills each job rounded up to a whole minute, so the bill tracks how many jobs run, not how long they take. The prior audit of this estate had already made that case (see why CI minutes track job count, not build speed).

This time the usage report said something blunter. One workflow, present in every repository, was the single largest line on the bill – and it did nothing.

The problem: a green check that ran nothing

The workflow was a shared-governance backstop. Its one step cloned a private rules repo and ran the checks, gated on a secret token. The token was never set anywhere in the estate, by an earlier decision: enforcement lived in a local pre-push git hook instead. So the step’s first line was a guard that skipped the rest and exited successfully:

- run: |
    if [ -z "$GUARDS_TOKEN" ]; then echo "token unset -> skipping"; exit 0; fi
    ...
Code language: YAML (yaml)

Every run booted a fresh runner, checked out the repository, printed one line, and stopped green. A no-op CI workflow in the exact sense: a passing check that verified nothing. And because it was wired to run on both push (all branches) and pull_request, with no concurrency cancellation, it fired twice for every push to a branch with an open pull request – a full billed minute each, across eight repositories.

A passing check is not proof of a working check. It is only proof that nothing failed – including the check itself.

Why the waste stayed invisible

Two things hid it. First, green reads as value. A red X gets investigated within the hour; a green tick is trusted and never opened. The workflow had been quietly succeeding, and succeeding at nothing, for weeks. Only the billing page – not the pull-request UI – showed what it cost.

Second, and more subtly, the check could not enforce anything on this plan. Required status checks – the setting that actually blocks a merge until a check passes – are a paid feature on private repositories. On the Free tier the API simply refuses them. So even if the workflow had run its real checks, a red result could not have stopped a merge; it would have been advisory only. An advisory backstop for a rule that is already enforced locally, for a contributor population of exactly one, is pure cost. (For the wider version of this — what such governance workflows are ever for, and where a guard actually belongs — see we built CI governance for a team we didn’t have.)

On a free plan a CI check cannot block a merge, so an unenforced backstop protects no one – it only bills.

The fix: retire the no-op, keep the enforcement

The change was a deletion, done carefully because deletions frighten people for good reason. The workflow was removed from all eight repositories and from the shared source it was synced from. Nothing was lost: the file stays in git history, and a one-line sync command re-deploys it the day a second developer or an outside pull request makes the backstop worth its cost. That trigger, and the restore steps, were written down in the repository so the removal reads as a decision, not an accident.

Enforcement did not change. The same governance checks still run locally on every push through the git hook – which was always the real gate. The estate’s genuine CI coverage, a full test suite in the main repository, was deliberately left untouched.

The lesson is not “delete governance.” It is that a check earns its cost by changing an outcome. This one changed no outcome: it enforced nothing a merge depended on and duplicated a local hook. Once that was true, keeping it was a subscription to a green light.

When trimming isn’t enough: raising the ceiling

Removing the no-op put the estate back under budget. But trimming waste has a floor; once the remaining jobs are real work, the only way down is to change what the budget can hold. Three levers do that, and one popular non-lever does not.

The non-lever first, because it is the common instinct: adding a developer does not add minutes. The included allowance belongs to the account that owns the repositories, not to each collaborator. A new teammate brings no minutes of their own; their pushes draw from the same pool, so the limit arrives sooner. On a paid organization plan more seats cost more, not less. Opening a second free account to hold some repositories is not a way around it either: GitHub’s terms allow an individual only one free account and prohibit creating accounts to evade usage limits, so that route risks a suspension, not more headroom. You cannot buy minutes by adding people or accounts.

What actually raises the ceiling:

  1. A self-hosted runner. Jobs that run on your own machine do not count against the hosted allowance at all on private repositories. For a heavy test suite that is the whole bill turned free. Two cautions: on a personal (non-organization) account a runner registers per-repository, so covering many repos means many registrations or a free organization first; and a self-hosted runner runs workflow code on your own hardware, which is fine for private repos with no outside contributors and unwise the moment a repository accepts pull requests from forks.
  2. Make a repository public. Hosted minutes are unmetered on public repositories. If the code can be open and holds no secrets, its Actions cost disappears entirely.
  3. A paid plan. The next tier up roughly raises the allowance, and a spending limit lets jobs run past it for a per-minute charge – a backstop, not a plan.

For a self-hosted runner the decisive property is not raw speed but availability: a runner only executes while its machine is awake, so a stationary always-on box beats a faster laptop that sleeps. Power only shortens a wall-clock that is already free.

You cannot buy more minutes by adding people. The allowance belongs to the account, not the team.

The short version

  • A no-op CI workflow bills like any other job. A green check that early-exits or skips its only step still boots a runner and costs a full rounded-up minute per run.
  • Green hides cost. A passing check is never opened; only the usage report shows a job that succeeds at nothing. Audit the bill, not the pull-request UI.
  • Advisory is not enforcement. On a free plan, required status checks are unavailable, so a CI backstop cannot block a merge – if the real gate is a local hook, the CI copy may be pure cost.
  • Retire, don’t hoard. Delete the no-op, record why and how to restore it, and keep enforcement where it actually runs. Deletions are safe when they are recoverable and written down.
  • Raising the ceiling is a different lever from trimming waste: a self-hosted runner, a public repo, or a paid plan – never adding a collaborator, which adds usage, not allowance.

Frequently Asked Questions

What is a no-op CI workflow?

A workflow that reports success while doing no useful work – it starts a runner, checks out the code, then hits an early exit 0, a skipped step, or an empty job, and stops. It looks identical to a real passing check in the pull-request UI, but it verifies nothing. It still bills a full rounded-up minute every time it runs.

How do I find a no-op workflow in my own estate?

Read the usage report, not the checks tab. On GitHub, Settings then Billing then the Actions usage report gives a CSV of minutes by workflow. Rank workflows by minutes and by run count, then open the YAML of the top ones and look for a job whose only step is a guard that exits early, or whose steps are all conditionally skipped.

Does a green check mean my CI is protecting me?

Not by itself. Green means nothing failed, which includes the possibility that the check did nothing. It also does not mean the check can block a merge: that requires a required-status-check rule, which is a paid feature on private repositories. Confirm both that the check does real work and that something enforces its result.

Is it safe to delete a CI workflow to save minutes?

Yes, when the deletion is recoverable and the check was not enforcing anything a merge depended on. The file remains in git history and can be restored in one command. Record what was removed and the trigger to bring it back, so a future reader does not rediscover a gap and rewire it by reflex.

Will adding a developer or a second account give me more Actions minutes?

No, and the two versions of the idea fail differently. A collaborator draws from the same pool the account owner already has, so more people reach the limit faster, not slower. A second free account created to hold some repositories would technically carry its own allowance – but GitHub’s terms permit an individual only one free account and forbid opening accounts to circumvent usage limits, so it risks suspension rather than headroom. To raise the ceiling legitimately, use a self-hosted runner, make a repository public, or move to a paid plan.

When is a self-hosted runner worth setting up?

When your remaining CI is genuine work – a real test suite – rather than waste, and you have a machine that is on when you push. Self-hosted jobs on private repositories are unmetered, so a heavy suite becomes free. Availability matters more than speed, and never attach a self-hosted runner to a public repository.

Does cleaning up merged branches cost Actions minutes?

Usually not – but it can, and the difference is how you archive, not how you delete. Deleting a branch fires GitHub’s delete event, which runs a workflow only if one explicitly listens for on: delete (few do), so a plain delete is normally free. The trap is a recoverable cleanup that keeps a copy: archiving a branch as a new branch or a tag is a push, and that fires push and create workflows – so a repo whose CI runs on every branch push bills a full minute for each archived branch, turning a hundred-branch tidy-up into a hundred billed runs. Archiving instead to a custom ref namespace – a ref that is neither a branch nor a tag, such as refs/archive/... – is invisible to Actions: the branch keeps a restorable copy on the remote, and the cleanup costs nothing. It is the same lesson as the rest of this piece: what triggers a billed run is a matter of which ref and which event, not of how much work is done.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.