Code coverage badge and history chart without Codecov
buildstats takes the coverage percentage your tests print and serves a README badge, a per-commit chart and a pull request comment with the delta. Free for public repos, no signup; private $9/month per owner.
Codecov and Coveralls parse your coverage report, which gives line-by-line views and patch coverage. Badge actions commit an SVG to the repository or a gist, which gives a badge and nothing else. buildstats does the middle thing: a number in, and out come a badge, a chart and a pull request delta, with no report upload and no commit to your repository.
Setup: extract the percentage, push it
Every coverage tool prints a total. The example uses a JSON summary from Istanbul or Vitest; the jq line is the only thing that changes for other tools.
name: Test
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
id-token: write
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npx vitest run --coverage --coverage.reporter=json-summary
- id: cov
run: |
echo "lines=$(jq '.total.lines.pct' coverage/coverage-summary.json)" >> "$GITHUB_OUTPUT"
echo "branches=$(jq '.total.branches.pct' coverage/coverage-summary.json)" >> "$GITHUB_OUTPUT"
- uses: bitgate/buildstats@v1
with:
metrics: |
coverage=${{ steps.cov.outputs.lines }} % higher
branch_coverage=${{ steps.cov.outputs.branches }} % higher
max-regression: 1
Other tools: coverage.py prints the total with coverage report --format=total; go tool cover -func ends with a total: line; cargo llvm-cov --summary-only --json has .data[0].totals.lines.percent; JaCoCo's XML has counters you can sum with xmllint; simplecov writes .last_run.json with result.line. The direction higher makes a drop show as worse, and max-regression: 1 fails the step when coverage falls more than 1% below the base branch.
The badge

The badge is a 20-pixel SVG in the shields.io flat style showing the latest value on the default branch, for example "coverage 87.2%". It updates by itself after each push; public badges are cached for 5 minutes. /coverage/badge.json serves the same number in the shields.io endpoint format, for a badge rendered by shields.io with your own colours and logo. ?ref=release shows a different branch.
The chart and the pull request comment
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://buildstats.io/acme/app/coverage.svg?theme=dark">
<img alt="line coverage per commit" src="https://buildstats.io/acme/app/coverage.svg">
</picture>
The chart shows up to 1,000 of the 50,000 points a metric can hold, 400 to 1,200 pixels wide, in light and dark. On pull requests the Action compares the coverage with the latest value on the base branch and posts one comment with the base value, the new value and the delta. The comment is kept up to date across pushes and matrix legs. With max-regression set, the step fails after the comment is written, so a required check blocks the merge while the numbers stay visible.
Coverage per package pushes as variants: variant: ${{ matrix.package }} stores coverage/api and coverage/web, which overlay on one chart and get separate rows in the comment.
Limits and pricing
| Public repositories | Private repositories | |
|---|---|---|
| Price | Free | $9/month per GitHub user or organization |
| Trial | Not needed | 14 days, no card |
| Projects per owner | 100 | 25 |
| Metrics per project | 100 | 100 |
| Points per metric and branch | 50,000 | 50,000 |
| Metrics per push | 50 | 50 |
| Badge and chart images | Public URLs, cached 5 minutes | Unguessable token URLs, cached 1 minute |
| Members | Unlimited | Unlimited |
Everything else is on the pricing page.
How this compares
| Tool | What it stores | Badge | History chart | Pull request | Price |
|---|---|---|---|---|---|
| buildstats | The percentage | Yes, served | Yes, in the README | Comment with delta, max-regression gate | Free public, $9/month per owner private |
| Codecov | Full report, line level | Yes | Trend views in the app | Patch and project coverage comment | Free for open source; Developer plan free with 250 private uploads/month, paid tiers above |
| Coveralls | Full report, line level | Yes | In the app | Comment and status | Free for open source; paid per organization for private |
| GitHub code coverage (actions/upload-code-coverage) | Cobertura XML | No | Default-branch scans | Comment from github-code-quality bot | Per active committer on Team or Enterprise |
| schneegans/dynamic-badges-action | JSON in a gist | Via shields.io endpoint | No | No | Free |
| tj-actions/coverage-badge-py | SVG committed to the repo | Yes, from the repo | No | No | Free |
Choose Codecov or Coveralls when reviewers need to see which lines a pull request left uncovered. Choose a gist or committed badge when the badge is all you want and a bot commit per run is acceptable. Choose buildstats when you want the badge, the per-commit chart and the pull request delta from one number and no uploads.
Questions
Does buildstats show which lines are uncovered?
No. It stores the percentage, not the report. Use Codecov, Coveralls or your coverage tool's HTML report for line-level views, and buildstats for the badge, the history and the pull request delta.
Which coverage tools work?
Any that prints a total: Istanbul, Vitest, Jest, coverage.py, pytest-cov, go tool cover, cargo llvm-cov, tarpaulin, JaCoCo, simplecov, gcov, lcov. One jq, grep or xmllint line turns the total into a metric.
Can the badge show a branch other than the default?
Yes. Add ?ref=release to the badge or chart URL.
Can I fail a pull request that lowers coverage?
Yes. Set max-regression on the step and require the check in branch protection. The step fails when coverage drops by more than that percentage against the base branch.
Does this add a commit to my repository?
No. The badge and the chart are served from buildstats.io and update after each push; nothing is written to your repository or a gist.