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.html becomes 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, no allow-same-origin).
  • 60 requests/minute per token.

Try it

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

Publish a Gradle report →

Related