Tools: GitHub Actions Screenshots: Auto-Capture Deploy Evidence (5-Minute Setup)
GitHub Actions Screenshots: Auto-Capture Deploy Evidence (5-Minute Setup)
The Problem: Manual Screenshot Chaos
The Solution: Automated Screenshots in GitHub Actions
The Screenshot Capture Script
Real-World Example: Compliance Evidence
Environment Setup
Why This Beats Manual Screenshots
Next Steps In regulated industries, "prove what shipped" isn't optional ā it's compliance. When you deploy to production, auditors want evidence: screenshots of the UI state, configuration pages, deployment logs, environment settings. Most teams solve this manually: engineers take screenshots, attach them to Slack, lose them when the thread archives. There's a better way: automatically capture screenshots in GitHub Actions and attach them to every PR. Here's how to set it up in 5 minutes. Right now, your team probably: The evidence trail is broken. Here's a GitHub Actions workflow that: Create capture-screenshots.js: For regulated industries (fintech, healthcare, insurance), this workflow creates an audit trail: Add to GitHub Secrets: For teams shipping frequently (10+ deploys/week), automated screenshots save money and create compliance evidence simultaneously. Start free at pagebolt.dev/pricing ā 100 screenshots/month, no credit card required. For regulated teams: Screenshot automation + GitHub PR history = SOC 2 / compliance-grade deployment evidence. No manual work. No lost screenshots. Just proof. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
$ name: Deploy & Screenshot on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy to Staging run: | # Your deployment commands -weight: 500;">npm run build -weight: 500;">npm run deploy:staging - name: Wait for Deploy run: sleep 30 - name: Capture Screenshots env: PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }} run: | node capture-screenshots.js - name: Upload Screenshots to PR uses: actions/upload-artifact@v3 with: name: deploy-screenshots path: screenshots/ - name: Comment on PR uses: actions/github-script@v7 with: script: | const fs = require('fs'); const screenshots = fs.readdirSync('screenshots/'); let comment = '## šø Deployment Screenshots\n\n'; screenshots.forEach(file => { comment += `- [${file}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }})\n`; }); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: comment });
name: Deploy & Screenshot on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy to Staging run: | # Your deployment commands -weight: 500;">npm run build -weight: 500;">npm run deploy:staging - name: Wait for Deploy run: sleep 30 - name: Capture Screenshots env: PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }} run: | node capture-screenshots.js - name: Upload Screenshots to PR uses: actions/upload-artifact@v3 with: name: deploy-screenshots path: screenshots/ - name: Comment on PR uses: actions/github-script@v7 with: script: | const fs = require('fs'); const screenshots = fs.readdirSync('screenshots/'); let comment = '## šø Deployment Screenshots\n\n'; screenshots.forEach(file => { comment += `- [${file}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }})\n`; }); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: comment });
name: Deploy & Screenshot on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy to Staging run: | # Your deployment commands -weight: 500;">npm run build -weight: 500;">npm run deploy:staging - name: Wait for Deploy run: sleep 30 - name: Capture Screenshots env: PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }} run: | node capture-screenshots.js - name: Upload Screenshots to PR uses: actions/upload-artifact@v3 with: name: deploy-screenshots path: screenshots/ - name: Comment on PR uses: actions/github-script@v7 with: script: | const fs = require('fs'); const screenshots = fs.readdirSync('screenshots/'); let comment = '## šø Deployment Screenshots\n\n'; screenshots.forEach(file => { comment += `- [${file}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }})\n`; }); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: comment });
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path'); const PAGEBOLT_API_KEY = process.env.PAGEBOLT_API_KEY;
const STAGING_URL = process.env.STAGING_URL || 'https://staging.example.com'; async function captureScreenshots() { const screenshots = [ { name: 'homepage.png', url: `${STAGING_URL}/`, viewport: { width: 1280, height: 720 } }, { name: 'dashboard.png', url: `${STAGING_URL}/dashboard`, viewport: { width: 1280, height: 720 } }, { name: 'settings.png', url: `${STAGING_URL}/settings`, viewport: { width: 1280, height: 720 } } ]; const dir = 'screenshots'; if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } for (const screenshot of screenshots) { try { console.log(`Capturing ${screenshot.name}...`); const response = await fetch('https://api.pagebolt.com/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: screenshot.url, viewport: screenshot.viewport, format: 'png' }) }); if (!response.ok) { console.error(`Failed to capture ${screenshot.name}: ${response.statusText}`); continue; } const buffer = await response.buffer(); fs.writeFileSync(path.join(dir, screenshot.name), buffer); console.log(`ā Saved ${screenshot.name}`); } catch (error) { console.error(`Error capturing ${screenshot.name}:`, error.message); } } console.log(`\nā All screenshots captured and saved to ${dir}/`);
} captureScreenshots().catch(console.error);
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path'); const PAGEBOLT_API_KEY = process.env.PAGEBOLT_API_KEY;
const STAGING_URL = process.env.STAGING_URL || 'https://staging.example.com'; async function captureScreenshots() { const screenshots = [ { name: 'homepage.png', url: `${STAGING_URL}/`, viewport: { width: 1280, height: 720 } }, { name: 'dashboard.png', url: `${STAGING_URL}/dashboard`, viewport: { width: 1280, height: 720 } }, { name: 'settings.png', url: `${STAGING_URL}/settings`, viewport: { width: 1280, height: 720 } } ]; const dir = 'screenshots'; if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } for (const screenshot of screenshots) { try { console.log(`Capturing ${screenshot.name}...`); const response = await fetch('https://api.pagebolt.com/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: screenshot.url, viewport: screenshot.viewport, format: 'png' }) }); if (!response.ok) { console.error(`Failed to capture ${screenshot.name}: ${response.statusText}`); continue; } const buffer = await response.buffer(); fs.writeFileSync(path.join(dir, screenshot.name), buffer); console.log(`ā Saved ${screenshot.name}`); } catch (error) { console.error(`Error capturing ${screenshot.name}:`, error.message); } } console.log(`\nā All screenshots captured and saved to ${dir}/`);
} captureScreenshots().catch(console.error);
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path'); const PAGEBOLT_API_KEY = process.env.PAGEBOLT_API_KEY;
const STAGING_URL = process.env.STAGING_URL || 'https://staging.example.com'; async function captureScreenshots() { const screenshots = [ { name: 'homepage.png', url: `${STAGING_URL}/`, viewport: { width: 1280, height: 720 } }, { name: 'dashboard.png', url: `${STAGING_URL}/dashboard`, viewport: { width: 1280, height: 720 } }, { name: 'settings.png', url: `${STAGING_URL}/settings`, viewport: { width: 1280, height: 720 } } ]; const dir = 'screenshots'; if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } for (const screenshot of screenshots) { try { console.log(`Capturing ${screenshot.name}...`); const response = await fetch('https://api.pagebolt.com/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: screenshot.url, viewport: screenshot.viewport, format: 'png' }) }); if (!response.ok) { console.error(`Failed to capture ${screenshot.name}: ${response.statusText}`); continue; } const buffer = await response.buffer(); fs.writeFileSync(path.join(dir, screenshot.name), buffer); console.log(`ā Saved ${screenshot.name}`); } catch (error) { console.error(`Error capturing ${screenshot.name}:`, error.message); } } console.log(`\nā All screenshots captured and saved to ${dir}/`);
} captureScreenshots().catch(console.error);
// capture-screenshots.js with compliance metadata
async function captureWithMetadata() { const metadata = { timestamp: new Date().toISOString(), commit: process.env.GITHUB_SHA, branch: process.env.GITHUB_REF, environment: 'staging', version: require('./package.json').version }; const response = await fetch('https://api.pagebolt.com/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://staging.example.com/dashboard', viewport: { width: 1280, height: 720 }, format: 'png', // Metadata persists with the screenshot metadata: metadata }) }); const buffer = await response.buffer(); // Save with metadata in filename const filename = `screenshot-${metadata.timestamp.replace(/[:.]/g, '-')}.png`; fs.writeFileSync(path.join('screenshots', filename), buffer); console.log(`ā Screenshot saved with compliance metadata`);
}
// capture-screenshots.js with compliance metadata
async function captureWithMetadata() { const metadata = { timestamp: new Date().toISOString(), commit: process.env.GITHUB_SHA, branch: process.env.GITHUB_REF, environment: 'staging', version: require('./package.json').version }; const response = await fetch('https://api.pagebolt.com/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://staging.example.com/dashboard', viewport: { width: 1280, height: 720 }, format: 'png', // Metadata persists with the screenshot metadata: metadata }) }); const buffer = await response.buffer(); // Save with metadata in filename const filename = `screenshot-${metadata.timestamp.replace(/[:.]/g, '-')}.png`; fs.writeFileSync(path.join('screenshots', filename), buffer); console.log(`ā Screenshot saved with compliance metadata`);
}
// capture-screenshots.js with compliance metadata
async function captureWithMetadata() { const metadata = { timestamp: new Date().toISOString(), commit: process.env.GITHUB_SHA, branch: process.env.GITHUB_REF, environment: 'staging', version: require('./package.json').version }; const response = await fetch('https://api.pagebolt.com/v1/screenshot', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PAGEBOLT_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://staging.example.com/dashboard', viewport: { width: 1280, height: 720 }, format: 'png', // Metadata persists with the screenshot metadata: metadata }) }); const buffer = await response.buffer(); // Save with metadata in filename const filename = `screenshot-${metadata.timestamp.replace(/[:.]/g, '-')}.png`; fs.writeFileSync(path.join('screenshots', filename), buffer); console.log(`ā Screenshot saved with compliance metadata`);
}
PAGEBOLT_API_KEY: pf_live_xxx
STAGING_URL: https://staging.example.com
PAGEBOLT_API_KEY: pf_live_xxx
STAGING_URL: https://staging.example.com
PAGEBOLT_API_KEY: pf_live_xxx
STAGING_URL: https://staging.example.com - Deploys to staging manually and takes screenshots
- Posts them to Slack (where they disappear in 90 days)
- Or writes them to a shared folder (that nobody actually accesses)
- Auditors ask "what did you ship?" ā panic ā search Slack archives - Runs after deployment
- Takes a screenshot of your app
- Attaches it to the PR
- Sends a notification to your team - Engineer screenshots locally (5 min per deploy)
- Posts to Slack (lost after 90 days)
- Audit asks "what shipped?" ā dig through archives
- No timestamp, no commit metadata, no proof - Zero manual work per deploy
- Screenshots attached to PR (permanent record)
- Metadata (commit, timestamp, environment) embedded
- Audit asks "what shipped?" ā GitHub link + screenshot = instant proof
- Cost: $29-79/month vs. 5 minutes Ć $150/hr Ć weekly deploys = $1,560/month in engineer time - Copy the workflow YAML to .github/workflows/deploy-screenshots.yml
- Copy capture-screenshots.js to your repo root
- Add PAGEBOLT_API_KEY to GitHub Secrets
- Deploy ā see screenshots auto-attached to PR
- Auditors see proof of what shipped ā