GitHub Actions won't show your HTML report

The workflow runs. actions/upload-artifact succeeds. The run page shows playwright-report in the Artifacts box. You click it and get playwright-report.zip in your Downloads folder.

That is the whole feature. There is no viewer.

Why it is a zip, always

The artifacts endpoint responds with Content-Type: application/zip no matter what the archive contains. A single index.html is still a zip of one file. This is not an oversight and there is no flag that changes it:

Artifacts are written by workflow code, and workflow code is attacker-influenceable on any repository that accepts pull requests. If github.com rendered that HTML at a github.com origin, every artifact would be a stored-XSS payload aimed at logged-in users, with access to their session for that origin. GitHub ships the same mitigation on raw.githubusercontent.com, which returns text/plain for .html so the browser refuses to execute it.

So the constraint is structural. Work with it rather than against it.

What people try, and how each one goes

Download and open locally. Works, for you. Everyone else in the PR thread has to repeat it, and half of them won't. Worse, many reports break when opened from file:// — see my HTML report lost its CSS.

GitHub Pages. Real hosting, real URL. But one Pages site per repository means concurrent branches fight over the same tree unless you invent /<branch>/<run-id>/ paths and a cleanup job. Private repositories need a paid plan for private Pages, and a pull_request run from a fork has no write token, so the branch that most needs a preview is the one that can't publish. Alternatives are covered in GitHub Pages alternatives.

S3 or a bucket behind CloudFront. Fine, and a lot of teams end here. The cost is the setup: a bucket policy, an OIDC role for the workflow, lifecycle rules, and a Content-Type mapping — get that last one wrong and the browser downloads the file instead of rendering it (why that happens). Comments still live somewhere else.

An artifact-proxy action. Several exist. They re-serve the zip from a third-party origin, which trades GitHub's XSS problem for that origin's, and none of them survive a repository going private.

The one-step version

Add a publish step. It does not need a bucket, a role, or a Pages branch:

- name: Publish report
  if: always() # a red build is when someone actually wants it
  run: |
    curl -sS -X PATCH \
      "https://commareports.com/api/v1/reports/${{ vars.COMMA_REPORT_ID }}" \
      -H "Authorization: Bearer ${{ secrets.COMMA_API_TOKEN }}" \
      -H "Content-Type: application/json" \
      --data @<(jq -Rs '{html: .}' playwright-report/index.html)

if: always() matters more than it looks. The default is to skip publish steps when a prior step failed, which means the report gets published on exactly the runs nobody needs it for.

Because the report id is fixed, every run appends a revision at the same URL. The link in your PR template never goes stale, and any two runs can be diffed against each other. Full setup, including creating the report id once and the multi-file variant for reports that ship an assets directory, is in publish from CI and GitHub Actions HTML reports.

Why a link beats a download for review

The reason you uploaded the artifact was so someone would look at it. Once it is a URL:

  • It opens on a phone, in a Slack unfurl, from a PR comment.
  • Reviewers select the failing assertion and comment on that line, instead of screenshotting it into chat — comment on HTML.
  • Access is per-report: private to you, to invited people, to anyone with the link, or public.
  • Nothing expires at 90 days.

Try it

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

Publish your first CI report →

Related