Share a Gradle test report
./gradlew test writes a perfectly good HTML report to
build/reports/tests/test/index.html. It has the failing class, the
stack trace, the standard output, the timing. And it exists on exactly one
machine: the one that ran the build.
In CI it becomes an artifact zip. Someone has to log into the CI server, find the run, download it, unzip it, and open a local file. Nobody does. They paste the stack trace into Slack instead.
It's a folder, not a file
That's the detail that breaks the naive fix:
build/reports/tests/test/
├── index.html
├── classes/ # one page per test class
├── packages/
├── css/
└── js/
Publish index.html on its own and you get an unstyled index with every
drill-down link dead. Upload the directory.
Drag it in
Drop build/reports/tests/test/ (or a zip of it) into
Comma:
index.htmlbecomes the report body — the page carrying the comment layer.- The per-class pages, CSS and JS upload alongside it, and relative references are rewritten to the uploaded copies, so the drill-down from summary to failing class to stack trace works.
From CI, so every run has an address
./gradlew test || 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/reports/tests/test/index.html \
--arg title "Tests — $(git rev-parse --short HEAD)" \
'{title: $title, html: $html}')"
The API is JSON-only, so the supporting pages go up through
POST /api/v1/reports/$REPORT_ID/assets as base64 — one call per file,
worth scripting once. See the API reference.
Note the || true: a failing test task fails the build, and the publish
step has to survive that. In GitHub Actions use if: always(); in Jenkins,
post { always { … } }. See publishing from CI.
Use a scoped token (reports:write) from CI secrets,
and PATCH a saved report id so one URL accumulates a revision per run
rather than spraying orphan links.
Multi-module builds
One report per subproject is not a thing anyone reads. Aggregate first:
plugins { id("test-report-aggregation") }
./gradlew testAggregateTestReport
# → build/reports/tests/unit-test/aggregated-results/
Publish the aggregate and the team gets one link for the whole build.
What changes once it has a URL
- The PR links the run. Reviewers click instead of reconstructing.
- Failures get discussed on the failure. Select the test, leave a thread, and it stays anchored across runs — so a known-flaky test carries its own history. See commenting on HTML.
- Revisions diff. Two runs, one diff, and you can see which tests changed state instead of eyeballing two summaries.
Limits
- Entry HTML: 5 MB. Assets: 25 MB per file, 250 MB and 500 files total. A very large suite can exceed 500 per-class pages — publish the aggregate index and the failing classes rather than all of them.
- Scripts run, sandboxed (
allow-scripts, noallow-same-origin). - 60 requests/minute per token.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.