# Turn JUnit XML Into a Shareable HTML Report

Canonical: https://commareports.com/share-junit-report
Published: 2026-08-22

> JUnit XML is a machine format your teammates can't read, and CI's test tab is locked to CI users. Convert it to HTML, publish it to Comma with one curl, and get a stable URL with comments on failing tests.

# Turn JUnit XML into a shareable HTML report

JUnit XML won by being boring. Every runner in every language can emit it,
so every CI provider learned to parse it, and a test-results tab appeared
in all of them. That tab is where the story stops: it renders inside CI,
for people who have CI accounts, on the CI provider's retention schedule,
with nowhere to say "this one is a known flake."

The XML itself is unreadable to a human — `<testcase>` elements and a
stack trace in a CDATA block. So the working pattern is two steps:
convert it once, publish the result.

## Convert, then publish

```bash
pytest --junitxml=results.xml || true          # keep going on red
junit2html results.xml report.html             # pip install junit2html

curl -fsS -X PATCH \
  "https://commareports.com/api/v1/reports/$COMMA_REPORT_ID" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html report.html \
        --arg title "Tests — $(git rev-parse --short HEAD)" \
        '{title: $title, html: $html}')"
```

In a Node pipeline, `npx xunit-viewer --results=results.xml --output=report.html`
writes the same kind of single self-contained file. Either way the output
is one HTML document with the data inlined, which is the shape that
publishes cleanly.

Two details that matter more than the converter you pick:

- **`|| true` on the test step.** The report is most valuable when the
  build is red, so don't let a non-zero exit skip the publish. In GitHub
  Actions this is `if: always()`; in Bitbucket it's `after-script`.
- **`PATCH`, not `POST`.** One report id, one URL, a revision per run.
  Bookmarks survive, and "what changed since Friday" is a
  [diff](/share-html-report).

Use a [scoped token](/docs/api-tokens) with `reports:write` and store it
as a secret. It can be revoked without touching anything else.

## Why this beats the test tab

- **Anyone can read it.** [Sharing](/docs/sharing) is per report: private,
  team-visible, domain-gated, or named reviewers — no CI seat required.
- **Failures get threads.** Highlight the failing case, pin a comment, and
  it stays anchored across the next twelve runs. See
  [commenting on HTML](/comment-on-html).
- **History outlives retention.** Artifact windows are days; the question
  "when did this start failing" is asked in quarters.
- **One format, every runner.** The same step works for pytest, Surefire,
  Jest, Go and PHPUnit, because they all speak JUnit XML.

## Limits

- **HTML body: 5 MB**, which a converted XML report almost never
  approaches. Screenshots and traces go in as [assets](/docs/api).
- **Scripts run, sandboxed** — `allow-scripts`, no `allow-same-origin`, so
  a converter's filter UI keeps working.
- **60 requests/minute per token.** One publish per build is nowhere near.

## Try it

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

**[Create your first report →](https://commareports.com/)**

### Related

- [Publish from CI](/docs/ci) · [GitHub Actions](/ci/github-actions-html-report) · [Jenkins](/ci/jenkins-html-report)
- [pytest](/share-pytest-report) · [Jest & Vitest](/share-jest-report) · [Cucumber](/share-cucumber-report)
- [Coverage reports](/share-coverage-report) — the other half of a test run
