@skippercorp/skipper 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,406 @@
1
+ import {
2
+ listWorkerChunkParameterKeys,
3
+ WORKERS_CHUNK_COUNT_PARAM,
4
+ WORKERS_ENCODING_PARAM,
5
+ WORKERS_SCHEMA_VERSION_PARAM,
6
+ WORKERS_SHA256_PARAM,
7
+ } from "../../worker/aws-params.js";
8
+
9
+ type JsonMap = Record<string, unknown>;
10
+
11
+ const WEBHOOK_TASK_COMMAND = [
12
+ "set -euo pipefail",
13
+ "apt-get update",
14
+ "DEBIAN_FRONTEND=noninteractive apt-get -y upgrade",
15
+ "DEBIAN_FRONTEND=noninteractive apt-get install -y curl git ca-certificates unzip nodejs gh",
16
+ 'REPO_URL="$REPOSITORY_URL"',
17
+ "id -u runner >/dev/null 2>&1 || useradd -m -s /bin/bash runner",
18
+ 'runuser -u runner -- bash -lc "curl -fsSL https://bun.sh/install | bash"',
19
+ 'runuser -u runner -- env PATH="/home/runner/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" bash -lc "bun add -g @anthropic-ai/claude-code opencode-ai"',
20
+ 'runuser -u runner -- env REPO_URL="$REPO_URL" PROMPT="$PROMPT" ECS_AGENT="${ECS_AGENT:-claude}" GITHUB_TOKEN="${GITHUB_TOKEN:-}" GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-}" OPENCODE_MODEL="${OPENCODE_MODEL:-amazon-bedrock/${ANTHROPIC_MODEL:-eu.anthropic.claude-sonnet-4-6}}" PATH="/home/runner/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" bash -lc \"if [ -n \\\"${GITHUB_TOKEN:-}\\\" ] && [ \\\"${REPO_URL#https://github.com/}\\\" != \\\"$REPO_URL\\\" ]; then REPO_URL=\\\"https://x-access-token:${GITHUB_TOKEN:-}@${REPO_URL#https://}\\\"; fi; rm -rf /home/runner/repo; git clone --depth 1 \\\"$REPO_URL\\\" /home/runner/repo; cd /home/runner/repo; if [ \\\"${ECS_AGENT}\\\" = \\\"opencode\\\" ]; then opencode run -m \\\"\\$OPENCODE_MODEL\\\" \\\"$PROMPT\\\"; else claude --dangerously-skip-permissions -p \\\"$PROMPT\\\"; fi\"',
21
+ ].join("; ");
22
+
23
+ const DEFAULT_BEDROCK_MODEL = "eu.anthropic.claude-sonnet-4-6";
24
+
25
+ const EVENTBRIDGE_REQUEST_TEMPLATE = [
26
+ '#set($context.requestOverride.header.X-Amz-Target = "AWSEvents.PutEvents")',
27
+ '#set($context.requestOverride.header.Content-Type = "application/x-amz-json-1.1")',
28
+ "{",
29
+ ' "Entries": [',
30
+ " {",
31
+ ' "Source": "${EventSource}",',
32
+ ' "DetailType": "${EventDetailType}",',
33
+ ' "EventBusName": "${EventBusName}",',
34
+ ' "Detail": "{\\"rawBodyB64\\":\\"$util.base64Encode($input.body)\\",\\"headers\\":{\\"x-github-event\\":\\"$util.escapeJavaScript($input.params().header.get(\'X-GitHub-Event\'))\\",\\"x-github-delivery\\":\\"$util.escapeJavaScript($input.params().header.get(\'X-GitHub-Delivery\'))\\",\\"x-hub-signature-256\\":\\"$util.escapeJavaScript($input.params().header.get(\'X-Hub-Signature-256\'))\\"},\\"repository\\":{\\"full_name\\":\\"$util.escapeJavaScript($util.parseJson($input.body).repository.full_name)\\"},\\"requestId\\":\\"$context.requestId\\"}"',
35
+ " }",
36
+ " ]",
37
+ "}",
38
+ ].join("\n");
39
+
40
+ /**
41
+ * Build CloudFormation template JSON string.
42
+ *
43
+ * @since 1.0.0
44
+ * @category AWS.Template
45
+ */
46
+ export function buildTemplate(): string {
47
+ const template = {
48
+ AWSTemplateFormatVersion: "2010-09-09",
49
+ Description: "Skipper API Gateway REST -> EventBridge",
50
+ Parameters: buildParameters(),
51
+ Resources: buildResources(),
52
+ Outputs: buildOutputs(),
53
+ };
54
+ return JSON.stringify(template, null, 2);
55
+ }
56
+
57
+ /**
58
+ * Build template parameters section.
59
+ *
60
+ * @since 1.0.0
61
+ * @category AWS.Template
62
+ */
63
+ function buildParameters(): JsonMap {
64
+ return {
65
+ ServiceName: { Type: "String" },
66
+ Environment: { Type: "String" },
67
+ ApiName: { Type: "String" },
68
+ StageName: { Type: "String" },
69
+ VpcId: { Type: "String" },
70
+ SubnetIds: { Type: "CommaDelimitedList" },
71
+ EventBusName: { Type: "String" },
72
+ EventSource: { Type: "String" },
73
+ EventDetailType: { Type: "String" },
74
+ WebhookSecret: { Type: "String", NoEcho: true },
75
+ ...buildWorkerParameters(),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Build worker manifest CloudFormation parameters.
81
+ *
82
+ * @since 1.0.0
83
+ * @category AWS.Template
84
+ */
85
+ function buildWorkerParameters(): JsonMap {
86
+ const parameters: JsonMap = {
87
+ [WORKERS_ENCODING_PARAM]: { Type: "String", Default: "" },
88
+ [WORKERS_SHA256_PARAM]: { Type: "String", Default: "" },
89
+ [WORKERS_SCHEMA_VERSION_PARAM]: { Type: "String", Default: "1" },
90
+ [WORKERS_CHUNK_COUNT_PARAM]: { Type: "Number", Default: 0 },
91
+ };
92
+ for (const key of listWorkerChunkParameterKeys()) {
93
+ parameters[key] = { Type: "String", Default: "" };
94
+ }
95
+ return parameters;
96
+ }
97
+
98
+ /**
99
+ * Build template outputs section.
100
+ *
101
+ * @since 1.0.0
102
+ * @category AWS.Template
103
+ */
104
+ function buildOutputs(): JsonMap {
105
+ return {
106
+ ApiInvokeUrl: {
107
+ Value: {
108
+ "Fn::Sub":
109
+ "https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/${StageName}/events",
110
+ },
111
+ },
112
+ ApiId: { Value: { Ref: "ApiGatewayRestApi" } },
113
+ ApiStageName: { Value: { Ref: "StageName" } },
114
+ EventBusName: { Value: { Ref: "IngressEventBus" } },
115
+ EventBusArn: { Value: { "Fn::GetAtt": ["IngressEventBus", "Arn"] } },
116
+ EventSource: { Value: { Ref: "EventSource" } },
117
+ EventDetailType: { Value: { Ref: "EventDetailType" } },
118
+ EcsClusterArn: { Value: { Ref: "WebhookEcsCluster" } },
119
+ EcsTaskDefinitionArn: { Value: { Ref: "WebhookTaskDefinition" } },
120
+ EcsTaskExecutionRoleArn: {
121
+ Value: { "Fn::GetAtt": ["WebhookTaskExecutionRole", "Arn"] },
122
+ },
123
+ EcsTaskRoleArn: { Value: { "Fn::GetAtt": ["WebhookTaskRole", "Arn"] } },
124
+ EcsSecurityGroupId: { Value: { Ref: "WebhookTaskSecurityGroup" } },
125
+ EcsSubnetIdsCsv: { Value: { "Fn::Join": [",", { Ref: "SubnetIds" }] } },
126
+ WebhookSecretParameterName: { Value: { Ref: "WebhookSecretParameter" } },
127
+ LambdaArtifactsBucketName: { Value: { Ref: "LambdaArtifactsBucket" } },
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Build all resources.
133
+ *
134
+ * @since 1.0.0
135
+ * @category AWS.Template
136
+ */
137
+ function buildResources(): JsonMap {
138
+ return {
139
+ IngressEventBus: {
140
+ Type: "AWS::Events::EventBus",
141
+ Properties: {
142
+ Name: { Ref: "EventBusName" },
143
+ },
144
+ },
145
+ ApiGatewayToEventBridgeRole: {
146
+ Type: "AWS::IAM::Role",
147
+ Properties: {
148
+ AssumeRolePolicyDocument: {
149
+ Version: "2012-10-17",
150
+ Statement: [
151
+ {
152
+ Effect: "Allow",
153
+ Principal: { Service: "apigateway.amazonaws.com" },
154
+ Action: "sts:AssumeRole",
155
+ },
156
+ ],
157
+ },
158
+ Policies: [
159
+ {
160
+ PolicyName: "put-events",
161
+ PolicyDocument: {
162
+ Version: "2012-10-17",
163
+ Statement: [
164
+ {
165
+ Effect: "Allow",
166
+ Action: ["events:PutEvents"],
167
+ Resource: { "Fn::GetAtt": ["IngressEventBus", "Arn"] },
168
+ },
169
+ ],
170
+ },
171
+ },
172
+ ],
173
+ },
174
+ },
175
+ ApiGatewayRestApi: {
176
+ Type: "AWS::ApiGateway::RestApi",
177
+ Properties: {
178
+ Name: { Ref: "ApiName" },
179
+ EndpointConfiguration: { Types: ["REGIONAL"] },
180
+ },
181
+ },
182
+ ApiGatewayResourceEvents: {
183
+ Type: "AWS::ApiGateway::Resource",
184
+ Properties: {
185
+ RestApiId: { Ref: "ApiGatewayRestApi" },
186
+ ParentId: { "Fn::GetAtt": ["ApiGatewayRestApi", "RootResourceId"] },
187
+ PathPart: "events",
188
+ },
189
+ },
190
+ ApiGatewayMethodPostEvents: {
191
+ Type: "AWS::ApiGateway::Method",
192
+ Properties: {
193
+ RestApiId: { Ref: "ApiGatewayRestApi" },
194
+ ResourceId: { Ref: "ApiGatewayResourceEvents" },
195
+ HttpMethod: "POST",
196
+ AuthorizationType: "NONE",
197
+ RequestParameters: {
198
+ "method.request.header.X-Amz-Target": false,
199
+ "method.request.header.Content-Type": false,
200
+ },
201
+ Integration: {
202
+ Type: "AWS",
203
+ IntegrationHttpMethod: "POST",
204
+ Credentials: { "Fn::GetAtt": ["ApiGatewayToEventBridgeRole", "Arn"] },
205
+ Uri: {
206
+ "Fn::Sub":
207
+ "arn:${AWS::Partition}:apigateway:${AWS::Region}:events:action/PutEvents",
208
+ },
209
+ PassthroughBehavior: "WHEN_NO_TEMPLATES",
210
+ RequestTemplates: {
211
+ "application/json": {
212
+ "Fn::Sub": [
213
+ EVENTBRIDGE_REQUEST_TEMPLATE,
214
+ {
215
+ EventBusName: { Ref: "EventBusName" },
216
+ EventSource: { Ref: "EventSource" },
217
+ EventDetailType: { Ref: "EventDetailType" },
218
+ },
219
+ ],
220
+ },
221
+ },
222
+ IntegrationResponses: [
223
+ {
224
+ StatusCode: "200",
225
+ ResponseTemplates: { "application/json": '{"ok":true}' },
226
+ },
227
+ ],
228
+ },
229
+ MethodResponses: [{ StatusCode: "200" }],
230
+ },
231
+ },
232
+ ApiGatewayDeploymentV2: {
233
+ Type: "AWS::ApiGateway::Deployment",
234
+ DependsOn: ["ApiGatewayMethodPostEvents"],
235
+ Properties: { RestApiId: { Ref: "ApiGatewayRestApi" } },
236
+ },
237
+ ApiGatewayStage: {
238
+ Type: "AWS::ApiGateway::Stage",
239
+ Properties: {
240
+ RestApiId: { Ref: "ApiGatewayRestApi" },
241
+ DeploymentId: { Ref: "ApiGatewayDeploymentV2" },
242
+ StageName: { Ref: "StageName" },
243
+ },
244
+ },
245
+ WebhookSecretParameter: {
246
+ Type: "AWS::SSM::Parameter",
247
+ Properties: {
248
+ Name: {
249
+ "Fn::Sub": "/skipper/${ServiceName}/${Environment}/webhook-secret",
250
+ },
251
+ Type: "String",
252
+ Value: { Ref: "WebhookSecret" },
253
+ },
254
+ },
255
+ LambdaArtifactsBucket: {
256
+ Type: "AWS::S3::Bucket",
257
+ },
258
+ ...buildEcsResources(),
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Build ECS resources for task execution runtime.
264
+ *
265
+ * @since 1.0.0
266
+ * @category AWS.Template
267
+ */
268
+ function buildEcsResources(): JsonMap {
269
+ return {
270
+ WebhookEcsCluster: {
271
+ Type: "AWS::ECS::Cluster",
272
+ Properties: {
273
+ ClusterName: { "Fn::Sub": "${ServiceName}-${Environment}-cluster" },
274
+ },
275
+ },
276
+ WebhookTaskLogGroup: {
277
+ Type: "AWS::Logs::LogGroup",
278
+ Properties: {
279
+ LogGroupName: {
280
+ "Fn::Sub": "/aws/ecs/${ServiceName}-${Environment}-webhook",
281
+ },
282
+ RetentionInDays: 14,
283
+ },
284
+ },
285
+ WebhookTaskExecutionRole: {
286
+ Type: "AWS::IAM::Role",
287
+ Properties: {
288
+ AssumeRolePolicyDocument: {
289
+ Version: "2012-10-17",
290
+ Statement: [
291
+ {
292
+ Effect: "Allow",
293
+ Principal: { Service: "ecs-tasks.amazonaws.com" },
294
+ Action: "sts:AssumeRole",
295
+ },
296
+ ],
297
+ },
298
+ ManagedPolicyArns: [
299
+ "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
300
+ ],
301
+ },
302
+ },
303
+ WebhookTaskRole: {
304
+ Type: "AWS::IAM::Role",
305
+ Properties: {
306
+ AssumeRolePolicyDocument: {
307
+ Version: "2012-10-17",
308
+ Statement: [
309
+ {
310
+ Effect: "Allow",
311
+ Principal: { Service: "ecs-tasks.amazonaws.com" },
312
+ Action: "sts:AssumeRole",
313
+ },
314
+ ],
315
+ },
316
+ Policies: [
317
+ {
318
+ PolicyName: "claude-bedrock-access",
319
+ PolicyDocument: {
320
+ Version: "2012-10-17",
321
+ Statement: [
322
+ {
323
+ Sid: "AllowModelAndInferenceProfileAccess",
324
+ Effect: "Allow",
325
+ Action: [
326
+ "bedrock:InvokeModel",
327
+ "bedrock:InvokeModelWithResponseStream",
328
+ "bedrock:ListInferenceProfiles",
329
+ ],
330
+ Resource: [
331
+ "arn:aws:bedrock:*:*:inference-profile/*",
332
+ "arn:aws:bedrock:*:*:application-inference-profile/*",
333
+ "arn:aws:bedrock:*:*:foundation-model/*",
334
+ ],
335
+ },
336
+ {
337
+ Sid: "AllowMarketplaceSubscription",
338
+ Effect: "Allow",
339
+ Action: ["aws-marketplace:ViewSubscriptions", "aws-marketplace:Subscribe"],
340
+ Resource: "*",
341
+ Condition: {
342
+ StringEquals: {
343
+ "aws:CalledViaLast": "bedrock.amazonaws.com",
344
+ },
345
+ },
346
+ },
347
+ ],
348
+ },
349
+ },
350
+ ],
351
+ },
352
+ },
353
+ WebhookTaskSecurityGroup: {
354
+ Type: "AWS::EC2::SecurityGroup",
355
+ Properties: {
356
+ GroupDescription: "Webhook ECS task security group",
357
+ VpcId: { Ref: "VpcId" },
358
+ SecurityGroupEgress: [
359
+ {
360
+ IpProtocol: "-1",
361
+ CidrIp: "0.0.0.0/0",
362
+ },
363
+ ],
364
+ },
365
+ },
366
+ WebhookTaskDefinition: {
367
+ Type: "AWS::ECS::TaskDefinition",
368
+ Properties: {
369
+ Family: { "Fn::Sub": "${ServiceName}-${Environment}-webhook" },
370
+ Cpu: "256",
371
+ Memory: "2048",
372
+ NetworkMode: "awsvpc",
373
+ RequiresCompatibilities: ["FARGATE"],
374
+ ExecutionRoleArn: { "Fn::GetAtt": ["WebhookTaskExecutionRole", "Arn"] },
375
+ TaskRoleArn: { "Fn::GetAtt": ["WebhookTaskRole", "Arn"] },
376
+ ContainerDefinitions: [
377
+ {
378
+ Name: "webhook",
379
+ Image: "public.ecr.aws/docker/library/ubuntu:24.04",
380
+ Essential: true,
381
+ Command: ["/bin/bash", "-lc", WEBHOOK_TASK_COMMAND],
382
+ Environment: [
383
+ { Name: "ECS_AGENT", Value: "claude" },
384
+ { Name: "CLAUDE_CODE_USE_BEDROCK", Value: "1" },
385
+ { Name: "AWS_REGION", Value: { Ref: "AWS::Region" } },
386
+ { Name: "AWS_DEFAULT_REGION", Value: { Ref: "AWS::Region" } },
387
+ { Name: "ANTHROPIC_MODEL", Value: DEFAULT_BEDROCK_MODEL },
388
+ {
389
+ Name: "ANTHROPIC_DEFAULT_SONNET_MODEL",
390
+ Value: DEFAULT_BEDROCK_MODEL,
391
+ },
392
+ ],
393
+ LogConfiguration: {
394
+ LogDriver: "awslogs",
395
+ Options: {
396
+ "awslogs-group": { Ref: "WebhookTaskLogGroup" },
397
+ "awslogs-region": { Ref: "AWS::Region" },
398
+ "awslogs-stream-prefix": "ecs",
399
+ },
400
+ },
401
+ },
402
+ ],
403
+ },
404
+ },
405
+ };
406
+ }