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
Screenshots add up fast. Compress images in CI before they are committed or uploaded, to keep both your repo and your storage small.
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. Free-plan images left unused for a long time may be cleaned up (the rule); paid plans keep everything.