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 ![badge](...) 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:

ParameterDescriptionDefault
limit-coverageThreshold0
color-under-limitColor below threshold#e32323
color-over-limitColor at/above threshold#2db21b
badge-labelBadge labelcoverage
badge-filenameFile written to wikibadge.svg
badge-valueCoverage 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:

ParameterDescriptionDefault
badge-colorBadge color#e32323
badge-labelBadge label
badge-filenameFile written to wiki
badge-valueRight-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:

![coverage](https://raw.githubusercontent.com/wiki/USERNAME/REPO/coverage-badge.svg)

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:

  1. No external dependency. No Codecov account, no Shields.io endpoint that may change. The badge lives in a repo I own.
  2. 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.
  3. Reusable. Once a project has the workflow, every push refreshes the badge. No human action required.

Where to go next

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.