Renders a coverage report into a GitHub Actions job summary, and optionally uploads the report as an artifact.
Reads Cobertura XML (coverage.py, pytest-cov) or istanbul
coverage-summary.json (jest, vitest), plus optional JUnit XML or jest/vitest
JSON test results for a pass/fail table.
This lives in a public repo for one reason: a public repository cannot resolve an action from a private one. Runpod consumes this from both public and private repos, so it has to be here.
- name: Coverage summary
if: always()
uses: runpod/coverage-summary-action@<full-commit-sha>
with:
format: cobertura
coverage-file: coverage.xml
results: pytest-results.xml
artifact-name: coveragePin a full commit SHA, not a tag. A tag can be repointed at new code by whoever owns it; a SHA cannot. It also means a change here cannot reach your CI until someone bumps the pin deliberately.
| Input | Required | Default | Description |
|---|---|---|---|
format |
yes | — | cobertura or json-summary |
coverage-file |
yes | — | Coverage report path, relative to working-directory |
results |
no | — | Test results for the pass/fail table. Cobertura takes a space-separated list of JUnit XML globs; json-summary takes a jest/vitest JSON file |
artifact-name |
no | — | Artifact name for the uploaded report. Empty skips the upload entirely |
artifact-path |
no | coverage-file |
What to upload, when it differs from the file being summarised |
overwrite |
no | true |
Replace an existing artifact of the same name rather than failing |
if-no-files-found |
no | warn |
warn or error |
working-directory |
no | . |
Directory the paths above are relative to |
title |
no | Coverage Report |
Heading for the summary section |
max-rows |
no | 300 |
Cap on per-file rows, to stay under GitHub's 1 MiB summary limit |
The script follows three rules, and the test suite exists to hold it to them:
- Never raise. It is meant to run under
if: always(), so throwing here would stack a second, misleading failure on top of whatever actually went wrong. Every parse path degrades to a message instead. - Never claim a pass without evidence. A suite that dies at import reports
zero failures. Status is computed from positive evidence — something ran,
nothing failed, every requested report was found and parsed — so a crashed
run renders as
⚠️ Unknown, not✅ Passed. - Say when a report was unreadable. A missing file and a corrupt file are reported differently, because they mean different things.
Line coverage goes in the job summary headline; statement, function and branch figures go in the body table. A metric with a zero denominator is omitted rather than shown as 0%.
- lcov totals are unioned, not summed. Adding per-file
LF/LHrecords across sharded reports double-counts the denominator — a sharded suite that is genuinely at 100% reads as 50%. Line hits are merged by(file, line)first. - Secondary metrics are only reported from a single artifact. Function and branch counts cannot be unioned from summary records, so they are skipped rather than approximated when more than one report is present.
The script is stdlib-only, so there is nothing to install:
python3 -m unittest discover -p 'test_*.py' -v59 unit tests, plus a smoke job that drives the action end to end — including a truncated report, a missing report, an unreadable results file, and a consumer whose paths sit in a subdirectory.
The script is a real file rather than an inline heredoc specifically so it can be linted, reviewed and tested. Four crash paths survived the heredoc era because none of that was possible.
MIT