Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2b57211
feat(screenshot): preview-deploy screenshot pipeline (no stack wiring…
isadeks May 20, 2026
ca5ab14
feat(screenshot): GitHubScreenshotIntegration construct + stack wiring
isadeks May 20, 2026
8138e86
fix(screenshot): suppress AwsSolutions-S2 on the public-read screensh…
isadeks May 20, 2026
235710e
fix(screenshot): private S3 bucket + CloudFront distribution
isadeks May 20, 2026
36e8d14
fix(waf): exempt /v1/github/webhook from CRS like /v1/linear/webhook
isadeks May 21, 2026
bb5e5d1
fix(screenshot): read environment_url from deployment_status, not dep…
isadeks May 21, 2026
8b7adf4
fix(agentcore-browser): use ws package for SigV4-signed WebSocket han…
isadeks May 21, 2026
043cb84
fix(agentcore-browser): SigV4-presign WSS URL instead of signing headers
isadeks May 21, 2026
a2466cb
fix(iam): grant bedrock-agentcore:* to the screenshot processor
isadeks May 21, 2026
7bd6412
feat(screenshot): also post screenshot comment to linked Linear issue
isadeks May 21, 2026
e7d3a19
fix(screenshot): retry PR lookup to handle deploy-before-PR race
isadeks May 21, 2026
b81eee6
fix(linear): silent label gate + default to 'abca' to stop unlabeled-…
May 21, 2026
bce3aa6
docs(screenshots): add the screenshot pipeline guide
isadeks May 21, 2026
62829a0
feat(github): bgagent github webhook-info + set-webhook-secret
isadeks May 27, 2026
734c124
docs/code(screenshots): de-Vercel-ize the screenshot pipeline
isadeks May 27, 2026
1ce013d
docs(screenshots): drop redundant Step 3 + condescending hardening pr…
isadeks May 27, 2026
99e2b06
docs(screenshots): drop 'followup' framing — describe gaps as current…
isadeks May 27, 2026
a444266
docs(screenshots): de-Linear-ize — Linear is opt-in, not required
isadeks May 27, 2026
6e57515
feat(screenshot): hide URL behind 'preview link' label in comments
isadeks May 28, 2026
7d994b8
docs(screenshots): add USER_GUIDE / COST_MODEL / ROADMAP coverage
isadeks Jun 1, 2026
f9824f4
docs(linear): clarify teammate-onboarding handshake
isadeks Jun 2, 2026
d4c3aa0
fix(github-cli): de-Vercel-ize webhook-info / set-webhook-secret strings
isadeks Jun 2, 2026
dac4e31
fix(github-cli): replace template literal with single quotes (eslint …
isadeks Jun 2, 2026
8c8b7e3
Merge branch 'main' into feat/240-agentcore-screenshots
krokoko Jun 2, 2026
e791e62
fix(screenshot): krokoko PR-241 review — scope IAM + cosmetic Vercel …
isadeks Jun 4, 2026
4be999f
Merge branch 'main' into feat/240-agentcore-screenshots
isadeks Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,29 @@
"@aws-cdk/aws-bedrock-agentcore-alpha": "2.257.0-alpha.0",
"@aws-cdk/aws-bedrock-alpha": "2.257.0-alpha.0",
"@aws-cdk/mixins-preview": "2.257.0-alpha.0",
"@aws-crypto/sha256-js": "^5.2.0",
"@aws-sdk/client-bedrock-agentcore": "^3.1046.0",
"@aws-sdk/client-bedrock-runtime": "^3.1021.0",
"@aws-sdk/client-dynamodb": "^3.1021.0",
"@aws-sdk/client-ecs": "^3.1021.0",
"@aws-sdk/client-lambda": "^3.1021.0",
"@aws-sdk/client-s3": "^3.1021.0",
"@aws-sdk/client-secrets-manager": "^3.1021.0",
"@aws-sdk/credential-provider-node": "^3.972.29",
"@aws-sdk/lib-dynamodb": "^3.1021.0",
"@aws-sdk/s3-presigned-post": "^3.1021.0",
"@aws-sdk/s3-request-presigner": "^3.1021.0",
"@aws/durable-execution-sdk-js": "^1.1.0",
"@cedar-policy/cedar-wasm": "4.10.0",
"@smithy/protocol-http": "^5.3.12",
"@smithy/signature-v4": "^5.3.14",
"aws-cdk-lib": "^2.257.0",
"cdk-nag": "^2.38.2",
"constructs": "^10.3.0",
"pdf-parse": "^1.1.1",
"js-yaml": "^4.1.1",
"ulid": "^3.0.2"
"pdf-parse": "^1.1.1",
"ulid": "^3.0.2",
"ws": "^8.18.0"
},
"devDependencies": {
"@cdklabs/eslint-plugin": "^2",
Expand All @@ -44,6 +49,7 @@
"@types/js-yaml": "^4.0.9",
"@types/node": "^20",
"@types/pdf-parse": "^1.1.4",
"@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^8",
"@typescript-eslint/parser": "^8",
"aws-cdk": "^2",
Expand Down
270 changes: 270 additions & 0 deletions cdk/src/constructs/github-screenshot-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/**
* MIT No Attribution
*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import * as path from 'path';
import { ArnFormat, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda';
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { NagSuppressions } from 'cdk-nag';
import { Construct } from 'constructs';
import { ScreenshotBucket } from './screenshot-bucket';

/**
* Properties for GitHubScreenshotIntegration construct.
*/
export interface GitHubScreenshotIntegrationProps {
/** The existing REST API to add the GitHub webhook route to. */
readonly api: apigw.RestApi;

/**
* Existing GitHub PAT secret. The processor reuses ABCA's main GitHub
* token to (a) look up which PR a deploy SHA belongs to via the
* Commits API, and (b) post the screenshot comment on that PR.
* No new GitHub credential is provisioned by this construct.
*/
readonly githubTokenSecret: secretsmanager.ISecret;

/**
* Optional — when provided, the processor also tries to post the
* screenshot to a linked Linear issue. Resolved from the GitHub PR
* title/body via a Linear-identifier regex (e.g. `ABCA-42`), then
* looked up across all `status='active'` workspaces in the registry
* via Linear's `issueVcsBranchSearch` GraphQL.
*/
readonly linearWorkspaceRegistryTable?: dynamodb.ITable;

/**
* Removal policy for the dedup table + screenshot bucket. Defaults
* to DESTROY so dev stacks don't accumulate orphans on `cdk destroy`.
*/
readonly removalPolicy?: RemovalPolicy;

/**
* Override for the GitHub deployment `environment` value we
* screenshot. Different providers use different conventions:
* `Preview` (Vercel's per-PR label, the default), branch names
* (Amplify Hosting), `Deploy Preview <PR#>` (Netlify), or whatever
* your GitHub Actions workflow passes. Set this when your provider
* uses a different name and you want per-PR-only screenshots.
* @default 'Preview'
*/
readonly screenshotTargetEnvironment?: string;
}

/**
* CDK construct that adds the GitHub-deployment-status → screenshot →
* PR-comment pipeline.
*
* Topology mirrors `LinearIntegration`:
* - Receiver Lambda (HMAC-verifies, dedups, async-invokes processor)
* - Async processor Lambda (drives AgentCore Browser, uploads PNG,
* posts the PR comment)
* - Dedup DynamoDB table (1h TTL — covers GitHub's 5-attempt retry
* window with slack)
* - Webhook signing-secret (Secrets Manager placeholder; populated
* manually when the operator pastes GitHub's value into the secret)
* - Public-read screenshot S3 bucket
* - API Gateway route `POST /v1/github/webhook`
*
* Inbound-only adapter — there's no outbound polling or stream
* consumer, just the webhook → screenshot → comment fan-out.
*/
export class GitHubScreenshotIntegration extends Construct {
/** Public-read bucket hosting the screenshot PNGs. */
public readonly screenshotBucket: ScreenshotBucket;

/**
* GitHub webhook signing secret — placeholder. The operator pastes
* GitHub's signing-secret value here after configuring the webhook
* in the demo repo's settings; the secret is otherwise empty.
*/
public readonly webhookSecret: secretsmanager.Secret;

/** Webhook dedup table (composite key = `repo#deployment_id#status_id`). */
public readonly webhookDedupTable: dynamodb.Table;

/** Webhook receiver Lambda (HMAC verifier + dispatcher). */
public readonly webhookFn: lambda.NodejsFunction;

/** Async processor Lambda (browser + S3 + PR comment). */
public readonly webhookProcessorFn: lambda.NodejsFunction;

constructor(scope: Construct, id: string, props: GitHubScreenshotIntegrationProps) {
super(scope, id);

const removalPolicy = props.removalPolicy ?? RemovalPolicy.DESTROY;

// --- Screenshot bucket (public-read on `screenshots/*`) ---
this.screenshotBucket = new ScreenshotBucket(this, 'ScreenshotBucket', {
removalPolicy,
});

// --- Webhook signing secret (operator-populated placeholder) ---
this.webhookSecret = new secretsmanager.Secret(this, 'WebhookSecret', {
description: 'GitHub deployment-status webhook signing secret — populate manually after configuring the GitHub webhook',
removalPolicy,
});

// --- Dedup table ---
this.webhookDedupTable = new dynamodb.Table(this, 'WebhookDedupTable', {
partitionKey: { name: 'dedup_key', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
timeToLiveAttribute: 'ttl',
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
removalPolicy,
});

const handlersDir = path.join(__dirname, '..', 'handlers');
const commonBundling: lambda.BundlingOptions = {
externalModules: ['@aws-sdk/*'],
};

// --- Async processor (browser + S3 + comment) ---
// Timeout budget: 60s screenshot + 5s navigate slack + 30s slack for
// the GitHub PR-lookup + comment + S3 PUT + JSON encode = 95s. Round
// to 120 for headroom on cold-start CDP handshake.
this.webhookProcessorFn = new lambda.NodejsFunction(this, 'WebhookProcessorFn', {
entry: path.join(handlersDir, 'github-webhook-processor.ts'),
handler: 'handler',
runtime: Runtime.NODEJS_24_X,
architecture: Architecture.ARM_64,
timeout: Duration.seconds(120),
memorySize: 512,
environment: {
SCREENSHOT_BUCKET_NAME: this.screenshotBucket.bucket.bucketName,
SCREENSHOT_PUBLIC_HOST: this.screenshotBucket.distribution.domainName,
GITHUB_TOKEN_SECRET_ARN: props.githubTokenSecret.secretArn,
...(props.linearWorkspaceRegistryTable && {
LINEAR_WORKSPACE_REGISTRY_TABLE_NAME: props.linearWorkspaceRegistryTable.tableName,
}),
},
bundling: commonBundling,
});

this.screenshotBucket.bucket.grantPut(this.webhookProcessorFn);
props.githubTokenSecret.grantRead(this.webhookProcessorFn);

// Optional Linear feedback path. Wired only when a registry table
// is provided. The processor scans the registry for active
// workspaces, then per-workspace looks up the OAuth token from
// Secrets Manager (`bgagent-linear-oauth-*` prefix, written by
// `bgagent linear setup`).
if (props.linearWorkspaceRegistryTable) {
props.linearWorkspaceRegistryTable.grantReadData(this.webhookProcessorFn);
this.webhookProcessorFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue', 'secretsmanager:PutSecretValue'],
resources: [
Stack.of(this).formatArn({
service: 'secretsmanager',
resource: 'secret',
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
resourceName: 'bgagent-linear-oauth-*',
}),
],
}));
}

// AgentCore Browser session lifecycle + automation-stream connect.
// The data-plane API doesn't support per-resource ARNs (sessions
// are ephemeral), so wildcards are required — annotated with a
// cdk-nag suppression below. The wildcard set covers
// `ConnectBrowserAutomationStream` (the SigV4-presigned WSS dial)
// which lives under the same prefix but isn't visible in the
// public CLI command list.
this.webhookProcessorFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['bedrock-agentcore:*'],
resources: ['*'],
}));

// --- Webhook receiver (verify, dedup, dispatch) ---
this.webhookFn = new lambda.NodejsFunction(this, 'WebhookFn', {
entry: path.join(handlersDir, 'github-webhook.ts'),
handler: 'handler',
runtime: Runtime.NODEJS_24_X,
architecture: Architecture.ARM_64,
timeout: Duration.seconds(10),
environment: {
GITHUB_WEBHOOK_SECRET_ARN: this.webhookSecret.secretArn,
GITHUB_WEBHOOK_DEDUP_TABLE_NAME: this.webhookDedupTable.tableName,
GITHUB_WEBHOOK_PROCESSOR_FUNCTION_NAME: this.webhookProcessorFn.functionName,
...(props.screenshotTargetEnvironment && {
SCREENSHOT_TARGET_ENVIRONMENT: props.screenshotTargetEnvironment,
}),
},
bundling: commonBundling,
});

this.webhookSecret.grantRead(this.webhookFn);
this.webhookDedupTable.grantReadWriteData(this.webhookFn);
this.webhookProcessorFn.grantInvoke(this.webhookFn);

// --- API Gateway route ---
const githubResource = props.api.root.addResource('github');
const webhookResource = githubResource.addResource('webhook');
const webhookMethod = webhookResource.addMethod(
'POST',
new apigw.LambdaIntegration(this.webhookFn),
{ authorizationType: apigw.AuthorizationType.NONE },
);

NagSuppressions.addResourceSuppressions(webhookMethod, [
{
id: 'AwsSolutions-APIG4',
reason: 'GitHub webhook endpoint authenticates via X-Hub-Signature-256 HMAC, not Cognito — required by GitHub webhook protocol.',
},
{
id: 'AwsSolutions-COG4',
reason: 'GitHub webhook endpoint authenticates via X-Hub-Signature-256 HMAC, not Cognito — required by GitHub webhook protocol.',
},
]);

NagSuppressions.addResourceSuppressions(this.webhookFn, [
{
id: 'AwsSolutions-IAM4',
reason: 'AWSLambdaBasicExecutionRole is the standard managed policy for Lambda CloudWatch Logs writes.',
},
{
id: 'AwsSolutions-IAM5',
reason: 'DynamoDB grants from CDK helpers expand to table-arn/index/* wildcards; receiver only writes to the dedup table.',
},
], true);

NagSuppressions.addResourceSuppressions(this.webhookProcessorFn, [
{
id: 'AwsSolutions-IAM4',
reason: 'AWSLambdaBasicExecutionRole is the standard managed policy for Lambda CloudWatch Logs writes.',
},
{
id: 'AwsSolutions-IAM5',
reason: 'AgentCore Browser sessions are ephemeral and have no per-resource ARN; the data-plane API requires wildcards. S3 PutObject uses CDK grant helpers that expand to bucket/* wildcards.',
},
], true);

NagSuppressions.addResourceSuppressions(this.webhookSecret, [
{
id: 'AwsSolutions-SMG4',
reason: 'GitHub webhook signing-secret rotation is owned by GitHub (operator regenerates on the GitHub side and pastes the new value here). No automated rotation Lambda needed.',
},
]);
}
}
Loading
Loading