<!-- Markdown twin of https://pixelvault.dev/blog/host-playwright-ci-screenshots -->

[← Back to blog](/blog)

By [PixelVault](/about) July 4, 2026 Updated July 12, 2026

# 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](https://github.com/marketplace/actions/pixelvault-upload-screenshots) 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 Actions

\# .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:

curl

\# 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](/docs) for it), an agent can [host the failure screenshot itself](/blog/image-hosting-for-ai-agents) 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](/pricing)) lift the private-image cap. Public hosting is always unlimited.

[Read the API docs →](/docs)
