Share a PHPUnit coverage report

--coverage-html produces one of the better artifacts in PHP tooling: the actual source, line by line, with what the suite touched and what it didn't. It answers the question a coverage percentage never does, which is which branch is untested.

Then it sits in build/coverage/ on a CI runner and expires.

Generate it

vendor/bin/phpunit --coverage-html build/coverage

You need a coverage driver. PCOV in CI — it is much faster than Xdebug and produces the same report. Xdebug is the right tool locally, where step-through debugging is why you have it installed.

The output is a directory:

build/coverage/
├── index.html
├── Foo/Bar.php.html   # one page per source file
├── _css/  _js/  _icons/
└── dashboard.html

That's the detail that breaks the naive share: publish index.html alone and you get an unstyled index whose every drill-down link is dead.

Drop the folder in

Drag build/coverage/ (or a zip of it) into Comma:

  • index.html becomes the report body — the page carrying the comment layer.
  • The per-file pages, CSS, JS and icons upload alongside it, and relative references are rewritten to the uploaded copies, so the drill-down from summary to namespace to annotated source resolves.
  • Scripts run inside a sandboxed iframe (allow-scripts, no allow-same-origin), so the dashboard charts and the sortable tables keep working.

From CI

vendor/bin/phpunit --coverage-html build/coverage || true

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 build/coverage/index.html \
        --arg title "Coverage — $(git rev-parse --short HEAD)" \
        '{title: $title, html: $html}')"

The API is JSON-only, so the per-file pages go up through POST /api/v1/reports/$REPORT_ID/assets as base64 — one call per file, worth scripting once. See the API reference.

The || true and an if: always() (or after_script in GitLab) keep the publish alive when tests fail, which is when the report matters most. Use a scoped token (reports:write) from CI secrets and PATCH a saved report id so one URL accumulates a revision per run. See publishing from CI and GitLab CI HTML reports.

What the URL changes

  • The PR links the coverage. Reviewers click instead of rebuilding.
  • Feedback lands on the line. Select the uncovered branch, leave a thread — "this is the failure path in the payment handler" — and it stays anchored across runs. See commenting on HTML.
  • Revisions diff. Which lines changed coverage, rather than a percentage drifting.

Who can see it

The coverage report contains your source code. Access is per report — private, team, anyone signed in at your domain, or anyone with the link — and team or private is the right default here (domain-gating is Enterprise). See sharing & access control.

Limits

  • Entry HTML: 5 MB. Assets: 25 MB per file, 250 MB and 500 files total. A large application can exceed 500 source pages — publish the namespaces under review rather than the whole tree, or point the report at the changed paths.
  • Scripts run, sandboxed — no same-origin access.
  • 60 requests/minute per token.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history.

Publish a PHPUnit report →

Related