# Share a Gradle Test Report — One Link Instead of build/reports

Canonical: https://commareports.com/share-gradle-test-report
Published: 2026-08-29

> Gradle writes an HTML test report to build/reports/tests/test that only exists on the machine that ran the build. Publish it from CI: one URL per module, threads on failing tests, a revision per run.

# 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](https://commareports.com/):

- `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

```bash
./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](/docs/api).

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](/docs/ci).

Use a [scoped token](/docs/api-tokens) (`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:

```kotlin
plugins { id("test-report-aggregation") }
```

```bash
./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](/comment-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 →](https://commareports.com/)**

### Related

- [Share a JUnit report](/share-junit-report) · [Share a TestNG report](/share-testng-report)
- [Share a JaCoCo coverage report](/share-jacoco-report)
- [Share an Allure report](/share-allure-report) · [Share Javadoc](/share-javadoc)
- [Publish from CI](/docs/ci) · [Jenkins HTML reports](/ci/jenkins-html-report)
