Skip to content
ship.page/blog
CI

How to share a GitHub Actions artifact without a login

Bart PelleChief Shipper

· 3 min read

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.

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: true

engine: 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

The report was always a website. Ship it like one — no login required.

Written by

Bart Pelle

Chief Shipper

Bart founded Bitgate and crowned himself Chief Shipper — a fancy title for “throws HTML at an API all day and occasionally writes about it.” If a build's on fire, he's the one who lit it. Usually on purpose.

Share

XHacker News