Publish an HTML report from GitHub Actions

GitHub Actions will run anything that produces HTML — coverage, Lighthouse, Playwright, benchmarks, eval scoreboards, a generated data digest — and then give you exactly two ways to look at it, neither of which is looking at it.

actions/upload-artifact stores a zip. A reviewer opens the run page, scrolls to Artifacts, downloads, unzips, and opens index.html from ~/Downloads — where the relative asset paths sometimes work and sometimes don't. $GITHUB_STEP_SUMMARY renders markdown inline, which is genuinely useful, but it strips scripts and iframes, ignores most styling, and caps out at 1 MiB. Neither one produces a link you can paste into Slack that opens a report.

So teams screenshot the artifact back into the PR thread, and the actual report — the thing with all the detail in it — never gets read.

The one-step fix

Store a scoped token (reports:write is enough) as a repository secret, then add one step:

- name: Publish report to Comma
  if: always()
  env:
    COMMA_API_TOKEN: ${{ secrets.COMMA_API_TOKEN }}
    REPORT_ID: ${{ vars.COMMA_COVERAGE_REPORT_ID }}
  run: |
    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 coverage/index.html \
            --arg title "Coverage — ${GITHUB_SHA:0:7}" \
            '{title: $title, html: $html}')"

Three details in there earn their keep:

  • PATCH, not POST. Create the report once by hand, put its id in a repository variable, and every run appends a revision at the same URL. One bookmark per suite, forever — and any two runs can be diffed.
  • if: always(). The runs worth sharing are the red ones. Without this, the publish step is skipped exactly when the report matters.
  • A commit-stamped title. Revisions titled Coverage — a1b2c3d make the history read like a log instead of a pile of timestamps.

Posting the link back to the PR

The report URL is stable, so a PR comment is one more step — no marketplace action required:

- name: Comment the report link
  if: github.event_name == 'pull_request'
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    gh pr comment "${{ github.event.number }}" \
      --body "📊 [Coverage report](https://commareports.com/p/$REPORT_ID) — updated for ${GITHUB_SHA:0:7}"

Reviewers click once and land on the rendered report with a comment layer on it, instead of a zip.

What you get that an artifact can't give you

  • Comments anchored to the report. A reviewer highlights the row for the module that lost 8% coverage and pins a thread to it. The thread survives the next twelve runs — see commenting on HTML.
  • Revision history with diffs. "What changed since the last green build?" becomes a diff instead of a memory exercise. Artifacts have no concept of a previous version.
  • Access without repo access. Artifacts require a GitHub seat on the repository. A Comma report can be team-visible, domain-gated, or shared with a named reviewer who will never have a GitHub login — see the sharing model.
  • No expiry. Revisions stay. The 90-day artifact clock doesn't apply.
  • Announcements for free. A webhook on revision.created posts each new run to Slack or Discord, so the Slack message goes back to being a notification and the report stays the artifact.

Limits worth knowing before you wire it up

  • HTML body: 5 MB. Screenshots, trace zips and JS bundles go in as assets instead — 25 MB per file, 250 MB per report.
  • Scripts are stripped on write. Comma sanitizes incoming HTML, so a report that is a JavaScript application (Playwright's default reporter, Allure) should be published as a static digest with the full archive attached as an asset. Details in sharing a Playwright report.
  • Rate limit: 60 requests/minute per token. One publish per build is not close.
  • Keep the token scoped. reports:write only. It can't read your other reports, and it's revocable from API tokens without touching CI.

When GitHub's own tools are the right answer

Be honest about it: if the artifact is a build output nobody reads, leave it in upload-artifact. If your report is genuinely a multi-page site with routing, GitHub Pages is the correct tool. Comma's unit is the report — one document, rendered faithfully, with a review layer on top.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history. Take the workflow that currently uploads a zip, add the step above, and send the link instead.

Create your first report →

Related