How to Run Screenshot Tests for a Website in GitHub Actions for Free
Set up Playwright screenshot tests in GitHub Actions, save reports as artifacts, and understand when runner usage is actually free.
You can run website screenshot and visual regression tests in GitHub Actions with Playwright. Add a workflow that installs your locked Node dependencies and Playwright browsers, runs your test suite on pushes and pull requests, and uploads the HTML report as an artifact. Standard GitHub-hosted runners are free for public repositories; private repository usage is free only within the minutes and storage included with your account plan.
This guide sets up the Playwright test and workflow, explains how to inspect failures, and shows what to check before calling a private workflow free. Playwright tests can run on any CI provider, including GitHub Actions. Playwright CI documentation
1. Add a visual assertion to your Playwright suite
The workflow only runs tests. The screenshot assertion belongs in the project’s Playwright test suite. If you already have Playwright configured, add a test like this to your existing test directory, for example tests/homepage.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage matches its visual baseline', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
});
});
On the first run, Playwright may report that the expected screenshot is missing. Create the baseline locally with:
npx playwright test --update-snapshots
Review the generated image before committing it. Commit screenshot baselines alongside the test so CI has the expected images to compare against. Use a stable test URL or your local application server instead of https://example.com for an actual project.
A minimal configuration can set the test directory, reporter and snapshot directory:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
reporter: [['html', { outputFolder: 'playwright-report' }]],
snapshotPathTemplate: '{testDir}/__screenshots__/{arg}{ext}',
use: {
browserName: 'chromium',
viewport: { width: 1280, height: 720 },
},
});
Keep the browser, viewport, operating system and rendering conditions consistent between baseline generation and CI. Fonts, animations, dynamic content, time-dependent data and differences in browser versions can cause visual diffs that are unrelated to a code regression. Playwright documents containers as one way to isolate dependencies and keep screenshot-testing environments consistent. Playwright CI: containers and stability
2. Create the GitHub Actions workflow
Create .github/workflows/playwright.yml in the repository. This example follows Playwright’s documented JavaScript workflow pattern, including browser installation and report upload. Adjust the branch names and Node version to match your project.
name: Playwright screenshot tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
The example uses the action versions and 30-day artifact retention shown in Playwright’s documentation; check the current official guide when updating a workflow. The retention value is configurable for this uploaded artifact, not a universal GitHub retention rule. The repository must have Playwright in its locked dependencies, and the tests need to produce the report directory for it to contain the HTML report. Playwright: running tests on GitHub Actions
What each step does
actions/checkoutmakes the repository files available to the job.actions/setup-nodeinstalls the selected Node version and configures npm caching.npm ciinstalls exactly the dependency versions in the lockfile. Commit a current lockfile.npx playwright install --with-depsinstalls the browser binaries and required operating-system packages on the runner.npx playwright testruns the project’s configured suite.actions/upload-artifactsaves the report for later inspection, including when the test step fails, because the condition skips upload only when the run is cancelled.
If the project uses a different package manager, replace npm ci with its lockfile-based clean install command and configure that package manager’s cache. Do not omit browser installation unless the runner environment already has the exact required browsers and dependencies installed.
3. Confirm the workflow is free for your repository
“Free” depends on the repository and runner. GitHub says usage of standard GitHub-hosted runners is free for public repositories. Private repositories use the minutes and storage included with the account plan; usage beyond included allowances can be billed. Self-hosted runners do not incur GitHub Actions minute charges, but you provide and operate the machine. Check the current billing and usage for your account before estimating a private repository’s cost. GitHub Actions billing · Usage limits and billing administration
| Setup | What to check | Cost consideration |
|---|---|---|
| Public repository, standard GitHub-hosted runner | Runner type and whether the workflow uses standard runners | Standard runner usage is free for public repositories. |
| Private repository, standard GitHub-hosted runner | Account plan, included minutes, workflow runtime and artifact storage | Included allowances vary by plan; overage is billed. |
| Self-hosted runner | Machine, maintenance, security and capacity | No GitHub Actions minute charge; you bear the cost of supplying and operating the machine. |
To estimate your real usage, look at how often pushes and pull requests trigger the workflow, how long each job runs, how many browser projects or workers execute, and how much report and artifact storage is retained. Avoid assuming private workflows are free just because a run succeeds without an immediate charge.
4. Make screenshot comparisons more reliable
Keep the environment stable
- Generate and update baselines with the same browser engine and a compatible browser version as CI.
- Set a fixed viewport and device scale factor when those affect the page layout.
- Use deterministic test data and avoid content that changes on every run, such as timestamps, rotating banners or random identifiers.
- Wait for the page state your assertion needs. Prefer a locator or explicit readiness signal over an arbitrary long delay.
- For pages with animations, disable or control animations during screenshot capture using Playwright’s screenshot options.
- Use a container when you need tighter control over operating-system packages and fonts across local development and CI.
Choose the right screenshot scope
A full-page screenshot can catch layout regressions below the fold, but it takes longer to render and can be more sensitive to dynamic content. A locator screenshot narrows the comparison to a component and can make failures easier to diagnose. Keep tests focused: one useful visual assertion per important page state is easier to maintain than snapshots of every page without a clear regression signal.
Control concurrency when stability matters
Parallel browser workers can shorten runs, but they also increase resource demand and may make a constrained runner less stable. Playwright documents running with a single worker in CI as one approach when stability is more important than speed. Configure this in playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
workers: process.env.CI ? 1 : undefined,
reporter: [['html', { outputFolder: 'playwright-report' }]],
});
Start with one worker if resource contention or inconsistent failures appear, then increase concurrency only if the runner has enough capacity and the results remain stable.
5. Inspect a failed run
- Open the failed workflow run in GitHub Actions and identify whether installation, test execution or artifact upload failed.
- Download the
playwright-reportartifact from the run’s artifacts section. - Open the report locally to inspect the failing test, expected image, actual image and diff. Playwright’s report and test output help distinguish a real page change from an environment or timing issue.
- Reproduce the failing test locally using the same browser project and viewport before updating a baseline.
- Update and commit a baseline only after reviewing the visual change and confirming that it is expected.
If the job is cancelled before upload, the condition cannot preserve the artifact. If no report appears after a test failure, confirm that the reporter writes to the configured path and that the upload step is reached.
6. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser executable is missing | The browser install step did not run, or a different Playwright version is used in CI than expected. | Run npx playwright install --with-deps after installing locked dependencies. Check that the lockfile and Playwright package version are committed. |
| Browser launch fails because a shared library is missing | Required operating-system browser dependencies are absent. | Use npx playwright install --with-deps on the supported Linux runner, or use a Playwright container with the needed dependencies. |
| Screenshot differs on every run | Dynamic data, animation, fonts, viewport or environment differences affect rendering. | Stabilize the page state and data, set a fixed viewport, control animation, and align local and CI browser and operating-system environments. |
| Test passes locally but fails in CI | CI may have different browser binaries, timing, dependencies, fonts or available resources. | Reproduce with the same Playwright version and browser, install dependencies in CI, wait for an explicit ready state, and try one worker if resource contention is suspected. |
npm ci fails |
The lockfile is missing, out of sync with the manifest, or generated using incompatible package-manager settings. | Regenerate and commit the lockfile using the project’s package manager, then confirm a clean install succeeds. |
| Report artifact is missing | The reporter output path differs from the upload path, or the workflow was cancelled before upload. | Set the reporter’s output folder to playwright-report, match the artifact path, and retain the if: ${{ !cancelled() }} condition. |
| Private workflow costs more than expected | Workflow minutes or artifact storage exceed the account’s included allowance. | Check the account’s current Actions usage and plan allowance. Reduce unnecessary triggers, test duration or artifact retention where appropriate. |
| Pull request workflow cannot access a secret | GitHub restricts secrets in some pull request contexts, especially for contributions from forks. | Do not make screenshot tests depend on a secret that untrusted pull request code must access. Use safe test data or a workflow design consistent with GitHub’s security guidance. |
7. Cost, runtime and maintenance tradeoffs
The main cost drivers are runner minutes for private repositories beyond the included allowance and storage for uploaded artifacts. The main runtime drivers are browser installation, number of tests, browser projects, workers and page readiness. Lockfile-based installs and npm caching can reduce repeated dependency installation work, but browser installation and test execution still take time.
Keep report retention long enough for the team to investigate regressions, then choose a shorter period if stored artifacts accumulate. A self-hosted runner avoids GitHub Actions minute charges but shifts machine cost, updates, security and reliability work to your team. Screenshot baselines also need maintenance: update them when an intentional visual change lands, and review changes instead of blindly replacing snapshots after a failure.
Or skip the browser setup
For a one-off screenshot or a capture step outside your visual regression suite, ScreenshotNeo takes a URL in one request and returns an image or PDF. It is a website screenshot API and MCP server from ScreenshotNeo; see the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, newsletter popups and chat widgets are removed before the shot.
- Bot checks, blank pages and failed loads are never billed.
- An MCP server lets AI agents use screenshot tools.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Do I need a separate visual testing service?
No. Playwright can compare screenshots using assertions in your test suite. A managed service may suit teams that want managed baselines or a broader review workflow, but it is not required for this GitHub Actions setup.
Can I run these tests only on pull requests?
Yes. Keep the pull_request trigger and remove the push trigger if that matches your review process. Consider retaining a push run on the main branch if you want a check after merge.
Does a 30-day artifact setting mean all GitHub artifacts are kept for 30 days?
No. It is the retention value configured for this workflow’s uploaded report. Artifact retention can be configured and is subject to repository or account settings.
Should I commit screenshot baselines?
Yes, when they are the expected images used by your Playwright assertions. Review baseline updates as code changes so an accidental visual regression is not accepted automatically.


