@aws/ml-container-creator 0.2.1 → 0.2.2

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.
Files changed (36) hide show
  1. package/bin/cli.js +88 -86
  2. package/config/bootstrap-stack.json +211 -0
  3. package/config/parameter-schema.json +88 -0
  4. package/infra/ci-harness/bin/ci-harness.ts +26 -0
  5. package/infra/ci-harness/buildspec.yml +352 -0
  6. package/infra/ci-harness/cdk.json +27 -0
  7. package/infra/ci-harness/lambda/scanner/index.ts +199 -0
  8. package/infra/ci-harness/lib/ci-harness-stack.ts +609 -0
  9. package/infra/ci-harness/package-lock.json +3979 -0
  10. package/infra/ci-harness/package.json +32 -0
  11. package/infra/ci-harness/tsconfig.json +38 -0
  12. package/package.json +13 -3
  13. package/src/app.js +318 -318
  14. package/src/copy-tpl.js +19 -19
  15. package/src/lib/asset-manager.js +74 -74
  16. package/src/lib/aws-profile-parser.js +45 -45
  17. package/src/lib/bootstrap-command-handler.js +560 -547
  18. package/src/lib/bootstrap-config.js +45 -45
  19. package/src/lib/ci-register-helpers.js +19 -19
  20. package/src/lib/ci-report-helpers.js +37 -37
  21. package/src/lib/ci-stage-helpers.js +49 -49
  22. package/src/lib/comment-generator.js +4 -4
  23. package/src/lib/config-manager.js +105 -105
  24. package/src/lib/deployment-config-resolver.js +10 -10
  25. package/src/lib/deployment-registry.js +153 -153
  26. package/src/lib/engine-prefix-resolver.js +8 -8
  27. package/src/lib/key-value-parser.js +6 -6
  28. package/src/lib/manifest-cli.js +108 -108
  29. package/src/lib/prompt-runner.js +224 -224
  30. package/src/lib/prompts.js +121 -121
  31. package/src/lib/registry-command-handler.js +174 -174
  32. package/src/lib/registry-loader.js +52 -52
  33. package/src/lib/sensitive-redactor.js +9 -9
  34. package/src/lib/template-engine.js +1 -1
  35. package/src/lib/template-manager.js +62 -62
  36. package/src/prompt-adapter.js +18 -18
@@ -0,0 +1,352 @@
1
+ version: 0.2
2
+
3
+ env:
4
+ variables:
5
+ CI_TABLE_NAME: ""
6
+ CI_LOG_GROUP: ""
7
+ CONFIG_ID: ""
8
+ CONFIG_JSON: ""
9
+ BUILD_STRATEGY: "codebuild-submit"
10
+
11
+ phases:
12
+ install:
13
+ runtime-versions:
14
+ nodejs: 22
15
+ commands:
16
+ - echo "=== MLCC CI Harness - Install Phase ==="
17
+ - npm install -g @aws/ml-container-creator
18
+ # Track overall build start time for total duration calculation
19
+ - BUILD_START_TIME=$(date +%s)
20
+ # Initialize stage results tracking as flat variables.
21
+ # Each stage gets: _STATUS, _DURATION, _LOG_POINTER, _ERROR_SUMMARY
22
+ - FIRST_FAILURE=""
23
+ - GENERATE_STATUS="skip"
24
+ - GENERATE_DURATION=0
25
+ - GENERATE_LOG_POINTER=""
26
+ - GENERATE_ERROR_SUMMARY=""
27
+ - VALIDATE_STATUS="skip"
28
+ - VALIDATE_DURATION=0
29
+ - VALIDATE_LOG_POINTER=""
30
+ - VALIDATE_ERROR_SUMMARY=""
31
+ - BUILD_STATUS="skip"
32
+ - BUILD_DURATION=0
33
+ - BUILD_LOG_POINTER=""
34
+ - BUILD_ERROR_SUMMARY=""
35
+ - DEPLOY_TEST_STATUS="skip"
36
+ - DEPLOY_TEST_DURATION=0
37
+ - DEPLOY_TEST_LOG_POINTER=""
38
+ - DEPLOY_TEST_ERROR_SUMMARY=""
39
+ - REGISTER_STATUS="skip"
40
+ - REGISTER_DURATION=0
41
+ - REGISTER_LOG_POINTER=""
42
+ - REGISTER_ERROR_SUMMARY=""
43
+ - TEARDOWN_STATUS="skip"
44
+ - TEARDOWN_DURATION=0
45
+ - TEARDOWN_LOG_POINTER=""
46
+ - TEARDOWN_ERROR_SUMMARY=""
47
+ - UPDATE_STATUS="skip"
48
+ - UPDATE_DURATION=0
49
+ - UPDATE_LOG_POINTER=""
50
+ - UPDATE_ERROR_SUMMARY=""
51
+ # Build the log pointer prefix for this execution
52
+ - BUILD_TIMESTAMP=$(date -u +%Y%m%d-%H%M%S)
53
+ - LOG_POINTER_PREFIX="${CI_LOG_GROUP}:build/${CONFIG_ID}/${BUILD_TIMESTAMP}"
54
+
55
+ pre_build:
56
+ commands:
57
+ # --- Stage: Generate ---
58
+ - echo "=== Stage: Generate ==="
59
+ - STAGE_START=$(date +%s)
60
+ - GENERATE_LOG_POINTER="$LOG_POINTER_PREFIX"
61
+ - STAGE_STDERR_FILE=$(mktemp)
62
+ - |
63
+ (
64
+ set -e
65
+ echo "$CONFIG_JSON" > /tmp/ci-config.json
66
+ mkdir -p /tmp/ci-project
67
+ cd /tmp/ci-project
68
+ ml-container-creator --config /tmp/ci-config.json --skip-prompts
69
+ chmod +x do/*
70
+ ) 2>"$STAGE_STDERR_FILE"; STAGE_EXIT=$?
71
+ - STAGE_END=$(date +%s)
72
+ - GENERATE_DURATION=$((STAGE_END - STAGE_START))
73
+ - |
74
+ if [ "$STAGE_EXIT" -eq 0 ]; then
75
+ GENERATE_STATUS="pass"
76
+ echo "Generate stage passed in ${GENERATE_DURATION}s"
77
+ else
78
+ GENERATE_STATUS="fail"
79
+ GENERATE_ERROR_SUMMARY=$(tail -c 500 "$STAGE_STDERR_FILE" | tr -d '\000' | tr '"' "'" | tr '\n' ' ')
80
+ FIRST_FAILURE="generate"
81
+ echo "Generate stage FAILED (exit code $STAGE_EXIT) in ${GENERATE_DURATION}s"
82
+ fi
83
+ - rm -f "$STAGE_STDERR_FILE"
84
+
85
+ build:
86
+ commands:
87
+ # --- Stage: Validate (placeholder) ---
88
+ - echo "=== Stage: Validate ==="
89
+ - STAGE_START=$(date +%s)
90
+ - VALIDATE_LOG_POINTER="$LOG_POINTER_PREFIX"
91
+ - STAGE_STDERR_FILE=$(mktemp)
92
+ - |
93
+ if [ -n "$FIRST_FAILURE" ]; then
94
+ echo "Skipping Validate stage due to prior failure in $FIRST_FAILURE"
95
+ VALIDATE_STATUS="skip"
96
+ VALIDATE_DURATION=0
97
+ else
98
+ (
99
+ set -e
100
+ cd /tmp/ci-project
101
+ echo "Validate stage placeholder — static checks are a backlog item (BL-001)"
102
+ ) 2>"$STAGE_STDERR_FILE"; STAGE_EXIT=$?
103
+ STAGE_END=$(date +%s)
104
+ VALIDATE_DURATION=$((STAGE_END - STAGE_START))
105
+ if [ "$STAGE_EXIT" -eq 0 ]; then
106
+ VALIDATE_STATUS="pass"
107
+ echo "Validate stage passed in ${VALIDATE_DURATION}s"
108
+ else
109
+ VALIDATE_STATUS="fail"
110
+ VALIDATE_ERROR_SUMMARY=$(tail -c 500 "$STAGE_STDERR_FILE" | tr -d '\000' | tr '"' "'" | tr '\n' ' ')
111
+ FIRST_FAILURE="validate"
112
+ echo "Validate stage FAILED (exit code $STAGE_EXIT) in ${VALIDATE_DURATION}s"
113
+ fi
114
+ fi
115
+ - rm -f "$STAGE_STDERR_FILE"
116
+
117
+ # --- Stage: Build (strategy-dependent) ---
118
+ - echo "=== Stage: Build ==="
119
+ - STAGE_START=$(date +%s)
120
+ - BUILD_LOG_POINTER="$LOG_POINTER_PREFIX"
121
+ - STAGE_STDERR_FILE=$(mktemp)
122
+ - |
123
+ if [ -n "$FIRST_FAILURE" ]; then
124
+ echo "Skipping Build stage due to prior failure in $FIRST_FAILURE"
125
+ BUILD_STATUS="skip"
126
+ BUILD_DURATION=0
127
+ else
128
+ (
129
+ set -e
130
+ cd /tmp/ci-project
131
+ if [ "$BUILD_STRATEGY" = "docker-in-docker" ]; then
132
+ echo "Build strategy: docker-in-docker"
133
+ ./do/build
134
+ ./do/push
135
+ else
136
+ echo "Build strategy: codebuild-submit"
137
+ ./do/submit
138
+ fi
139
+ ) 2>"$STAGE_STDERR_FILE"; STAGE_EXIT=$?
140
+ STAGE_END=$(date +%s)
141
+ BUILD_DURATION=$((STAGE_END - STAGE_START))
142
+ if [ "$STAGE_EXIT" -eq 0 ]; then
143
+ BUILD_STATUS="pass"
144
+ echo "Build stage passed in ${BUILD_DURATION}s"
145
+ else
146
+ BUILD_STATUS="fail"
147
+ BUILD_ERROR_SUMMARY=$(tail -c 500 "$STAGE_STDERR_FILE" | tr -d '\000' | tr '"' "'" | tr '\n' ' ')
148
+ FIRST_FAILURE="build"
149
+ echo "Build stage FAILED (exit code $STAGE_EXIT) in ${BUILD_DURATION}s"
150
+ fi
151
+ fi
152
+ - rm -f "$STAGE_STDERR_FILE"
153
+
154
+ # --- Stage: Deploy_Test ---
155
+ - echo "=== Stage: Deploy_Test ==="
156
+ - STAGE_START=$(date +%s)
157
+ - DEPLOY_TEST_LOG_POINTER="$LOG_POINTER_PREFIX"
158
+ - STAGE_STDERR_FILE=$(mktemp)
159
+ - |
160
+ if [ -n "$FIRST_FAILURE" ]; then
161
+ echo "Skipping Deploy_Test stage due to prior failure in $FIRST_FAILURE"
162
+ DEPLOY_TEST_STATUS="skip"
163
+ DEPLOY_TEST_DURATION=0
164
+ else
165
+ (
166
+ set -e
167
+ cd /tmp/ci-project
168
+ ./do/deploy
169
+ ./do/test
170
+ ) 2>"$STAGE_STDERR_FILE"; STAGE_EXIT=$?
171
+ STAGE_END=$(date +%s)
172
+ DEPLOY_TEST_DURATION=$((STAGE_END - STAGE_START))
173
+ if [ "$STAGE_EXIT" -eq 0 ]; then
174
+ DEPLOY_TEST_STATUS="pass"
175
+ echo "Deploy_Test stage passed in ${DEPLOY_TEST_DURATION}s"
176
+ else
177
+ DEPLOY_TEST_STATUS="fail"
178
+ DEPLOY_TEST_ERROR_SUMMARY=$(tail -c 500 "$STAGE_STDERR_FILE" | tr -d '\000' | tr '"' "'" | tr '\n' ' ')
179
+ FIRST_FAILURE="deploy_test"
180
+ echo "Deploy_Test stage FAILED (exit code $STAGE_EXIT) in ${DEPLOY_TEST_DURATION}s"
181
+ fi
182
+ fi
183
+ - rm -f "$STAGE_STDERR_FILE"
184
+
185
+ # --- Stage: Register (placeholder) ---
186
+ - echo "=== Stage: Register ==="
187
+ - STAGE_START=$(date +%s)
188
+ - REGISTER_LOG_POINTER="$LOG_POINTER_PREFIX"
189
+ - STAGE_STDERR_FILE=$(mktemp)
190
+ - |
191
+ if [ -n "$FIRST_FAILURE" ]; then
192
+ echo "Skipping Register stage due to prior failure in $FIRST_FAILURE"
193
+ REGISTER_STATUS="skip"
194
+ REGISTER_DURATION=0
195
+ else
196
+ (
197
+ set -e
198
+ cd /tmp/ci-project
199
+ echo "Register stage placeholder — future registration capabilities"
200
+ ) 2>"$STAGE_STDERR_FILE"; STAGE_EXIT=$?
201
+ STAGE_END=$(date +%s)
202
+ REGISTER_DURATION=$((STAGE_END - STAGE_START))
203
+ if [ "$STAGE_EXIT" -eq 0 ]; then
204
+ REGISTER_STATUS="pass"
205
+ echo "Register stage passed in ${REGISTER_DURATION}s"
206
+ else
207
+ REGISTER_STATUS="fail"
208
+ REGISTER_ERROR_SUMMARY=$(tail -c 500 "$STAGE_STDERR_FILE" | tr -d '\000' | tr '"' "'" | tr '\n' ' ')
209
+ FIRST_FAILURE="register"
210
+ echo "Register stage FAILED (exit code $STAGE_EXIT) in ${REGISTER_DURATION}s"
211
+ fi
212
+ fi
213
+ - rm -f "$STAGE_STDERR_FILE"
214
+
215
+ post_build:
216
+ commands:
217
+ # --- Stage: Teardown (always runs regardless of prior failures) ---
218
+ - echo "=== Stage: Teardown ==="
219
+ - STAGE_START=$(date +%s)
220
+ - TEARDOWN_LOG_POINTER="$LOG_POINTER_PREFIX"
221
+ - STAGE_STDERR_FILE=$(mktemp)
222
+ - |
223
+ (
224
+ set -e
225
+ cd /tmp/ci-project
226
+ ./do/clean all --force
227
+ ) 2>"$STAGE_STDERR_FILE"; TEARDOWN_EXIT=$?
228
+ - STAGE_END=$(date +%s)
229
+ - TEARDOWN_DURATION=$((STAGE_END - STAGE_START))
230
+ - |
231
+ if [ "$TEARDOWN_EXIT" -eq 0 ]; then
232
+ TEARDOWN_STATUS="pass"
233
+ echo "Teardown stage passed in ${TEARDOWN_DURATION}s"
234
+ else
235
+ TEARDOWN_STATUS="fail"
236
+ TEARDOWN_ERROR_SUMMARY=$(tail -c 500 "$STAGE_STDERR_FILE" | tr -d '\000' | tr '"' "'" | tr '\n' ' ')
237
+ echo "Teardown stage FAILED (exit code $TEARDOWN_EXIT) in ${TEARDOWN_DURATION}s"
238
+ fi
239
+ - rm -f "$STAGE_STDERR_FILE"
240
+
241
+ # --- Stage: Update (always runs — writes results to DynamoDB) ---
242
+ - echo "=== Stage: Update ==="
243
+ - STAGE_START=$(date +%s)
244
+ - UPDATE_LOG_POINTER="$LOG_POINTER_PREFIX"
245
+ - STAGE_STDERR_FILE=$(mktemp)
246
+ # Determine final testStatus
247
+ - |
248
+ if [ -n "$FIRST_FAILURE" ]; then
249
+ FINAL_TEST_STATUS="fail-${FIRST_FAILURE}"
250
+ else
251
+ FINAL_TEST_STATUS="pass"
252
+ fi
253
+ # Compute total duration as wall-clock elapsed time from build start
254
+ - TOTAL_DURATION=$(($(date +%s) - BUILD_START_TIME))
255
+ # Build the error message from the first failing stage
256
+ - |
257
+ if [ -n "$FIRST_FAILURE" ]; then
258
+ case "$FIRST_FAILURE" in
259
+ generate) FINAL_ERROR_MESSAGE="$GENERATE_ERROR_SUMMARY" ;;
260
+ validate) FINAL_ERROR_MESSAGE="$VALIDATE_ERROR_SUMMARY" ;;
261
+ build) FINAL_ERROR_MESSAGE="$BUILD_ERROR_SUMMARY" ;;
262
+ deploy_test) FINAL_ERROR_MESSAGE="$DEPLOY_TEST_ERROR_SUMMARY" ;;
263
+ register) FINAL_ERROR_MESSAGE="$REGISTER_ERROR_SUMMARY" ;;
264
+ *) FINAL_ERROR_MESSAGE="Unknown failure stage" ;;
265
+ esac
266
+ else
267
+ FINAL_ERROR_MESSAGE=""
268
+ fi
269
+ # Escape special characters in error messages for JSON
270
+ - |
271
+ ESCAPED_GENERATE_ERROR=$(printf '%s' "$GENERATE_ERROR_SUMMARY" | sed 's/\\/\\\\/g; s/"/\\"/g')
272
+ ESCAPED_VALIDATE_ERROR=$(printf '%s' "$VALIDATE_ERROR_SUMMARY" | sed 's/\\/\\\\/g; s/"/\\"/g')
273
+ ESCAPED_BUILD_ERROR=$(printf '%s' "$BUILD_ERROR_SUMMARY" | sed 's/\\/\\\\/g; s/"/\\"/g')
274
+ ESCAPED_DEPLOY_TEST_ERROR=$(printf '%s' "$DEPLOY_TEST_ERROR_SUMMARY" | sed 's/\\/\\\\/g; s/"/\\"/g')
275
+ ESCAPED_REGISTER_ERROR=$(printf '%s' "$REGISTER_ERROR_SUMMARY" | sed 's/\\/\\\\/g; s/"/\\"/g')
276
+ ESCAPED_TEARDOWN_ERROR=$(printf '%s' "$TEARDOWN_ERROR_SUMMARY" | sed 's/\\/\\\\/g; s/"/\\"/g')
277
+ ESCAPED_FINAL_ERROR=$(printf '%s' "$FINAL_ERROR_MESSAGE" | sed 's/\\/\\\\/g; s/"/\\"/g')
278
+ # Write results to DynamoDB
279
+ - |
280
+ (
281
+ set -e
282
+ LAST_TEST_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
283
+ aws dynamodb update-item \
284
+ --table-name "$CI_TABLE_NAME" \
285
+ --key "{\"configId\":{\"S\":\"$CONFIG_ID\"}}" \
286
+ --update-expression "SET testStatus = :ts, lastTestTimestamp = :ltt, lastTestDuration = :ltd, errorMessage = :em, stageResults = :sr" \
287
+ --expression-attribute-values "{
288
+ \":ts\": {\"S\": \"$FINAL_TEST_STATUS\"},
289
+ \":ltt\": {\"S\": \"$LAST_TEST_TIMESTAMP\"},
290
+ \":ltd\": {\"N\": \"$TOTAL_DURATION\"},
291
+ \":em\": {\"S\": \"$ESCAPED_FINAL_ERROR\"},
292
+ \":sr\": {\"M\": {
293
+ \"generate\": {\"M\": {
294
+ \"status\": {\"S\": \"$GENERATE_STATUS\"},
295
+ \"durationSeconds\": {\"N\": \"$GENERATE_DURATION\"},
296
+ \"logPointer\": {\"S\": \"$GENERATE_LOG_POINTER\"},
297
+ \"errorSummary\": {\"S\": \"$ESCAPED_GENERATE_ERROR\"}
298
+ }},
299
+ \"validate\": {\"M\": {
300
+ \"status\": {\"S\": \"$VALIDATE_STATUS\"},
301
+ \"durationSeconds\": {\"N\": \"$VALIDATE_DURATION\"},
302
+ \"logPointer\": {\"S\": \"$VALIDATE_LOG_POINTER\"},
303
+ \"errorSummary\": {\"S\": \"$ESCAPED_VALIDATE_ERROR\"}
304
+ }},
305
+ \"build\": {\"M\": {
306
+ \"status\": {\"S\": \"$BUILD_STATUS\"},
307
+ \"durationSeconds\": {\"N\": \"$BUILD_DURATION\"},
308
+ \"logPointer\": {\"S\": \"$BUILD_LOG_POINTER\"},
309
+ \"errorSummary\": {\"S\": \"$ESCAPED_BUILD_ERROR\"}
310
+ }},
311
+ \"deploy_test\": {\"M\": {
312
+ \"status\": {\"S\": \"$DEPLOY_TEST_STATUS\"},
313
+ \"durationSeconds\": {\"N\": \"$DEPLOY_TEST_DURATION\"},
314
+ \"logPointer\": {\"S\": \"$DEPLOY_TEST_LOG_POINTER\"},
315
+ \"errorSummary\": {\"S\": \"$ESCAPED_DEPLOY_TEST_ERROR\"}
316
+ }},
317
+ \"register\": {\"M\": {
318
+ \"status\": {\"S\": \"$REGISTER_STATUS\"},
319
+ \"durationSeconds\": {\"N\": \"$REGISTER_DURATION\"},
320
+ \"logPointer\": {\"S\": \"$REGISTER_LOG_POINTER\"},
321
+ \"errorSummary\": {\"S\": \"$ESCAPED_REGISTER_ERROR\"}
322
+ }},
323
+ \"teardown\": {\"M\": {
324
+ \"status\": {\"S\": \"$TEARDOWN_STATUS\"},
325
+ \"durationSeconds\": {\"N\": \"$TEARDOWN_DURATION\"},
326
+ \"logPointer\": {\"S\": \"$TEARDOWN_LOG_POINTER\"},
327
+ \"errorSummary\": {\"S\": \"$ESCAPED_TEARDOWN_ERROR\"}
328
+ }},
329
+ \"update\": {\"M\": {
330
+ \"status\": {\"S\": \"pass\"},
331
+ \"durationSeconds\": {\"N\": \"0\"},
332
+ \"logPointer\": {\"S\": \"$UPDATE_LOG_POINTER\"},
333
+ \"errorSummary\": {\"S\": \"\"}
334
+ }}
335
+ }}
336
+ }"
337
+ ) 2>"$STAGE_STDERR_FILE"; UPDATE_EXIT=$?
338
+ - STAGE_END=$(date +%s)
339
+ - UPDATE_DURATION=$((STAGE_END - STAGE_START))
340
+ - |
341
+ if [ "$UPDATE_EXIT" -eq 0 ]; then
342
+ UPDATE_STATUS="pass"
343
+ echo "Update stage passed in ${UPDATE_DURATION}s"
344
+ else
345
+ UPDATE_STATUS="fail"
346
+ UPDATE_ERROR_SUMMARY=$(tail -c 500 "$STAGE_STDERR_FILE" | tr -d '\000' | tr '"' "'" | tr '\n' ' ')
347
+ echo "Update stage FAILED (exit code $UPDATE_EXIT) in ${UPDATE_DURATION}s"
348
+ fi
349
+ - rm -f "$STAGE_STDERR_FILE"
350
+ - echo "=== MLCC CI Harness Complete ==="
351
+ - echo "Final status: $FINAL_TEST_STATUS"
352
+ - echo "Total duration: ${TOTAL_DURATION}s"
@@ -0,0 +1,27 @@
1
+ {
2
+ "app": "npx ts-node bin/ci-harness.ts",
3
+ "watch": {
4
+ "include": [
5
+ "**"
6
+ ],
7
+ "exclude": [
8
+ "README.md",
9
+ "cdk*.json",
10
+ "**/*.d.ts",
11
+ "**/*.js",
12
+ "tsconfig.json",
13
+ "package*.json",
14
+ "node_modules",
15
+ "dist",
16
+ "test"
17
+ ]
18
+ },
19
+ "context": {
20
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
21
+ "@aws-cdk/core:checkSecretUsage": true,
22
+ "@aws-cdk/core:target-partitions": [
23
+ "aws",
24
+ "aws-cn"
25
+ ]
26
+ }
27
+ }
@@ -0,0 +1,199 @@
1
+ import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb';
2
+ import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn';
3
+
4
+ /**
5
+ * Scanner Lambda handler — queries the CI_Table GSI for records that need
6
+ * re-testing and starts Step Functions executions directly.
7
+ *
8
+ * Query pattern (uses GSI `testStatus-lastTestTimestamp-index`):
9
+ * 1. All records with testStatus = 'untested'
10
+ * 2. Records with testStatus IN (pass, fail-generate, fail-validate,
11
+ * fail-build, fail-deploy, fail-test) AND lastTestTimestamp < now - 24h
12
+ *
13
+ * Records with testStatus = 'running' are always excluded.
14
+ *
15
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 14.1
16
+ */
17
+
18
+ const TABLE_NAME = process.env.CI_TABLE_NAME ?? '';
19
+ const STATE_MACHINE_ARN = process.env.STATE_MACHINE_ARN ?? '';
20
+ const GSI_NAME = process.env.GSI_NAME ?? 'testStatus-lastTestTimestamp-index';
21
+
22
+ const dynamodb = new DynamoDBClient({});
23
+ const sfn = new SFNClient({});
24
+
25
+ /** Default build strategy when the attribute is missing from a record. */
26
+ const DEFAULT_BUILD_STRATEGY = 'codebuild-submit';
27
+
28
+ /**
29
+ * Status values that qualify for stale-record re-testing (all except 'running'
30
+ * and 'untested', which is handled separately without a timestamp filter).
31
+ */
32
+ const STALE_STATUSES = [
33
+ 'pass',
34
+ 'fail-generate',
35
+ 'fail-validate',
36
+ 'fail-build',
37
+ 'fail-deploy',
38
+ 'fail-test',
39
+ ];
40
+
41
+ interface CiRecord {
42
+ configId: string;
43
+ configJson: string;
44
+ buildStrategy: string;
45
+ }
46
+
47
+ /**
48
+ * Query all 'untested' records from the GSI. No timestamp filter needed —
49
+ * every untested record should be picked up.
50
+ */
51
+ async function queryUntestedRecords(): Promise<CiRecord[]> {
52
+ const records: CiRecord[] = [];
53
+ let exclusiveStartKey: Record<string, any> | undefined;
54
+
55
+ do {
56
+ const command = new QueryCommand({
57
+ TableName: TABLE_NAME,
58
+ IndexName: GSI_NAME,
59
+ KeyConditionExpression: 'testStatus = :status',
60
+ ExpressionAttributeValues: {
61
+ ':status': { S: 'untested' },
62
+ },
63
+ ProjectionExpression: 'configId, configJson, buildStrategy',
64
+ ExclusiveStartKey: exclusiveStartKey,
65
+ });
66
+
67
+ const result = await dynamodb.send(command);
68
+
69
+ for (const item of result.Items ?? []) {
70
+ records.push({
71
+ configId: item.configId?.S ?? '',
72
+ configJson: item.configJson?.S ?? '',
73
+ buildStrategy: item.buildStrategy?.S ?? DEFAULT_BUILD_STRATEGY,
74
+ });
75
+ }
76
+
77
+ exclusiveStartKey = result.LastEvaluatedKey;
78
+ } while (exclusiveStartKey);
79
+
80
+ return records;
81
+ }
82
+
83
+ /**
84
+ * Query records for a specific testStatus where lastTestTimestamp is older
85
+ * than the given cutoff (ISO 8601 string comparison works because the format
86
+ * is lexicographically sortable).
87
+ */
88
+ async function queryStaleRecordsByStatus(
89
+ status: string,
90
+ cutoffTimestamp: string
91
+ ): Promise<CiRecord[]> {
92
+ const records: CiRecord[] = [];
93
+ let exclusiveStartKey: Record<string, any> | undefined;
94
+
95
+ do {
96
+ const command = new QueryCommand({
97
+ TableName: TABLE_NAME,
98
+ IndexName: GSI_NAME,
99
+ KeyConditionExpression:
100
+ 'testStatus = :status AND lastTestTimestamp < :cutoff',
101
+ ExpressionAttributeValues: {
102
+ ':status': { S: status },
103
+ ':cutoff': { S: cutoffTimestamp },
104
+ },
105
+ ProjectionExpression: 'configId, configJson, buildStrategy',
106
+ ExclusiveStartKey: exclusiveStartKey,
107
+ });
108
+
109
+ const result = await dynamodb.send(command);
110
+
111
+ for (const item of result.Items ?? []) {
112
+ records.push({
113
+ configId: item.configId?.S ?? '',
114
+ configJson: item.configJson?.S ?? '',
115
+ buildStrategy: item.buildStrategy?.S ?? DEFAULT_BUILD_STRATEGY,
116
+ });
117
+ }
118
+
119
+ exclusiveStartKey = result.LastEvaluatedKey;
120
+ } while (exclusiveStartKey);
121
+
122
+ return records;
123
+ }
124
+
125
+ /**
126
+ * Start a Step Functions execution for a CI record.
127
+ * Returns the execution ARN on success, or null on failure.
128
+ */
129
+ async function startExecution(record: CiRecord): Promise<string | null> {
130
+ try {
131
+ const input = JSON.stringify({
132
+ configId: record.configId,
133
+ configJson: record.configJson,
134
+ buildStrategy: record.buildStrategy,
135
+ });
136
+
137
+ const command = new StartExecutionCommand({
138
+ stateMachineArn: STATE_MACHINE_ARN,
139
+ input,
140
+ });
141
+
142
+ const result = await sfn.send(command);
143
+ return result.executionArn ?? null;
144
+ } catch (error) {
145
+ console.error(
146
+ `Failed to start execution for ${record.configId}:`,
147
+ error
148
+ );
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Lambda entry point. Invoked on an hourly EventBridge schedule or manually via do/ci trigger.
155
+ */
156
+ export async function handler(): Promise<{ executionArns: string[] }> {
157
+ const now = new Date();
158
+ const cutoff = new Date(now.getTime() - 24 * 60 * 60 * 1000);
159
+ const cutoffTimestamp = cutoff.toISOString();
160
+
161
+ // 1. Collect qualifying records from multiple GSI queries
162
+ const allRecords: CiRecord[] = [];
163
+
164
+ // 1a. All untested records (no timestamp filter)
165
+ const untestedRecords = await queryUntestedRecords();
166
+ allRecords.push(...untestedRecords);
167
+
168
+ // 1b. Stale records for each non-running status
169
+ for (const status of STALE_STATUSES) {
170
+ const staleRecords = await queryStaleRecordsByStatus(
171
+ status,
172
+ cutoffTimestamp
173
+ );
174
+ allRecords.push(...staleRecords);
175
+ }
176
+
177
+ const totalFound = allRecords.length;
178
+
179
+ if (totalFound === 0) {
180
+ console.log('Found 0 qualifying records, started 0 executions');
181
+ return { executionArns: [] };
182
+ }
183
+
184
+ // 2. Start Step Functions execution for each record
185
+ const executionArns: string[] = [];
186
+
187
+ for (const record of allRecords) {
188
+ const arn = await startExecution(record);
189
+ if (arn) {
190
+ executionArns.push(arn);
191
+ }
192
+ }
193
+
194
+ console.log(
195
+ `Found ${totalFound} qualifying records, started ${executionArns.length} executions`
196
+ );
197
+
198
+ return { executionArns };
199
+ }