@aws/ml-container-creator 0.2.0 → 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.
- package/bin/cli.js +88 -86
- package/config/bootstrap-stack.json +211 -0
- package/config/parameter-schema.json +88 -0
- package/infra/ci-harness/bin/ci-harness.ts +26 -0
- package/infra/ci-harness/buildspec.yml +352 -0
- package/infra/ci-harness/cdk.json +27 -0
- package/infra/ci-harness/lambda/scanner/index.ts +199 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +609 -0
- package/infra/ci-harness/package-lock.json +3979 -0
- package/infra/ci-harness/package.json +32 -0
- package/infra/ci-harness/tsconfig.json +38 -0
- package/package.json +19 -10
- package/src/app.js +318 -318
- package/src/copy-tpl.js +19 -19
- package/src/lib/asset-manager.js +74 -74
- package/src/lib/aws-profile-parser.js +45 -45
- package/src/lib/bootstrap-command-handler.js +560 -547
- package/src/lib/bootstrap-config.js +45 -45
- package/src/lib/ci-register-helpers.js +19 -19
- package/src/lib/ci-report-helpers.js +37 -37
- package/src/lib/ci-stage-helpers.js +49 -49
- package/src/lib/comment-generator.js +4 -4
- package/src/lib/config-manager.js +105 -105
- package/src/lib/deployment-config-resolver.js +10 -10
- package/src/lib/deployment-registry.js +153 -153
- package/src/lib/engine-prefix-resolver.js +8 -8
- package/src/lib/key-value-parser.js +6 -6
- package/src/lib/manifest-cli.js +108 -108
- package/src/lib/prompt-runner.js +224 -224
- package/src/lib/prompts.js +121 -121
- package/src/lib/registry-command-handler.js +174 -174
- package/src/lib/registry-loader.js +52 -52
- package/src/lib/sensitive-redactor.js +9 -9
- package/src/lib/template-engine.js +1 -1
- package/src/lib/template-manager.js +62 -62
- 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
|
+
}
|