Every README I write ends up sprouting badges. Build status, lint status, version, coverage. Most of those are easy because they come for free from a service — Shields.io, Codecov, the GitHub Actions badge endpoint. The annoying one is always coverage, because it requires either a third-party service or a separate pipeline that publishes the number somewhere. So I built gh-action-badge to host the badge SVGs in the one place I always have available: the repo’s own wiki.
The trick: wiki as a badge host
Every GitHub repository has a wiki. The wiki is a separate git repo, and its files are accessible at a predictable URL:
https://raw.githubusercontent.com/wiki/USERNAME/REPOSITORY/<file>That’s all you need to host an SVG and reference it from your main README.md. No external service, no token to rotate, no rate limits. The action generates a badge SVG, commits it to the wiki, and your README’s  tag picks up the new image.
The only setup: the wiki needs to be initialized, which means creating one (empty) page through the GitHub UI. After that, the action takes over.
Two badge flavors
Coverage badge
Pass a coverage value, get a badge whose color depends on whether you’re above or below your threshold:
| Parameter | Description | Default |
|---|---|---|
limit-coverage | Threshold | 0 |
color-under-limit | Color below threshold | #e32323 |
color-over-limit | Color at/above threshold | #2db21b |
badge-label | Badge label | coverage |
badge-filename | File written to wiki | badge.svg |
badge-value | Coverage value (e.g. 87%) |
You feed badge-value from whatever produced your coverage number — go tool cover, pytest --cov, nyc, whatever — and the action handles the rest.
Fixed-color badge
For everything else: status flags, build labels, “made with love” badges:
| Parameter | Description | Default |
|---|---|---|
badge-color | Badge color | #e32323 |
badge-label | Badge label | |
badge-filename | File written to wiki | |
badge-value | Right-side value |
Wiring it into a workflow
The repo includes a runnable coverage example and a fixed-color example. The shape looks like this:
name: coverage-badge
on:
push:
branches: [main]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... run your tests, compute COVERAGE_VALUE somehow ...
- name: generate coverage badge
uses: sgaunet/gh-action-badge@v1
with:
badge-label: coverage
badge-value: ${{ env.COVERAGE_VALUE }}
badge-filename: coverage-badge.svg
limit-coverage: 80
color-under-limit: "#e32323"
color-over-limit: "#2db21b"Then in your README.md:
That’s it. The badge updates each time the workflow runs.
The one footgun
GitHub’s raw content URLs are cached for around 300 seconds. After a workflow updates the SVG, the change can take a few minutes to appear in your README. Don’t panic the first time — refresh in five minutes.
Why I keep using it
Three reasons:
- No external dependency. No Codecov account, no Shields.io endpoint that may change. The badge lives in a repo I own.
- Threshold-aware coverage colors. The whole point of the coverage badge is to flag regressions visually. The under/over-limit colors do that for free.
- Reusable. Once a project has the workflow, every push refreshes the badge. No human action required.
Where to go next
- Source: github.com/sgaunet/gh-action-badge
- Coverage example workflow:
.github/workflows/gh-action-coverage-test.yml - Fixed badge example workflow:
.github/workflows/gh-action-badge-test.yml
Open source, MIT-licensed. If you’re tired of fighting third-party badge services for a number that already lives in your CI, this might be the dumbest, most useful piece of plumbing you add this week.