ScreenshotNeo

BlogHow-to

How to Capture Cypress Screenshots in GitHub Actions

Save Cypress screenshots in GitHub Actions, upload them as artifacts, keep only failures, and troubleshoot paths, retention, and CI failures.

By the ScreenshotNeo team1 October 20267 min read

How to Capture Cypress Screenshots in GitHub Actions

Use Cypress’s built-in screenshots and upload cypress/screenshots with actions/upload-artifact. Cypress captures an explicit checkpoint when your test calls cy.screenshot(). During cypress run, it also captures a screenshot when a test fails unless screenshotOnRunFailure is disabled. The default output directory is cypress/screenshots.

The workflow below runs Cypress in Chrome and uploads screenshots when the job fails. The if: failure() condition matters: it lets the upload step run after a failed Cypress step. if-no-files-found: ignore keeps a run without screenshots from producing an artifact warning or error.

1. Add the GitHub Actions workflow

name: Cypress tests

on: [push, pull_request]

jobs:
  cypress-run:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7

      - name: Cypress run
        uses: cypress-io/github-action@v7
        with:
          build: npm run build
          start: npm start
          browser: chrome

      - name: Upload Cypress screenshots
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: cypress-screenshots
          path: cypress/screenshots
          if-no-files-found: ignore

Save this as .github/workflows/cypress.yml. The maintained Cypress GitHub Action README documents this pattern. Check action major versions and runner images when you edit an existing workflow because those releases change over time.

A failed Cypress run creates a screenshot that the workflow stores as a downloadable artifact.
A failed Cypress run creates a screenshot that the workflow stores as a downloadable artifact.

Upload screenshots on every run

Remove if: failure() when you want to publish deliberate screenshots from passing tests as well as failure screenshots:

- name: Upload Cypress screenshots
  if: always()
  uses: actions/upload-artifact@v7
  with:
    name: cypress-screenshots
    path: cypress/screenshots
    if-no-files-found: ignore

always() is useful when the Cypress command succeeds and you still want checkpoint images. It also allows the upload step to run after a failed test. Keep the upload after the Cypress step so the directory has been generated.

2. Capture deliberate checkpoints with cy.screenshot()

Call cy.screenshot() wherever a visual checkpoint helps diagnose a flow:

describe('checkout', () => {
  it('shows the payment page', () => {
    cy.visit('/checkout');
    cy.get('[data-cy=continue-to-payment]').click();
    cy.screenshot('checkout/payment');
    cy.get('[data-cy=payment-form]').should('be.visible');
  });
});

The named image is written below the screenshots folder, so this example produces a path similar to cypress/screenshots/checkout/payment.png. Cypress creates nested directories as needed. If a name already exists, Cypress appends (1), (2), and so on. Use { overwrite: true } when replacement is intentional:

cy.screenshot('login-page', { overwrite: true });

Screenshot capture is asynchronous and takes around 100 ms. A very fast UI transition can therefore change a small part of the image after the command is issued; assert the state you need before taking the screenshot.

3. Configure automatic failure screenshots

Automatic screenshots happen in headed or headless cypress run executions when a test fails. They use Cypress’s normal naming scheme with (failed) appended. To keep the default behavior explicit, set it in cypress.config.js:

const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    screenshotOnRunFailure: true,
    screenshotsFolder: 'cypress/screenshots',
    trashAssetsBeforeRuns: true
  }
});

Use screenshotOnRunFailure: false when failure images contain data you cannot retain or when another reporter owns screenshots. Cypress clears the screenshots directory before a run by default. Set trashAssetsBeforeRuns: false only when you deliberately need files from earlier runs; otherwise stale images can be mistaken for current evidence.

Changing the output folder

module.exports = defineConfig({
  e2e: {
    screenshotsFolder: 'artifacts/cypress/screenshots'
  }
});

If you change this value, change the artifact path to match it:

- name: Upload Cypress screenshots
  if: failure()
  uses: actions/upload-artifact@v7
  with:
    name: cypress-screenshots
    path: artifacts/cypress/screenshots
    if-no-files-found: ignore

4. Understand paths and naming

Failure images and explicit images are placed beneath the screenshots folder. Cypress mirrors the spec structure after removing the common ancestor. The resulting path can change when the set of specs changes, so consume the artifact as a directory rather than hard-coding one absolute filename.

Keep generated files out of source control:

# .gitignore
cypress/screenshots/
cypress/videos/

They are regenerated in CI and should be retained through workflow artifacts or Cypress Cloud. The Cypress test organization guide covers the generated asset directories.

5. Upload videos separately when needed

The Cypress GitHub Action README shows a separate artifact upload for videos. Add one if your run records them:

- name: Upload Cypress videos
  if: failure()
  uses: actions/upload-artifact@v7
  with:
    name: cypress-videos
    path: cypress/videos
    if-no-files-found: ignore

Separate names make it clear whether a reviewer should download screenshots, videos, or both. Use if: always() for videos from successful runs as well.

6. Choose GitHub artifacts or Cypress Cloud

Need Best fit
Download PNGs from one workflow run GitHub workflow artifacts
Failure-only retention Artifact upload guarded by if: failure()
Every-run checkpoint images Artifact upload with if: always()
Centralized run history, replay, and contextual failure details Cypress Cloud

GitHub provides actions/upload-artifact and actions/download-artifact for storing and retrieving files tied to a workflow run. Cypress Cloud is an optional hosted layer with shareable reports, Test Replay, screenshots, videos, and contextual failure details. The Cypress GitHub Actions guide describes both choices.

Decide how long artifacts should be retained and whether storage usage is worth keeping screenshots from passing runs. Failure-only uploads reduce storage and make the useful evidence easier to find.

7. Troubleshooting

No artifact appears

  • The upload step was skipped: a failed job skips later steps unless the step condition permits it. Use if: failure() for failure evidence or if: always() for every run.
  • The path is wrong: confirm screenshotsFolder and make the artifact path identical.
  • No screenshot was generated: the test may have passed without calling cy.screenshot(), or automatic failure screenshots may be disabled. Use if-no-files-found: ignore when an empty directory is expected.

The folder is empty even though a previous run had images

Cypress clears screenshots before each run by default. This prevents stale evidence. If preserving files between runs is intentional, set trashAssetsBeforeRuns: false, but use unique artifact names or cleanup rules so old images are not confused with current results.

The screenshot shows the wrong UI state

Wait for the state you want before capturing it, for example with cy.get('[data-cy=ready]').should('be.visible'). Remember that capture is asynchronous and can take around 100 ms.

Failure paths changed between commits

Cypress derives asset paths from the spec layout after removing the common ancestor. Adding, removing, or moving specs can change paths. Download the artifact directory and inspect its current tree instead of relying on a fixed path.

The workflow fails before Cypress starts

Check that npm run build and npm start exist in package.json, that the application listens on the expected port, and that the selected browser is available on the runner. The upload step cannot create screenshots when Cypress never launches.

8. Performance, reliability, and cost

  • Capture only useful states: each explicit screenshot adds work and artifact bytes. Put checkpoints around transitions or important visual states rather than every command.
  • Prefer failure-only uploads for large suites: this keeps routine runs fast and limits artifact storage.
  • Use deterministic waits: assert visible selectors and stable content instead of fixed delays wherever possible.
  • Keep upload after test execution: this guarantees generated files exist and allows a failure condition to collect them.
  • Plan retention: GitHub artifact retention and storage settings determine how long old images remain available. Cypress Cloud is useful when you need searchable history and replay rather than isolated downloads.
  • Protect sensitive data: screenshots can contain account details, tokens displayed in the UI, or customer data. Redact test fixtures and restrict artifact access accordingly.
Screenshot cleanup removes common overlays before the final image is captured.
Screenshot cleanup removes common overlays before the final image is captured.

Or skip the browser setup

For a one-off page image outside a Cypress test, ScreenshotNeo provides a website screenshot API. Its cleanup steps accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers. It also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options. This minimal call returns a WebP image:

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}`);

ScreenshotNeo supports full-page and element captures, dark mode, device presets, custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, async webhooks, bulk capture, and a usage API. Every feature is on every plan. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

FAQ

Does cy.screenshot() work in headed mode?

Yes. It is an explicit Cypress command and can be used during interactive development as well as CI runs.

Why do duplicate screenshot names get numbered?

Cypress avoids overwriting by appending (1), (2), and so on. Pass { overwrite: true } when replacing an existing name is intended.

Should screenshots be committed to Git?

No. Generated screenshot and video directories belong in .gitignore; retain them as workflow artifacts or in Cypress Cloud.

Can I upload screenshots from passing and failing tests?

Yes. Use if: always() on the artifact step and include explicit cy.screenshot() checkpoints.

What is the default screenshot directory?

cypress/screenshots, unless you change Cypress’s screenshotsFolder configuration.

When should I use Cypress Cloud?

Use it when your team needs centralized run history, shareable reports, Test Replay, or contextual failure details. Use GitHub artifacts for lightweight files tied to individual workflow runs.

Sources