Share a Cobertura coverage report
Cobertura XML has become the lingua franca of coverage — coverage.py, gcovr, coverlet, JaCoCo's converter and simplecov all emit it — because CI systems know how to read it.
CI systems. Not people.
<class name="orders.service" filename="src/orders/service.py"
line-rate="0.784" branch-rate="0.61">
<lines><line number="42" hits="0" branch="false"/>…
So what gets shared is the one number the CI plugin surfaces: 78.4%. And a coverage percentage is the least actionable metric in engineering. It goes into a status update, it drifts, and nobody ever opened a file because of it.
The useful artifact is the drill-down: which lines, in which file, are not covered. That is a work queue somebody can pick up.
Render it
# Python-friendly, single file, fast
pip install pycobertura
pycobertura show --format html coverage.xml -o report.html
# richer, multi-page, .NET-native but format-agnostic
reportgenerator -reports:coverage.xml -targetdir:report -reporttypes:Html
pycobertura gives you one self-contained page. ReportGenerator gives you a
folder with per-file source views and the uncovered lines highlighted inline —
better for reading, and it needs the folder publish path rather than the single
file.
Publish it
Drag report.html (or ReportGenerator's report/ folder) into
the app, or POST it:
curl -fsS -X POST "https://commareports.com/api/v1/reports" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html report.html \
--arg t "Coverage — orders service" '{title: $t, html: $html}')"
For the folder version, index.html becomes the report body and the per-file
pages upload alongside it with their relative links rewritten to the uploaded
copies — so clicking through from the summary into a specific file works, which
is the whole reason to use ReportGenerator.
Publish the diff, not the total
For a pull request, the project-wide figure is noise. What a reviewer needs is whether this change is tested:
pycobertura diff base-coverage.xml head-coverage.xml \
--format html --output diff.html
That renders only the files whose coverage moved, with the newly-uncovered lines called out. It is a much shorter report, and it is the one that changes a review decision.
From CI
coverage run -m pytest && coverage xml # → coverage.xml
pycobertura show --format html coverage.xml -o report.html
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html report.html \
--arg t "Coverage — orders @ $GITHUB_SHA" '{title: $t, html: $html}')"
PATCHing one report id keeps a single URL per service and appends a revision per run. Coverage that drops 4% over two months is invisible in per-build artifacts that expire in 30 days and obvious in a revision list.
What review adds
- Anchored threads on an uncovered branch, so "unreachable, guarded upstream" is argued once and recorded — see commenting on HTML.
- Revisions, so the trend is evidence rather than anecdote.
- Access per report — a coverage report exposes your source structure and often source itself. See the sharing model.
Limits
- Entry HTML: 5 MB. Assets: 25 MB per file, 250 MB and 500 files total. A ReportGenerator run over a large codebase generates a page per source file — the 500-file cap is the one to watch; scope the report to the module under review.
- 60 requests/minute per token.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.