Deploy preview screenshots guide
Deploy preview screenshots setup guide
Section titled “Deploy preview screenshots setup guide”Wire your repo into ABCA so that every preview deploy gets screenshotted and posted as a comment on the open GitHub PR. If you also have Linear configured, the same screenshot is posted to the linked Linear issue as a bonus.
The pipeline only needs GitHub. Linear posting is opt-in: present iff
LinearWorkspaceRegistryTablehas at least one active row (configured via Linear setup guide). Without Linear, the GitHub-side screenshot still works; the Linear-side just no-ops silently.
Works with any provider that posts deployment_status
Section titled “Works with any provider that posts deployment_status”The pipeline doesn’t care who built the deploy — it only listens for GitHub deployment_status events. Any provider that calls the GitHub Deployments API works:
| Provider | Out of the box? | Notes |
|---|---|---|
| Vercel (managed hosting + GitHub app) | ✅ | The worked example below uses this. Default environment is Preview. |
| AWS Amplify Hosting (Connected to GitHub) | ✅ | Posts deployment_status for each branch deploy. environment is the branch name — set SCREENSHOT_TARGET_ENVIRONMENT to your preview branch (or use the same value on every branch via the BackgroundAgentStack construct prop). |
| Netlify (managed hosting + GitHub app) | ⚠ | environment is Deploy Preview <PR#>, which the current single-string SCREENSHOT_TARGET_ENVIRONMENT filter doesn’t match across all PRs. Workable today only by picking one specific PR’s environment string; broader pattern matching isn’t shipped. |
GitHub Actions that calls POST /repos/.../deployments (typical for ECS/Fargate, Cloud Run, Fly.io, Railway, Cloudflare Pages, etc.) | ✅ | Your workflow controls the environment field; pass whatever you want and set SCREENSHOT_TARGET_ENVIRONMENT to match. |
| External CI (CircleCI, GitLab, ArgoCD) that doesn’t touch GitHub Deployments | ❌ | Add a final job that calls the GitHub Deployments API after the deploy succeeds — see GitHub’s example. |
ABCA needs only two things from a deploy:
- The
deployment_statusevent has reachedstate: success. deployment_status.environment_urlis populated with the live preview URL.
If your provider gives you that, you’re done. The example below is Vercel because that’s what we smoke-tested on; the pipeline doesn’t otherwise prefer one provider over another.
What you get
Section titled “What you get”When you (or the agent) push to a branch that triggers a preview deploy, your provider deploys the preview, posts a deployment_status event back to GitHub, and ABCA’s webhook receiver:
- Captures a full-page screenshot of the preview URL via AgentCore Browser
- Uploads the PNG to a private S3 bucket served via CloudFront
- Posts a markdown image comment on the open GitHub PR
- (Optional) If Linear is wired: looks up the Linear issue by identifier in the PR title/body (e.g.
ABCA-42) and posts the same screenshot as a Linear comment. Skipped silently if Linear isn’t configured or no identifier is present.
End-to-end latency: typically 10–15 seconds after your provider reports the deploy.
How it works
Section titled “How it works”agent push → provider preview build → deployment_status webhook ↓ POST /v1/github/webhook ↓ receiver Lambda (HMAC verify, dedup, state=success + environment filter) ↓ processor Lambda ↓ AgentCore Browser session ↓ PNG → private S3 (30-day TTL) ↓ CloudFront-served public URL ↓ GitHub PR comment (+ Linear issue comment if linked)Architecture notes:
- Lambda-only. No agent runtime is involved post-PR — the screenshot job is deterministic; an LLM would only add cost without changing behavior.
- AWS-managed default browser. AgentCore Browser ships an
aws.browser.v1session you can attach to without provisioning your own browser resource. - Private S3 + CloudFront with OAC. Screenshot bucket is fully private; CloudFront serves images anonymously over HTTPS so GitHub markdown image embeds (and Linear’s, when configured) can render them without auth.
- WAF exemption. The
/v1/github/webhookpath is exempted from theSizeRestrictions_BODYrule inAWSManagedRulesCommonRuleSetbecause the fulldeployment_statuspayload (workflow run history + deploy URLs + deployment metadata) exceeds the 8 KB body-size limit. All other CRS rules (LFI, RFI, XSS, SQLi, …) still evaluate against the path; HMAC verification in Lambda authenticates the body. - Skips non-2xx pages. The processor enables CDP’s
Networkdomain and captures the main-document HTTP status. If the preview URL returns 4xx/5xx (404 / 503 / a 3xx that doesn’t redirect cleanly), the processor logsPreview URL returned HTTP <status>; skipping screenshotand posts no PR/Linear comment. This avoids posting a confidently-wrong screenshot of a 404 page as if it were the deploy. Auth walls that return HTTP 200 (e.g. Vercel deployment protection) are out of scope — disable deployment protection or use a public preview, see the Vercel setup section below.
Prerequisites
Section titled “Prerequisites”- ABCA stack deployed (
mise //cdk:deploy) — confirmGitHubWebhookUrl+GitHubWebhookSecretArn+ScreenshotCloudFrontDomainare listed in the stack outputs - (Optional) Linear OAuth installed for at least one workspace (
bgagent linear setup <slug>) — only required if you want screenshots posted to Linear issues in addition to the GitHub PR - A GitHub repo you own
- Your deploy provider connected to that repo (the example uses Vercel)
- AWS CLI logged in to the same account as the ABCA stack
- The
bgagentCLI installed (bgagent configure,bgagent login)
Step-by-step setup (Vercel example)
Section titled “Step-by-step setup (Vercel example)”Step 1 — Connect Vercel to your GitHub repo
Section titled “Step 1 — Connect Vercel to your GitHub repo”- Open https://vercel.com/dashboard.
- Add New → Project.
- Find your repo in the list. If it’s not visible, click “Adjust GitHub App Permissions” and grant access.
- Click Import.
- Accept the framework defaults — Vercel auto-detects most stacks.
- Click Deploy. Wait for the first deploy to finish.
Step 2 — Vercel project settings
Section titled “Step 2 — Vercel project settings”Go to your-project → Settings in the Vercel dashboard.
Settings → Git
Section titled “Settings → Git”- Connected Git Repository: confirm the repo is listed.
deployment_statusEvents: toggle Enabled (this is what tells Vercel to post the webhook to GitHub when each deploy finishes).- Pull Request Comments: optional — Vercel’s own comment with the preview URL. Doesn’t affect ABCA either way.
Settings → Deployment Protection
Section titled “Settings → Deployment Protection”- Vercel Authentication: set to Disabled (or “Only Production Deployments”) for the demo. Otherwise AgentCore Browser will hit a Vercel auth wall and screenshot the login page instead of your app.
Production hardening. Real deployments should keep Vercel Authentication on Standard Protection and use a signed bypass token. The screenshot processor would need to inject the bypass token as a query parameter on the preview URL it navigates to — currently not implemented.
Using a different provider? Skip Steps 1–2 and follow your provider’s instructions to publish
deployment_statusevents to GitHub. For Amplify Hosting, that’s automatic when the app is connected via GitHub. For self-hosted CI, add agh api repos/.../deploymentsstep at the end of your deploy job. Then continue with Step 3.
Step 3 — Configure the GitHub webhook
Section titled “Step 3 — Configure the GitHub webhook”This wires deploys back to ABCA’s screenshot pipeline.
3a. Get the webhook config
Section titled “3a. Get the webhook config”bgagent github webhook-infoThe CLI prints the webhook URL and the values to paste into GitHub.
3b. Add the webhook on the GitHub repo
Section titled “3b. Add the webhook on the GitHub repo”- Open
https://github.com/<your-org>/<your-repo>/settings/hooks. - Click Add webhook.
- Fill in the values printed by
webhook-info:- Payload URL: the URL it printed
- Content type:
application/json - Secret: generate any random string — paste it both here AND into the next step
- SSL verification: leave enabled
- Which events?: choose “Let me select individual events”, uncheck Pushes, check Deployment statuses only
- Active: ✓
- Add webhook. GitHub fires a
pingevent right away — under “Recent Deliveries” you should see ✅ within seconds.
3c. Mirror the signing secret into AWS
Section titled “3c. Mirror the signing secret into AWS”bgagent github set-webhook-secretPaste the same secret you generated in 3b. The CLI writes it to the stack’s GitHubWebhookSecret Secrets Manager entry, where the receiver Lambda reads it for HMAC verification.
Step 4 — Smoke test
Section titled “Step 4 — Smoke test”Open any PR on the configured repo (push a commit, open a PR however you normally do — GitHub UI, gh pr create, GitHub Actions, agent, etc.) Wait 2–5 minutes for your provider to build the preview. The screenshot should land on the PR as a markdown image comment.
If you also have Linear configured: create a Linear issue in a mapped project (e.g. “Update homepage heading”), apply the trigger label, and watch the agent open a PR. The same screenshot lands on both the GitHub PR and the Linear issue. If the GitHub comment shows but Linear doesn’t, see Troubleshooting.
Configuring for non-Vercel providers
Section titled “Configuring for non-Vercel providers”The pipeline filters incoming webhooks against SCREENSHOT_TARGET_ENVIRONMENT (default Preview, matches Vercel’s per-PR environment label). To use a different value, pass screenshotTargetEnvironment to the GitHubScreenshotIntegration construct in your CDK app and redeploy.
| Provider | Typical environment value | What to set |
|---|---|---|
| Vercel | Preview | leave default |
| Amplify Hosting | branch name (e.g. main, staging) | the branch you treat as preview |
| Netlify | Deploy Preview <PR#> | currently not directly matchable across all PRs (single fixed-string filter only) |
| GitHub Actions custom | whatever your workflow passes | match it exactly |
Troubleshooting
Section titled “Troubleshooting”GitHub webhook deliveries return 401 / 403
Section titled “GitHub webhook deliveries return 401 / 403”- 401 “Missing signature”: the request didn’t reach our Lambda — check that you saved the webhook with the right signing secret.
- 401 “Invalid signature”: the secret you pasted into GitHub doesn’t match what’s stored in AWS. Re-run
bgagent github set-webhook-secretwith the value from the GitHub webhook page. - 403 “Forbidden” with
X-Amzn-Errortype: ForbiddenException: WAF rejected the body. Should not happen on the/v1/github/webhookpath because that path is exempted from the CommonRuleSet, but if you see it, check theBlockedRequestsmetric on theTaskApiWebAclregional WebACL in CloudWatch.
Webhook delivers 200 but no screenshot lands
Section titled “Webhook delivers 200 but no screenshot lands”Check the screenshot processor logs:
aws lambda list-functions --region us-east-1 \ --query "Functions[?contains(FunctionName, 'GitHubScreenshot') && contains(FunctionName, 'Processor')].FunctionName" \ --output textThen tail the function’s CloudWatch log group. Common silent skips:
skipped_state— the delivery was for a non-successstatus (e.g.pending,in_progress); ignore.skipped_environment— the deploy’senvironmentfield doesn’t matchSCREENSHOT_TARGET_ENVIRONMENT. Common cause for non-Vercel providers; see “Configuring for non-Vercel providers” above.skipped_no_url— thesuccessstatus didn’t includeenvironment_url. Some providers post URL-less success events; the next push usually carries the URL.No open PR found for SHA after retries— the deploy provider built and reported faster than the agent couldgh pr create(race window > 35s). Rare; redeliver the webhook from GitHub’s UI to retry.
Screenshot lands on GitHub PR but not on Linear
Section titled “Screenshot lands on GitHub PR but not on Linear”The GitHub-side post is the primary path; Linear is opt-in and best-effort. Skipping the Linear post is normal if you don’t have Linear configured. If you do, look for the processor log line Linear identifier did not resolve to an issue — usually means:
- The PR title and body don’t contain a Linear-style identifier (e.g.
ABCA-42). The agent’s task description includes the identifier by default; if you opened the PR manually it might not. - The identifier’s workspace isn’t OAuth-installed. Run
bgagent linear list-projectsto confirm the issue’s project is in the registry.
CloudFront serves a 403
Section titled “CloudFront serves a 403”Visit the public URL directly:
https://<ScreenshotCloudFrontDomain>/screenshots/<owner>_<repo>/<sha>-<deploymentId>-<16hex>.png(Copy the exact URL from the PR comment — the <16hex> suffix is random per capture, so you can’t hand-construct it.)
If it 403s, check that the bucket policy includes the OAC service principal (CDK should generate this automatically — re-deploy if it doesn’t).
Screenshot shows a login page (Vercel only)
Section titled “Screenshot shows a login page (Vercel only)”You forgot Step 2’s “Vercel Authentication: Disabled” toggle. Toggle it off, push another commit, and confirm the next screenshot renders the actual app.
Production hardening considerations
Section titled “Production hardening considerations”Things to think about before using this on a real product:
- Deploy protection. This guide turns Vercel Authentication off so the headless browser can render the preview. For real use, you’ll want it back on with a signed bypass token (or your provider’s equivalent) and the bypass injected onto the preview URL the screenshot processor navigates to.
- IAM scope. The screenshot processor’s IAM is scoped to the three AgentCore Browser actions the handler calls —
StartBrowserSession,StopBrowserSession,ConnectBrowserAutomationStream— plus standard Lambda + S3 + Secrets Manager grants. The first two are control-plane writes; the third is the data-plane SigV4-presigned WSS handshake (it’s published in the AWS Service Authorization Reference forbedrock-agentcore, which also notes it takes no resource types or condition keys). Resource is*because Browser sessions are ephemeral and the data-plane stream actions don’t support resource-level scoping. A cdk-nag IAM5 suppression annotates the resource wildcard. - SSRF surface. The processor navigates AgentCore Browser to
deployment_status.environment_urlfrom the verified webhook payload. The handler validates the URL up front (https only, no literal-IP, no localhost / link-local / loopback) so a forged payload can’t pivot the browser at private hosts. AgentCore Browser also runs outside the customer VPC, so IMDS and private-subnet pivots are neutralized regardless. Stricter operators can add an explicit hostname allowlist by editingisAllowedScreenshotUrlincdk/src/handlers/shared/screenshot-url.ts. - Screenshot URL enumerability. The bucket is private, but CloudFront serves anonymously and the path follows
screenshots/<owner>_<repo>/<sha>-<8-byte-random>.png. The 64-bit random suffix makes URLs unguessable for an outside reader (the prefix is enumerable from the public PR; the suffix is not). If your previews regularly render PII or other regulated content, consider also enabling CloudFront access logs + a WAF in front of the CDN and shortening screenshot retention below the 30-day default (constant incdk/src/constructs/screenshot-bucket.ts).