feat(screenshot): Vercel preview screenshot → GitHub PR + Linear issue#3
Open
isadeks wants to merge 14 commits into
Open
feat(screenshot): Vercel preview screenshot → GitHub PR + Linear issue#3isadeks wants to merge 14 commits into
isadeks wants to merge 14 commits into
Conversation
… wiring yet) Lands the runtime pieces of the screenshot-on-preview-deploy feature: - `ScreenshotBucket` construct (`cdk/src/constructs/screenshot-bucket.ts`): public-read on `screenshots/*`, SSE-S3, 30-day TTL. Bucket policy scoped to the prefix so anything written outside is invisible. - GitHub webhook receiver (`cdk/src/handlers/github-webhook.ts`): HMAC-verifies `X-Hub-Signature-256`, filters to `deployment_status` events with `state=success` and `environment=Preview`, dedups on `(repo, deployment_id, status_id)`, async-invokes the processor. Topology mirrors `linear-webhook.ts`. - Webhook processor (`cdk/src/handlers/github-webhook-processor.ts`): Looks up the open PR for the deploy SHA via the GitHub Commits API, captures a screenshot of `deployment.environment_url` via AgentCore Browser, PUTs the PNG to the screenshot bucket, posts a markdown embed in a fresh PR comment. - AgentCore Browser wrapper (`cdk/src/handlers/shared/agentcore-browser.ts`): Drives Chrome DevTools Protocol over WebSocket directly, avoiding Playwright bloat. SigV4-signs the WSS handshake. Smoke-tested locally against example.com and a Vercel demo URL — 6.5s end-to-end, valid PNG. - GitHub webhook verify helper (`cdk/src/handlers/shared/github-webhook-verify.ts`): Mirrors `linear-verify.ts` — secret cache with 5min TTL, transparent re-fetch once on signature failure. Stack wiring (IAM grants, API Gateway route, Lambda construction) is the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New `GitHubScreenshotIntegration` construct (mirrors `LinearIntegration`): bundles the screenshot bucket, dedup table, signing-secret placeholder, receiver Lambda, processor Lambda, and the API Gateway route. cdk-nag suppressions added inline (HMAC auth instead of Cognito; AgentCore Browser sessions have no per-resource ARN; Secrets Manager rotation is owned by GitHub). - Wired into `agent.ts` after the LinearIntegration block. Reuses the existing `githubTokenSecret` (the processor uses ABCA's main GitHub token to look up which PR a deploy SHA belongs to and post the screenshot comment — no new credential). - Three new stack outputs: `GitHubWebhookUrl`, `GitHubWebhookSecretArn`, `ScreenshotBucketName`. - Bumped agent.test.ts table count from 13 to 14 to account for the new dedup table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ot bucket cdk-nag's S2 fires on any bucket that has `blockPublicPolicy: false` even when the policy is intentionally permissive. Add the suppression with the same rationale as S1/S5 — public reads are required by GitHub Markdown renderers and Linear `imageUploadFromUrl`, and the read grant is prefix-scoped to `screenshots/*`. Caught when the first deploy attempt aborted at synth-time on the new GitHubScreenshotIntegration construct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first deploy attempt failed at CFN-execute time on the bucket policy: s3:PutBucketPolicy ... because public policies are prevented by the BlockPublicPolicy setting in S3 Block Public Access. Account-level Block Public Access is on for this AWS account, which overrides per-bucket BPA settings. Disabling it would change the security posture of the whole account, so route around the constraint with the AWS-recommended pattern: private S3 + CloudFront with Origin Access Control. Changes: - `ScreenshotBucket` is now `BLOCK_ALL` BPA, no public bucket policy. Adds a `cloudfront.Distribution` whose origin is the bucket via `S3BucketOrigin.withOriginAccessControl`. The distribution policy is scoped to the CloudFront service principal only, so account-level BPA accepts it. - Processor reads `SCREENSHOT_PUBLIC_HOST` (the CloudFront domain) instead of building an S3 URL. PR comments now embed `https://<dist>.cloudfront.net/screenshots/...` URLs. - New stack output `ScreenshotCloudFrontDomain`. - Bucket-level S2/S5 suppressions removed (no longer applicable — bucket is private). Distribution gets CFR1/CFR2/CFR3/CFR4/CFR7 suppressions with rationales. Heads up on deploy time: CloudFront distributions take 5-15 min to provision on first create. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CommonRuleSet was 403'ing GitHub deployment_status webhooks before the request reached our Lambda — the deployment payload contains absolute Vercel preview URLs in the body, which trips GenericRFI_BODY. Mirror the Linear webhook exemption: the GitHub webhook path is HMAC-verified in the Lambda, parsed as strict JSON, never interpolated into SQL/HTML, and rate-limited by the priority-3 rule. CRS still applies to every other route. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…loyment
GitHub's `deployment_status` webhook puts the deployed URL on the
*status* object, not the deployment itself. The deployment object is
immutable per (sha, environment); the status changes through the
deploy lifecycle (`pending` → `success`) and carries the URL only
once the deploy finishes.
Symptom: receiver kept short-circuiting `success` events from Vercel
with `{ok: true, skipped_no_url: true}` because we read the wrong
field. Verified by inspecting the webhook delivery payload via
`gh api .../deliveries/<id> --jq .request.payload.deployment_status` —
URL was there all along.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dshake
Node 24's global WebSocket (from undici) does NOT support arbitrary
HTTP headers on the upgrade request — passing them as the second arg
gets silently ignored. AgentCore Browser's WSS handshake requires
SigV4-signed Authorization + X-Amz-* headers, so the connection was
opening but then getting rejected, which surfaced as an empty
`error` event ("AgentCore Browser WebSocket error: ").
Switch to the `ws` package which natively supports `options.headers`.
Also add an `unexpected-response` handler so HTTP-level handshake
failures (403, 400) surface with status codes instead of empty errors.
Smoke verified locally — the ws-based path opens cleanly against
example.com and Vercel preview URLs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lambda runtime returned a 403 on the WSS upgrade despite well-formed SigV4 headers — `ws` rewrites the Host header during the upgrade GET, which invalidates the canonical-request signature we computed against the original Host. This works locally because Node's tooling on macOS keeps the original Host through the handshake, but the Lambda runtime's TLS stack normalizes differently. Switch to query-parameter SigV4 (presigned URL): SignatureV4.presign returns a wss://...?X-Amz-Algorithm=...&X-Amz-Signature=... URL where the auth lives in the URL itself, so any Host-header rewriting downstream doesn't break the signature. Smoke verified locally — presigned URL connects cleanly to AgentCore Browser and the screenshot pipeline runs end-to-end (6.3s, valid PNG, captures example.com correctly). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The minimal IAM I shipped earlier (`StartBrowserSession`, `StopBrowserSession`, `GetBrowserSession`, `UpdateBrowserStream`) wasn't enough — the WSS automation-stream connect requires an additional `ConnectBrowserAutomationStream`-flavored action that isn't in the public CLI command list. Lambda invocations were opening sessions cleanly but 403'ing on the WSS upgrade. Widen to `bedrock-agentcore:*` to unblock the e2e flow. Followup: scope back down to the specific connect action once it's documented or surfaced via CloudTrail decoded-message-on-deny. Smoke verified: PR #1 on isadeks/vercel-abca-linear now receives a screenshot comment within ~7s of the deployment_status webhook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the screenshot processor to find a Linear issue via the PR's title/body and post the same image comment there. Approach (no GSI write-back needed): - Regex-extract Linear identifier (e.g. `ABCA-42`) from PR title/body. These are present whether the agent put them there (`task_description` carries the identifier) or Linear's own GitHub integration auto-injected the back-reference on PR open. - Scan `LinearWorkspaceRegistryTable` for `status=active` workspaces. Per-workspace, query Linear's `issueVcsBranchSearch` (which accepts the human-readable identifier) and accept the first exact-match hit. - Post the markdown image comment via the existing `postIssueComment` helper from Phase 2.0b. The Linear post is best-effort — if the registry table isn't wired, the identifier doesn't extract, or the lookup misses, the GitHub PR comment still lands. New env var `LINEAR_WORKSPACE_REGISTRY_TABLE_NAME` is optional on the processor; the construct only sets it when the prop is provided. CDK: `GitHubScreenshotIntegrationProps` gains an optional `linearWorkspaceRegistryTable`. When provided, the processor's IAM grows: ReadData on the registry, GetSecretValue+PutSecretValue on `bgagent-linear-oauth-*`. `agent.ts` wires `linearIntegration.workspaceRegistryTable` into the screenshot construct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The command still pulled from the parked PAK secret
(`LinearApiTokenSecretArn`), which we removed in Phase 2.0b. Symptom:
`Could not find LinearApiTokenSecretArn in stack outputs.`
Rewrite to scan Secrets Manager for `bgagent-linear-oauth-*` secrets
and query each workspace's projects with its own OAuth token. Supports
`--slug <slug>` to scope to one workspace; without it, queries every
installed workspace and labels each project with its source.
Also: switch to the `Bearer <token>` auth header and the
`teams(first: 1) { nodes { name } }` shape (the old `team` field on
Project no longer exists in Linear's GraphQL).
Adds a `LINEAR_OAUTH_SECRET_PREFIX` const in `linear-oauth.ts` to
keep the secret-name contract in one place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vercel posts the success `deployment_status` webhook the moment its build finishes, which on the Linear-driven path is ~7-15s before the agent's `gh pr create` returns. The processor's first lookup against the GitHub commit-pulls API came back empty and we'd silently drop the screenshot. Add a retry wrapper with backoff (0s, 5s, 10s, 20s — total max ~35s) around the PR lookup. The first hit returns immediately, so the warm-cache happy path is unchanged. Verified end-to-end on backgroundagent-dev: Linear issue ABCA-70 → agent → PR #2 in vercel-abca-linear → Vercel preview → screenshot landed on both the GitHub PR and the Linear issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…issue comment spam Move the trigger-label check ahead of every user-facing comment path in the Linear webhook processor, and switch the default trigger label from 'bgagent' to 'abca'. An unlabeled issue is now a true no-op: no comment, no reaction, no createTaskCore, no DDB writes — regardless of whether the project is onboarded. Why: workspace webhooks fire workspace-wide. A single un-onboarded team in the same Linear workspace produced 47 identical "❌ project isn't onboarded" comments on GRO-783 in 5 minutes because every Issue event (create/update/label-change) hit the not-onboarded gate before the label gate. With the gate order flipped, only issues that explicitly opt in via the trigger label can ever generate user-facing feedback. Per-project label_filter override is still respected — the project mapping lookup now happens once, before the label gate, instead of after. Tests: two new regression tests pin the spam scenario (unlabeled issue in a non-onboarded project, and unlabeled issue with no projectId) to zero side effects. Full CDK suite (89 suites / 1572 tests) passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator-facing setup walkthrough: 1. Connect Vercel to the GitHub repo 2. Vercel project settings (Git events on, Deployment Protection off for the demo, with a "production hardening" caveat for signed bypass) 3. Onboard the repo to ABCA (RepoTable put + bgagent linear onboard-project) 4. Configure the GitHub webhook (URL + secret from stack outputs, subscribe to Deployment statuses only) 5. Smoke test (label a Linear issue, watch screenshot land on PR + Linear) Includes a troubleshooting section indexed by symptom (401/403 from webhook, no comment lands, Linear post missing, CloudFront 403, Vercel auth wall) and a forward-looking "production hardening" list for when the feature graduates from demo. Wires the new guide into the Starlight sync (docs/scripts/sync-starlight.mjs) and sidebar (docs/astro.config.mjs).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After ABCA opens a PR for a Linear-driven task, Vercel deploys the preview, posts a
deployment_statusevent back to GitHub, and ABCA's webhook receiver:Smoke-validated end-to-end on
backgroundagent-dev: Linear issue ABCA-70 → vercel-abca-linear PR #2 → screenshot landed on both GitHub PR and Linear issue, ~10s after Vercel reported the deploy.Architecture
aws.browser.v1) driven over CDP via thewspackage, with SigV4-presigned WSS URL. Avoids Playwright bloat in the Lambda bundle./v1/github/webhookis excluded fromAWSManagedRulesCommonRuleSetfor the same reason/v1/linear/webhookis — webhook payloads with absolute URLs tripGenericRFI_BODYotherwise.deployment_status5-15s before the agent'sgh pr createreturns; the processor retries the PR lookup with backoff (0s/5s/10s/20s) to handle the race.What's in this PR
cdk/src/{constructs,handlers}/for the GitHub webhook → screenshot pipelineGitHubWebhookUrl,GitHubWebhookSecretArn,ScreenshotCloudFrontDomain)bgagent linear list-projectsrewritten to use OAuth (was still on the parked PAK secret)docs/guides/VERCEL_SETUP_GUIDE.md— operator-facing setup walkthrough + troubleshootingTest plan
Followups (tracked, not blocking)
bedrock-agentcore:*to the specific Browser action once discoverable via CloudTrailbgagent github webhook-infoCLI command for setup ergonomics