Skip to content

Commit d3d4167

Browse files
authored
Merge branch 'main' into feat/308-automation-workflows
2 parents 7cbdbdb + d912ad2 commit d3d4167

35 files changed

Lines changed: 3635 additions & 45 deletions

cdk/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,29 @@
1717
"@aws-cdk/aws-bedrock-agentcore-alpha": "2.257.0-alpha.0",
1818
"@aws-cdk/aws-bedrock-alpha": "2.257.0-alpha.0",
1919
"@aws-cdk/mixins-preview": "2.257.0-alpha.0",
20+
"@aws-crypto/sha256-js": "^5.2.0",
2021
"@aws-sdk/client-bedrock-agentcore": "^3.1046.0",
2122
"@aws-sdk/client-bedrock-runtime": "^3.1021.0",
2223
"@aws-sdk/client-dynamodb": "^3.1021.0",
2324
"@aws-sdk/client-ecs": "^3.1021.0",
2425
"@aws-sdk/client-lambda": "^3.1021.0",
2526
"@aws-sdk/client-s3": "^3.1021.0",
2627
"@aws-sdk/client-secrets-manager": "^3.1021.0",
28+
"@aws-sdk/credential-provider-node": "^3.972.29",
2729
"@aws-sdk/lib-dynamodb": "^3.1021.0",
2830
"@aws-sdk/s3-presigned-post": "^3.1021.0",
2931
"@aws-sdk/s3-request-presigner": "^3.1021.0",
3032
"@aws/durable-execution-sdk-js": "^1.1.0",
3133
"@cedar-policy/cedar-wasm": "4.8.2",
34+
"@smithy/protocol-http": "^5.3.12",
35+
"@smithy/signature-v4": "^5.3.14",
3236
"aws-cdk-lib": "^2.257.0",
3337
"cdk-nag": "^2.38.2",
3438
"constructs": "^10.3.0",
35-
"pdf-parse": "^1.1.1",
3639
"js-yaml": "^4.1.1",
37-
"ulid": "^3.0.2"
40+
"pdf-parse": "^1.1.1",
41+
"ulid": "^3.0.2",
42+
"ws": "^8.18.0"
3843
},
3944
"devDependencies": {
4045
"@aws-cdk/integ-runner": "2.199.0",
@@ -46,6 +51,7 @@
4651
"@types/js-yaml": "^4.0.9",
4752
"@types/node": "^20",
4853
"@types/pdf-parse": "^1.1.4",
54+
"@types/ws": "^8.5.13",
4955
"@typescript-eslint/eslint-plugin": "^8",
5056
"@typescript-eslint/parser": "^8",
5157
"aws-cdk": "^2",
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/**
2+
* MIT No Attribution
3+
*
4+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
7+
* the Software without restriction, including without limitation the rights to
8+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
* the Software, and to permit persons to whom the Software is furnished to do so.
10+
*
11+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17+
* SOFTWARE.
18+
*/
19+
20+
import * as path from 'path';
21+
import { ArnFormat, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
22+
import * as apigw from 'aws-cdk-lib/aws-apigateway';
23+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
24+
import * as iam from 'aws-cdk-lib/aws-iam';
25+
import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda';
26+
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
27+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
28+
import { NagSuppressions } from 'cdk-nag';
29+
import { Construct } from 'constructs';
30+
import { ScreenshotBucket } from './screenshot-bucket';
31+
32+
/**
33+
* Properties for GitHubScreenshotIntegration construct.
34+
*/
35+
export interface GitHubScreenshotIntegrationProps {
36+
/** The existing REST API to add the GitHub webhook route to. */
37+
readonly api: apigw.RestApi;
38+
39+
/**
40+
* Existing GitHub PAT secret. The processor reuses ABCA's main GitHub
41+
* token to (a) look up which PR a deploy SHA belongs to via the
42+
* Commits API, and (b) post the screenshot comment on that PR.
43+
* No new GitHub credential is provisioned by this construct.
44+
*/
45+
readonly githubTokenSecret: secretsmanager.ISecret;
46+
47+
/**
48+
* Optional — when provided, the processor also tries to post the
49+
* screenshot to a linked Linear issue. Resolved from the GitHub PR
50+
* title/body via a Linear-identifier regex (e.g. `ABCA-42`), then
51+
* looked up across all `status='active'` workspaces in the registry
52+
* via Linear's `issueVcsBranchSearch` GraphQL.
53+
*/
54+
readonly linearWorkspaceRegistryTable?: dynamodb.ITable;
55+
56+
/**
57+
* Removal policy for the dedup table + screenshot bucket. Defaults
58+
* to DESTROY so dev stacks don't accumulate orphans on `cdk destroy`.
59+
*/
60+
readonly removalPolicy?: RemovalPolicy;
61+
62+
/**
63+
* Override for the GitHub deployment `environment` value we
64+
* screenshot. Different providers use different conventions:
65+
* `Preview` (Vercel's per-PR label, the default), branch names
66+
* (Amplify Hosting), `Deploy Preview <PR#>` (Netlify), or whatever
67+
* your GitHub Actions workflow passes. Set this when your provider
68+
* uses a different name and you want per-PR-only screenshots.
69+
* @default 'Preview'
70+
*/
71+
readonly screenshotTargetEnvironment?: string;
72+
}
73+
74+
/**
75+
* CDK construct that adds the GitHub-deployment-status → screenshot →
76+
* PR-comment pipeline.
77+
*
78+
* Topology mirrors `LinearIntegration`:
79+
* - Receiver Lambda (HMAC-verifies, dedups, async-invokes processor)
80+
* - Async processor Lambda (drives AgentCore Browser, uploads PNG,
81+
* posts the PR comment)
82+
* - Dedup DynamoDB table (1h TTL — covers GitHub's 5-attempt retry
83+
* window with slack)
84+
* - Webhook signing-secret (Secrets Manager placeholder; populated
85+
* manually when the operator pastes GitHub's value into the secret)
86+
* - Private screenshot S3 bucket; served anonymously via CloudFront OAC
87+
* - API Gateway route `POST /v1/github/webhook`
88+
*
89+
* Inbound-only adapter — there's no outbound polling or stream
90+
* consumer, just the webhook → screenshot → comment fan-out.
91+
*/
92+
export class GitHubScreenshotIntegration extends Construct {
93+
/** Private bucket; served via CloudFront OAC. Hosts the screenshot PNGs. */
94+
public readonly screenshotBucket: ScreenshotBucket;
95+
96+
/**
97+
* GitHub webhook signing secret — placeholder. The operator pastes
98+
* GitHub's signing-secret value here after configuring the webhook
99+
* in the demo repo's settings; the secret is otherwise empty.
100+
*/
101+
public readonly webhookSecret: secretsmanager.Secret;
102+
103+
/** Webhook dedup table (composite key = `repo#deployment_id#status_id`). */
104+
public readonly webhookDedupTable: dynamodb.Table;
105+
106+
/** Webhook receiver Lambda (HMAC verifier + dispatcher). */
107+
public readonly webhookFn: lambda.NodejsFunction;
108+
109+
/** Async processor Lambda (browser + S3 + PR comment). */
110+
public readonly webhookProcessorFn: lambda.NodejsFunction;
111+
112+
constructor(scope: Construct, id: string, props: GitHubScreenshotIntegrationProps) {
113+
super(scope, id);
114+
115+
const removalPolicy = props.removalPolicy ?? RemovalPolicy.DESTROY;
116+
117+
// --- Screenshot bucket (private; served via CloudFront with OAC) ---
118+
this.screenshotBucket = new ScreenshotBucket(this, 'ScreenshotBucket', {
119+
removalPolicy,
120+
});
121+
122+
// --- Webhook signing secret (operator-populated placeholder) ---
123+
this.webhookSecret = new secretsmanager.Secret(this, 'WebhookSecret', {
124+
description: 'GitHub deployment-status webhook signing secret — populate manually after configuring the GitHub webhook',
125+
removalPolicy,
126+
});
127+
128+
// --- Dedup table ---
129+
this.webhookDedupTable = new dynamodb.Table(this, 'WebhookDedupTable', {
130+
partitionKey: { name: 'dedup_key', type: dynamodb.AttributeType.STRING },
131+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
132+
timeToLiveAttribute: 'ttl',
133+
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
134+
removalPolicy,
135+
});
136+
137+
const handlersDir = path.join(__dirname, '..', 'handlers');
138+
const commonBundling: lambda.BundlingOptions = {
139+
externalModules: ['@aws-sdk/*'],
140+
};
141+
142+
// --- Async processor (browser + S3 + comment) ---
143+
// Lambda timeout: 120s. The handler enforces a single wall-clock
144+
// deadline of 110s (TOTAL_BUDGET_MS in github-webhook-processor.ts)
145+
// shared across PR-lookup retry + screenshot capture + S3 PUT +
146+
// comment POST, so no individual sub-step can run past the Lambda
147+
// timeout even on the worst-case path. The 10s headroom covers
148+
// SDK auto-retries + the runtime's shutdown grace so a hard timeout
149+
// never severs an in-flight comment-post. (theagenticguy PR-241
150+
// review item B1: previous comment under-counted the 35s retry
151+
// ladder that runs before captureScreenshot's 60s budget.)
152+
this.webhookProcessorFn = new lambda.NodejsFunction(this, 'WebhookProcessorFn', {
153+
entry: path.join(handlersDir, 'github-webhook-processor.ts'),
154+
handler: 'handler',
155+
runtime: Runtime.NODEJS_24_X,
156+
architecture: Architecture.ARM_64,
157+
timeout: Duration.seconds(120),
158+
memorySize: 512,
159+
environment: {
160+
SCREENSHOT_BUCKET_NAME: this.screenshotBucket.bucket.bucketName,
161+
SCREENSHOT_PUBLIC_HOST: this.screenshotBucket.distribution.domainName,
162+
GITHUB_TOKEN_SECRET_ARN: props.githubTokenSecret.secretArn,
163+
...(props.linearWorkspaceRegistryTable && {
164+
LINEAR_WORKSPACE_REGISTRY_TABLE_NAME: props.linearWorkspaceRegistryTable.tableName,
165+
}),
166+
},
167+
bundling: commonBundling,
168+
});
169+
170+
this.screenshotBucket.bucket.grantPut(this.webhookProcessorFn);
171+
props.githubTokenSecret.grantRead(this.webhookProcessorFn);
172+
173+
// Optional Linear feedback path. Wired only when a registry table
174+
// is provided. The processor scans the registry for active
175+
// workspaces, then per-workspace looks up the OAuth token from
176+
// Secrets Manager (`bgagent-linear-oauth-*` prefix, written by
177+
// `bgagent linear setup`).
178+
//
179+
// PutSecretValue is intentional, not a typo: resolveLinearOauthToken
180+
// rotates Linear's refresh token in place when it expires (the same
181+
// pattern as the orchestrator role). This is a write grant by
182+
// design — see linear-oauth-resolver.ts.
183+
if (props.linearWorkspaceRegistryTable) {
184+
props.linearWorkspaceRegistryTable.grantReadData(this.webhookProcessorFn);
185+
this.webhookProcessorFn.addToRolePolicy(new iam.PolicyStatement({
186+
actions: ['secretsmanager:GetSecretValue', 'secretsmanager:PutSecretValue'],
187+
resources: [
188+
Stack.of(this).formatArn({
189+
service: 'secretsmanager',
190+
resource: 'secret',
191+
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
192+
resourceName: 'bgagent-linear-oauth-*',
193+
}),
194+
],
195+
}));
196+
}
197+
198+
// AgentCore Browser session lifecycle + automation-stream connect.
199+
// Action set scoped to the three calls the handler actually makes;
200+
// resource is `*` because Browser sessions are ephemeral and the
201+
// two `Connect*Stream` data-plane actions in the AWS Service
202+
// Authorization Reference for `bedrock-agentcore` declare no
203+
// resource types or condition keys (they require Resource:"*"
204+
// anyway). cdk-nag IAM5 suppression annotates the resource
205+
// wildcard.
206+
//
207+
// Source: AWS Service Authorization Reference for bedrock-agentcore,
208+
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrockagentcore.html
209+
// - bedrock-agentcore:StartBrowserSession (Write)
210+
// - bedrock-agentcore:StopBrowserSession (Write)
211+
// - bedrock-agentcore:ConnectBrowserAutomationStream (Read; no resource scoping)
212+
this.webhookProcessorFn.addToRolePolicy(new iam.PolicyStatement({
213+
actions: [
214+
'bedrock-agentcore:StartBrowserSession',
215+
'bedrock-agentcore:StopBrowserSession',
216+
'bedrock-agentcore:ConnectBrowserAutomationStream',
217+
],
218+
resources: ['*'],
219+
}));
220+
221+
// --- Webhook receiver (verify, dedup, dispatch) ---
222+
this.webhookFn = new lambda.NodejsFunction(this, 'WebhookFn', {
223+
entry: path.join(handlersDir, 'github-webhook.ts'),
224+
handler: 'handler',
225+
runtime: Runtime.NODEJS_24_X,
226+
architecture: Architecture.ARM_64,
227+
timeout: Duration.seconds(10),
228+
environment: {
229+
GITHUB_WEBHOOK_SECRET_ARN: this.webhookSecret.secretArn,
230+
GITHUB_WEBHOOK_DEDUP_TABLE_NAME: this.webhookDedupTable.tableName,
231+
GITHUB_WEBHOOK_PROCESSOR_FUNCTION_NAME: this.webhookProcessorFn.functionName,
232+
...(props.screenshotTargetEnvironment && {
233+
SCREENSHOT_TARGET_ENVIRONMENT: props.screenshotTargetEnvironment,
234+
}),
235+
},
236+
bundling: commonBundling,
237+
});
238+
239+
this.webhookSecret.grantRead(this.webhookFn);
240+
this.webhookDedupTable.grantReadWriteData(this.webhookFn);
241+
this.webhookProcessorFn.grantInvoke(this.webhookFn);
242+
243+
// --- API Gateway route ---
244+
const githubResource = props.api.root.addResource('github');
245+
const webhookResource = githubResource.addResource('webhook');
246+
const webhookMethod = webhookResource.addMethod(
247+
'POST',
248+
new apigw.LambdaIntegration(this.webhookFn),
249+
{ authorizationType: apigw.AuthorizationType.NONE },
250+
);
251+
252+
NagSuppressions.addResourceSuppressions(webhookMethod, [
253+
{
254+
id: 'AwsSolutions-APIG4',
255+
reason: 'GitHub webhook endpoint authenticates via X-Hub-Signature-256 HMAC, not Cognito — required by GitHub webhook protocol.',
256+
},
257+
{
258+
id: 'AwsSolutions-COG4',
259+
reason: 'GitHub webhook endpoint authenticates via X-Hub-Signature-256 HMAC, not Cognito — required by GitHub webhook protocol.',
260+
},
261+
]);
262+
263+
NagSuppressions.addResourceSuppressions(this.webhookFn, [
264+
{
265+
id: 'AwsSolutions-IAM4',
266+
reason: 'AWSLambdaBasicExecutionRole is the standard managed policy for Lambda CloudWatch Logs writes.',
267+
},
268+
{
269+
id: 'AwsSolutions-IAM5',
270+
reason: 'DynamoDB grants from CDK helpers expand to table-arn/index/* wildcards; receiver only writes to the dedup table.',
271+
},
272+
], true);
273+
274+
NagSuppressions.addResourceSuppressions(this.webhookProcessorFn, [
275+
{
276+
id: 'AwsSolutions-IAM4',
277+
reason: 'AWSLambdaBasicExecutionRole is the standard managed policy for Lambda CloudWatch Logs writes.',
278+
},
279+
{
280+
id: 'AwsSolutions-IAM5',
281+
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.',
282+
},
283+
], true);
284+
285+
NagSuppressions.addResourceSuppressions(this.webhookSecret, [
286+
{
287+
id: 'AwsSolutions-SMG4',
288+
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.',
289+
},
290+
]);
291+
}
292+
}

0 commit comments

Comments
 (0)