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

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.

Use a scoped token 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 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.
  • 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.
  • Scripts run, sandboxedallow-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 →

Related