Coverage checks for GitHub Actions — no external services, no secrets, no accounts. Add one step to your workflow, set a threshold, and get pass/fail results with annotations and a job summary.
Coverlint parses coverage reports in all major formats, enforces configurable thresholds, and runs entirely within your GitHub Actions runner. Supports Linux, macOS, and Windows.
- LCOV (
lcov) —cargo llvm-cov,c8,istanbul,jest,vitest - Go cover profile (
gocover) —go test -coverprofile - Cobertura XML (
cobertura) —pytest-cov,istanbul,cargo tarpaulin - Clover XML (
clover) —phpunit, some JS tools - JaCoCo XML (
jacoco) — Gradle/Maven JaCoCo plugin
Add coverlint after your test step. Without any inputs, it auto-detects the format, finds your report, and shows coverage without enforcing a threshold — handy for tracking trends before you commit to a minimum:
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5To enforce a minimum, set min-coverage — a combined score across line, branch, and function coverage (see Custom Weights for how the score is computed):
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
min-coverage: 80Set format explicitly for faster runs and to avoid ambiguity when files share names (e.g. coverage.xml could be Cobertura or Clover):
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: lcov
min-coverage: 80Go
- run: go test -coverprofile=cover.out ./...
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: gocover
min-coverage: 80Rust
- run: cargo llvm-cov --lcov --output-path lcov.info
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: lcov
min-coverage: 80TypeScript / JavaScript
- run: npx vitest run --coverage --coverage.reporter=lcov
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: lcov
min-coverage: 80Python
- run: pytest --cov --cov-report=xml:coverage.xml
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: cobertura
min-coverage: 80PHP
- run: vendor/bin/phpunit --coverage-clover=coverage.xml
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: clover
min-coverage: 80Java (Gradle)
- run: ./gradlew test jacocoTestReport
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: jacoco
min-coverage: 80When you set min-coverage, coverlint computes a weighted score from line (50), branch (30), and function (20) coverage. If your format doesn't report a metric — like branch and function in gocover — its weight redistributes to the rest.
Weights are relative — adjust them to match what matters to your project:
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: lcov
min-coverage: 80
weight-line: 100 # only line coverage counts toward the score
weight-branch: 0
weight-function: 0Set min-line, min-branch, or min-function to require a minimum for a single metric, regardless of the overall score. Combine with min-coverage to enforce both:
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: lcov
min-coverage: 80
min-branch: 60 # fails if branch drops below 60%, even if the overall score passesNote
If you set a floor that your format doesn't support (e.g. min-branch with gocover), it's skipped with a warning annotation.
Use separate steps when parts of your project need different bars:
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: gocover
path: cover.out
min-coverage: 80
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: lcov
path: coverage/lcov.info
min-coverage: 90Combine coverage from multiple languages in one step — you'll get a job summary that breaks down each format with a combined total. Use YAML block scalars (|) to pass multiple values:
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
with:
format: |
gocover
lcov
cobertura
path: |
go-service/cover.out
node-service/coverage/lcov.info
python-service/coverage.xml
min-coverage: 80You don't need to specify format or path — coverlint can figure both out. It tries each parser until one succeeds, and looks for reports in common locations:
| Format | Searched Paths |
|---|---|
lcov |
coverage/lcov.info, lcov.info, coverage.lcov |
gocover |
cover.out, coverage.out, c.out |
cobertura |
coverage.xml, cobertura.xml, cobertura-coverage.xml |
clover |
coverage.xml, clover.xml |
jacoco |
build/reports/jacoco/test/jacocoTestReport.xml, target/site/jacoco/jacoco.xml, jacoco.xml |
Catch coverage regressions before they land. Pass a previous run's baseline as JSON and set min-delta to control how far the score can drop — 0 fails on any decrease, -2 allows up to a 2-point drop. Skip baseline entirely if you don't need delta comparison yet.
Each run emits its own baseline output as JSON — store it and feed it back next time. The workflow below keeps the baseline on an orphan branch, loading it before each run and updating it after merges to main:
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
baseline: ${{ steps.coverage.outputs.baseline }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# ... your test steps ...
- name: Load previous baseline
id: load-baseline
env:
REPO: ${{ github.repository }}
run: |
baseline=$(curl -fsL "https://raw.githubusercontent.com/${REPO}/coverlint/coverage-baseline.json" 2>/dev/null || true)
echo "baseline=${baseline}" >> "$GITHUB_OUTPUT"
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
id: coverage
with:
format: gocover
min-coverage: 80
baseline: ${{ steps.load-baseline.outputs.baseline }}
min-delta: -2
update-baseline:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
concurrency:
group: coverlint-update
cancel-in-progress: false
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Push baseline
env:
BASELINE: ${{ needs.test.outputs.baseline }}
run: |
tmpdir=$(mktemp -d)
printf '%s' "$BASELINE" > "$tmpdir/coverage-baseline.json"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git ls-remote --exit-code origin coverlint &>/dev/null; then
git fetch origin coverlint
git checkout coverlint
else
git checkout --orphan coverlint
git rm -rf . 2>/dev/null || true
fi
cp "$tmpdir/coverage-baseline.json" .
git add coverage-baseline.json
git diff --cached --quiet && exit 0
git commit -m "Update coverage baseline"
git push origin coverlintSee uncovered lines and blocks right in GitHub's Code Scanning tab. Enable sarif: true and upload the SARIF output alongside your test results:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
sarif: ${{ steps.coverage.outputs.sarif }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# ... your test steps ...
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
id: coverage
with:
format: lcov
sarif: true
upload-sarif:
needs: test
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Write SARIF file
env:
SARIF: ${{ needs.test.outputs.sarif }}
run: printf '%s' "$SARIF" > coverage.sarif
- uses: github/codeql-action/upload-sarif@820e3160e279568db735cee8ed8f8e77a6da7818 # v3
with:
sarif_file: coverage.sarifResults are capped at 500 by default. For large codebases, lower the cap to stay within GitHub's 1 MB action output limit — pass a number instead of true (e.g. sarif: 200).
Give reviewers coverage context without leaving the PR. Use the results output to post a summary comment:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
results: ${{ steps.coverage.outputs.results }}
passed: ${{ steps.coverage.outputs.passed }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# ... your test steps ...
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
id: coverage
with:
format: gocover
min-coverage: 80
comment:
needs: test
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Comment on PR
env:
GH_TOKEN: ${{ github.token }}
RESULTS: ${{ needs.test.outputs.results }}
PASSED: ${{ needs.test.outputs.passed }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
score=$(echo "$RESULTS" | jq -r '.[-1].score // empty') || exit 0
status="Pass"
if [[ "$PASSED" != "true" ]]; then status="**Fail**"; fi
body="<!-- coverlint-coverage -->**Coverage:** ${score}% — ${status}"
gh pr comment "$PR_NUMBER" --edit-last --body "$body" 2>/dev/null || \
gh pr comment "$PR_NUMBER" --body "$body"Show live coverage in your README — no external services or secrets needed:
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
badge-svg: ${{ steps.coverage.outputs.badge-svg }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# ... your test steps ...
- uses: evansims/coverlint@aa15ba0901ffaad70113c3ab5a54bf2e676614f8 # v1.2.5
id: coverage
with:
format: gocover
min-coverage: 80
update-badges:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
concurrency:
group: coverlint-update
cancel-in-progress: false
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Push coverage badge
env:
BADGE_SVG: ${{ needs.test.outputs.badge-svg }}
run: |
tmpdir=$(mktemp -d)
printf '%s' "$BADGE_SVG" > "$tmpdir/coverage.svg"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git ls-remote --exit-code origin coverlint &>/dev/null; then
git fetch origin coverlint
git checkout coverlint
else
git checkout --orphan coverlint
git rm -rf . 2>/dev/null || true
fi
cp "$tmpdir/coverage.svg" .
git add coverage.svg
git diff --cached --quiet && exit 0
git commit -m "Update coverage badge"
git push origin coverlintAdd to your README:
Prefer shields.io styling? Use badge-json instead:
| Input | Description |
|---|---|
format |
Coverage format(s), one per line or comma-separated. Auto-detected if omitted |
path |
Path(s) to coverage files, one per line or comma-separated. Supports globs. Auto-discovered if omitted |
min-coverage |
Minimum weighted coverage score (0-100), computed from line, branch, and function coverage |
min-line |
Minimum line coverage (0-100), checked independently of the weighted score |
min-branch |
Minimum branch coverage (0-100), checked independently |
min-function |
Minimum function coverage (0-100), checked independently |
weight-line |
Relative weight for line coverage in score (default: 50) |
weight-branch |
Relative weight for branch coverage in score (default: 30) |
weight-function |
Relative weight for function coverage in score (default: 20) |
working-directory |
Working directory for resolving relative paths (default: .) |
fail-on-error |
Fail the action when thresholds are not met (default: true) |
suggestions |
Show top coverage improvement opportunities in job summary (default: true) |
annotations |
Annotation output: true (default), false, or a max count |
baseline |
JSON string of previous baseline data for delta comparison |
min-delta |
Minimum allowed score change (e.g. 0 = no regression, -2 = max 2pt drop). Ignored without baseline |
sarif |
SARIF output: true (default max 500 results), false, or a number (default: false) |
| Output | Description |
|---|---|
passed |
Whether all thresholds were met (true or false) |
results |
Coverage data as JSON |
badge-svg |
Ready-to-use SVG coverage badge |
badge-json |
Coverage badge as shields.io endpoint JSON |
baseline |
Current run's baseline as JSON — store and feed back as the baseline input next run |
sarif |
SARIF JSON for uploading to GitHub Code Scanning |
Example results output
The results JSON has one entry per format, each with a weighted score and available metrics. Multi-format runs include a Total:
[
{ "name": "gocover", "score": 85, "line": 85, "passed": true },
{
"name": "lcov",
"score": 77,
"line": 78.3,
"branch": 65.2,
"function": 90.1,
"passed": true
},
{
"name": "Total",
"score": 79,
"line": 81.1,
"branch": 65.2,
"function": 90.1,
"passed": true
}
]Use GitHub Actions' fromJSON() expression to read values in later steps:
- env:
LINE: ${{ fromJSON(steps.coverage.outputs.results)[0].line }}
run: echo "Line coverage is ${LINE}%"| Code | Meaning |
|---|---|
| 0 | All checks passed |
| 1 | Coverage below threshold |
| 2 | Configuration, parse, or unexpected error |
This distinction helps you tell apart "coverage is too low" from "something is broken." If you see exit 1, your tests ran fine but coverage fell short. Exit 2 usually means the action step itself needs fixing.
Pin actions by commit SHA in production workflows and use Dependabot to keep them current. All releases use immutable tags, and the binary is checksum-verified on every download.
Clone and run the tests — standard Go tooling, nothing extra needed:
git clone https://github.com/evansims/coverlint.git && cd coverlint
go test -race -cover ./...
go vet ./...Dual-licensed under Apache 2.0 and MIT. Choose whichever you prefer.