@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 +10 -2
- package/src/app.js +14 -0
- package/src/lib/prove-pipeline-executor.js +294 -0
- package/templates/do/README.md +37 -0
- package/templates/do/adapter +3 -0
- package/templates/do/build +3 -0
- package/templates/do/clean.d/async-inference.ejs +3 -0
- package/templates/do/clean.d/batch-transform.ejs +3 -0
- package/templates/do/clean.d/hyperpod-eks.ejs +3 -0
- package/templates/do/clean.d/managed-inference.ejs +3 -0
- package/templates/do/deploy.d/async-inference.ejs +3 -0
- package/templates/do/deploy.d/batch-transform.ejs +3 -0
- package/templates/do/deploy.d/hyperpod-eks.ejs +3 -0
- package/templates/do/deploy.d/managed-inference.ejs +3 -0
- package/templates/do/lib/staged-assets.sh +217 -0
- package/templates/do/push +3 -0
- package/templates/do/register +3 -0
- package/templates/do/stage +2 -0
- package/templates/do/submit +3 -0
- package/templates/do/tune +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws/ml-container-creator",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
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
|
+
}
|
package/templates/do/README.md
CHANGED
|
@@ -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
|
|
package/templates/do/adapter
CHANGED
|
@@ -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
|
|
package/templates/do/build
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 "🚀 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}"
|
package/templates/do/register
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
|
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
|
package/templates/do/stage
CHANGED
|
@@ -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
|
package/templates/do/submit
CHANGED
|
@@ -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"
|