Allscreenshots Docs
Guides

CI/CD integration

Automate screenshot capture in your deployment pipeline

CI/CD integration

Integrate screenshot capture into your CI/CD pipeline for automated visual testing, documentation updates, and deployment previews.

Use cases

  • Visual regression testing: Catch UI bugs before they reach production
  • Deploy previews: Generate screenshots of preview deployments for PR reviews
  • Documentation automation: Update screenshots in docs when UI changes
  • Release artifacts: Generate marketing screenshots for app store submissions

GitHub Actions

Visual testing on pull requests

# .github/workflows/visual-tests.yml
name: Visual Tests

on:
  pull_request:
    branches: [main]

jobs:
  visual-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build
        env:
          NODE_ENV: production

      - name: Start application
        run: npm start &

      - name: Wait for server
        run: npx wait-on http://localhost:3000 --timeout 60000

      - name: Capture screenshots
        run: node scripts/capture-screenshots.js
        env:
          ALLSCREENSHOTS_API_KEY: ${{ secrets.ALLSCREENSHOTS_API_KEY }}
          BASE_URL: http://localhost:3000

      - name: Compare with baselines
        run: node scripts/compare-screenshots.js

      - name: Upload diff images
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: visual-diffs
          path: screenshots/diffs/

      - name: Comment on PR
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '⚠️ Visual differences detected. See artifacts for details.'
            })

Deploy preview screenshots

Generate screenshots after Vercel/Netlify deployments:

# .github/workflows/preview-screenshots.yml
name: Preview Screenshots

on:
  deployment_status:

jobs:
  screenshot:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Get preview URL
        id: preview
        run: |
          echo "url=${{ github.event.deployment_status.target_url }}" >> $GITHUB_OUTPUT

      - name: Capture preview screenshots
        run: |
          curl -X POST 'https://api.allscreenshots.com/v1/screenshots/bulk' \
            -H 'Authorization: Bearer ${{ secrets.ALLSCREENSHOTS_API_KEY }}' \
            -H 'Content-Type: application/json' \
            -d '{
              "urls": [
                { "url": "${{ steps.preview.outputs.url }}" },
                { "url": "${{ steps.preview.outputs.url }}/pricing" },
                { "url": "${{ steps.preview.outputs.url }}/about" }
              ],
              "defaults": {
                "viewport": { "width": 1920, "height": 1080 },
                "blockAds": true
              }
            }' > bulk-job.json

      - name: Wait for screenshots
        run: |
          BULK_JOB_ID=$(jq -r '.bulkJobId' bulk-job.json)
          for i in {1..30}; do
            STATUS=$(curl -s -H "Authorization: Bearer ${{ secrets.ALLSCREENSHOTS_API_KEY }}" \
              "https://api.allscreenshots.com/v1/screenshots/bulk/$BULK_JOB_ID" \
              | jq -r '.status')
            if [ "$STATUS" = "completed" ]; then
              break
            fi
            sleep 5
          done

      - name: Comment screenshots on PR
        uses: actions/github-script@v7
        with:
          script: |
            const response = await fetch(
              `https://api.allscreenshots.com/v1/screenshots/bulk/${process.env.BULK_JOB_ID}`,
              { headers: { 'Authorization': `Bearer ${process.env.API_KEY}` } }
            );
            const job = await response.json();

            const screenshots = job.jobs
              .filter(j => j.status === 'completed')
              .map(j => `![${j.url}](${j.result.url})`)
              .join('\n\n');

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Deploy Preview Screenshots\n\n${screenshots}`
            });
        env:
          BULK_JOB_ID: ${{ steps.bulk.outputs.job_id }}
          API_KEY: ${{ secrets.ALLSCREENSHOTS_API_KEY }}

GitLab CI

# .gitlab-ci.yml
stages:
  - build
  - test
  - screenshot

visual-tests:
  stage: screenshot
  image: node:20
  script:
    - npm ci
    - npm run build
    - npm start &
    - npx wait-on http://localhost:3000
    - node scripts/visual-tests.js
  variables:
    ALLSCREENSHOTS_API_KEY: $ALLSCREENSHOTS_API_KEY
  artifacts:
    when: on_failure
    paths:
      - screenshots/diffs/
    expire_in: 1 week
  only:
    - merge_requests

CircleCI

# .circleci/config.yml
version: 2.1

jobs:
  visual-tests:
    docker:
      - image: cimg/node:20.0
    steps:
      - checkout

      - restore_cache:
          keys:
            - npm-deps-{{ checksum "package-lock.json" }}

      - run:
          name: Install dependencies
          command: npm ci

      - save_cache:
          paths:
            - node_modules
          key: npm-deps-{{ checksum "package-lock.json" }}

      - run:
          name: Build and start app
          command: |
            npm run build
            npm start &
          background: true

      - run:
          name: Wait for app
          command: npx wait-on http://localhost:3000

      - run:
          name: Run visual tests
          command: node scripts/visual-tests.js

      - store_artifacts:
          path: screenshots/diffs
          destination: visual-diffs

workflows:
  test:
    jobs:
      - visual-tests

Screenshot capture script

A reusable script for CI environments:

// scripts/capture-screenshots.js
const pages = [
  { name: 'home', path: '/' },
  { name: 'pricing', path: '/pricing' },
  { name: 'login', path: '/login' },
  { name: 'dashboard', path: '/dashboard' },
];

const viewports = [
  { name: 'desktop', width: 1920, height: 1080 },
  { name: 'mobile', width: 375, height: 667 },
];

async function captureScreenshots() {
  const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
  const outputDir = process.env.OUTPUT_DIR || './screenshots/current';

  // Ensure output directory exists
  await fs.mkdir(outputDir, { recursive: true });

  const urls = [];
  for (const page of pages) {
    for (const viewport of viewports) {
      urls.push({
        url: `${baseUrl}${page.path}`,
        options: {
          viewport: { width: viewport.width, height: viewport.height },
        },
        filename: `${page.name}-${viewport.name}.png`,
      });
    }
  }

  // Use bulk API for efficiency
  const response = await fetch('https://api.allscreenshots.com/v1/screenshots/bulk', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.ALLSCREENSHOTS_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      urls: urls.map(u => ({
        url: u.url,
        options: u.options,
      })),
      defaults: {
        format: 'png',
        waitUntil: 'networkidle',
        blockAds: true,
        blockCookieBanners: true,
      },
    }),
  });

  const { bulkJobId } = await response.json();
  console.log(`Bulk job started: ${bulkJobId}`);

  // Poll for completion
  let result;
  for (let i = 0; i < 60; i++) {
    const statusResponse = await fetch(
      `https://api.allscreenshots.com/v1/screenshots/bulk/${bulkJobId}`,
      { headers: { 'Authorization': `Bearer ${process.env.ALLSCREENSHOTS_API_KEY}` } }
    );
    result = await statusResponse.json();

    if (result.status === 'completed') break;
    if (result.status === 'failed') throw new Error('Bulk job failed');

    console.log(`Progress: ${result.completedUrls}/${result.totalUrls}`);
    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  // Download screenshots
  for (let i = 0; i < result.jobs.length; i++) {
    const job = result.jobs[i];
    if (job.status !== 'completed') continue;

    const imageResponse = await fetch(job.result.url);
    const buffer = await imageResponse.arrayBuffer();

    await fs.writeFile(
      `${outputDir}/${urls[i].filename}`,
      Buffer.from(buffer)
    );
    console.log(`Saved: ${urls[i].filename}`);
  }

  console.log('Screenshots captured successfully');
}

captureScreenshots().catch(err => {
  console.error(err);
  process.exit(1);
});

Secrets management

Never commit API keys to your repository. Use your CI provider's secrets management.

GitHub Actions

env:
  ALLSCREENSHOTS_API_KEY: ${{ secrets.ALLSCREENSHOTS_API_KEY }}

GitLab CI

variables:
  ALLSCREENSHOTS_API_KEY: $ALLSCREENSHOTS_API_KEY  # Set in CI/CD settings

CircleCI

# Set in Project Settings > Environment Variables
- run:
    command: echo $ALLSCREENSHOTS_API_KEY

Best practices

Parallel execution

Run visual tests in parallel with other test suites:

jobs:
  unit-tests:
    # ...

  integration-tests:
    # ...

  visual-tests:
    # ...

  # All jobs run in parallel by default

Caching

Cache dependencies to speed up builds:

- name: Cache node modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}

Conditional execution

Only run visual tests when relevant files change:

on:
  pull_request:
    paths:
      - 'src/**'
      - 'public/**'
      - 'package.json'

Baseline management

Store baselines in your repository or a separate storage:

# Update baselines on main branch merges
- name: Update baselines
  if: github.ref == 'refs/heads/main'
  run: |
    mv screenshots/current/* screenshots/baseline/
    git add screenshots/baseline/
    git commit -m "Update visual baselines"
    git push

On this page