How to Run BackstopJS in GitHub Actions
Set up BackstopJS visual regression checks in GitHub Actions with a pinned Node environment, reviewable artifacts, and safe baseline updates.
To run BackstopJS in GitHub Actions, add a workflow under .github/workflows that checks out the repository, sets up the Node.js version your project supports, installs dependencies from the lockfile, and runs backstop test. Keep reference images under version control, upload test screenshots and reports even after failures, and approve new references only after someone reviews the visual changes.
BackstopJS compares screenshots over time to automate visual regression testing. This guide shows a practical workflow, the configuration it expects, how to run an app before capture, how to retain reports, and how to diagnose noisy or failed runs. [BackstopJS README]
1. Add BackstopJS to the project
Use a project-local dependency so the version is recorded in the package manifest and lockfile. The example below assumes npm and a compatible package-lock.json.
npm install --save-dev backstopjs
Add scripts to package.json so local development and CI use the same commands:
{
"scripts": {
"backstop:test": "backstop test",
"backstop:approve": "backstop approve"
}
}
If BackstopJS is new to the repository, initialize its files in a local working tree, then inspect the generated files before committing. The project documents that backstop init scaffolds files and can overwrite existing files. The default config is backstop.json; an alternate JSON or JavaScript config can be supplied with --config. [BackstopJS README]
npx backstop init
2. Create the GitHub Actions workflow
Save this as .github/workflows/backstop.yml. The Node version is an example: use the version supported by your project and keep it aligned with local development. Verify action versions against their official documentation when adopting or updating the workflow.
name: Visual regression
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
backstop:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- name: Install dependencies
run: npm ci
- name: Run BackstopJS
run: npm run backstop:test
- name: Upload BackstopJS output
if: always()
uses: actions/upload-artifact@v4
with:
name: backstop-output
path: |
backstop_data/html_report
backstop_data/ci_report
backstop_data/bitmaps_test
if-no-files-found: ignore
This follows GitHub’s workflow structure: workflow files live in .github/workflows, checkout retrieves the repository, and setup-node configures the Node runtime. [GitHub Actions quickstart] [setup-node documentation]
If you do not add the npm script, invoke the local binary directly with ./node_modules/.bin/backstop test. Avoid installing an unpinned global CLI in CI: it may differ from the version represented by the project lockfile. BackstopJS documents both global and local installation approaches. [BackstopJS README]
3. Configure scenarios and viewports
BackstopJS needs at least one viewport and one scenario. A scenario requires a label and url. Point the URL at a page reachable from the runner, and use stable, representative data so a legitimate visual change can be distinguished from incidental variation.
{
"id": "site-visual-checks",
"viewports": [
{ "label": "desktop", "width": 1365, "height": 900 },
{ "label": "mobile", "width": 390, "height": 844 }
],
"scenarios": [
{
"label": "home page",
"url": "https://example.com/",
"delay": 500
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"html_report": "backstop_data/html_report",
"ci_report": "backstop_data/ci_report"
},
"report": ["browser", "CI"],
"engine": "puppet"
}
Save the configuration as backstop.json at the repository root, or pass a custom file with npx backstop test --config=path/to/config.js. The exact scenario options depend on what the page needs. Keep the initial set small and add scenarios for important pages and states, such as navigation open or a form validation message, when those states matter to users.
Start a local app before capturing it
If the target page is created by the same repository, the workflow must build and start the app before BackstopJS runs. Use the project’s own scripts and make sure the server is ready before invoking the test. For example, when the project has a build script and a command that serves the built site:
- name: Build site
run: npm run build
- name: Start site
run: npm run start:test &
- name: Wait for site
run: npx wait-on http://127.0.0.1:3000
- name: Run BackstopJS
run: npm run backstop:test
The command and readiness URL are framework-specific; replace them with the repository’s actual scripts. A fixed sleep can be flaky because startup time varies. A readiness check gives a concrete signal that the server is responding before the browser captures pages.
4. Keep reference images reviewed and reproducible
BackstopJS’s workflow is init to create configuration and scaffolding, test to capture and compare against references, and approve to promote recent test captures as the new reference set. [BackstopJS README]
Run backstop test automatically in CI. When a visual change is expected, inspect the report and diff images, then run backstop approve in a controlled review process and commit the updated reference images. Do not make baseline approval an automatic step on every pull request: approval changes what future runs treat as correct.
For trustworthy comparisons, keep the capture environment steady: browser engine, viewport dimensions, fonts, test data, and page state should match between reference creation and CI. The BackstopJS project documents Puppeteer as its default engine, Playwright support, and Docker rendering as an option for reducing cross-platform variation. [BackstopJS README]
5. Reports and artifacts
Use report: ["CI"] to generate a CI/JUnit report; add "browser" when an interactive HTML report is useful. BackstopJS documents report path and filename configuration, including paths.ci_report and ci.testReportFileName. [BackstopJS README]
The workflow’s artifact step includes reports and test bitmaps. GitHub lists screenshots, failed test results, and reports among common artifact uses. With if: always(), the upload step is attempted even if the test step failed, so a failed run can still leave evidence to inspect. [GitHub: storing workflow data as artifacts]
Reference bitmaps should generally remain available to CI, usually committed with the project. Test captures and reports are run output and are useful as uploaded artifacts. Check artifact contents and retention settings against your team’s review needs.
6. Pull request security and permissions
The example grants only contents: read, which is enough for a workflow that checks out source and runs visual checks without publishing results back to GitHub. GitHub allows workflow token permissions to be restricted with the permissions key; once explicit permissions are set, unspecified permissions are set to none. [GitHub token permissions]
Use pull_request for ordinary pull request checks that do not need secrets. Do not casually switch to pull_request_target and execute the pull request’s code in that privileged context. GitHub warns against checking out, building, or running untrusted pull request code where secrets or a privileged token are available. [GitHub secure use reference]
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
npm ci fails |
No compatible lockfile is present, or the package manifest and lockfile disagree. | Commit the correct npm lockfile and update it together with package.json; keep using npm ci for clean CI installs. |
| Backstop command is not found | BackstopJS is not installed in the project or the workflow skipped dependency installation. | Add it as a project dependency, run npm ci, and invoke the package script or ./node_modules/.bin/backstop. |
| Scenario fails to load or times out | The URL is wrong, inaccessible from the runner, or the local app has not started yet. | Confirm the URL from the runner environment, start the app before capture, and wait for a readiness endpoint. |
| Unexpected diffs on every run | Rendering inputs vary, such as browser engine, fonts, viewport, dynamic content, or test data. | Match capture settings and state; stabilize dynamic content and consider the project’s Docker rendering option for environment consistency. |
CI cannot reach localhost in Docker |
Inside the container, localhost refers to the container itself rather than the host environment. | Use the host address appropriate to the Docker setup. BackstopJS documents this environment-specific networking issue. |
| Artifact upload says no files were found | The run failed before generating output, configured paths differ, or the path list is wrong. | Check the Backstop paths configuration and artifact path against the generated directories; keep if-no-files-found: ignore only if missing output is acceptable. |
| Visual change is accepted by mistake | Reference approval ran without a human reviewing the diff. | Restore the prior reference set where appropriate, review the report, and make approval a deliberate maintainer action. |
8. Performance, reliability, and cost
BackstopJS captures each configured scenario at each configured viewport, so expanding either list increases browser work and CI duration. Start with high-value pages and viewports, then add coverage where a failure would matter. Reuse the project’s dependency cache through setup-node’s npm cache option, while relying on the lockfile for dependency reproducibility.
Reliability mostly comes from controlling the capture inputs: wait for the app and important content to be ready, use stable test data, match browser and viewport settings, and retain artifacts so failures can be diagnosed. Public hosted pages can change independently, so local or controlled test environments are preferable when the purpose is to detect changes in your own code.
Runner consumption and artifact storage are governed by your GitHub plan and workflow usage. This setup does not require a screenshot API: it runs BackstopJS in your CI environment. If you consider a hosted screenshot service for other capture workflows, compare its billing rules and output behavior with your needs rather than assuming it replaces BackstopJS’s reference comparison and approval process.
Or skip the browser setup
BackstopJS is the right fit when you need repeatable reference-image comparisons in your repository. For a one-off screenshot or capture endpoint in another developer workflow, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a screenshot or PDF, and the parameters used by other screenshot APIs also work.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
See the ScreenshotNeo API documentation for options and response details. ScreenshotNeo accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response indicates the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can I run this only on pull requests?
Yes. Keep the pull_request trigger and remove the push trigger if you do not want checks on pushes to the main branch.
Can I use a JavaScript config file?
Yes. BackstopJS supports an alternate JSON or JavaScript config file through --config. Ensure the chosen file is included in the repository and use the same config path in CI.
Should CI update references automatically?
No. The test job should report differences; a reviewer should inspect and deliberately approve changed references.
Does BackstopJS replace ScreenshotNeo?
They serve different jobs. BackstopJS compares current captures against approved baselines; ScreenshotNeo is a hosted screenshot API and MCP server for requesting screenshots or PDFs.


