@aws/ml-container-creator 0.2.0
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/LICENSE +202 -0
- package/LICENSE-THIRD-PARTY +68620 -0
- package/NOTICE +2 -0
- package/README.md +106 -0
- package/bin/cli.js +365 -0
- package/config/defaults.json +32 -0
- package/config/presets/transformers-djl.json +26 -0
- package/config/presets/transformers-gpu.json +24 -0
- package/config/presets/transformers-lmi.json +27 -0
- package/package.json +129 -0
- package/servers/README.md +419 -0
- package/servers/base-image-picker/catalogs/model-servers.json +1191 -0
- package/servers/base-image-picker/catalogs/python-slim.json +38 -0
- package/servers/base-image-picker/catalogs/triton-backends.json +51 -0
- package/servers/base-image-picker/catalogs/triton.json +38 -0
- package/servers/base-image-picker/index.js +495 -0
- package/servers/base-image-picker/manifest.json +17 -0
- package/servers/base-image-picker/package.json +15 -0
- package/servers/hyperpod-cluster-picker/LICENSE +202 -0
- package/servers/hyperpod-cluster-picker/index.js +424 -0
- package/servers/hyperpod-cluster-picker/manifest.json +14 -0
- package/servers/hyperpod-cluster-picker/package.json +17 -0
- package/servers/instance-recommender/LICENSE +202 -0
- package/servers/instance-recommender/catalogs/instances.json +852 -0
- package/servers/instance-recommender/index.js +284 -0
- package/servers/instance-recommender/manifest.json +16 -0
- package/servers/instance-recommender/package.json +15 -0
- package/servers/lib/LICENSE +202 -0
- package/servers/lib/bedrock-client.js +160 -0
- package/servers/lib/custom-validators.js +46 -0
- package/servers/lib/dynamic-resolver.js +36 -0
- package/servers/lib/package.json +11 -0
- package/servers/lib/schemas/image-catalog.schema.json +185 -0
- package/servers/lib/schemas/instances.schema.json +124 -0
- package/servers/lib/schemas/manifest.schema.json +64 -0
- package/servers/lib/schemas/model-catalog.schema.json +91 -0
- package/servers/lib/schemas/regions.schema.json +26 -0
- package/servers/lib/schemas/triton-backends.schema.json +51 -0
- package/servers/model-picker/catalogs/jumpstart-public.json +66 -0
- package/servers/model-picker/catalogs/popular-diffusors.json +88 -0
- package/servers/model-picker/catalogs/popular-transformers.json +226 -0
- package/servers/model-picker/index.js +1693 -0
- package/servers/model-picker/manifest.json +18 -0
- package/servers/model-picker/package.json +20 -0
- package/servers/region-picker/LICENSE +202 -0
- package/servers/region-picker/catalogs/regions.json +263 -0
- package/servers/region-picker/index.js +230 -0
- package/servers/region-picker/manifest.json +16 -0
- package/servers/region-picker/package.json +15 -0
- package/src/app.js +1007 -0
- package/src/copy-tpl.js +77 -0
- package/src/lib/accelerator-validator.js +39 -0
- package/src/lib/asset-manager.js +385 -0
- package/src/lib/aws-profile-parser.js +181 -0
- package/src/lib/bootstrap-command-handler.js +1647 -0
- package/src/lib/bootstrap-config.js +238 -0
- package/src/lib/ci-register-helpers.js +124 -0
- package/src/lib/ci-report-helpers.js +158 -0
- package/src/lib/ci-stage-helpers.js +268 -0
- package/src/lib/cli-handler.js +529 -0
- package/src/lib/comment-generator.js +544 -0
- package/src/lib/community-reports-validator.js +91 -0
- package/src/lib/config-manager.js +2106 -0
- package/src/lib/configuration-exporter.js +204 -0
- package/src/lib/configuration-manager.js +695 -0
- package/src/lib/configuration-matcher.js +221 -0
- package/src/lib/cpu-validator.js +36 -0
- package/src/lib/cuda-validator.js +57 -0
- package/src/lib/deployment-config-resolver.js +103 -0
- package/src/lib/deployment-entry-schema.js +125 -0
- package/src/lib/deployment-registry.js +598 -0
- package/src/lib/docker-introspection-validator.js +51 -0
- package/src/lib/engine-prefix-resolver.js +60 -0
- package/src/lib/huggingface-client.js +172 -0
- package/src/lib/key-value-parser.js +37 -0
- package/src/lib/known-flags-validator.js +200 -0
- package/src/lib/manifest-cli.js +280 -0
- package/src/lib/mcp-client.js +303 -0
- package/src/lib/mcp-command-handler.js +532 -0
- package/src/lib/neuron-validator.js +80 -0
- package/src/lib/parameter-schema-validator.js +284 -0
- package/src/lib/prompt-runner.js +1349 -0
- package/src/lib/prompts.js +1138 -0
- package/src/lib/registry-command-handler.js +519 -0
- package/src/lib/registry-loader.js +198 -0
- package/src/lib/rocm-validator.js +80 -0
- package/src/lib/schema-validator.js +157 -0
- package/src/lib/sensitive-redactor.js +59 -0
- package/src/lib/template-engine.js +156 -0
- package/src/lib/template-manager.js +341 -0
- package/src/lib/validation-engine.js +314 -0
- package/src/prompt-adapter.js +63 -0
- package/templates/Dockerfile +300 -0
- package/templates/IAM_PERMISSIONS.md +84 -0
- package/templates/MIGRATION.md +488 -0
- package/templates/PROJECT_README.md +439 -0
- package/templates/TEMPLATE_SYSTEM.md +243 -0
- package/templates/buildspec.yml +64 -0
- package/templates/code/chat_template.jinja +1 -0
- package/templates/code/flask/gunicorn_config.py +35 -0
- package/templates/code/flask/wsgi.py +10 -0
- package/templates/code/model_handler.py +387 -0
- package/templates/code/serve +300 -0
- package/templates/code/serve.py +175 -0
- package/templates/code/serving.properties +105 -0
- package/templates/code/start_server.py +39 -0
- package/templates/code/start_server.sh +39 -0
- package/templates/diffusors/Dockerfile +72 -0
- package/templates/diffusors/patch_image_api.py +35 -0
- package/templates/diffusors/serve +115 -0
- package/templates/diffusors/start_server.sh +114 -0
- package/templates/do/.gitkeep +1 -0
- package/templates/do/README.md +541 -0
- package/templates/do/build +83 -0
- package/templates/do/ci +681 -0
- package/templates/do/clean +811 -0
- package/templates/do/config +260 -0
- package/templates/do/deploy +1560 -0
- package/templates/do/export +306 -0
- package/templates/do/logs +319 -0
- package/templates/do/manifest +12 -0
- package/templates/do/push +119 -0
- package/templates/do/register +580 -0
- package/templates/do/run +113 -0
- package/templates/do/submit +417 -0
- package/templates/do/test +1147 -0
- package/templates/hyperpod/configmap.yaml +24 -0
- package/templates/hyperpod/deployment.yaml +71 -0
- package/templates/hyperpod/pvc.yaml +42 -0
- package/templates/hyperpod/service.yaml +17 -0
- package/templates/nginx-diffusors.conf +74 -0
- package/templates/nginx-predictors.conf +47 -0
- package/templates/nginx-tensorrt.conf +74 -0
- package/templates/requirements.txt +61 -0
- package/templates/sample_model/test_inference.py +123 -0
- package/templates/sample_model/train_abalone.py +252 -0
- package/templates/test/test_endpoint.sh +79 -0
- package/templates/test/test_local_image.sh +80 -0
- package/templates/test/test_model_handler.py +180 -0
- package/templates/triton/Dockerfile +128 -0
- package/templates/triton/config.pbtxt +163 -0
- package/templates/triton/model.py +130 -0
- package/templates/triton/requirements.txt +11 -0
package/templates/do/ci
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
set -u
|
|
7
|
+
set -o pipefail
|
|
8
|
+
|
|
9
|
+
# Source configuration
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
source "${SCRIPT_DIR}/config"
|
|
12
|
+
|
|
13
|
+
# ============================================================
|
|
14
|
+
# CI Integration Harness — local entry point
|
|
15
|
+
# Subcommands: report, status, trigger, dashboard
|
|
16
|
+
# ============================================================
|
|
17
|
+
|
|
18
|
+
CI_TABLE_NAME="${CI_TABLE_NAME:-mlcc-ci-table}"
|
|
19
|
+
CI_LOG_GROUP="${CI_LOG_GROUP:-ml-container-creator-ci}"
|
|
20
|
+
|
|
21
|
+
# 15 known deployment configurations from the catalog
|
|
22
|
+
KNOWN_CONFIGS=(
|
|
23
|
+
"transformers-vllm"
|
|
24
|
+
"transformers-sglang"
|
|
25
|
+
"transformers-lmi"
|
|
26
|
+
"transformers-djl"
|
|
27
|
+
"transformers-tensorrt-llm"
|
|
28
|
+
"http-flask"
|
|
29
|
+
"http-fastapi"
|
|
30
|
+
"http-nginx"
|
|
31
|
+
"triton-fil"
|
|
32
|
+
"triton-python"
|
|
33
|
+
"triton-onnx"
|
|
34
|
+
"triton-tensorrt"
|
|
35
|
+
"diffusors-vllm"
|
|
36
|
+
"diffusors-sglang"
|
|
37
|
+
"diffusors-comfyui"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ============================================================
|
|
41
|
+
# Prerequisite check: CI infrastructure must be provisioned
|
|
42
|
+
# ============================================================
|
|
43
|
+
|
|
44
|
+
check_ci_infrastructure() {
|
|
45
|
+
if ! aws dynamodb describe-table --table-name "${CI_TABLE_NAME}" --region "${AWS_REGION}" &>/dev/null; then
|
|
46
|
+
echo "❌ CI infrastructure not provisioned."
|
|
47
|
+
echo " Run 'ml-container-creator bootstrap' with CI enabled."
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ============================================================
|
|
53
|
+
# ci_usage — display help when no subcommand is given
|
|
54
|
+
# ============================================================
|
|
55
|
+
|
|
56
|
+
ci_usage() {
|
|
57
|
+
echo "Usage: ./do/ci <subcommand> [options]"
|
|
58
|
+
echo ""
|
|
59
|
+
echo "Subcommands:"
|
|
60
|
+
echo " report Show coverage report across all deployment configurations"
|
|
61
|
+
echo " status Show current CI system status summary"
|
|
62
|
+
echo " trigger Manually invoke the CI scanner to queue stale records"
|
|
63
|
+
echo " dashboard Start a local web dashboard for CI results"
|
|
64
|
+
echo ""
|
|
65
|
+
echo "Options:"
|
|
66
|
+
echo " report --json Output report in machine-parseable JSON"
|
|
67
|
+
echo " dashboard --port <N> Start dashboard on a custom port (default: 3939)"
|
|
68
|
+
echo ""
|
|
69
|
+
echo "Examples:"
|
|
70
|
+
echo " ./do/ci report"
|
|
71
|
+
echo " ./do/ci report --json"
|
|
72
|
+
echo " ./do/ci status"
|
|
73
|
+
echo " ./do/ci trigger"
|
|
74
|
+
echo " ./do/ci dashboard"
|
|
75
|
+
echo " ./do/ci dashboard --port 8080"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ============================================================
|
|
80
|
+
# ci_report — coverage report across deployment configurations
|
|
81
|
+
# ============================================================
|
|
82
|
+
|
|
83
|
+
ci_report() {
|
|
84
|
+
local json_output=false
|
|
85
|
+
|
|
86
|
+
# Parse report-specific flags
|
|
87
|
+
while [[ $# -gt 0 ]]; do
|
|
88
|
+
case "$1" in
|
|
89
|
+
--json)
|
|
90
|
+
json_output=true
|
|
91
|
+
shift
|
|
92
|
+
;;
|
|
93
|
+
*)
|
|
94
|
+
echo "⚠️ Unknown option for report: $1"
|
|
95
|
+
echo "Usage: ./do/ci report [--json]"
|
|
96
|
+
exit 1
|
|
97
|
+
;;
|
|
98
|
+
esac
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
echo "📊 CI Coverage Report"
|
|
102
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
103
|
+
echo ""
|
|
104
|
+
|
|
105
|
+
# Scan all records from CI_Table
|
|
106
|
+
local scan_result
|
|
107
|
+
scan_result=$(aws dynamodb scan \
|
|
108
|
+
--table-name "${CI_TABLE_NAME}" \
|
|
109
|
+
--region "${AWS_REGION}" \
|
|
110
|
+
--output json 2>/dev/null) || {
|
|
111
|
+
echo "❌ Failed to scan CI table: ${CI_TABLE_NAME}"
|
|
112
|
+
exit 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
local item_count
|
|
116
|
+
item_count=$(echo "${scan_result}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Count', 0))" 2>/dev/null || echo "0")
|
|
117
|
+
|
|
118
|
+
if [ "${item_count}" -eq 0 ]; then
|
|
119
|
+
echo "No test configurations registered. Use './do/register --ci' to add configurations."
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Process records with python3 for grouping, regression detection, and summary
|
|
124
|
+
local json_flag="False"
|
|
125
|
+
if [ "${json_output}" = true ]; then
|
|
126
|
+
json_flag="True"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
echo "${scan_result}" | python3 -c "
|
|
130
|
+
import sys, json
|
|
131
|
+
|
|
132
|
+
data = json.load(sys.stdin)
|
|
133
|
+
items = data.get('Items', [])
|
|
134
|
+
json_output = ${json_flag}
|
|
135
|
+
|
|
136
|
+
known_configs = [
|
|
137
|
+
'transformers-vllm', 'transformers-sglang', 'transformers-lmi',
|
|
138
|
+
'transformers-djl', 'transformers-tensorrt-llm',
|
|
139
|
+
'http-flask', 'http-fastapi', 'http-nginx',
|
|
140
|
+
'triton-fil', 'triton-python', 'triton-onnx', 'triton-tensorrt',
|
|
141
|
+
'diffusors-vllm', 'diffusors-sglang', 'diffusors-comfyui'
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
# Group records by deploymentConfig
|
|
145
|
+
groups = {}
|
|
146
|
+
for item in items:
|
|
147
|
+
dc = item.get('deploymentConfig', {}).get('S', 'unknown')
|
|
148
|
+
if dc not in groups:
|
|
149
|
+
groups[dc] = []
|
|
150
|
+
groups[dc].append(item)
|
|
151
|
+
|
|
152
|
+
# Compute per-config status (latest record wins based on lastTestTimestamp)
|
|
153
|
+
config_status = {}
|
|
154
|
+
for dc, records in groups.items():
|
|
155
|
+
# Sort by lastTestTimestamp descending to get latest
|
|
156
|
+
sorted_records = sorted(records, key=lambda r: r.get('lastTestTimestamp', {}).get('S', ''), reverse=True)
|
|
157
|
+
latest = sorted_records[0]
|
|
158
|
+
status = latest.get('testStatus', {}).get('S', 'untested')
|
|
159
|
+
project = latest.get('projectName', {}).get('S', '')
|
|
160
|
+
last_ts = latest.get('lastTestTimestamp', {}).get('S', '')
|
|
161
|
+
duration = latest.get('lastTestDuration', {}).get('N', '0')
|
|
162
|
+
config_status[dc] = {
|
|
163
|
+
'status': status,
|
|
164
|
+
'project': project,
|
|
165
|
+
'lastTest': last_ts,
|
|
166
|
+
'duration': int(duration) if duration else 0,
|
|
167
|
+
'recordCount': len(records)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Detect regressions: configs that have a previous 'pass' but latest is 'fail-*'
|
|
171
|
+
regressions = []
|
|
172
|
+
for dc, records in groups.items():
|
|
173
|
+
sorted_records = sorted(records, key=lambda r: r.get('lastTestTimestamp', {}).get('S', ''))
|
|
174
|
+
latest_status = config_status[dc]['status']
|
|
175
|
+
if latest_status.startswith('fail-'):
|
|
176
|
+
# Check if any previous record had 'pass'
|
|
177
|
+
for r in sorted_records[:-1]:
|
|
178
|
+
if r.get('testStatus', {}).get('S', '') == 'pass':
|
|
179
|
+
regressions.append(dc)
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
# Compute summary
|
|
183
|
+
total = len(known_configs)
|
|
184
|
+
tested_configs = set(groups.keys()) & set(known_configs)
|
|
185
|
+
tested = len(tested_configs)
|
|
186
|
+
untested = total - tested
|
|
187
|
+
passing = sum(1 for dc in tested_configs if config_status.get(dc, {}).get('status') == 'pass')
|
|
188
|
+
failing = sum(1 for dc in tested_configs if config_status.get(dc, {}).get('status', '').startswith('fail-'))
|
|
189
|
+
coverage_pct = (tested / total * 100) if total > 0 else 0
|
|
190
|
+
|
|
191
|
+
if json_output:
|
|
192
|
+
report = {
|
|
193
|
+
'summary': {
|
|
194
|
+
'total': total,
|
|
195
|
+
'tested': tested,
|
|
196
|
+
'passing': passing,
|
|
197
|
+
'failing': failing,
|
|
198
|
+
'untested': untested,
|
|
199
|
+
'coveragePercent': round(coverage_pct, 1)
|
|
200
|
+
},
|
|
201
|
+
'configurations': {},
|
|
202
|
+
'regressions': regressions,
|
|
203
|
+
'untestedConfigs': [c for c in known_configs if c not in groups]
|
|
204
|
+
}
|
|
205
|
+
for dc in known_configs:
|
|
206
|
+
if dc in config_status:
|
|
207
|
+
report['configurations'][dc] = config_status[dc]
|
|
208
|
+
else:
|
|
209
|
+
report['configurations'][dc] = {'status': 'untested', 'project': '', 'lastTest': '', 'duration': 0, 'recordCount': 0}
|
|
210
|
+
# Include non-catalog configs
|
|
211
|
+
for dc in config_status:
|
|
212
|
+
if dc not in known_configs:
|
|
213
|
+
report['configurations'][dc] = config_status[dc]
|
|
214
|
+
print(json.dumps(report, indent=2))
|
|
215
|
+
else:
|
|
216
|
+
# Human-readable table
|
|
217
|
+
print(f' {\"Config\":<30} {\"Status\":<16} {\"Project\":<20} {\"Last Test\":<22} {\"Duration\":>10}')
|
|
218
|
+
print(' ' + '-' * 100)
|
|
219
|
+
for dc in known_configs:
|
|
220
|
+
if dc in config_status:
|
|
221
|
+
cs = config_status[dc]
|
|
222
|
+
status_str = cs['status']
|
|
223
|
+
if dc in regressions:
|
|
224
|
+
status_str += ' ⚠️ REGRESSION'
|
|
225
|
+
dur = f\"{cs['duration']}s\" if cs['duration'] > 0 else '-'
|
|
226
|
+
last = cs['lastTest'] if cs['lastTest'] and cs['lastTest'] != '1970-01-01T00:00:00Z' else '-'
|
|
227
|
+
print(f' {dc:<30} {status_str:<16} {cs[\"project\"]:<20} {last:<22} {dur:>10}')
|
|
228
|
+
else:
|
|
229
|
+
print(f' {dc:<30} {\"untested\":<16} {\"-\":<20} {\"-\":<22} {\"-\":>10}')
|
|
230
|
+
|
|
231
|
+
# Show non-catalog configs if any
|
|
232
|
+
extra = [dc for dc in config_status if dc not in known_configs]
|
|
233
|
+
if extra:
|
|
234
|
+
print()
|
|
235
|
+
print(' Non-catalog configurations:')
|
|
236
|
+
print(' ' + '-' * 100)
|
|
237
|
+
for dc in extra:
|
|
238
|
+
cs = config_status[dc]
|
|
239
|
+
dur = f\"{cs['duration']}s\" if cs['duration'] > 0 else '-'
|
|
240
|
+
last = cs['lastTest'] if cs['lastTest'] and cs['lastTest'] != '1970-01-01T00:00:00Z' else '-'
|
|
241
|
+
print(f' {dc:<30} {cs[\"status\"]:<16} {cs[\"project\"]:<20} {last:<22} {dur:>10}')
|
|
242
|
+
|
|
243
|
+
print()
|
|
244
|
+
if regressions:
|
|
245
|
+
print(' ⚠️ Regressions detected:')
|
|
246
|
+
for r in regressions:
|
|
247
|
+
print(f' • {r}')
|
|
248
|
+
print()
|
|
249
|
+
|
|
250
|
+
print(f' Summary: {total} total | {tested} tested | {passing} passing | {failing} failing | {untested} untested | {coverage_pct:.1f}% coverage')
|
|
251
|
+
print()
|
|
252
|
+
"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ============================================================
|
|
257
|
+
# ci_status — current CI system status summary
|
|
258
|
+
# ============================================================
|
|
259
|
+
|
|
260
|
+
ci_status() {
|
|
261
|
+
echo "📋 CI System Status"
|
|
262
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
263
|
+
echo ""
|
|
264
|
+
|
|
265
|
+
# Scan all records from CI_Table
|
|
266
|
+
local scan_result
|
|
267
|
+
scan_result=$(aws dynamodb scan \
|
|
268
|
+
--table-name "${CI_TABLE_NAME}" \
|
|
269
|
+
--region "${AWS_REGION}" \
|
|
270
|
+
--output json 2>/dev/null) || {
|
|
271
|
+
echo "❌ Failed to scan CI table: ${CI_TABLE_NAME}"
|
|
272
|
+
exit 1
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
local item_count
|
|
276
|
+
item_count=$(echo "${scan_result}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Count', 0))" 2>/dev/null || echo "0")
|
|
277
|
+
|
|
278
|
+
if [ "${item_count}" -eq 0 ]; then
|
|
279
|
+
echo "CI table is empty. Register test configurations with './do/register --ci'."
|
|
280
|
+
return 0
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
# Process records with python3 for status counts
|
|
284
|
+
echo "${scan_result}" | python3 -c "
|
|
285
|
+
import sys, json
|
|
286
|
+
|
|
287
|
+
data = json.load(sys.stdin)
|
|
288
|
+
items = data.get('Items', [])
|
|
289
|
+
|
|
290
|
+
total = len(items)
|
|
291
|
+
running = 0
|
|
292
|
+
passing = 0
|
|
293
|
+
failing = 0
|
|
294
|
+
untested = 0
|
|
295
|
+
last_completed = ''
|
|
296
|
+
|
|
297
|
+
for item in items:
|
|
298
|
+
status = item.get('testStatus', {}).get('S', 'untested')
|
|
299
|
+
ts = item.get('lastTestTimestamp', {}).get('S', '')
|
|
300
|
+
|
|
301
|
+
if status == 'running':
|
|
302
|
+
running += 1
|
|
303
|
+
elif status == 'pass':
|
|
304
|
+
passing += 1
|
|
305
|
+
elif status.startswith('fail-'):
|
|
306
|
+
failing += 1
|
|
307
|
+
elif status == 'untested':
|
|
308
|
+
untested += 1
|
|
309
|
+
|
|
310
|
+
# Track last completed test (exclude running and untested, exclude epoch)
|
|
311
|
+
if status not in ('running', 'untested') and ts and ts != '1970-01-01T00:00:00Z':
|
|
312
|
+
if not last_completed or ts > last_completed:
|
|
313
|
+
last_completed = ts
|
|
314
|
+
|
|
315
|
+
print(f' Total records: {total}')
|
|
316
|
+
print(f' Running: {running}')
|
|
317
|
+
print(f' Passing: {passing}')
|
|
318
|
+
print(f' Failing: {failing}')
|
|
319
|
+
print(f' Untested: {untested}')
|
|
320
|
+
print(f' Last completed: {last_completed if last_completed else \"N/A\"}')
|
|
321
|
+
print()
|
|
322
|
+
"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ============================================================
|
|
327
|
+
# ci_trigger — manually invoke the Scanner Lambda
|
|
328
|
+
# ============================================================
|
|
329
|
+
|
|
330
|
+
ci_trigger() {
|
|
331
|
+
echo "🚀 Triggering CI Scanner Lambda"
|
|
332
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
333
|
+
echo ""
|
|
334
|
+
|
|
335
|
+
local response_file
|
|
336
|
+
response_file=$(mktemp /tmp/ci-trigger-XXXXXX.json)
|
|
337
|
+
|
|
338
|
+
if aws lambda invoke \
|
|
339
|
+
--function-name "mlcc-ci-scanner" \
|
|
340
|
+
--region "${AWS_REGION}" \
|
|
341
|
+
--log-type Tail \
|
|
342
|
+
"${response_file}" &>/dev/null; then
|
|
343
|
+
|
|
344
|
+
echo "✅ Scanner Lambda invoked successfully"
|
|
345
|
+
echo ""
|
|
346
|
+
|
|
347
|
+
# Display response payload
|
|
348
|
+
if [ -s "${response_file}" ]; then
|
|
349
|
+
echo " Response:"
|
|
350
|
+
python3 -c "
|
|
351
|
+
import sys, json
|
|
352
|
+
try:
|
|
353
|
+
with open('${response_file}') as f:
|
|
354
|
+
data = json.load(f)
|
|
355
|
+
print(json.dumps(data, indent=2))
|
|
356
|
+
except:
|
|
357
|
+
with open('${response_file}') as f:
|
|
358
|
+
print(f.read())
|
|
359
|
+
" 2>/dev/null || cat "${response_file}"
|
|
360
|
+
fi
|
|
361
|
+
else
|
|
362
|
+
echo "❌ Failed to invoke Scanner Lambda"
|
|
363
|
+
echo " Check that:"
|
|
364
|
+
echo " • CI infrastructure is provisioned"
|
|
365
|
+
echo " • Your IAM credentials have lambda:InvokeFunction permission"
|
|
366
|
+
echo " • The function 'mlcc-ci-scanner' exists in region: ${AWS_REGION}"
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
rm -f "${response_file}"
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ============================================================
|
|
374
|
+
# ci_dashboard — local web dashboard for CI results
|
|
375
|
+
# ============================================================
|
|
376
|
+
|
|
377
|
+
ci_dashboard() {
|
|
378
|
+
local port=3939
|
|
379
|
+
|
|
380
|
+
# Parse dashboard-specific flags
|
|
381
|
+
while [[ $# -gt 0 ]]; do
|
|
382
|
+
case "$1" in
|
|
383
|
+
--port)
|
|
384
|
+
port="$2"
|
|
385
|
+
shift 2
|
|
386
|
+
;;
|
|
387
|
+
--port=*)
|
|
388
|
+
port="${1#*=}"
|
|
389
|
+
shift
|
|
390
|
+
;;
|
|
391
|
+
*)
|
|
392
|
+
echo "⚠️ Unknown option for dashboard: $1"
|
|
393
|
+
echo "Usage: ./do/ci dashboard [--port <N>]"
|
|
394
|
+
exit 1
|
|
395
|
+
;;
|
|
396
|
+
esac
|
|
397
|
+
done
|
|
398
|
+
|
|
399
|
+
echo "🖥️ Starting CI Dashboard"
|
|
400
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
401
|
+
echo " Table: ${CI_TABLE_NAME}"
|
|
402
|
+
echo " Region: ${AWS_REGION}"
|
|
403
|
+
echo " Port: ${port}"
|
|
404
|
+
echo ""
|
|
405
|
+
echo " Open http://localhost:${port} in your browser"
|
|
406
|
+
echo " Press Ctrl+C to stop"
|
|
407
|
+
echo ""
|
|
408
|
+
|
|
409
|
+
# Export environment variables for the Node.js server
|
|
410
|
+
export DASHBOARD_PORT="${port}"
|
|
411
|
+
export CI_TABLE_NAME="${CI_TABLE_NAME}"
|
|
412
|
+
export AWS_REGION="${AWS_REGION}"
|
|
413
|
+
export CI_LOG_GROUP="${CI_LOG_GROUP}"
|
|
414
|
+
|
|
415
|
+
# Write the server script directly to a temp file (avoids heredoc-in-subshell bash parsing issues)
|
|
416
|
+
local tmp_script
|
|
417
|
+
tmp_script=$(mktemp /tmp/mlcc-ci-dashboard-XXXXXX.js)
|
|
418
|
+
trap "rm -f '${tmp_script}'" EXIT
|
|
419
|
+
|
|
420
|
+
cat > "${tmp_script}" <<'DASHBOARD_EOF'
|
|
421
|
+
const http = require('http');
|
|
422
|
+
const { execSync } = require('child_process');
|
|
423
|
+
|
|
424
|
+
const PORT = parseInt(process.env.DASHBOARD_PORT || '3939', 10);
|
|
425
|
+
const CI_TABLE_NAME = process.env.CI_TABLE_NAME || 'mlcc-ci-table';
|
|
426
|
+
const AWS_REGION = process.env.AWS_REGION || 'us-east-1';
|
|
427
|
+
const CI_LOG_GROUP = process.env.CI_LOG_GROUP || 'ml-container-creator-ci';
|
|
428
|
+
|
|
429
|
+
function fetchCIData() {
|
|
430
|
+
try {
|
|
431
|
+
const raw = execSync(
|
|
432
|
+
`aws dynamodb scan --table-name "${CI_TABLE_NAME}" --region "${AWS_REGION}" --output json`,
|
|
433
|
+
{ encoding: 'utf-8', timeout: 30000 }
|
|
434
|
+
);
|
|
435
|
+
const data = JSON.parse(raw);
|
|
436
|
+
const items = (data.Items || []).map(item => ({
|
|
437
|
+
configId: (item.configId || {}).S || '',
|
|
438
|
+
configJson: (item.configJson || {}).S || '{}',
|
|
439
|
+
projectName: (item.projectName || {}).S || '',
|
|
440
|
+
deploymentConfig: (item.deploymentConfig || {}).S || '',
|
|
441
|
+
testStatus: (item.testStatus || {}).S || 'untested',
|
|
442
|
+
lastTestTimestamp: (item.lastTestTimestamp || {}).S || '',
|
|
443
|
+
lastTestDuration: parseInt((item.lastTestDuration || {}).N || '0', 10),
|
|
444
|
+
stageResults: item.stageResults ? parseStageResults(item.stageResults) : {},
|
|
445
|
+
baseImage: (item.baseImage || {}).S || '',
|
|
446
|
+
errorMessage: (item.errorMessage || {}).S || ''
|
|
447
|
+
}));
|
|
448
|
+
return items;
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function parseStageResults(attr) {
|
|
455
|
+
if (!attr || !attr.M) return {};
|
|
456
|
+
const result = {};
|
|
457
|
+
for (const [stage, val] of Object.entries(attr.M)) {
|
|
458
|
+
if (val.M) {
|
|
459
|
+
result[stage] = {
|
|
460
|
+
status: (val.M.status || {}).S || '',
|
|
461
|
+
durationSeconds: parseInt((val.M.durationSeconds || {}).N || '0', 10),
|
|
462
|
+
logPointer: (val.M.logPointer || {}).S || ''
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function statusColor(status) {
|
|
470
|
+
if (status === 'pass') return '#22c55e';
|
|
471
|
+
if (status && status.startsWith('fail')) return '#ef4444';
|
|
472
|
+
if (status === 'running') return '#eab308';
|
|
473
|
+
return '#9ca3af';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function statusBadge(status) {
|
|
477
|
+
const color = statusColor(status);
|
|
478
|
+
return `<span style="display:inline-block;padding:2px 8px;border-radius:4px;background:${color};color:#fff;font-size:12px;font-weight:600;">${status}</span>`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function formatDuration(seconds) {
|
|
482
|
+
if (!seconds || seconds === 0) return '-';
|
|
483
|
+
if (seconds < 60) return `${seconds}s`;
|
|
484
|
+
const m = Math.floor(seconds / 60);
|
|
485
|
+
const s = seconds % 60;
|
|
486
|
+
return `${m}m ${s}s`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function logLink(logPointer) {
|
|
490
|
+
if (!logPointer) return '-';
|
|
491
|
+
const parts = logPointer.split(':');
|
|
492
|
+
const group = parts[0] || CI_LOG_GROUP;
|
|
493
|
+
const stream = parts.slice(1).join(':') || logPointer;
|
|
494
|
+
const encodedGroup = encodeURIComponent(encodeURIComponent(group));
|
|
495
|
+
const encodedStream = encodeURIComponent(encodeURIComponent(stream));
|
|
496
|
+
const url = `https://console.aws.amazon.com/cloudwatch/home?region=${AWS_REGION}#logsV2:log-groups/log-group/${encodedGroup}/log-events/${encodedStream}`;
|
|
497
|
+
return `<a href="${url}" target="_blank" style="color:#3b82f6;text-decoration:none;">logs</a>`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function renderStages(stageResults) {
|
|
501
|
+
const stageOrder = ['generate', 'validate', 'build', 'deploy_test', 'register', 'teardown', 'update'];
|
|
502
|
+
if (!stageResults || Object.keys(stageResults).length === 0) return '<span style="color:#9ca3af;">-</span>';
|
|
503
|
+
return stageOrder.map(stage => {
|
|
504
|
+
const sr = stageResults[stage];
|
|
505
|
+
if (!sr) return `<span style="color:#9ca3af;" title="${stage}: N/A">⬜</span>`;
|
|
506
|
+
const color = statusColor(sr.status);
|
|
507
|
+
const dur = formatDuration(sr.durationSeconds);
|
|
508
|
+
const link = sr.logPointer ? ` (${logLink(sr.logPointer)})` : '';
|
|
509
|
+
return `<span style="color:${color};" title="${stage}: ${sr.status} ${dur}${sr.logPointer ? ' — click for logs' : ''}">${sr.status === 'pass' ? '✅' : sr.status === 'fail' ? '❌' : sr.status === 'skip' ? '⏭️' : '⬜'}</span>`;
|
|
510
|
+
}).join(' ');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function buildHTML(items) {
|
|
514
|
+
const sorted = items.sort((a, b) => a.deploymentConfig.localeCompare(b.deploymentConfig));
|
|
515
|
+
const rows = sorted.map(item => {
|
|
516
|
+
const lastTest = item.lastTestTimestamp && item.lastTestTimestamp !== '1970-01-01T00:00:00Z'
|
|
517
|
+
? item.lastTestTimestamp.replace('T', ' ').replace('Z', '')
|
|
518
|
+
: '-';
|
|
519
|
+
const escapedConfig = (item.configJson || '{}').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
520
|
+
return `<tr class="clickable-row" data-config="${escapedConfig}" data-project="${item.projectName || '-'}" data-id="${item.configId || ''}">
|
|
521
|
+
<td>${item.projectName || '-'}</td>
|
|
522
|
+
<td><code>${item.deploymentConfig || '-'}</code></td>
|
|
523
|
+
<td>${statusBadge(item.testStatus)}</td>
|
|
524
|
+
<td>${lastTest}</td>
|
|
525
|
+
<td>${formatDuration(item.lastTestDuration)}</td>
|
|
526
|
+
<td>${renderStages(item.stageResults)}</td>
|
|
527
|
+
</tr>`;
|
|
528
|
+
}).join('\n');
|
|
529
|
+
|
|
530
|
+
const total = items.length;
|
|
531
|
+
const passing = items.filter(i => i.testStatus === 'pass').length;
|
|
532
|
+
const failing = items.filter(i => i.testStatus && i.testStatus.startsWith('fail')).length;
|
|
533
|
+
const running = items.filter(i => i.testStatus === 'running').length;
|
|
534
|
+
const untested = items.filter(i => i.testStatus === 'untested').length;
|
|
535
|
+
|
|
536
|
+
return `<!DOCTYPE html>
|
|
537
|
+
<html lang="en">
|
|
538
|
+
<head>
|
|
539
|
+
<meta charset="UTF-8">
|
|
540
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
541
|
+
<title>MLCC CI Dashboard</title>
|
|
542
|
+
<meta http-equiv="refresh" content="60">
|
|
543
|
+
<style>
|
|
544
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
545
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 20px; }
|
|
546
|
+
h1 { font-size: 24px; margin-bottom: 8px; }
|
|
547
|
+
.subtitle { color: #94a3b8; margin-bottom: 20px; font-size: 14px; }
|
|
548
|
+
.summary { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
|
|
549
|
+
.stat { background: #1e293b; padding: 12px 20px; border-radius: 8px; text-align: center; min-width: 100px; }
|
|
550
|
+
.stat .value { font-size: 28px; font-weight: 700; }
|
|
551
|
+
.stat .label { font-size: 12px; color: #94a3b8; margin-top: 4px; }
|
|
552
|
+
table { width: 100%; border-collapse: collapse; background: #1e293b; border-radius: 8px; overflow: hidden; }
|
|
553
|
+
th { background: #334155; padding: 10px 12px; text-align: left; font-size: 13px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
554
|
+
td { padding: 10px 12px; border-top: 1px solid #334155; font-size: 14px; }
|
|
555
|
+
tr:hover td { background: #263348; }
|
|
556
|
+
tr.clickable-row { cursor: pointer; }
|
|
557
|
+
code { background: #334155; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
|
|
558
|
+
a { color: #3b82f6; }
|
|
559
|
+
.footer { margin-top: 20px; color: #64748b; font-size: 12px; text-align: center; }
|
|
560
|
+
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; }
|
|
561
|
+
.modal-overlay.active { display: flex; }
|
|
562
|
+
.modal { background: #1e293b; border-radius: 12px; padding: 24px; max-width: 700px; width: 90%; max-height: 80vh; overflow-y: auto; position: relative; border: 1px solid #334155; }
|
|
563
|
+
.modal h2 { font-size: 18px; margin-bottom: 4px; }
|
|
564
|
+
.modal .modal-subtitle { color: #94a3b8; font-size: 13px; margin-bottom: 16px; }
|
|
565
|
+
.modal pre { background: #0f172a; padding: 16px; border-radius: 8px; overflow-x: auto; font-size: 13px; line-height: 1.5; color: #a5f3fc; }
|
|
566
|
+
.modal-close { position: absolute; top: 12px; right: 16px; background: none; border: none; color: #94a3b8; font-size: 24px; cursor: pointer; }
|
|
567
|
+
.modal-close:hover { color: #e2e8f0; }
|
|
568
|
+
</style>
|
|
569
|
+
</head>
|
|
570
|
+
<body>
|
|
571
|
+
<h1>🔬 MLCC CI Dashboard</h1>
|
|
572
|
+
<p class="subtitle">Table: ${CI_TABLE_NAME} | Region: ${AWS_REGION} | Auto-refresh: 60s</p>
|
|
573
|
+
<div class="summary">
|
|
574
|
+
<div class="stat"><div class="value">${total}</div><div class="label">Total</div></div>
|
|
575
|
+
<div class="stat"><div class="value" style="color:#22c55e;">${passing}</div><div class="label">Passing</div></div>
|
|
576
|
+
<div class="stat"><div class="value" style="color:#ef4444;">${failing}</div><div class="label">Failing</div></div>
|
|
577
|
+
<div class="stat"><div class="value" style="color:#eab308;">${running}</div><div class="label">Running</div></div>
|
|
578
|
+
<div class="stat"><div class="value" style="color:#9ca3af;">${untested}</div><div class="label">Untested</div></div>
|
|
579
|
+
</div>
|
|
580
|
+
<table>
|
|
581
|
+
<thead>
|
|
582
|
+
<tr><th>Project</th><th>Config</th><th>Status</th><th>Last Test</th><th>Duration</th><th>Stages</th></tr>
|
|
583
|
+
</thead>
|
|
584
|
+
<tbody>
|
|
585
|
+
${rows || '<tr><td colspan="6" style="text-align:center;color:#94a3b8;padding:40px;">No records found</td></tr>'}
|
|
586
|
+
</tbody>
|
|
587
|
+
</table>
|
|
588
|
+
<div class="footer">Generated at ${new Date().toISOString()} | Refresh page or wait for auto-refresh</div>
|
|
589
|
+
<div class="modal-overlay" id="configModal">
|
|
590
|
+
<div class="modal">
|
|
591
|
+
<button class="modal-close" onclick="closeModal()">×</button>
|
|
592
|
+
<h2 id="modalTitle">Configuration</h2>
|
|
593
|
+
<p class="modal-subtitle" id="modalSubtitle"></p>
|
|
594
|
+
<pre id="modalContent"></pre>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
<script>
|
|
598
|
+
document.querySelectorAll('.clickable-row').forEach(row => {
|
|
599
|
+
row.addEventListener('click', () => {
|
|
600
|
+
const config = row.getAttribute('data-config');
|
|
601
|
+
const project = row.getAttribute('data-project');
|
|
602
|
+
const configId = row.getAttribute('data-id');
|
|
603
|
+
let formatted;
|
|
604
|
+
try { formatted = JSON.stringify(JSON.parse(config), null, 2); }
|
|
605
|
+
catch { formatted = config; }
|
|
606
|
+
document.getElementById('modalTitle').textContent = project || 'Configuration';
|
|
607
|
+
document.getElementById('modalSubtitle').textContent = 'configId: ' + (configId || 'unknown');
|
|
608
|
+
document.getElementById('modalContent').textContent = formatted;
|
|
609
|
+
document.getElementById('configModal').classList.add('active');
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
function closeModal() { document.getElementById('configModal').classList.remove('active'); }
|
|
613
|
+
document.getElementById('configModal').addEventListener('click', (e) => {
|
|
614
|
+
if (e.target === e.currentTarget) closeModal();
|
|
615
|
+
});
|
|
616
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
|
|
617
|
+
</script>
|
|
618
|
+
</body>
|
|
619
|
+
</html>`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const server = http.createServer((req, res) => {
|
|
623
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
624
|
+
const items = fetchCIData();
|
|
625
|
+
const html = buildHTML(items);
|
|
626
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
627
|
+
res.end(html);
|
|
628
|
+
} else if (req.url === '/api/data') {
|
|
629
|
+
const items = fetchCIData();
|
|
630
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
631
|
+
res.end(JSON.stringify(items, null, 2));
|
|
632
|
+
} else {
|
|
633
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
634
|
+
res.end('Not Found');
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
server.on('error', (err) => {
|
|
639
|
+
if (err.code === 'EADDRINUSE') {
|
|
640
|
+
console.error(`Port ${PORT} in use, try --port <N>`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
console.error('Server error:', err.message);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
server.listen(PORT, () => {
|
|
648
|
+
console.log(`CI Dashboard running at http://localhost:${PORT}`);
|
|
649
|
+
});
|
|
650
|
+
DASHBOARD_EOF
|
|
651
|
+
|
|
652
|
+
node "${tmp_script}"
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ============================================================
|
|
657
|
+
# Subcommand routing
|
|
658
|
+
# ============================================================
|
|
659
|
+
|
|
660
|
+
# Check CI infrastructure before any subcommand
|
|
661
|
+
check_ci_infrastructure
|
|
662
|
+
|
|
663
|
+
case "${1:-}" in
|
|
664
|
+
report)
|
|
665
|
+
shift
|
|
666
|
+
ci_report "$@"
|
|
667
|
+
;;
|
|
668
|
+
status)
|
|
669
|
+
ci_status
|
|
670
|
+
;;
|
|
671
|
+
trigger)
|
|
672
|
+
ci_trigger
|
|
673
|
+
;;
|
|
674
|
+
dashboard)
|
|
675
|
+
shift
|
|
676
|
+
ci_dashboard "$@"
|
|
677
|
+
;;
|
|
678
|
+
*)
|
|
679
|
+
ci_usage
|
|
680
|
+
;;
|
|
681
|
+
esac
|