Playwright captures a screenshot the moment a test fails — genuinely useful evidence. The problem is where it ends up. In GitHub Actions the standard move is actions/upload-artifact, which bundles everything into a zip. To actually see the failure you download the zip, unzip it, and hunt for the right PNG. Nobody does that at 2 AM during an incident, and you can't drop a zip into a PR comment.
This isn't a job for a full visual-testing platform, either. If you already diff with Playwright's built-in toHaveScreenshot(), you don't need to buy a second product to review the result — you just need the image, somewhere a human (or a bot) can click it. That's hosting, not a platform.
One GitHub Action
Drop the PixelVault screenshots action into your workflow behind if: failure(). It globs the screenshots, uploads them in a single grouped batch, and posts (or updates) one sticky comment on the pull request with a diff table — no comment spam, no artifact spelunking:
jobs:
test:
runs-on: ubuntu-latest
permissions:
pull-requests: write # for the sticky PR comment
steps:
- run: npx playwright test
- name: Host failure screenshots
if: failure()
uses: pixelvault-dev/screenshots-action@v1
with:
api-key: ${{ secrets.PIXELVAULT_API_KEY }}
path: test-results
pattern: "*-diff.png"
visibility: private
Set PIXELVAULT_API_KEY once as a repository secret. Re-running the job updates the same comment in place (the collection is idempotent by run), so a flaky retry doesn't stack five comments.
Private by default
CI screenshots usually show your app mid-flow — staging data, internal UI, sometimes a logged-in account. You don't want those on a public, crawlable CDN. With visibility: private every screenshot is served behind a signed URL: the link in the PR works for reviewers, but strip the signature and the CDN returns 403. It's the review convenience of a public link without putting your UI on the open web.
Not on GitHub? One API call
The action is a thin wrapper over a plain endpoint, so GitLab CI, CircleCI, Jenkins, or a local run all work the same way — POST a run's screenshots as one batch and get back a signed URL per image:
curl -s -X POST https://api.pixelvault.dev/v1/images/batch \
-H "Authorization: Bearer $PIXELVAULT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"collection": {"type": "ci_build", "visibility": "private"},
"images": [{"data": "<base64-png>", "filename": "diff.png"}]}'
# → each item returns a signed https://img.pixelvault.dev/… URL
Why a hosting API beats artifacts here
- Clickable, not zipped. A URL renders inline in a PR, an issue, or a chat message. A zip renders nowhere.
- Private when it matters. Signed URLs keep internal UI off the public web; public collections are there when you want a shareable link.
- One sticky PR comment. The action keeps a single comment per PR and updates it — reviewers see the latest run, not a wall of duplicates.
- Zero egress fees. Screenshots get viewed by teammates and bots alike; bandwidth doesn't bill you by surprise.
- Same API everywhere. GitHub Actions, GitLab CI, CircleCI, or a local run — it's just an HTTP POST.
For agents running the tests
When a coding agent runs your E2E suite, it hits the same wall — a screenshot it can't easily surface to a human. Because upload is a plain API call (and there's an MCP tool for it), an agent can host the failure screenshot itself and hand you back a link in its summary. No browser, no artifact download.
Free to start
PixelVault's free tier includes 200 MB storage, 500 uploads/month, 1 GB bandwidth, and up to 100 private images — no credit card, no trial expiry. Paid plans (from $9/month) lift the private-image cap. Public hosting is always unlimited.