Skip to content
Back to skills

Actions Billing Usage

ASecurity

Measure GitHub Actions cost with the billing-usage API — per repo, month and SKU; net vs gross; per-job rounding. Use when optimizing CI cost or speed, or before removing a workflow as expensive.

  • 58 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 25, 2026
code-qualitygobashdebugginggitapi

Works with

  • api

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add laurigates/claude-plugins --skill actions-billing-usage --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Actions Billing Usage?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Actions Billing Usage
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/laurigates-actions-billing-usage/badge)](https://www.skillsdirectory.com/skills/laurigates-actions-billing-usage)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: actions-billing-usage
description: Measure GitHub Actions cost with the billing-usage API — per repo, month and SKU; net vs gross; per-job rounding. Use when optimizing CI cost or speed, or before removing a workflow as expensive.
allowed-tools: Bash(gh api *), Bash(gh run list *), Bash(jq *), Read, TodoWrite
created: 2026-09-24
modified: 2026-09-26
reviewed: 2026-09-24
---

# Before Optimizing CI Cost, Read the Billing-Usage API

Promoted from the always-loaded `ci-cost-read-the-billing-api.md` portfolio
rule, whose stub keeps the gate line.

Any "make CI cheaper / faster" task starts with a guess about *where the cost
is*, and across a portfolio that guess is reliably wrong. The intuition follows
**where the interesting work is** (the private repos, the big builds); the
actual minutes follow **how often a workflow fires**, which is dominated by
automation nobody thinks of as expensive. GitHub will tell you exactly, per
repo, per month, per SKU — ask it first, then optimize.

## When to Use This Skill

| Use this skill when... | Skip when... |
|---|---|
| A runner-tier, concurrency, caching, or scheduling change is pitched as a cost or speed win | The target is a single known-slow workflow you are already measuring directly |
| About to *remove* a workflow "because it must be expensive" | |
| A bill moved and the cause isn't obvious — the per-repo × per-month breakdown localizes it in one call | |
| Adding automation, to decide whether it gets its own job | |

## The endpoint (the obvious one is gone)

```
gh api "/users/<user>/settings/billing/usage"
```

> `/users/<user>/settings/billing/actions` — the endpoint most examples and most
> recall still name — now returns **HTTP 410** *"This endpoint has been moved."*
> That 410 is easy to misread as "no billing data available on this plan" and
> skip the measurement entirely.

Org equivalent of the working endpoint: `/orgs/<org>/settings/billing/usage`.

Aggregate before reading; the raw response is one row per repo × month × SKU:

```
gh api "/users/<u>/settings/billing/usage" --jq '[.usageItems[] | select(.product=="actions" and .unitType=="Minutes")] | group_by(.repositoryName) | map({repo:.[0].repositoryName, minutes:(map(.quantity)|add), net:(map(.netAmount)|add)}) | sort_by(-.minutes)'
```

## Reading it correctly

- **The filter takes the *bare* repo name, not `owner/repo`.**
  `select(.repositoryName == "<owner>/<repo>")` returns `[]`, which is
  indistinguishable from "this repo bills nothing"; `"<repo>"` returns the rows.
  The owner-qualified filter has reported nothing for a repo billing thousands
  of minutes that month, and a cost argument was nearly built on that empty
  result. Control-test an empty billing filter against a repo you know is
  active before believing it.
- **`netAmount`, not `grossAmount`, is the spend.** Public-repo minutes are free
  and unlimited, so their rows read `grossAmount == discountAmount` and
  `netAmount == 0`. A repo showing a large gross may be costing nothing — but
  it's still where a runner-tier change would pay off *once* charges begin, so
  read both: gross for **exposure**, net for **current spend**.
- **Group by SKU too** (`Actions Linux` vs `Actions Linux Slim` vs
  `Actions macOS 3-core`) — it reveals which tiers are already in use and their
  `pricePerUnit`, so the cheaper-runner arithmetic needs no lookup.
- **Read the monthly trend, not just the total.** A flat annual figure hides an
  exponential; the decision usually turns on the slope.
- **The default response is windowed — the obvious call will not give you the
  trend.** A bare `/orgs/<org>/settings/billing/usage` returns only a slice
  (observed 2026-08: Jan–Feb only, for a repo with a full year of activity), and
  nothing in the payload says it was truncated. Build the trend from explicit
  per-month calls and let the months you asked for be the months you got:

  ```
  for m in $(seq 1 12); do gh api "/orgs/<org>/settings/billing/usage?year=2026&month=$m" --jq "[.usageItems[] | select(.product==\"actions\" and .unitType==\"Minutes\" and .repositoryName==\"<repo>\")] | map(.quantity) | add // 0" ; done
  ```
- **Read the outcome distribution, not just the minutes.** Minutes tell you
  something is expensive; outcomes tell you whether you are buying anything.
  An E2E suite billing close to an hour per run while `cancel-in-progress`
  killed it about halfway through succeeded a few percent of the time.
  "Expensive" argues for a cheaper runner; "expensive and almost never
  succeeding" argues for deleting or fixing the workflow. Same bill, different
  decision. Pull conclusions alongside cost:

  ```
  gh run list -R <owner>/<repo> --workflow <name> -L 400 --json conclusion --jq 'group_by(.conclusion) | map({(.[0].conclusion // "null"): length}) | add'
  ```
- **`/actions/workflows/<id>/timing` is empty for public repos** — billable ms
  is only populated where minutes are billed. Approximate per-workflow cost
  instead:

  ```
  gh run list -R <owner>/<repo> -L 400 --json workflowName,startedAt,updatedAt
  ```

  then group durations by `workflowName`. Good enough to rank; don't quote it as
  billing truth — run wall-clock includes queue and finalisation time that is not
  billed, so it **over-counts** (measured ~16% above the billing API for the
  same repo and period). Rank with wall-clock; quote the API.

## Minutes are billed per job, rounded up

> "GitHub rounds the minutes and partial minutes **each job** uses up to the
> nearest whole minute" — [Actions runner pricing](https://docs.github.com/en/billing/reference/actions-runner-pricing).

Per *job*, not per run: a workflow with three four-second jobs bills three
minutes. Linux 2-core is $0.006/min, Linux Slim $0.002/min. The billing rows
show the rounding directly — the `quantity` for `Actions Linux` and
`Actions Linux Slim` is always a whole number.

Two consequences when adding automation rather than optimizing it:

- **Fold trivial work into a job that already runs.** A seconds-long check (for
  example, renumbering a docs file) belongs in an existing docs job rather than
  a workflow of its own, which would bill a whole minute per trigger.
- **A job skipped by an `if:` bills nothing; a job that starts and exits early
  still bills its rounded minute.** When the point is to *not* pay, put the
  condition on the job — or on the expensive step, which also skips the model
  spend of an LLM-backed check.

## Why it changes the answer

In a portfolio runner audit, the plan was to move cheap jobs to a 1-CPU runner,
and the expected win sat in the private repos with real pipelines. The billing
API said **one** repo — a *public* plugin collection with no build step worth
mentioning — was the large majority of standard-Linux minutes, driven purely by
scheduled audits and PR automation. Every private repo that would have been
optimized first was ≈0 minutes. The month-over-month trend (roughly tripling
over a quarter, with the first net charges arriving) was what justified acting
at all. Without that one query the work would have landed almost entirely in
the wrong repos.

## Agentic Optimizations

| Context | Command |
|---|---|
| Minutes and net spend per repo | the aggregate query under *The endpoint* |
| Which runner tiers bill | that query with `group_by(.sku)` and `sku:.[0].sku` in place of the repo grouping |
| One month, one repo (bare name) | `gh api "/orgs/<org>/settings/billing/usage?year=2026&month=9" --jq '[.usageItems[] \| select(.product=="actions" and .unitType=="Minutes" and .repositoryName=="<repo>")] \| map(.quantity) \| add // 0'` |
| Outcomes per workflow | `gh run list -R <o>/<r> --workflow <name> -L 400 --json conclusion --jq 'group_by(.conclusion) \| map({(.[0].conclusion // "null"): length}) \| add'` |

## Related

- `finops-plugin:github-actions-finops` — the org/repo waste sweep (skipped
  runs, bot triggers, missing concurrency) that reads this same endpoint; this
  skill owns reading the endpoint itself

- `offload-to-deterministic-substrate.md` (in `~/.claude/rules/`) — one API call
  beats re-deriving cost from run logs by hand, every time.
- `code-quality-plugin:debugging-methodology` § failure point — same law applied
  to runtime: measure the thing, don't reason about it. Here the "failure point"
  is the invoice.
- `github-actions-plugin:multirepo-ci-cd` — the portfolio-sweep mechanics you'll
  use once the billing data has told you which repos to sweep.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…