@aws/ml-container-creator 0.12.1 → 0.13.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/ml-container-creator",
3
- "version": "0.12.1",
3
+ "version": "0.13.3",
4
4
  "description": "Build and deploy custom ML containers on AWS SageMaker with minimal configuration.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -102,7 +102,7 @@
102
102
  "validate:doc-commands": "node scripts/validate-docs-commands.js",
103
103
  "sbom": "sbom --format spdx --output sbom.json",
104
104
  "prepublishOnly": "npm run lint && npm run test:all",
105
- "prepare": "git config core.hooksPath .githooks || true"
105
+ "prepare": "husky || true"
106
106
  },
107
107
  "dependencies": {
108
108
  "@inquirer/prompts": "^8.4.2",
@@ -120,11 +120,19 @@
120
120
  "@aws-sdk/client-service-quotas": "^3.700.0",
121
121
  "@microsoft/eslint-formatter-sarif": "^3.1.0",
122
122
  "eslint": "^8.57.0",
123
+ "eslint-plugin-property-test-rules": "file:eslint-rules",
123
124
  "fast-check": "^4.5.2",
125
+ "husky": "^9.1.7",
124
126
  "license-report": "^6.8.0",
127
+ "lint-staged": "^17.0.7",
125
128
  "mocha": "^10.2.0",
126
129
  "npm-force-resolutions": "^0.0.10",
127
130
  "nyc": "^15.1.0",
128
131
  "sbom": "^0.0.0"
132
+ },
133
+ "lint-staged": {
134
+ "*.js": [
135
+ "eslint --fix --quiet --max-warnings 0"
136
+ ]
129
137
  }
130
138
  }
package/src/app.js CHANGED
@@ -562,6 +562,20 @@ export async function writeProject(templateDir, destDir, answers, registryConfig
562
562
  fs.writeFileSync(gitignorePath, gitignoreContent);
563
563
  }
564
564
  }
565
+
566
+ // Add .mlcc/ to .gitignore (staged-assets tracking — account-specific URIs)
567
+ {
568
+ const gitignorePath = path.join(destDir, '.gitignore');
569
+ const mlccIgnore = '# Staged assets tracking (account-specific, generated by do/stage)\n.mlcc/\n';
570
+ if (fs.existsSync(gitignorePath)) {
571
+ const existing = fs.readFileSync(gitignorePath, 'utf8');
572
+ if (!existing.includes('.mlcc/')) {
573
+ fs.appendFileSync(gitignorePath, `\n${mlccIgnore}`);
574
+ }
575
+ } else {
576
+ fs.writeFileSync(gitignorePath, mlccIgnore);
577
+ }
578
+ }
565
579
  }
566
580
 
567
581
  /**
@@ -0,0 +1,294 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Prove Pipeline Executor
6
+ *
7
+ * Executes lifecycle stages for validation targets in the `mcc prove` workflow.
8
+ * Handles stage-specific logic including idempotency checks, status tracking,
9
+ * and fail-fast behavior.
10
+ *
11
+ * Feature: s3-model-loading
12
+ * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5
13
+ */
14
+
15
+ import { execFile } from 'node:child_process';
16
+ import { promisify } from 'node:util';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ // ── Valid Lifecycle Stages ────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * All recognized lifecycle stages for the prove pipeline.
26
+ * The "stage" step pre-stages model weights from HuggingFace to S3.
27
+ */
28
+ export const VALID_LIFECYCLE_STAGES = [
29
+ 'generate',
30
+ 'stage',
31
+ 'build',
32
+ 'push',
33
+ 'deploy',
34
+ 'test',
35
+ 'tune',
36
+ 'adapter',
37
+ 'test-adapter',
38
+ 'benchmark',
39
+ 'register',
40
+ 'clean'
41
+ ];
42
+
43
+ /**
44
+ * Possible staging states for status output.
45
+ */
46
+ export const STAGING_STATES = {
47
+ STAGED: 'staged',
48
+ NOT_STAGED: 'not-staged',
49
+ STAGE_FAILED: 'stage-failed'
50
+ };
51
+
52
+ // ── Stage Lifecycle Step ─────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Check if a model has already been staged by looking for `.mlcc/staged-assets.json`.
56
+ *
57
+ * @param {string} projectDir - Path to the generated project directory
58
+ * @returns {boolean} True if the model has already been staged
59
+ */
60
+ export function isAlreadyStaged(projectDir) {
61
+ const stagedAssetsPath = path.join(projectDir, '.mlcc', 'staged-assets.json');
62
+ if (!existsSync(stagedAssetsPath)) {
63
+ return false;
64
+ }
65
+
66
+ try {
67
+ const content = readFileSync(stagedAssetsPath, 'utf8');
68
+ const data = JSON.parse(content);
69
+ // Check that there's a valid staged URI
70
+ return !!(data?.models?.default?.staged_uri);
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get the current staging state for a project.
78
+ *
79
+ * @param {string} projectDir - Path to the generated project directory
80
+ * @param {object} [stepResults] - Previous step results (to check for stage-failed)
81
+ * @returns {string} One of: 'staged', 'not-staged', 'stage-failed'
82
+ */
83
+ export function getStagingState(projectDir, stepResults = null) {
84
+ // Check if stage previously failed
85
+ if (stepResults?.stage?.status === 'fail') {
86
+ return STAGING_STATES.STAGE_FAILED;
87
+ }
88
+
89
+ if (isAlreadyStaged(projectDir)) {
90
+ return STAGING_STATES.STAGED;
91
+ }
92
+
93
+ return STAGING_STATES.NOT_STAGED;
94
+ }
95
+
96
+ /**
97
+ * Execute the stage lifecycle step with idempotency support.
98
+ *
99
+ * If the model is already staged (`.mlcc/staged-assets.json` exists with a valid URI),
100
+ * the step is skipped and marked as passed.
101
+ *
102
+ * If `do/stage` exits non-zero, the model is marked as stage-failed.
103
+ *
104
+ * @param {string} projectDir - Path to the generated project directory
105
+ * @param {object} [options] - Execution options
106
+ * @param {number} [options.timeout=1800] - Timeout in seconds (default: 30 minutes)
107
+ * @param {boolean} [options.verbose=false] - Stream stdout/stderr in real time
108
+ * @returns {Promise<object>} StepResult with name, status, duration, stagingState, and optional error
109
+ */
110
+ export async function executeStageStep(projectDir, options = {}) {
111
+ const { timeout = 1800, verbose = false } = options;
112
+ const startTime = Date.now();
113
+
114
+ // Idempotency check: skip if already staged (Requirement 5.4)
115
+ if (isAlreadyStaged(projectDir)) {
116
+ return {
117
+ name: 'stage',
118
+ status: 'pass',
119
+ duration: Date.now() - startTime,
120
+ stagingState: STAGING_STATES.STAGED,
121
+ skipped: true,
122
+ message: '✓ Model already staged — skipping'
123
+ };
124
+ }
125
+
126
+ // Execute do/stage and verify exit code 0 (Requirement 5.2)
127
+ const command = './do/stage';
128
+
129
+ try {
130
+ if (verbose) {
131
+ // Verbose: stream output in real time
132
+ const { spawn } = await import('node:child_process');
133
+ const result = await new Promise((resolve) => {
134
+ const child = spawn('bash', ['-c', command], {
135
+ cwd: projectDir,
136
+ stdio: ['pipe', 'inherit', 'inherit']
137
+ });
138
+
139
+ let killed = false;
140
+ const timer = setTimeout(() => {
141
+ killed = true;
142
+ child.kill('SIGTERM');
143
+ }, timeout * 1000);
144
+
145
+ child.on('close', (code) => {
146
+ clearTimeout(timer);
147
+ if (code === 0) {
148
+ resolve({
149
+ name: 'stage',
150
+ status: 'pass',
151
+ duration: Date.now() - startTime,
152
+ stagingState: STAGING_STATES.STAGED
153
+ });
154
+ } else {
155
+ const error = killed
156
+ ? `Timeout after ${timeout}s`
157
+ : `do/stage exited with code ${code}`;
158
+ resolve({
159
+ name: 'stage',
160
+ status: 'fail',
161
+ duration: Date.now() - startTime,
162
+ stagingState: STAGING_STATES.STAGE_FAILED,
163
+ error
164
+ });
165
+ }
166
+ });
167
+
168
+ child.on('error', (err) => {
169
+ clearTimeout(timer);
170
+ resolve({
171
+ name: 'stage',
172
+ status: 'fail',
173
+ duration: Date.now() - startTime,
174
+ stagingState: STAGING_STATES.STAGE_FAILED,
175
+ error: err.message.slice(-500)
176
+ });
177
+ });
178
+ });
179
+ return result;
180
+ }
181
+
182
+ // Non-verbose: buffer output
183
+ await execFileAsync('bash', ['-c', command], {
184
+ cwd: projectDir,
185
+ timeout: timeout * 1000,
186
+ maxBuffer: 10 * 1024 * 1024
187
+ });
188
+
189
+ return {
190
+ name: 'stage',
191
+ status: 'pass',
192
+ duration: Date.now() - startTime,
193
+ stagingState: STAGING_STATES.STAGED
194
+ };
195
+ } catch (err) {
196
+ // Mark model as failed if staging fails (Requirement 5.3)
197
+ const error = err.killed
198
+ ? `Timeout after ${timeout}s`
199
+ : (err.stderr || err.message).slice(-500);
200
+
201
+ return {
202
+ name: 'stage',
203
+ status: 'fail',
204
+ duration: Date.now() - startTime,
205
+ stagingState: STAGING_STATES.STAGE_FAILED,
206
+ error
207
+ };
208
+ }
209
+ }
210
+
211
+ // ── Stage Validation ─────────────────────────────────────────────────────────
212
+
213
+ /**
214
+ * Validate that a lifecycle stage name is recognized by the prove pipeline.
215
+ *
216
+ * @param {string} stageName - The stage name to validate
217
+ * @returns {boolean} True if the stage is valid
218
+ */
219
+ export function isValidLifecycleStage(stageName) {
220
+ return VALID_LIFECYCLE_STAGES.includes(stageName);
221
+ }
222
+
223
+ /**
224
+ * Validate a stages array from validation-targets configuration.
225
+ *
226
+ * @param {string[]} stages - Array of stage names
227
+ * @returns {object} Validation result: { valid: boolean, errors: string[] }
228
+ */
229
+ export function validateStagesArray(stages) {
230
+ const errors = [];
231
+
232
+ if (!Array.isArray(stages)) {
233
+ return { valid: false, errors: ['stages must be an array'] };
234
+ }
235
+
236
+ if (stages.length === 0) {
237
+ return { valid: false, errors: ['stages array must not be empty'] };
238
+ }
239
+
240
+ for (const stage of stages) {
241
+ if (typeof stage !== 'string') {
242
+ errors.push(`Invalid stage type: expected string, got ${typeof stage}`);
243
+ continue;
244
+ }
245
+ if (!isValidLifecycleStage(stage)) {
246
+ errors.push(`Unrecognized lifecycle stage: "${stage}"`);
247
+ }
248
+ }
249
+
250
+ return { valid: errors.length === 0, errors };
251
+ }
252
+
253
+ // ── Status Output ────────────────────────────────────────────────────────────
254
+
255
+ /**
256
+ * Format the staging state for status output display.
257
+ *
258
+ * @param {string} state - One of STAGING_STATES values
259
+ * @returns {string} Formatted status string with emoji
260
+ */
261
+ export function formatStagingStatus(state) {
262
+ switch (state) {
263
+ case STAGING_STATES.STAGED:
264
+ return '✓ staged';
265
+ case STAGING_STATES.NOT_STAGED:
266
+ return '○ not-staged';
267
+ case STAGING_STATES.STAGE_FAILED:
268
+ return '✗ stage-failed';
269
+ default:
270
+ return '? unknown';
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Build a status summary for a prove target including staging state.
276
+ *
277
+ * @param {object} target - The validation target
278
+ * @param {string} target.model_name - Model name
279
+ * @param {string} projectDir - Path to the project directory
280
+ * @param {object} [stepResults] - Results of executed steps
281
+ * @returns {object} Status summary including stagingState
282
+ */
283
+ export function buildTargetStatus(target, projectDir, stepResults = null) {
284
+ const stagingState = getStagingState(projectDir, stepResults);
285
+ const stages = target.stages || [];
286
+ const includesStage = stages.includes('stage');
287
+
288
+ return {
289
+ model_name: target.model_name,
290
+ stagingState,
291
+ stagingStatus: formatStagingStatus(stagingState),
292
+ includesStageStep: includesStage
293
+ };
294
+ }
@@ -262,6 +262,43 @@ Clean everything:
262
262
 
263
263
  ---
264
264
 
265
+ ### `./do/stage`
266
+
267
+ Pre-stage model weights from HuggingFace to S3 for faster builds and deploys.
268
+
269
+ **What it does:**
270
+ - Downloads model weights from HuggingFace using `huggingface-cli`
271
+ - Uses `hf_transfer` for accelerated parallel downloads
272
+ - Syncs downloaded weights to S3 (regional, fast access)
273
+ - Records the staged S3 URI in `.mlcc/staged-assets.json`
274
+ - Idempotent: skips if model is already staged (use `--force` to re-stage)
275
+
276
+ **Prerequisites:**
277
+ - AWS credentials configured
278
+ - `huggingface-cli` installed (`pip install huggingface_hub[cli] hf_transfer`)
279
+ - Bootstrap profile configured (`ml-container-creator bootstrap`)
280
+
281
+ **Usage:**
282
+ ```bash
283
+ # Stage model to S3
284
+ ./do/stage
285
+
286
+ # Force re-stage even if already present
287
+ ./do/stage --force
288
+
289
+ # Stage and update MODEL_NAME in do/config
290
+ ./do/stage --update-config
291
+
292
+ # Submit as SageMaker Processing Job (for models >500GB)
293
+ ./do/stage --submit
294
+ ```
295
+
296
+ **Output:**
297
+ - Staged model S3 URI
298
+ - Updated `.mlcc/staged-assets.json` tracking file
299
+
300
+ ---
301
+
265
302
  <% if (typeof includeBenchmark !== 'undefined' && includeBenchmark) { %>
266
303
  ### `./do/benchmark`
267
304
 
@@ -21,7 +21,10 @@ source "${SCRIPT_DIR}/config"
21
21
  source "${SCRIPT_DIR}/lib/profile.sh"
22
22
 
23
23
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
24
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
25
+ set +u
24
26
  ADAPTER_S3_BUCKET="${ADAPTER_S3_BUCKET:-mlcc-adapters-${_PROFILE[accountId]:-unknown}-${_PROFILE[awsRegion]:-us-east-1}}"
27
+ set -u
25
28
 
26
29
  source "${SCRIPT_DIR}/lib/wait.sh"
27
30
 
@@ -12,8 +12,11 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
16
18
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
19
+ set -u
17
20
 
18
21
  echo "🚀 Building Docker image for ${PROJECT_NAME}"
19
22
  echo " Deployment config: ${DEPLOYMENT_CONFIG}"
@@ -12,8 +12,11 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
16
18
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
19
+ set -u
17
20
 
18
21
  # Parse arguments
19
22
  CLEANUP_TARGET=""
@@ -12,8 +12,11 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
16
18
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
19
+ set -u
17
20
 
18
21
  # Parse arguments
19
22
  CLEANUP_TARGET=""
@@ -12,8 +12,11 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
16
18
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
19
+ set -u
17
20
 
18
21
  # Parse arguments
19
22
  CLEANUP_TARGET=""
@@ -12,8 +12,11 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
16
18
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
19
+ set -u
17
20
 
18
21
  # Parse arguments
19
22
  CLEANUP_TARGET=""
@@ -41,6 +41,8 @@ source "${SCRIPT_DIR}/config"
41
41
  source "${SCRIPT_DIR}/lib/profile.sh"
42
42
 
43
43
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
44
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
45
+ set +u
44
46
  ROLE_ARN="${ROLE_ARN:-${_PROFILE[roleArn]:-}}"
45
47
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
46
48
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
@@ -50,6 +52,7 @@ _ASYNC_BUCKET="${_PROFILE[asyncS3Bucket]:-mlcc-async-${_PROFILE[accountId]:-unkn
50
52
  ASYNC_S3_OUTPUT_PATH="${ASYNC_S3_OUTPUT_PATH:-s3://${_ASYNC_BUCKET}/${PROJECT_NAME}/output/}"
51
53
  ASYNC_SNS_SUCCESS_TOPIC="${ASYNC_SNS_SUCCESS_TOPIC:-arn:aws:sns:${_PROFILE[awsRegion]:-us-east-1}:${_PROFILE[accountId]:-unknown}:ml-container-creator-${PROJECT_NAME}-async-success}"
52
54
  ASYNC_SNS_ERROR_TOPIC="${ASYNC_SNS_ERROR_TOPIC:-arn:aws:sns:${_PROFILE[awsRegion]:-us-east-1}:${_PROFILE[accountId]:-unknown}:ml-container-creator-${PROJECT_NAME}-async-error}"
55
+ set -u
53
56
 
54
57
  echo "🚀 Deploying to AWS"
55
58
  echo " Project: ${PROJECT_NAME}"
@@ -41,6 +41,8 @@ source "${SCRIPT_DIR}/config"
41
41
  source "${SCRIPT_DIR}/lib/profile.sh"
42
42
 
43
43
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
44
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
45
+ set +u
44
46
  ROLE_ARN="${ROLE_ARN:-${_PROFILE[roleArn]:-}}"
45
47
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
46
48
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
@@ -49,6 +51,7 @@ export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
49
51
  _BATCH_BUCKET="${_PROFILE[batchS3Bucket]:-mlcc-batch-${_PROFILE[accountId]:-unknown}-${_PROFILE[awsRegion]:-us-east-1}}"
50
52
  BATCH_INPUT_PATH="${BATCH_INPUT_PATH:-s3://${_BATCH_BUCKET}/${PROJECT_NAME}/input/}"
51
53
  BATCH_OUTPUT_PATH="${BATCH_OUTPUT_PATH:-s3://${_BATCH_BUCKET}/${PROJECT_NAME}/output/}"
54
+ set -u
52
55
 
53
56
  echo "🚀 Deploying to AWS"
54
57
  echo " Project: ${PROJECT_NAME}"
@@ -41,7 +41,10 @@ source "${SCRIPT_DIR}/config"
41
41
  source "${SCRIPT_DIR}/lib/profile.sh"
42
42
 
43
43
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
44
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
45
+ set +u
44
46
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
47
+ set -u
45
48
 
46
49
  echo "🚀 Deploying to AWS"
47
50
  echo " Project: ${PROJECT_NAME}"
@@ -214,9 +214,12 @@ source "${SCRIPT_DIR}/config"
214
214
  source "${SCRIPT_DIR}/lib/profile.sh"
215
215
 
216
216
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
217
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
218
+ set +u
217
219
  ROLE_ARN="${ROLE_ARN:-${_PROFILE[roleArn]:-}}"
218
220
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
219
221
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
222
+ set -u
220
223
 
221
224
  echo "🚀 Deploying to AWS"
222
225
  echo " Project: ${PROJECT_NAME}"
@@ -0,0 +1,217 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Shared helper: read/write the .mlcc/staged-assets.json tracking file.
5
+ # Sourced by do/stage, do/submit, and other lifecycle scripts.
6
+ #
7
+ # ─── Schema (.mlcc/staged-assets.json) ───────────────────────────────────────
8
+ #
9
+ # {
10
+ # "version": "1",
11
+ # "models": {
12
+ # "<ic-name>": {
13
+ # "source": "<HuggingFace model ID, e.g. google/gemma-4-31B-it>",
14
+ # "staged_uri": "<S3 URI with trailing slash>",
15
+ # "staged_at": "<ISO 8601 timestamp>",
16
+ # "region": "<AWS region where the model was staged>",
17
+ # "size_gb": <numeric size in GB>
18
+ # }
19
+ # },
20
+ # "adapters": {}
21
+ # }
22
+ #
23
+ # Notes:
24
+ # - "version" is for forward-compatible schema evolution (start at "1")
25
+ # - "models" is keyed by IC name; use "default" for single-model projects
26
+ # - "adapters" is reserved for future LoRA adapter staging (BL-122)
27
+ # - This file is git-ignored (.mlcc/ contains account-specific URIs)
28
+ # - The file SHALL NOT be created unless a valid staging operation completes
29
+ # ──────────────────────────────────────────────────────────────────────────────
30
+
31
+ # Path to the staged-assets file (relative to project root)
32
+ STAGED_ASSETS_DIR=".mlcc"
33
+ STAGED_ASSETS_FILE="${STAGED_ASSETS_DIR}/staged-assets.json"
34
+
35
+ # _staged_assets_has_jq()
36
+ # Check if jq is available on the system.
37
+ # Returns 0 if available, 1 if not.
38
+ _staged_assets_has_jq() {
39
+ command -v jq &>/dev/null
40
+ }
41
+
42
+ # _staged_assets_warn_no_jq()
43
+ # Print a one-time warning when jq is not available.
44
+ _staged_assets_warn_no_jq() {
45
+ if [ -z "${_STAGED_ASSETS_JQ_WARNED:-}" ]; then
46
+ echo "⚠️ jq not found — using fallback parser (install jq for full functionality)" >&2
47
+ _STAGED_ASSETS_JQ_WARNED=1
48
+ fi
49
+ }
50
+
51
+ # staged_assets_read_model_uri()
52
+ # Read the staged S3 URI for the default model from the staged-assets file.
53
+ # Echoes the S3 URI if found, or an empty string if not available.
54
+ #
55
+ # Uses jq when available; falls back to grep/sed extraction.
56
+ #
57
+ # Arguments: none
58
+ # Output: S3 URI string (stdout) or empty string
59
+ staged_assets_read_model_uri() {
60
+ local uri=""
61
+
62
+ # No file → empty string
63
+ if [ ! -f "${STAGED_ASSETS_FILE}" ]; then
64
+ echo ""
65
+ return 0
66
+ fi
67
+
68
+ if _staged_assets_has_jq; then
69
+ uri=$(jq -r '.models.default.staged_uri // empty' "${STAGED_ASSETS_FILE}" 2>/dev/null) || uri=""
70
+ else
71
+ _staged_assets_warn_no_jq
72
+ # Fallback: grep/sed extraction for the staged_uri field within the default model block
73
+ # This handles the common single-model case reliably
74
+ uri=$(grep -A 5 '"default"' "${STAGED_ASSETS_FILE}" 2>/dev/null \
75
+ | grep '"staged_uri"' \
76
+ | sed 's/.*"staged_uri"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' \
77
+ | head -1) || uri=""
78
+ fi
79
+
80
+ echo "${uri}"
81
+ }
82
+
83
+ # staged_assets_write_model()
84
+ # Create or update the staged-assets file with model staging information.
85
+ # Creates the .mlcc directory if it does not exist.
86
+ #
87
+ # Arguments:
88
+ # $1 - source: HuggingFace model ID (e.g. "google/gemma-4-31B-it")
89
+ # $2 - uri: S3 URI where the model was staged (with trailing slash)
90
+ # $3 - region: AWS region where the model was staged
91
+ # $4 - size_gb: Total size of the staged model in GB (numeric)
92
+ staged_assets_write_model() {
93
+ local source="$1"
94
+ local uri="$2"
95
+ local region="$3"
96
+ local size_gb="$4"
97
+ local timestamp
98
+ timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
99
+
100
+ # Ensure the .mlcc directory exists
101
+ mkdir -p "${STAGED_ASSETS_DIR}"
102
+
103
+ if _staged_assets_has_jq; then
104
+ if [ -f "${STAGED_ASSETS_FILE}" ]; then
105
+ # Update existing file — merge the new model entry
106
+ local tmp_file="${STAGED_ASSETS_FILE}.tmp"
107
+ jq --arg source "${source}" \
108
+ --arg uri "${uri}" \
109
+ --arg ts "${timestamp}" \
110
+ --arg region "${region}" \
111
+ --argjson size "${size_gb}" \
112
+ '.models.default = {
113
+ "source": $source,
114
+ "staged_uri": $uri,
115
+ "staged_at": $ts,
116
+ "region": $region,
117
+ "size_gb": $size
118
+ }' "${STAGED_ASSETS_FILE}" > "${tmp_file}" && mv "${tmp_file}" "${STAGED_ASSETS_FILE}"
119
+ else
120
+ # Create new file from scratch
121
+ jq -n --arg source "${source}" \
122
+ --arg uri "${uri}" \
123
+ --arg ts "${timestamp}" \
124
+ --arg region "${region}" \
125
+ --argjson size "${size_gb}" \
126
+ '{
127
+ "version": "1",
128
+ "models": {
129
+ "default": {
130
+ "source": $source,
131
+ "staged_uri": $uri,
132
+ "staged_at": $ts,
133
+ "region": $region,
134
+ "size_gb": $size
135
+ }
136
+ },
137
+ "adapters": {}
138
+ }' > "${STAGED_ASSETS_FILE}"
139
+ fi
140
+ else
141
+ _staged_assets_warn_no_jq
142
+ # Fallback: write the JSON directly (create-only, no merge support without jq)
143
+ cat > "${STAGED_ASSETS_FILE}" << EOF
144
+ {
145
+ "version": "1",
146
+ "models": {
147
+ "default": {
148
+ "source": "${source}",
149
+ "staged_uri": "${uri}",
150
+ "staged_at": "${timestamp}",
151
+ "region": "${region}",
152
+ "size_gb": ${size_gb}
153
+ }
154
+ },
155
+ "adapters": {}
156
+ }
157
+ EOF
158
+ fi
159
+ }
160
+
161
+ # staged_assets_status()
162
+ # Print a human-readable table of all staged assets.
163
+ # Shows models and adapters with their source, URI, region, size, and timestamp.
164
+ #
165
+ # Arguments: none
166
+ # Output: formatted table to stdout
167
+ staged_assets_status() {
168
+ if [ ! -f "${STAGED_ASSETS_FILE}" ]; then
169
+ echo "No staged assets found."
170
+ echo " Run do/stage to stage model weights to S3."
171
+ return 0
172
+ fi
173
+
174
+ echo "Staged Assets (.mlcc/staged-assets.json)"
175
+ echo "─────────────────────────────────────────────────────────────────"
176
+
177
+ if _staged_assets_has_jq; then
178
+ # Print models section
179
+ local model_count
180
+ model_count=$(jq -r '.models | length' "${STAGED_ASSETS_FILE}" 2>/dev/null) || model_count=0
181
+
182
+ if [ "${model_count}" -gt 0 ]; then
183
+ echo ""
184
+ echo " Models:"
185
+ echo " ┌──────────────┬─────────────────────────────────┬──────────────────────────────────────────────────────┬────────────┬─────────┐"
186
+ printf " │ %-12s │ %-31s │ %-52s │ %-10s │ %-7s │\n" "IC Name" "Source" "S3 URI" "Region" "Size"
187
+ echo " ├──────────────┼─────────────────────────────────┼──────────────────────────────────────────────────────┼────────────┼─────────┤"
188
+
189
+ jq -r '.models | to_entries[] | "\(.key)\t\(.value.source)\t\(.value.staged_uri)\t\(.value.region)\t\(.value.size_gb)"' "${STAGED_ASSETS_FILE}" 2>/dev/null | \
190
+ while IFS=$'\t' read -r ic_name source staged_uri region size_gb; do
191
+ printf " │ %-12s │ %-31s │ %-52s │ %-10s │ %5s GB│\n" \
192
+ "${ic_name}" "${source}" "${staged_uri}" "${region}" "${size_gb}"
193
+ done
194
+
195
+ echo " └──────────────┴─────────────────────────────────┴──────────────────────────────────────────────────────┴────────────┴─────────┘"
196
+ fi
197
+
198
+ # Print adapters section (future — show placeholder if empty)
199
+ local adapter_count
200
+ adapter_count=$(jq -r '.adapters | length' "${STAGED_ASSETS_FILE}" 2>/dev/null) || adapter_count=0
201
+
202
+ if [ "${adapter_count}" -gt 0 ]; then
203
+ echo ""
204
+ echo " Adapters:"
205
+ jq -r '.adapters | to_entries[] | " \(.key): \(.value.staged_uri // "not staged")"' "${STAGED_ASSETS_FILE}" 2>/dev/null
206
+ fi
207
+ else
208
+ _staged_assets_warn_no_jq
209
+ # Fallback: basic display without jq
210
+ echo ""
211
+ echo " Raw contents:"
212
+ echo ""
213
+ cat "${STAGED_ASSETS_FILE}"
214
+ fi
215
+
216
+ echo ""
217
+ }
package/templates/do/push CHANGED
@@ -12,8 +12,11 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
16
18
  export AWS_REGION="${AWS_REGION:-${_PROFILE[awsRegion]:-us-east-1}}"
19
+ set -u
17
20
 
18
21
  echo "🚀 Pushing Docker image to Amazon ECR"
19
22
  echo " Project: ${PROJECT_NAME}"
@@ -12,8 +12,11 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ROLE_ARN="${ROLE_ARN:-${_PROFILE[roleArn]:-}}"
16
18
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
19
+ set -u
17
20
 
18
21
  # ============================================================
19
22
  # Register deployment to the deployment registry
@@ -23,6 +23,8 @@ set -o pipefail
23
23
  # ── Source project configuration ──────────────────────────────────────────────
24
24
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
25
  source "${SCRIPT_DIR}/config"
26
+ source "${SCRIPT_DIR}/lib/profile.sh"
27
+ source "${SCRIPT_DIR}/lib/staged-assets.sh"
26
28
 
27
29
  # ── Parse flags ───────────────────────────────────────────────────────────────
28
30
  FORCE=false
@@ -12,7 +12,10 @@ source "${SCRIPT_DIR}/config"
12
12
  source "${SCRIPT_DIR}/lib/profile.sh"
13
13
 
14
14
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
15
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
16
+ set +u
15
17
  ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-${_PROFILE[ecrRepositoryName]:-ml-container-creator}}"
18
+ set -u
16
19
 
17
20
  # ── Derived variables (env var > computed default) ────────────────────────────
18
21
  CODEBUILD_PROJECT_NAME="${CODEBUILD_PROJECT_NAME:-${PROJECT_NAME}-build-$(date +%Y%m%d)}"
package/templates/do/tune CHANGED
@@ -16,7 +16,10 @@ source "${SCRIPT_DIR}/config"
16
16
  source "${SCRIPT_DIR}/lib/profile.sh"
17
17
 
18
18
  # ── Profile-resolved variables (env var > profile > default) ──────────────────
19
+ # Disable unbound-variable checking for associative array access (bash 3.2 compat)
20
+ set +u
19
21
  TUNE_S3_BUCKET="${TUNE_S3_BUCKET:-mlcc-tune-${_PROFILE[accountId]:-unknown}-${_PROFILE[awsRegion]:-us-east-1}}"
22
+ set -u
20
23
 
21
24
  # ── Constants ─────────────────────────────────────────────────────────────────
22
25
  CATALOG_FILE="${SCRIPT_DIR}/.tune_catalog.json"