You dropped the artifact link in Slack, your client clicked it, and GitHub asked them to sign in. Artifacts were built for the people who run workflows, not for the people who read results. Here's the honest picture of why that link fails — and the two-line fix that turns the artifact into a page anyone can open.
Why artifact links fail outsiders
GitHub artifacts are login-walled, full stop. Opening one — even on a public repository — requires a GitHub account with read access to the repo. The REST API is no loophole either: downloading an artifact programmatically demands a token. There's no "anyone with the link" toggle to flip; that's a product decision, not a setting you missed.
And even for teammates who clear the login wall, the prize is a zip. Download, extract, double-click index.html — then watch relative asset paths and client-side routing fall apart over file://. For HTML test reports specifically this is the dead-end: the report already is a website, GitHub just refuses to serve it as one.
Two smaller nails in the coffin: artifacts default to 90-day retention, and every stored megabyte counts against your storage quota. For sharing, the format was never the right shape.
The fix: publish the report, skip the artifact
Instead of uploading the report as an artifact, deploy it to a live URL in the same workflow. The official action finds common report formats on its own:
.github/workflows/tests.yml
- uses: bitgate/ship-page-action@v1 if: always() with: engine: auto comment: trueengine: auto probes for Playwright, JaCoCo, Gradle test, coverage.py, Storybook, and Allure reports — one match deploys as-is, several matches land behind a single URL with a built-in switcher. Prefer explicit? path: playwright-report does the same thing. if: always() is the detail that matters: the failed run is the run you want to read. comment: true posts the URL as one sticky PR comment, updated on every re-run (the workflow needs pull-requests: write).
The link that comes out asks nothing of the viewer: no GitHub account, no ship.page account, no zip, no extract. It opens in a browser on any device, like the website the report always was. Anonymous deploys expire after 7 days — add an api-key and a name (e.g. pr-${{ github.event.number }}) for a stable per-PR URL that redeploys in place, so the link pasted in the ticket never goes stale.
What the viewer needs
Nothing. No GitHub login, no ship.page login, no VPN. The URL is a plain page on an unguessable subdomain — not listed anywhere, served with noindex so search engines stay out. Share the link, they open it, done.
Not on GitHub Actions? One curl
There's no lock-in — a deploy is a single HTTP request, so any CI with curl can do the same:
publish-report.sh
URL=$(curl -fsS -X POST https://ship.page/deploy \ -H "Content-Type: text/html" \ --data-binary @build/report.html | jq -r .url)echo "### Report: $URL" >> "$GITHUB_STEP_SUMMARY"A whole report directory goes up as a zip — zip -qr - . | curl --data-binary @- streams it without temp files. The full walkthrough, including stable named URLs, lives in CI with plain curl.
Keep reading
- GitHub Action reference — every input, output, and engine preset.
- Test & build reports in CI — copy-paste recipes per tool.
- Host your Playwright report — the same fix, Playwright-specific.
The report was always a website. Ship it like one — no login required.