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 => ``)
.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_requestsCircleCI
# .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-testsScreenshot 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 settingsCircleCI
# Set in Project Settings > Environment Variables
- run:
command: echo $ALLSCREENSHOTS_API_KEYBest 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 defaultCaching
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