Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 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
984f4fc
Merge branch 'main' into feat/240-agentcore-screenshots
isadeks Jun 8, 2026
be7b527
fix(screenshot): krokoko PR-241 review — WS leak + commit-pulls guard
isadeks Jun 8, 2026
873ecea
docs(screenshot): krokoko PR-241 review — reconcile WAF rationale
isadeks Jun 8, 2026
8a6fc86
fix(linear): revert DEFAULT_LABEL_FILTER to 'bgagent'; scope PR-241 t…
isadeks Jun 8, 2026
9c35160
Merge remote-tracking branch 'upstream/main' into feat/240-agentcore-…
isadeks Jun 8, 2026
57167ff
Merge branch 'main' into feat/240-agentcore-screenshots
krokoko Jun 10, 2026
79ef5b7
Merge branch 'main' into feat/240-agentcore-screenshots
isadeks Jun 10, 2026
808b836
fix(screenshot): theagenticguy PR-241 review — blockers + nits + tests
isadeks Jun 10, 2026
2892673
fix(screenshot): preserve env_url skip-path; update tests for budget+…
isadeks Jun 10, 2026
0b8fb15
Merge branch 'main' into feat/240-agentcore-screenshots
krokoko Jun 10, 2026
e553b2c
Merge branch 'main' into feat/240-agentcore-screenshots
krokoko Jun 10, 2026
f284e04
fix(screenshot): encode markdown URL + sync stale key-layout comments…
Jun 10, 2026
335eea4
fix(screenshot): reject all IPv6 literals + cover the deployment-stat…
Jun 10, 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.8.2",
"@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": {
"@aws-cdk/integ-runner": "2.199.0",
Expand All @@ -46,6 +51,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
292 changes: 292 additions & 0 deletions cdk/src/constructs/github-screenshot-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/**
* 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)
* - Private screenshot S3 bucket; served anonymously via CloudFront OAC
* - 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 {
/** Private bucket; served via CloudFront OAC. Hosts 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 (private; served via CloudFront with OAC) ---
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) ---
// Lambda timeout: 120s. The handler enforces a single wall-clock
// deadline of 110s (TOTAL_BUDGET_MS in github-webhook-processor.ts)
// shared across PR-lookup retry + screenshot capture + S3 PUT +
// comment POST, so no individual sub-step can run past the Lambda
// timeout even on the worst-case path. The 10s headroom covers
// SDK auto-retries + the runtime's shutdown grace so a hard timeout
// never severs an in-flight comment-post. (theagenticguy PR-241
// review item B1: previous comment under-counted the 35s retry
// ladder that runs before captureScreenshot's 60s budget.)
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`).
//
// PutSecretValue is intentional, not a typo: resolveLinearOauthToken
// rotates Linear's refresh token in place when it expires (the same
// pattern as the orchestrator role). This is a write grant by
// design — see linear-oauth-resolver.ts.
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.
// Action set scoped to the three calls the handler actually makes;
// resource is `*` because Browser sessions are ephemeral and the
// two `Connect*Stream` data-plane actions in the AWS Service
// Authorization Reference for `bedrock-agentcore` declare no
// resource types or condition keys (they require Resource:"*"
// anyway). cdk-nag IAM5 suppression annotates the resource
// wildcard.
//
// Source: AWS Service Authorization Reference for bedrock-agentcore,
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrockagentcore.html
// - bedrock-agentcore:StartBrowserSession (Write)
// - bedrock-agentcore:StopBrowserSession (Write)
// - bedrock-agentcore:ConnectBrowserAutomationStream (Read; no resource scoping)
this.webhookProcessorFn.addToRolePolicy(new iam.PolicyStatement({
actions: [
'bedrock-agentcore:StartBrowserSession',
'bedrock-agentcore:StopBrowserSession',
'bedrock-agentcore:ConnectBrowserAutomationStream',
],
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