# Share a PHPUnit Coverage Report — One Link From CI

Canonical: https://commareports.com/share-phpunit-report
Published: 2026-08-29

> PHPUnit's --coverage-html writes a folder that only exists in your build. Publish it to a URL: annotated source at a stable link, threads on the uncovered branch, a revision per run.

# 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

```bash
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](https://commareports.com/):

- `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

```bash
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](/docs/api).

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](/docs/api-tokens) (`reports:write`) from CI secrets and
`PATCH` a saved report id so one URL accumulates a revision per run. See
[publishing from CI](/docs/ci) and
[GitLab CI HTML reports](/ci/gitlab-ci-html-report).

## 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](/comment-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](/docs/sharing).

## 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 →](https://commareports.com/)**

### Related

- [Share a coverage report](/share-coverage-report) · [Share an lcov report](/share-lcov-report)
- [Share a JUnit report](/share-junit-report) — `--log-junit` for the CI-native view
- [Share a Go test report](/share-go-test-report) · [Share a pytest report](/share-pytest-report)
- [Publish from CI](/docs/ci)
