A single Go binary (coverage) that aggregates Cobertura coverage XML and
JUnit test-result XML from every project in a repo into one Markdown
summary for the CI run — with optional regression detection against a
baseline.
It is language-agnostic: any toolchain that emits Cobertura + JUnit works
(Go, TypeScript/JavaScript, Rust, Python, Java, C#), in any mix. Each project
produces two files: coverage-<id>.xml and tests-<id>.xml.
## Test Coverage
### Summary
| Workspace | Tests | Lines | % | Branches | % |
|---|---|---|---|---|---|
| thingy | 412 | 1842 / 2310 | 79.7% | 215/310 | 69.4% |
| thinger | 175 | 3120 / 3680 | 84.8% | 480/620 | 77.4% |
| shared/widget | — | 320 / 400 | 80.0% | — | — |
| **Total** | **587** | 5282 / 6390 | **82.7%** | 695/930 | **74.7%** |- Tests renders
—when no JUnit artifact was uploaded (distinct from0). - Branches renders
—when a workspace reports no branch data. - Totals are recomputed from the leaf
<line>elements — the tool ignores the unreliable top-level totals emitters put on<coverage>.
| Language | Coverage → Cobertura | Tests → JUnit | Guide |
|---|---|---|---|
| Go | gocover-cobertura |
gotestsum |
GO.md |
| TypeScript / JS | Jest · Vitest · nyc | jest-junit · vitest · mocha-junit-reporter | TYPESCRIPT.md |
| Rust | cargo-llvm-cov --cobertura |
cargo-nextest · cargo2junit |
RUST.md |
| Python | pytest --cov ... --cov-report=xml |
pytest --junitxml |
PYTHON.md |
| Java | JaCoCo (→ Cobertura) | Surefire / Gradle | JAVA.md |
| C# / .NET | coverlet (cobertura) | JunitXml.TestLogger | CSHARP.md |
Any other tool that produces Cobertura + JUnit works too — see docs/.
New to it? coverage init detects your languages and scaffolds the workflow
wiring + config non-destructively — see docs/INIT.md. Setting
this up with an AI assistant? Point it at llms.txt.
go install github.com/aanantaco/coverage/cmd/coverage@latestOr use the composite GitHub Action, pinned to a commit SHA (recommended):
# Pin to a reviewed commit SHA (latest main shown — check for a newer one).
- uses: aanantaco/coverage@942b0be7af719b81fb5033591c80e065b0c9179eWhen pinned by a full commit SHA (or a version tag like @v0.1.0), the Action
downloads the prebuilt binary — no Go toolchain on your runner, which matters
for non-Go repos. With any other ref (a branch or moving tag) it falls back to
building from the Action's own source. Either way the pin selects the exact tool
version. (go install …@<sha> works the same way for the CLI.)
There are no version tags. Every merge to main runs the release job in
.github/workflows/ci.yml, which cross-compiles
coverage (linux/macOS/Windows × amd64/arm64) with GoReleaser and publishes the
archives + checksums.txt as a per-commit prerelease tagged sha-<shortsha>
(and, for the run itself, a coverage-binaries-<sha> workflow artifact).
Archives are versioned by the commit SHA
(coverage_0.0.0-<shortsha>_<os>_<arch>), e.g.:
SHA=<short-commit-sha> # 7 chars, e.g. 9d36b21
curl -fsSL -O "https://github.com/aanantaco/coverage/releases/download/sha-${SHA}/coverage_0.0.0-${SHA}_linux_amd64.tar.gz"
tar -xzf coverage_0.0.0-${SHA}_linux_amd64.tar.gz
./coverage versionThe prereleases are what the composite Action downloads, so non-Go projects need no Go toolchain. (They're marked prerelease, so they don't clutter the "Latest release" slot.)
coverage --input ./coverage-artifacts --output "$GITHUB_STEP_SUMMARY"| Flag | Default | Meaning |
|---|---|---|
--input |
(required) | directory containing coverage-*.xml and tests-*.xml |
--output |
- |
output path; - is stdout. A file is appended. |
--ignore |
(auto) | path to a .coverageignore (gitignore syntax). Defaults to ./.coverageignore if present. |
--config |
(auto) | path to coverage.yaml. Defaults to ./coverage.yaml if present. |
--baseline |
— | baseline coverage-summary.json to diff against. |
--fail-on-drop |
— | exit non-zero if total line coverage drops by more than this many percentage points. |
--emit-json |
— | also write a machine-readable coverage-summary.json to this path. |
--format |
(auto) | output format: markdown or html. Auto-detects html from an .html/.htm --output. |
--verbose |
false |
log warnings for workspaces missing a config entry. |
CLI flags override coverage.yaml, which overrides built-in defaults.
coverage version prints the build version and the commit SHA it was built
from — handy for confirming which SHA-pinned build you're running.
- Markdown (default) — for
$GITHUB_STEP_SUMMARY. Written in append mode. - HTML — a self-contained, theme-aware page (
--format html, or an.html/.htmoutput path). Written in truncate mode.
Both formats are rendered from templates in
internal/render/templates/ (report.md.tmpl,
report.html.tmpl).
| Thing | Convention | Example |
|---|---|---|
| Coverage artifact | coverage-<id>.xml (Cobertura) |
coverage-thingy.xml |
| Test-count artifact | tests-<id>.xml (JUnit) |
tests-thingy.xml |
| Workspace id | the <id> in the filenames; may contain dashes |
shared-widget |
| Input dir | all coverage-*.xml + tests-*.xml flattened together |
./coverage-artifacts |
Three moving parts:
- Each project's test job emits two files —
coverage-<id>.xml(Cobertura) andtests-<id>.xml(JUnit) — and uploads them as artifacts. The test command is language-specific; see the per-language guides. - One report job downloads every
coverage-*/tests-*artifact into a single directory and runs the Action once. - Pin the Action by commit SHA. It then downloads a prebuilt binary for that commit — no Go toolchain on your runner — falling back to build-from-source for a loose ref (branch/tag).
name: Coverage
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
# One test job per project. Swap the test step for your language's command
# (see docs/) so it produces coverage-<id>.xml + tests-<id>.xml.
test-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# e.g. Vitest — other frameworks in docs/TYPESCRIPT.md:
- run: |
npx vitest run --coverage \
--coverage.reporter=cobertura \
--reporter=junit --outputFile.junit=tests-web.xml
cp coverage/cobertura-coverage.xml coverage-web.xml
- uses: actions/upload-artifact@v7
if: always()
with: { name: coverage-web, path: coverage-web.xml }
- uses: actions/upload-artifact@v7
if: always()
with: { name: tests-web, path: tests-web.xml }
# One report job aggregates everything and writes the run's Summary tab.
report:
needs: [test-web]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with: { pattern: coverage-*, path: ./cov, merge-multiple: true }
- uses: actions/download-artifact@v8
with: { pattern: tests-*, path: ./cov, merge-multiple: true }
- uses: aanantaco/coverage@942b0be7af719b81fb5033591c80e065b0c9179e # pin to a reviewed SHA
with:
input: ./cov
output: $GITHUB_STEP_SUMMARY
# ignore: .coverageignore # optional
# baseline: baseline.json # optional, for Δ columns
# fail-on-drop: "0.5" # optional, fail on >0.5pp total dropDon't want to hand-write it? coverage init scaffolds this whole structure
for your detected languages — see docs/INIT.md. The Action's
inputs mirror the CLI flags; the full annotated workflow is
examples/coverage.yml.
.coverageignore— gitignore syntax, matched against repo-root-relative paths, for excluding generated code, test files, vendored deps, etc. Start from.coverageignore.example.coverage.yaml— optional and zero-config by default. Sets display names, bridges coverage paths to ignore patterns (prefix/strip_prefix), folder depth, and the regression baseline. Annotated schema:coverage.yaml.example. A present-but-malformed file is a hard error.
prefix/strip_prefix bridge emitter paths to a repo-root ignore file: the tool
computes rel = strip_prefix removed from filename (used for folder grouping)
and full = prefix + rel (matched against .coverageignore). Go module import
paths are the usual reason to set strip_prefix — see docs/GO.md.
Emit a baseline with --emit-json coverage-summary.json, then pass it back on a
later run with --baseline to get Δ columns, a "coverage decreased" callout, and
new/removed markers; add --fail-on-drop 0.5 to fail on a total drop. The full
recipe (default-branch cache, artifact, or committed baseline) is in
docs/REGRESSION.md.
Full docs — per-language guides, the regression guide, and references — live in
docs/.
go build ./...
go test ./...A single runtime dependency: github.com/goccy/go-yaml
(config parsing) — actively maintained and dependency-free. Everything else —
Cobertura/JUnit parsing, .coverageignore gitignore matching, folder grouping,
delta computation, baseline JSON — is implemented in-repo on the standard
library. Tests use the standard library only. The .coverageignore matcher is a
dependency-free port of github.com/sabhiram/go-gitignore
(MIT) — see THIRD_PARTY_NOTICES.md.
MIT — see LICENSE.