buildstats.io

Custom README badge from any CI number, without a gist

buildstats turns a number your workflow pushes into a README badge at a stable URL, in the shields.io flat style, plus a shields endpoint JSON and a history chart. Free for public repos; private $9/month.

The common recipe for a dynamic badge is a gist: a workflow writes a JSON file to a gist with a token that has gist scope, and shields.io renders it through its endpoint badge. It works, and it needs a personal token in a secret, a gist per badge and a step that never fails. The other recipe commits an SVG to the repository on every run. buildstats drops both: the Action pushes the number, and the badge is served.

Setup: push the number

permissions:
  contents: read
  id-token: write

steps:
  - uses: actions/checkout@v4
  - run: npm ci && npm test -- --reporter=json --outputFile=report.json

  - uses: bitgate/buildstats@v1
    with:
      metrics: |
        tests=$(jq '.numPassedTests' report.json) higher
        dependencies=$(jq '.dependencies | length' package.json) lower
        bundle=$(wc -c < dist/app.js) bytes lower

Each line is name=value [unit] [higher|lower]; a metric name is up to 64 characters of letters, digits, _, . and -, and a unit up to 16 characters. The Action signs in with the workflow's OIDC token, which is what id-token: write is for, so there is no secret. A public repository's first push creates the project at buildstats.io/<owner>/<repo>.

Embed the badge

![tests](https://buildstats.io/acme/app/tests/badge.svg)
![bundle](https://buildstats.io/acme/app/bundle/badge.svg?ref=release)

The badge is a 20-pixel SVG in the shields.io flat style with the metric name on the left and the latest value with its unit on the right, read from the default branch, or from the branch in ?ref=. Byte values are shown as kB or MB. The colour follows the last change: green when the metric improved against the previous commit, red when it got worse, blue when it did not change or has no direction. GitHub proxies the image through its camo cache; public badges carry a 5-minute cache, so a new value appears a few minutes after the push without a commit.

Or let shields.io render it

![tests](https://img.shields.io/endpoint?url=https://buildstats.io/acme/app/tests/badge.json&color=blue&logo=vitest)

/badge.json returns the shields.io endpoint format, {"schemaVersion": 1, "label": "tests", "message": "1832", "color": "brightgreen"}, so every shields.io option works: style, color, label, logo, logoColor, labelColor. shields.io caches endpoint badges for at least 300 seconds. The same JSON works with badgen.net's /https generator.

The chart behind the badge

Every badge has a chart: /tests.svg draws up to 1,000 of the stored points, 400 to 1,200 pixels wide, in light and dark. A metric keeps 50,000 points per branch, so the badge is the latest value and the chart is everything before it. Pull requests get a comment with the delta against the base branch for each metric, and max-regression can fail the step when a metric with a direction gets worse. README charts has the embed snippets.

Limits and pricing

Public repositoriesPrivate repositories
PriceFree$9/month per GitHub user or organization
TrialNot needed14 days, no card
Metrics per project100100
Points per metric and branch50,00050,000
Metrics per push5050
Badge URLPublicUnguessable token URL, revocable
Image cache5 minutes1 minute
Pushes per minute and project6060

Everything else is on the pricing page.

How this compares with gist and committed badges

ApproachNeedsUpdatesHistoryWorks for private repos
buildstats /badge.svgOne Action step, OIDCAfter each push, servedChart and APIYes, token URL
shields.io endpoint + gist (schneegans/dynamic-badges-action)A gist per badge, a token with gist scope in a secretWorkflow writes the gistNoYes, but the gist is public or secret, not private
Committed SVG (tj-actions/coverage-badge-py, jaywcjlove/coverage-badges-cli)A commit per run, write permissionOn the next push of the badge commitGit log onlyYes
shields.io dynamic JSON badgeA public JSON URL you hostWhen your JSON changesNoOnly if the JSON is public

Questions

What can be a badge?

Any number: test count, dependency count, bundle size, coverage, Lighthouse score, binary size, build time, open TODOs, lines of code, model eval score. If a shell command can print it, the Action can push it.

Can I change the badge colour or add a logo?

Serve it through shields.io: point the endpoint badge at /badge.json and use shields.io's color, logo, style and label parameters.

Does the badge need a token in my repository?

No. The workflow uses its own OIDC token; nothing is stored as a secret. Only workflows outside GitHub Actions need a project API key.

How fast does the badge update?

Public badges are cached for 5 minutes and private ones for 1 minute, plus GitHub's own image cache. Expect the README to show a new value within a few minutes of the push.

Can I show the value from a release branch?

Yes. Add ?ref=release, or any branch name, to the badge or chart URL.

Last updated 2026-10-07.