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.

Publish a coverage report →

Related