Desktop Pricing Docs Blog About GitHub Get started
← 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 permanent, clickable URL you can paste straight into a PR.

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.

What you want is a plain URL — one you can click from the CI log, paste into a review, or post to Slack. That's a one-call upload.

Upload a screenshot, get a URL

From a shell step in any CI system:

# In your CI job, after a test fails: publish the screenshot,
# get back a URL you can paste into a PR or Slack.
curl -s -X POST https://api.pixelvault.dev/v1/images \
  -H "Authorization: Bearer $PIXELVAULT_API_KEY" \
  -F "file=@test-results/failure.png" | jq -r '.url'

# → https://img.pixelvault.dev/proj_abc/img_xyz.png

Do it automatically on test failure

Better still, host the screenshot from inside the test and attach the URL to the report, so every failure comes with a link — no extra CI step, no artifact spelunking:

import { test } from "@playwright/test";

test("checkout flow", async ({ page }, testInfo) => {
  // ... your test steps ...

  // On failure, host the screenshot and attach the URL to the report
  if (testInfo.status !== testInfo.expectedStatus) {
    const shot = await page.screenshot();
    const form = new FormData();
    form.append("file", new Blob([shot], { type: "image/png" }), "failure.png");

    const res = await fetch("https://api.pixelvault.dev/v1/images", {
      method: "POST",
      headers: { "Authorization": `Bearer ${process.env.PIXELVAULT_API_KEY}` },
      body: form,
    });
    const { url } = await res.json();
    // Now clickable straight from the test report or CI log
    testInfo.annotations.push({ type: "screenshot", description: url });
  }
});

Set the API key once as a CI secret and the tests pick it up from the environment:

# .github/workflows/e2e.yml
  - name: Run Playwright tests
    run: npx playwright test
    env:
      PIXELVAULT_API_KEY: ${{ secrets.PIXELVAULT_API_KEY }}

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.
  • Test keys that clean themselves up. Use a pv_test_ key and uploads auto-expire after 24h and don't count against your limits — perfect for ephemeral CI noise.
  • 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.
  • Keep the ones that matter. Promote a flaky-test screenshot to a permanent live upload when you want a durable record.

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 registration and upload are both plain API calls, an agent can host the failure screenshot itself and hand you back a link in its summary. Same two calls, no browser required.

Free to start

PixelVault's free tier includes 200 MB storage, 500 uploads/month, and 1 GB bandwidth — no credit card, no trial expiry. Paid plans start at $9/month.

Read the API docs →