How to Run Percy Visual Tests in GitHub Actions
Connect Playwright, Cypress, or a static site to Percy in GitHub Actions, keep the project token secret, and review your first visual baseline.
To run Percy visual tests in GitHub Actions, install the Percy CLI and the SDK for your browser test framework, add snapshots at the states you want to compare, store the Percy project token as a GitHub Actions secret, and run your tests with percy exec --. For Playwright, the command is typically npx percy exec -- npx playwright test; for Cypress, use npx percy exec -- npx cypress run. The CLI coordinates snapshot collection and uploads the build to Percy.
This guide covers Playwright, Cypress, static site captures, workflow configuration, baselines, troubleshooting, and practical CI considerations. Percy’s exact package and integration requirements can change, so check its current SDK instructions before pinning versions.
1. Create a Percy project and protect its token
- Create or select a Percy web project and copy its project token.
- In your GitHub repository, open Settings → Secrets and variables → Actions.
- Create a repository or environment secret named
PERCY_TOKENand paste the token as its value. - Expose that secret only to the job or step that uploads snapshots, using the workflow
envfield.
Do not put the token directly in a workflow file, test, or committed environment file. Percy’s CI integration uses PERCY_TOKEN to associate uploads with the project. See the Percy GitHub Actions integration guide.
2. Set up Playwright snapshots
Install the Percy CLI and Playwright SDK as development dependencies:
npm install --save-dev @percy/cli @percy/playwright
In a Playwright test, capture the page at a meaningful, deterministic state. For example, create tests/visual.spec.ts:
import { test, expect } from '@playwright/test';
import percySnapshot from '@percy/playwright';
test('homepage visual snapshot', async ({ page }) => {
await page.goto('https://example.com');
await expect(page.getByRole('heading', { name: 'Example Domain' })).toBeVisible();
await percySnapshot(page, 'Homepage');
});
Replace the example URL and expected content with your application. A snapshot is most useful after navigation, data loading, and any interaction that establishes the state you want to compare.
Run the suite locally through Percy when you have a project token available:
PERCY_TOKEN=YOUR_PERCY_PROJECT_TOKEN npx percy exec -- npx playwright test
For CI, the secret supplies the token, so the workflow does not contain its value:
name: Visual tests
on:
pull_request:
push:
branches: [main]
jobs:
percy-playwright:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright with Percy
run: npx percy exec -- npx playwright test
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
The action and Node versions shown are an example workflow shape. Check current GitHub Actions and Percy compatibility and update or pin versions according to your repository’s policy. The official Percy Playwright client library documents installation and snapshot usage.
Using existing Playwright screenshot assertions
Percy’s Playwright repository also describes a drop-in option for existing toHaveScreenshot() assertions. That can reduce changes when a suite already uses Playwright screenshot assertions, but it has version and configuration requirements. Verify the current SDK documentation against your installed Playwright and Percy versions before adopting it. Do not assume that an ordinary Playwright screenshot assertion uploads a Percy snapshot on its own.
3. Set up Cypress snapshots
Install the Percy CLI and Cypress SDK:
npm install --save-dev @percy/cli @percy/cypress
Import the SDK from your Cypress support file. For example, in cypress/support/e2e.js:
import '@percy/cypress';
Call cy.percySnapshot() after the test has reached the state you want to capture:
describe('Homepage', () => {
it('captures the loaded homepage', () => {
cy.visit('https://example.com');
cy.contains('Example Domain').should('be.visible');
cy.percySnapshot('Homepage');
});
});
Then run Cypress under the Percy CLI:
npx percy exec -- npx cypress run
A matching GitHub Actions job can use the same token secret pattern:
name: Cypress visual tests
on:
pull_request:
push:
branches: [main]
jobs:
percy-cypress:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Run Cypress with Percy
run: npx percy exec -- npx cypress run
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
See the Percy Cypress SDK repository for its current integration details.
4. Capture a static site build
If you need to compare generated pages rather than states reached by browser tests, build the site and use Percy’s static snapshot command. The exact CLI options depend on the current CLI version; follow the official GitHub Actions guide for the documented command and directory syntax for your project.
A typical workflow shape is to install dependencies, generate the static output directory, and run the Percy snapshot command with the project token in the step environment. For example, after confirming the supported syntax for your installed CLI:
- name: Build site
run: npm run build
- name: Upload static pages to Percy
run: npx percy snapshot ./dist
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
Use the directory your build actually produces; common project output directories differ. The static snapshot workflow is separate from placing SDK snapshot calls inside Playwright or Cypress tests.
5. Establish and review the visual baseline
The first Percy build establishes the reference against which later snapshots are compared. Review the first build in Percy and approve or establish its baseline as required by your project. Later builds show visual changes for review. A snapshot upload by itself does not mean that every visual difference should be accepted automatically.
For Playwright, baseline discovery and automatic seeding can depend on the default Playwright configuration path. If the baseline does not map to the expected screenshots, check whether the test command points to a custom config and consult the Percy example Playwright project. The example also notes that its Automate integration has additional BrowserStack session requirements; those requirements apply to that setup, not automatically to every Percy workflow.
6. Tune snapshots for useful, repeatable comparisons
- Choose stable checkpoints. Wait for the page content that matters before capturing. Avoid snapshots while a loading state, animation, or transient notification is changing.
- Use descriptive names. Names such as
Product page — signed-outmake it easier to identify the test state in review. - Control test data. Prefer predictable fixtures and stable content. Dates, randomized values, and changing third-party content can create visual changes unrelated to your code.
- Keep snapshots focused. Capture states that protect important user-facing layouts and flows. An indiscriminate snapshot at every test step can increase review burden.
- Run the same intended suite under Percy. The wrapper is what activates Percy collection and upload in these SDK workflows. Running the tests without it can leave snapshots disabled.
- Keep integration versions maintained. CLI, SDK, Node runtime, and GitHub Actions versions all affect the workflow. Review updates periodically instead of copying old example pins as universal recommendations.
7. Troubleshoot common failures
| Symptom | Likely cause | What to do |
|---|---|---|
| No Percy build or snapshots appear | The test command ran without percy exec, or the Percy token was not available to the step. |
Wrap the actual test command in npx percy exec --. Confirm the secret is named PERCY_TOKEN and is exposed to that step. |
| Authentication or project association fails | The secret is missing, misspelled, unavailable in the event context, or contains the wrong project token. | Check the secret name and selected Percy project. For pull requests from forks, GitHub does not provide repository secrets to the workflow; use a trusted event/workflow design rather than placing the token in source. |
| The workflow passes but no test snapshot is present | The test did not reach the snapshot call, or the SDK was not imported or used as required. | Check test output and assertions, confirm the SDK installation/import, and verify that the snapshot call executes after the page is ready. |
| Snapshots are noisy or change between identical runs | Dynamic content, animations, delayed data, or unstable test fixtures differ between captures. | Make test data deterministic, wait for relevant content, and capture at a stable point in the user flow. |
| Playwright baseline seeding maps incorrectly | The example’s baseline discovery may assume Playwright’s default config path. | Check the config path used by the test command and the current Percy example guidance; align configuration where possible. |
| Static snapshots contain the wrong pages or no pages | The command points at a directory that was not generated or is not the site output directory. | Run the build first, inspect its output directory, then pass the correct directory using the current Percy CLI syntax. |
| A Percy integration example does not work with current packages | Examples can contain version-specific package, runtime, or action settings. | Use current official SDK and CLI instructions, check compatibility, and update the example’s version pins deliberately. |
| Automate-specific setup cannot find a session | The Automate drop-in path requires BrowserStack session setup described by its example. | Follow the Automate requirements in the example project; do not apply those extra steps to a standard local-browser setup unless you use Automate. |
8. Performance, reliability, and cost considerations
The cited Percy integration materials describe how to collect and upload snapshots, but do not establish a quantitative runtime or cost comparison between Playwright, Cypress, and static capture. Snapshot work adds browser and upload activity to the CI job; keep the captured states purposeful, avoid duplicating equivalent states, and use the project’s normal CI timeout and retry practices.
For reliability, run Percy in the same environment and at the same application revision as the tests being evaluated. Ensure the application is reachable before the snapshot call, use deterministic test data, and treat the initial baseline review as part of setup. Protecting the token as a secret also matters: a workflow that cannot access it cannot upload to the intended project.
Check Percy’s current product and project terms for your plan’s limits and pricing. The research sources for this guide do not establish current rates, quotas, or performance figures, so none are stated here.
9. Choose the integration that matches the output
| Approach | Use it when | Core setup |
|---|---|---|
| Playwright SDK | You want named captures from browser test states, or want to investigate the documented drop-in for existing screenshot assertions. | @percy/cli, @percy/playwright, snapshot call, and percy exec. |
| Cypress SDK | Your existing browser suite is Cypress and snapshots belong at Cypress test checkpoints. | @percy/cli, @percy/cypress, support import, cy.percySnapshot(), and percy exec. |
| Static CLI capture | You need to compare generated site output rather than interactive browser test states. | Build the site, then use the documented percy snapshot CLI workflow on its output. |
Or skip the browser setup
If your goal is to capture a rendered page image or PDF rather than compare Percy baselines inside your test suite, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo 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
ScreenshotNeo accepts cookie and consent banners like a visitor, then removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers say which page verdict and billing outcome applied. Its MCP server offers take_screenshot, get_page_info, and capture_pdf to AI agents and MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. ScreenshotNeo captures pages on request; it does not replace Percy’s visual baseline and review workflow.
Sign up free for 1,000 screenshots a month with no card.
FAQ
Can I run Percy only on pull requests?
Yes. Configure the workflow triggers to match your review process, such as pull requests and pushes to the baseline branch. Make sure the workflow event can access the token secret.
Do I need a Percy SDK for static pages?
For generated static output, Percy documents a CLI snapshot workflow. SDK calls are for capturing states within supported browser test frameworks.
Does Percy approve visual changes automatically?
Plan to review the first build and establish its baseline, then review later visual changes in Percy according to your team’s approval process.
Will an ordinary Playwright screenshot assertion upload to Percy?
Not by itself. Percy documents a drop-in option for existing toHaveScreenshot() assertions, with compatibility requirements. Check the current library documentation for the setup that applies to your versions.


