How to Automate Website Screenshots with GitHub Actions
Capture website screenshots on every push, on a schedule, or by hand with GitHub Actions. Compare Playwright, shot-scraper, and a dedicated action, then save the results safely.
To automate website screenshots with GitHub Actions, run a browser capture tool in a workflow job, write its image files to the workspace, and upload them as workflow artifacts. Use Playwright when screenshots belong to browser tests or need browser interaction; use shot-scraper when you want to define targets in YAML; use a dedicated screenshot action for a simple URL or element capture.
This guide builds a runnable Playwright workflow, explains the alternatives, shows how to keep screenshots as artifacts or repository files, and covers security, troubleshooting, and cost considerations.
1. Choose where screenshots should go
First decide how the captures will be used. Workflow artifacts are usually the most direct choice for review and download: GitHub describes them as files produced by a run that persist after a job finishes. Commit images only when version history is part of the intended workflow, such as maintaining a visual record in the repository. GitHub’s workflow artifact documentation covers artifact storage and sharing.
| Approach | Best fit | Tradeoff |
|---|---|---|
| Playwright | Existing Playwright projects, browser tests, multiple page states, or custom browser interaction | Requires a project script and matching browser installation. |
| shot-scraper | Python workflows where targets and capture settings belong in YAML | Committing output requires repository write permission; artifacts avoid that permission. |
| Webpage Screenshot Action | A focused capture of a URL or element with little custom setup | Review the action’s inputs and maintenance/security posture, and handle artifact storage in your workflow. |
Choose based on browser behavior, output destination, and configuration needs. There is no universal speed or reliability winner; measure your own pages and workflow.
2. Build a Playwright workflow
The example below captures two URLs on pushes to the main branch, pull requests, and manual runs. It writes PNG files to screenshots/ and uploads that directory as an artifact even if the capture script fails. The Playwright CI guide demonstrates installing the project dependencies and browser dependencies before running a job, then uploading its output. See Playwright’s CI documentation.
Create the capture script
In a Node.js project, install Playwright and create scripts/capture.mjs. The script uses the Chromium browser installed by the workflow. It waits for the page’s load event, then captures a full-page PNG. Edit the target list and selectors for your own site.
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
const targets = [
{ name: 'home', url: 'https://example.com' },
{ name: 'docs', url: 'https://playwright.dev/docs/intro' },
];
await mkdir('screenshots', { recursive: true });
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
for (const target of targets) {
const response = await page.goto(target.url, {
waitUntil: 'load',
timeout: 45_000,
});
if (!response || !response.ok()) {
throw new Error(`${target.url} returned ${response?.status() ?? 'no response'}`);
}
await page.screenshot({
path: `screenshots/${target.name}.png`,
fullPage: true,
animations: 'disabled',
});
}
} finally {
await browser.close();
}
Initialize the project with npm init -y, then install Playwright with npm install --save-dev playwright. Commit the resulting package.json and package-lock.json so CI can use npm ci. The workflow installs the browser version associated with that package.
Add the workflow
Save this as .github/workflows/screenshots.yml. The artifact contains both captures and is retained for seven days. Adjust retention to suit your review cycle and repository policy.
name: Website screenshots
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
capture:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- name: Install project dependencies
run: npm ci
- name: Install Chromium and system dependencies
run: npx playwright install --with-deps chromium
- name: Capture pages
run: node scripts/capture.mjs
- name: Upload screenshots
if: ${{ always() }}
uses: actions/upload-artifact@v5
with:
name: website-screenshots-${{ github.run_id }}
path: screenshots/
if-no-files-found: warn
retention-days: 7
Action major versions and runner images change. Review upstream documentation and pin actions according to your organization’s supply-chain policy; immutable commit SHA references provide stronger version pinning than floating tags. The examples use current references shown in Playwright’s rolling CI guide. In a pull request from a fork, do not expose secrets to untrusted code.
Run captures on a schedule
Add a schedule trigger to the workflow if you want periodic captures. GitHub Actions cron expressions use UTC. Keep the manual trigger for ad hoc runs, and set a deliberate concurrency policy if overlapping runs would create duplicate work.
on:
schedule:
- cron: '17 6 * * 1'
workflow_dispatch:
Replace the existing on section rather than creating a second one. A scheduled workflow must exist on the repository’s default branch to run as scheduled. Confirm timing and event behavior against GitHub’s workflow documentation.
Capture one element or a specific state
For a component capture, navigate first and then take a locator screenshot:
const card = page.locator('[data-testid="pricing-card"]');
await card.waitFor({ state: 'visible', timeout: 10_000 });
await card.screenshot({ path: 'screenshots/pricing-card.png' });
Use stable selectors such as test IDs rather than fragile positional selectors. For a known interaction, perform it explicitly before capture, for example await page.getByRole('button', { name: 'Show details' }).click(). If your page has asynchronous content, wait for the relevant locator or application-specific readiness signal. A fixed sleep can help diagnose timing, but it can also waste runner time or remain too short on a slow run.
3. Use shot-scraper for YAML-defined targets
shot-scraper suits teams that want target URLs and capture settings maintained in a YAML file. Its project template documents the workflow pattern of reading shots.yml, capturing the listed pages, and committing images. See the shot-scraper template and configuration examples.
A target file can look like this:
- url: https://example.com/
output: screenshots/home.png
height: 900
- url: https://example.com/docs/
output: screenshots/docs.png
wait: 1500
Install shot-scraper and its browser dependencies in the job, then run shot-scraper multi shots.yml. Follow the current project installation instructions for your Python and browser environment rather than assuming that a previously cached browser will match the installed package.
If you commit generated images, the workflow needs contents: write, a bot identity for the commit, and a push step. Grant that permission only to the job that needs it. If the purpose is review or download, upload the directory as an artifact instead and keep contents: read.
4. Use a dedicated screenshot action
The Webpage Screenshot Action accepts a fully qualified url, an optional output path, a mode, a selector or xpath, and optional scriptBefore JavaScript. Its documented modes are page, wholePage, scrollToElement, and element. The action captures the image; add an artifact upload step to preserve it. Check its Marketplace listing for current inputs and version details. Review the upstream source, maintenance, permissions, and pinning approach before adopting a third-party action.
5. Make the captures reproducible
- Use explicit dimensions. Set a fixed viewport and device scale factor so layout and output dimensions do not vary with defaults.
- Wait for meaningful readiness. Prefer a visible locator or app-specific ready state over arbitrary delay. Some sites keep network requests open indefinitely, so a network-idle condition may be unsuitable.
- Control animation. Playwright’s screenshot option can disable animations, reducing motion-related variation.
- Keep target state consistent. Use test data and deterministic routes where possible; do not rely on personalized content or a logged-in session unless that is what you intend to capture.
- Handle failures deliberately. Check navigation responses, use a job timeout, and upload any available images or diagnostics when a later capture fails.
- Mind fonts and locale. A runner may not have the same fonts or locale as a developer’s machine. Install required fonts and set locale/timezone where visual consistency requires it.
For pull requests that run untrusted changes, do not pass private credentials into the browser. If authentication is required, use a protected environment and carefully scope when the job can run. Captures and browser traces may reveal credentials, user data, source code, or private product details. Restrict artifact access and retention, and encrypt files before sharing if your policy requires it. Playwright’s CI guidance also calls out sensitive information in reports, traces, and logs.
6. Artifacts or committed files?
| Output choice | Use it when | Consider |
|---|---|---|
| Workflow artifact | Reviewers need to download or inspect a run’s output | Set a useful artifact name and retention period; restrict access for private captures. |
| Repository commit | Images are intentional source-controlled snapshots or published assets | Generated binaries add history and require write permission and a commit step. |
| Another storage destination | A separate system must host or process the captures | Use purpose-specific credentials and avoid making sensitive captures public. |
Artifacts are outputs, not dependency caches. Use caching for reusable dependencies; use artifacts to preserve files produced by a run. GitHub explains the distinction.
7. Performance, reliability, and cost
Workflow time depends on runner setup, browser installation, page behavior, image dimensions, and the number of targets. Do not assume caching the browser will make a job faster: Playwright’s CI guide says restoring browser binaries can take about as long as downloading them, while Linux system dependencies cannot be cached. Its guidance is to avoid browser caching by default; if you choose to cache, key it to the Playwright version. Read the browser caching notes.
Reduce unnecessary work by capturing only the pages and states that matter, selecting a single browser when cross-browser coverage is not needed, and choosing a sensible viewport and full-page setting. Avoid unbounded retries: retrying a genuinely unavailable target consumes more runner time without guaranteeing a useful image. Use a timeout and make failures visible in the job summary.
GitHub Actions usage and artifact storage are subject to your GitHub plan and repository configuration. Check the current GitHub billing documentation for applicable minutes and storage limits. Browser automation itself does not make a remote website’s content stable or guarantee that a capture succeeded; validate status and output when correctness matters.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser executable missing | Installed Playwright package and browser binaries do not match, or browser installation was skipped. | Run npx playwright install --with-deps chromium after npm ci in the same job. |
| Browser fails to launch on Linux | Required system libraries are missing or the installed browser does not match. | Use Playwright’s supported install command with --with-deps. For diagnostics, set DEBUG=pw:browser as described in the CI guide. |
| Navigation timeout | The site is slow, blocked, redirects, or waits on requests that never finish. | Inspect the URL and response, adjust the navigation timeout, and wait for the page element that indicates the content is ready instead of waiting for every request to stop. |
| Screenshot is blank or incomplete | Capture ran before client-rendered content appeared, an element was hidden, or the page failed to load. | Check the response and browser errors; wait for a page-specific locator before capturing. |
| Artifact is missing | The capture wrote to a different working directory, no files were produced, or the upload path is wrong. | Use an explicit output path, inspect the job workspace, and set if-no-files-found intentionally. |
| Workflow cannot push screenshot commits | The job lacks repository write permission or branch protection disallows the bot commit. | Grant contents: write only where required and configure branch policy; otherwise upload an artifact. |
| Images differ between runs | Dynamic content, fonts, animation, viewport, locale, or remote data changed. | Fix viewport and relevant environment settings, disable animation, wait for a stable state, and use controlled test data. |
| Pull request workflow cannot access a secret | Secrets are intentionally unavailable to workflows from untrusted forks. | Do not weaken that protection. Use public test data or run authenticated capture only in a protected, trusted workflow. |
9. Or skip the browser setup
If the goal is the screenshot rather than maintaining a browser in CI, ScreenshotNeo is a website screenshot API and MCP server. A GET request returns a PNG, JPEG, WebP, or PDF. Its capture can accept cookie/consent banners like a visitor and remove 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers say the page verdict and billing outcome.
Use the API from a workflow step; store the key as a GitHub Actions secret such as SCREENSHOTNEO_API_KEY. The API details and supported options are in the ScreenshotNeo documentation.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key="$SCREENSHOTNEO_API_KEY" \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import os
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": os.environ["SCREENSHOTNEO_API_KEY"], "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: process.env.SCREENSHOTNEO_API_KEY,
url: 'https://stripe.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);
In Node.js versions that do not support top-level await in the current file mode, put the request in an async function. In GitHub Actions, upload shot.webp with actions/upload-artifact just as you would a Playwright output. Keep the API key in a repository or environment secret and do not print it in logs.
ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Sign up for the free plan.
10. Frequently asked questions
Can I capture a page running locally in the workflow?
Yes. Start the app as a background process in a workflow step, wait for its local URL to respond, then navigate Playwright to that URL. Ensure the server remains running until capture completes.
Can I capture screenshots on every pull request?
Yes. Add the pull_request trigger. Keep secrets out of jobs that may execute code from untrusted contributors.
Can the workflow take a screenshot of an element?
Yes. Playwright can screenshot a locator, and the dedicated Webpage Screenshot Action supports element mode using a CSS selector or XPath.
Should screenshots be committed to Git?
Commit them when their history is useful to the project. For routine review and downloads, an artifact avoids adding generated images to source history.


