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:
|| trueon 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 isif: always(); in Bitbucket it'safter-script.PATCH, notPOST. 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, sandboxed —
allow-scripts, noallow-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.
Related
- Publish from CI · GitHub Actions · Jenkins
- pytest · Jest & Vitest · Cucumber
- Coverage reports — the other half of a test run