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


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

/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 repositories | Private repositories | |
|---|---|---|
| Price | Free | $9/month per GitHub user or organization |
| Trial | Not needed | 14 days, no card |
| Metrics per project | 100 | 100 |
| Points per metric and branch | 50,000 | 50,000 |
| Metrics per push | 50 | 50 |
| Badge URL | Public | Unguessable token URL, revocable |
| Image cache | 5 minutes | 1 minute |
| Pushes per minute and project | 60 | 60 |
Everything else is on the pricing page.
How this compares with gist and committed badges
| Approach | Needs | Updates | History | Works for private repos |
|---|---|---|---|---|
| buildstats /badge.svg | One Action step, OIDC | After each push, served | Chart and API | Yes, token URL |
| shields.io endpoint + gist (schneegans/dynamic-badges-action) | A gist per badge, a token with gist scope in a secret | Workflow writes the gist | No | Yes, 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 permission | On the next push of the badge commit | Git log only | Yes |
| shields.io dynamic JSON badge | A public JSON URL you host | When your JSON changes | No | Only 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.