Desktop Pricing Docs Blog About GitHub View as Markdown (Agent) Get started
Human Agent
← Back to blog

Host Playwright & CI screenshots on a real CDN

A test failed in CI and left a screenshot. Now it's buried in a zipped artifact you have to download, unzip, and open locally. Here's how to turn it into a private, clickable URL that lands straight in the pull request.

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:

# .github/workflows/e2e.yml
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:

# Any CI system: upload a run's screenshots in one call.
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.

Read the API docs →