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 →