@mutmutco/cli 4.5.43 → 4.5.45
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/dist/main.cjs +187 -18
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -7620,6 +7620,33 @@ function cadenceContainsCron(cadence, cron) {
|
|
|
7620
7620
|
if (!cronTokens.length || cronTokens.length > cadenceTokens.length) return false;
|
|
7621
7621
|
return cadenceTokens.some((_, i) => i + cronTokens.length <= cadenceTokens.length && cronTokens.every((t, j) => cadenceTokens[i + j] === t));
|
|
7622
7622
|
}
|
|
7623
|
+
var DECLARED_UNSCHEDULED = /* @__PURE__ */ new Set(["on demand", "on-demand", "manual", "manual only", "event-driven"]);
|
|
7624
|
+
var CRON_FIELD2 = /^[0-9A-Za-z*,/\-]+$/;
|
|
7625
|
+
var DOW_NAMES = /* @__PURE__ */ new Set(["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]);
|
|
7626
|
+
function dowOk(dow) {
|
|
7627
|
+
return dow.split(",").every((part) => {
|
|
7628
|
+
const slash2 = part.indexOf("/");
|
|
7629
|
+
const step = slash2 === -1 ? "" : part.slice(slash2);
|
|
7630
|
+
if (step !== "" && !/^\/[1-9]\d*$/.test(step)) return false;
|
|
7631
|
+
const tokens2 = (slash2 === -1 ? part : part.slice(0, slash2)).split("-");
|
|
7632
|
+
if (tokens2.length > 2 || tokens2.some((t) => t === "")) return false;
|
|
7633
|
+
return tokens2.every((t) => t === "*" || (/^\d+$/.test(t) ? Number(t) <= 7 : DOW_NAMES.has(t.toUpperCase())));
|
|
7634
|
+
});
|
|
7635
|
+
}
|
|
7636
|
+
function cadenceProblem(cadence) {
|
|
7637
|
+
const c = cadence.trim();
|
|
7638
|
+
const cut = c.search(/[(+]/);
|
|
7639
|
+
const lead = (cut === -1 ? c : c.slice(0, cut)).trim();
|
|
7640
|
+
if (DECLARED_UNSCHEDULED.has(lead.toLowerCase())) return null;
|
|
7641
|
+
if (/^((?:rate|cron)\([^()]+\))(?:\s*[(+].*)?$/is.test(c)) return null;
|
|
7642
|
+
const f = lead.split(/\s+/);
|
|
7643
|
+
if (f.length !== 5) return `cadence not mappable to a Scheduler expression: ${JSON.stringify(cadence)}`;
|
|
7644
|
+
const [minute, hour, dom, month, dow] = f;
|
|
7645
|
+
if (![minute, hour, dom, month].every((x) => CRON_FIELD2.test(x)) || dom !== "*" && dow !== "*" || !dowOk(dow)) {
|
|
7646
|
+
return `cadence not mappable to a Scheduler expression: ${JSON.stringify(cadence)}`;
|
|
7647
|
+
}
|
|
7648
|
+
return null;
|
|
7649
|
+
}
|
|
7623
7650
|
function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
7624
7651
|
const crons = extractWorkflowCrons(yamlText);
|
|
7625
7652
|
const header = parseScheduleHeader(yamlText);
|
|
@@ -7638,6 +7665,10 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
7638
7665
|
if (!SCHEDULE_EXECUTORS.has(h.executor)) {
|
|
7639
7666
|
throw new Error(`schedules register: ${expectedId} header \`executor: ${h.executor}\` is not a member of the closed executor taxonomy (${[...SCHEDULE_EXECUTORS].join(", ")}) \u2014 an unknown executor would register a row the harbour can never dispatch (docs/schedules.md "The entry template", H2 #4429).`);
|
|
7640
7667
|
}
|
|
7668
|
+
const problem = cadenceProblem(h.cadence);
|
|
7669
|
+
if (problem) {
|
|
7670
|
+
throw new Error(`schedules register: ${expectedId} ${problem} \u2014 lead with a 5-field cron, rate(...) or cron(...), then any note in (...) or after +; or declare one of: ${[...DECLARED_UNSCHEDULED].join(", ")} (docs/schedules.md "The entry template", #7415).`);
|
|
7671
|
+
}
|
|
7641
7672
|
for (const cron of crons) {
|
|
7642
7673
|
if (!cadenceContainsCron(h.cadence, cron)) {
|
|
7643
7674
|
throw new Error(`schedules register: ${expectedId} cadence \`${h.cadence}\` does not contain the workflow cron \`${cron}\` (whole-token comparison) \u2014 the declared cadence disagrees with the actual schedule.`);
|
|
@@ -14366,7 +14397,7 @@ function loadBootstrapSeeds(manifestJson) {
|
|
|
14366
14397
|
};
|
|
14367
14398
|
}
|
|
14368
14399
|
function renderSeed(template, vars) {
|
|
14369
|
-
return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key] : match);
|
|
14400
|
+
return template.replace(PLACEHOLDER_RE, (match, key) => key in vars ? vars[key].replace(PLACEHOLDER_RE, (inner, innerKey) => innerKey !== key && innerKey in vars ? vars[innerKey] : inner) : match);
|
|
14370
14401
|
}
|
|
14371
14402
|
function missingPlaceholders(rendered) {
|
|
14372
14403
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -15370,7 +15401,10 @@ function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches
|
|
|
15370
15401
|
GATE_MAX_SECONDS: DEFAULT_GATE_MAX_SECONDS,
|
|
15371
15402
|
// The pin is the CLI's blessed SHA, not an operator knob — one central place to bump (#3178).
|
|
15372
15403
|
GATE_BUDGET_SHA: BLESSED_RUN_WITH_BUDGET_SHA,
|
|
15373
|
-
GATE_RUNNER_SHA: BLESSED_RUNNER_GATE_SHA
|
|
15404
|
+
GATE_RUNNER_SHA: BLESSED_RUNNER_GATE_SHA,
|
|
15405
|
+
// #7407: repo-declared service containers and extra check steps; empty renders today's gate byte-for-byte.
|
|
15406
|
+
GATE_SERVICES_YAML: "",
|
|
15407
|
+
GATE_EXTRA_STEPS_YAML: ""
|
|
15374
15408
|
};
|
|
15375
15409
|
const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
|
|
15376
15410
|
const trackBranches = track === "trunk" ? ["main"] : track === "direct" ? ["development", "main"] : ["development", "rc", "main"];
|
|
@@ -15417,6 +15451,112 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack, requiredCheckBranc
|
|
|
15417
15451
|
if (!out.GATE_AFFECTED_CMD.trim()) out.GATE_AFFECTED_CMD = out.GATE_CMD;
|
|
15418
15452
|
return out;
|
|
15419
15453
|
}
|
|
15454
|
+
var GATE_SERVICE_KEYS = /* @__PURE__ */ new Set(["image", "env", "ports", "options", "tmpfs"]);
|
|
15455
|
+
var GATE_STEP_KEYS = /* @__PURE__ */ new Set(["name", "command", "env", "maxSeconds"]);
|
|
15456
|
+
function gateLine(value, field) {
|
|
15457
|
+
if (typeof value !== "string" || !value.trim() || /[\r\n]/.test(value)) throw new Error(`${field} must be a non-empty single-line string`);
|
|
15458
|
+
return value.trim();
|
|
15459
|
+
}
|
|
15460
|
+
function gateEnv(value, field) {
|
|
15461
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object of NAME: "value" strings`);
|
|
15462
|
+
const out = {};
|
|
15463
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
15464
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`${field}.${name} is not a valid environment variable name`);
|
|
15465
|
+
if (typeof entry !== "string" || /[\r\n]/.test(entry)) throw new Error(`${field}.${name} must be a single-line string`);
|
|
15466
|
+
out[name] = entry;
|
|
15467
|
+
}
|
|
15468
|
+
return out;
|
|
15469
|
+
}
|
|
15470
|
+
function gateSeconds(value, field) {
|
|
15471
|
+
const text = typeof value === "number" ? String(value) : value;
|
|
15472
|
+
if (typeof text !== "string" || !/^\d+$/.test(text.trim()) || Number(text) <= 0) throw new Error(`${field} must be a positive whole number of seconds`);
|
|
15473
|
+
return text.trim();
|
|
15474
|
+
}
|
|
15475
|
+
function validateGateExtras(gate) {
|
|
15476
|
+
const out = {};
|
|
15477
|
+
if (gate.services !== void 0) {
|
|
15478
|
+
if (!gate.services || typeof gate.services !== "object" || Array.isArray(gate.services)) throw new Error("gate.services must be an object keyed by service name");
|
|
15479
|
+
out.services = {};
|
|
15480
|
+
for (const [name, raw] of Object.entries(gate.services)) {
|
|
15481
|
+
const field = `gate.services.${name}`;
|
|
15482
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`${field}: service name must be lowercase letters, digits, - or _`);
|
|
15483
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${field} must be an object with an image`);
|
|
15484
|
+
const service = raw;
|
|
15485
|
+
for (const key of Object.keys(service)) if (!GATE_SERVICE_KEYS.has(key)) throw new Error(`${field}.${key} is not a service field (expected image/env/ports/options/tmpfs)`);
|
|
15486
|
+
const parsed = { image: gateLine(service.image, `${field}.image`) };
|
|
15487
|
+
if (service.env !== void 0) parsed.env = gateEnv(service.env, `${field}.env`);
|
|
15488
|
+
if (service.ports !== void 0) {
|
|
15489
|
+
if (!Array.isArray(service.ports) || service.ports.length === 0) throw new Error(`${field}.ports must be a non-empty array`);
|
|
15490
|
+
parsed.ports = service.ports.map((port, index) => {
|
|
15491
|
+
const ok = typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536 || typeof port === "string" && /^\d{1,5}(:\d{1,5})?$/.test(port);
|
|
15492
|
+
if (!ok) throw new Error(`${field}.ports[${index}] must be a port number or "host:container"`);
|
|
15493
|
+
return port;
|
|
15494
|
+
});
|
|
15495
|
+
}
|
|
15496
|
+
if (service.options !== void 0) parsed.options = gateLine(service.options, `${field}.options`);
|
|
15497
|
+
if (service.tmpfs !== void 0) {
|
|
15498
|
+
if (!Array.isArray(service.tmpfs) || service.tmpfs.length === 0) throw new Error(`${field}.tmpfs must be a non-empty array of paths`);
|
|
15499
|
+
parsed.tmpfs = service.tmpfs.map((path2, index) => gateLine(path2, `${field}.tmpfs[${index}]`));
|
|
15500
|
+
}
|
|
15501
|
+
out.services[name] = parsed;
|
|
15502
|
+
}
|
|
15503
|
+
}
|
|
15504
|
+
if (gate.steps !== void 0) {
|
|
15505
|
+
if (!Array.isArray(gate.steps)) throw new Error("gate.steps must be an array");
|
|
15506
|
+
out.steps = gate.steps.map((raw, index) => {
|
|
15507
|
+
const field = `gate.steps[${index}]`;
|
|
15508
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${field} must be an object with name and command`);
|
|
15509
|
+
const step = raw;
|
|
15510
|
+
for (const key of Object.keys(step)) if (!GATE_STEP_KEYS.has(key)) throw new Error(`${field}.${key} is not a step field (expected name/command/env/maxSeconds)`);
|
|
15511
|
+
const parsed = { name: gateLine(step.name, `${field}.name`), command: gateLine(step.command, `${field}.command`) };
|
|
15512
|
+
if (step.env !== void 0) parsed.env = gateEnv(step.env, `${field}.env`);
|
|
15513
|
+
if (step.maxSeconds !== void 0) parsed.maxSeconds = gateSeconds(step.maxSeconds, `${field}.maxSeconds`);
|
|
15514
|
+
return parsed;
|
|
15515
|
+
});
|
|
15516
|
+
}
|
|
15517
|
+
return out;
|
|
15518
|
+
}
|
|
15519
|
+
function yamlScalar(value) {
|
|
15520
|
+
const plainSafe = /^[A-Za-z0-9_./$(][^\r\n]*$/.test(value) && !/: |\s#|[\s:]$/.test(value) && !/^(?:true|false|yes|no|on|off|null|~|[-+]?[\d.][\d_.eE+-]*)$/i.test(value);
|
|
15521
|
+
return plainSafe ? value : JSON.stringify(value);
|
|
15522
|
+
}
|
|
15523
|
+
function envYaml(env, indent) {
|
|
15524
|
+
return Object.entries(env).map(([name, value]) => `
|
|
15525
|
+
${indent}${name}: ${yamlScalar(value)}`).join("");
|
|
15526
|
+
}
|
|
15527
|
+
function gateServicesYaml(services) {
|
|
15528
|
+
let out = "\n services:";
|
|
15529
|
+
for (const [name, service] of Object.entries(services)) {
|
|
15530
|
+
out += `
|
|
15531
|
+
${name}:
|
|
15532
|
+
image: ${yamlScalar(service.image)}`;
|
|
15533
|
+
if (service.env && Object.keys(service.env).length) out += `
|
|
15534
|
+
env:${envYaml(service.env, " ")}`;
|
|
15535
|
+
if (service.ports) out += `
|
|
15536
|
+
ports:${service.ports.map((port) => `
|
|
15537
|
+
- ${typeof port === "number" ? port : yamlScalar(port)}`).join("")}`;
|
|
15538
|
+
const options = [...service.options ? [service.options] : [], ...(service.tmpfs ?? []).map((path2) => `--tmpfs ${path2}`)];
|
|
15539
|
+
if (options.length) out += `
|
|
15540
|
+
options: >-${options.map((line) => `
|
|
15541
|
+
${line}`).join("")}`;
|
|
15542
|
+
}
|
|
15543
|
+
return out;
|
|
15544
|
+
}
|
|
15545
|
+
function gateExtraStepsYaml(steps) {
|
|
15546
|
+
return steps.map((step) => [
|
|
15547
|
+
`
|
|
15548
|
+
- name: ${yamlScalar(step.name)}`,
|
|
15549
|
+
step.env && Object.keys(step.env).length ? `
|
|
15550
|
+
env:${envYaml(step.env, " ")}` : "",
|
|
15551
|
+
"\n uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}",
|
|
15552
|
+
"\n with:",
|
|
15553
|
+
`
|
|
15554
|
+
command: ${yamlScalar(step.command)}`,
|
|
15555
|
+
`
|
|
15556
|
+
max-seconds: ${step.maxSeconds ?? "{{GATE_MAX_SECONDS}}"}`,
|
|
15557
|
+
"\n working-directory: {{GATE_WORKDIR}}"
|
|
15558
|
+
].join("")).join("");
|
|
15559
|
+
}
|
|
15420
15560
|
function gateConfigToVars(gate) {
|
|
15421
15561
|
const out = {};
|
|
15422
15562
|
if (!gate || typeof gate !== "object") return out;
|
|
@@ -15428,6 +15568,9 @@ function gateConfigToVars(gate) {
|
|
|
15428
15568
|
if (typeof gate.pyVersion === "string" && gate.pyVersion.trim()) out.GATE_PY_VERSION = gate.pyVersion;
|
|
15429
15569
|
const seconds = typeof gate.maxSeconds === "number" ? String(gate.maxSeconds) : gate.maxSeconds;
|
|
15430
15570
|
if (typeof seconds === "string" && /^\d+$/.test(seconds.trim()) && Number(seconds) > 0) out.GATE_MAX_SECONDS = seconds.trim();
|
|
15571
|
+
const extras = validateGateExtras(gate);
|
|
15572
|
+
if (extras.services && Object.keys(extras.services).length) out.GATE_SERVICES_YAML = gateServicesYaml(extras.services);
|
|
15573
|
+
if (extras.steps?.length) out.GATE_EXTRA_STEPS_YAML = gateExtraStepsYaml(extras.steps);
|
|
15431
15574
|
return out;
|
|
15432
15575
|
}
|
|
15433
15576
|
function seedMatchesDeployModel(seed, deployModel) {
|
|
@@ -16871,10 +17014,10 @@ var rollout_plan_default = {
|
|
|
16871
17014
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
16872
17015
|
},
|
|
16873
17016
|
baseline: {
|
|
16874
|
-
version: "4.5.
|
|
16875
|
-
tag: "v4.5.
|
|
16876
|
-
commit: "
|
|
16877
|
-
npm: "@mutmutco/cli@4.5.
|
|
17017
|
+
version: "4.5.45",
|
|
17018
|
+
tag: "v4.5.45",
|
|
17019
|
+
commit: "f66545d593d2",
|
|
17020
|
+
npm: "@mutmutco/cli@4.5.45"
|
|
16878
17021
|
},
|
|
16879
17022
|
exitCriterion: "fleet-n-of-n",
|
|
16880
17023
|
hubOnlyShortcut: "forbidden",
|
|
@@ -16891,14 +17034,14 @@ var rollout_plan_default = {
|
|
|
16891
17034
|
repo: "mutmutco/mmi-hub",
|
|
16892
17035
|
role: "canary",
|
|
16893
17036
|
schedule: "train",
|
|
16894
|
-
v3Target: "v4.5.
|
|
17037
|
+
v3Target: "v4.5.45"
|
|
16895
17038
|
}
|
|
16896
17039
|
],
|
|
16897
17040
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
16898
17041
|
rollback: {
|
|
16899
17042
|
independent: true,
|
|
16900
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
16901
|
-
v3Target: "v4.5.
|
|
17043
|
+
mechanism: "npm dist-tag latest -> 4.5.45 and redeploy the Hub Lambda from tag v4.5.45 (f66545d593d2); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17044
|
+
v3Target: "v4.5.45 (@mutmutco/cli@4.5.45, tag commit f66545d593d2 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
16902
17045
|
}
|
|
16903
17046
|
},
|
|
16904
17047
|
{
|
|
@@ -27801,8 +27944,15 @@ async function seedGateYml(repo, deps, meta, result) {
|
|
|
27801
27944
|
result.skipped.push(`gate.yml missing \u2014 no registry gate config; set \`mmi-cli oracle org project set ${repo} --var gate={...}\` first, then re-run reconcile`);
|
|
27802
27945
|
return "none";
|
|
27803
27946
|
}
|
|
27947
|
+
let gateVars;
|
|
27948
|
+
try {
|
|
27949
|
+
gateVars = gateConfigToVars(gate);
|
|
27950
|
+
} catch (e) {
|
|
27951
|
+
result.errors.push(`gate.yml re-seed: registry gate config is invalid \u2014 ${e.message}`);
|
|
27952
|
+
return "none";
|
|
27953
|
+
}
|
|
27804
27954
|
const derivedVars = withDerivedRepoVars(
|
|
27805
|
-
{ ...
|
|
27955
|
+
{ ...gateVars, REPO_SLUG: parsed.slug },
|
|
27806
27956
|
parsed,
|
|
27807
27957
|
"deployable",
|
|
27808
27958
|
releaseTrack,
|
|
@@ -29906,13 +30056,15 @@ function parseManifest(text) {
|
|
|
29906
30056
|
const where = `box control manifest: files[${i}]`;
|
|
29907
30057
|
if (typeof f.src !== "string" || !f.src || f.src.startsWith("/") || f.src.includes("..")) throw new Error(`${where}: src must be a repo-relative path`);
|
|
29908
30058
|
if (f.mode !== "0644" && f.mode !== "0755") throw new Error(`${where} (${f.src}): mode must be "0644" or "0755"`);
|
|
29909
|
-
if (f.post !== void 0 && (typeof f.post !== "string" || !ALLOWED_POST.has(f.post))) {
|
|
29910
|
-
throw new Error(`${where} (${f.src}): post must be one of ${[...ALLOWED_POST].join(", ")}`);
|
|
29911
|
-
}
|
|
29912
30059
|
const targets = [];
|
|
29913
30060
|
if (Array.isArray(f.targets)) targets.push(...f.targets);
|
|
29914
30061
|
if (f.lanes !== void 0) targets.push(...expandLanes(f.lanes, where));
|
|
29915
30062
|
if (!targets.length) throw new Error(`${where} (${f.src}): needs targets or lanes`);
|
|
30063
|
+
const ownSysctl = typeof f.post === "string" && /^sysctl -p (\/etc\/sysctl\.d\/[A-Za-z0-9._-]+\.conf)$/.exec(f.post);
|
|
30064
|
+
const postOk = f.post === void 0 || typeof f.post === "string" && ALLOWED_POST.has(f.post) || !!ownSysctl && targets.every((t) => t.dest === ownSysctl[1]);
|
|
30065
|
+
if (!postOk) {
|
|
30066
|
+
throw new Error(`${where} (${f.src}): post must be one of ${[...ALLOWED_POST].join(", ")}, or \`sysctl -p <its own /etc/sysctl.d dest>\``);
|
|
30067
|
+
}
|
|
29916
30068
|
for (const t of targets) {
|
|
29917
30069
|
if (typeof t.box !== "string" || !/^[a-z0-9-]+$/.test(t.box)) throw new Error(`${where} (${f.src}): bad box name`);
|
|
29918
30070
|
if (typeof t.dest !== "string" || !/^\/[A-Za-z0-9._\/-]+$/.test(t.dest) || t.dest.includes("..")) {
|
|
@@ -29947,9 +30099,19 @@ function planRemoteScript(dests) {
|
|
|
29947
30099
|
for (const d of dests) {
|
|
29948
30100
|
lines2.push(`if [ -f '${d}' ]; then printf '%s %s %s\\n' "$(md5sum < '${d}' | cut -d' ' -f1)" "$(stat -c %a '${d}')" '${d}'; else printf 'MISSING - %s\\n' '${d}'; fi`);
|
|
29949
30101
|
}
|
|
30102
|
+
lines2.push(`for f in ${ALERT_DIR}/*; do [ -f "$f" ] && printf 'ALERT %s %s\\n' "$(basename "$f")" "$(head -c 300 "$f" | tr '\\n' ' ')"; done; true`);
|
|
29950
30103
|
return `${lines2.join("\n")}
|
|
29951
30104
|
`;
|
|
29952
30105
|
}
|
|
30106
|
+
var ALERT_DIR = "/var/lib/mmi-control/alerts";
|
|
30107
|
+
function parseAlerts(output) {
|
|
30108
|
+
const out = [];
|
|
30109
|
+
for (const line of output.split("\n")) {
|
|
30110
|
+
const m = /^ALERT ([A-Za-z0-9._-]+) ?(.*)$/.exec(line.trim());
|
|
30111
|
+
if (m) out.push({ name: m[1], message: m[2].trim() });
|
|
30112
|
+
}
|
|
30113
|
+
return out;
|
|
30114
|
+
}
|
|
29953
30115
|
function parsePlanOutput(output, rows) {
|
|
29954
30116
|
const seen = /* @__PURE__ */ new Map();
|
|
29955
30117
|
for (const line of output.split("\n")) {
|
|
@@ -30415,14 +30577,20 @@ function parseGateVar(raw) {
|
|
|
30415
30577
|
const map = parsed;
|
|
30416
30578
|
const out = {};
|
|
30417
30579
|
for (const [key, value] of Object.entries(map)) {
|
|
30418
|
-
if (key === "
|
|
30580
|
+
if (key === "services" || key === "steps") {
|
|
30581
|
+
try {
|
|
30582
|
+
Object.assign(out, validateGateExtras({ [key]: value }));
|
|
30583
|
+
} catch (error) {
|
|
30584
|
+
throw new Error(`org project set: ${error.message}`);
|
|
30585
|
+
}
|
|
30586
|
+
} else if (key === "runtime") {
|
|
30419
30587
|
if (value !== "node" && value !== "python") throw new Error('org project set: gate.runtime must be "node" or "python"');
|
|
30420
30588
|
out.runtime = value;
|
|
30421
30589
|
} else if (key === "cmd" || key === "affectedCmd" || key === "workdir" || key === "cacheDepPath" || key === "pyVersion") {
|
|
30422
30590
|
if (typeof value !== "string" || !value.trim()) throw new Error(`org project set: gate.${key} must be a non-empty string`);
|
|
30423
30591
|
out[key] = value.trim();
|
|
30424
30592
|
} else {
|
|
30425
|
-
throw new Error(`org project set: gate key "${key}" \u2014 expected only runtime/cmd/affectedCmd/workdir/cacheDepPath/pyVersion`);
|
|
30593
|
+
throw new Error(`org project set: gate key "${key}" \u2014 expected only runtime/cmd/affectedCmd/workdir/cacheDepPath/pyVersion/services/steps`);
|
|
30426
30594
|
}
|
|
30427
30595
|
}
|
|
30428
30596
|
return out;
|
|
@@ -31625,10 +31793,11 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
31625
31793
|
const results = parsePlanOutput(output, probeRows);
|
|
31626
31794
|
const drift = results.filter((r) => r.status !== "in-sync").length;
|
|
31627
31795
|
const unread = results.some((r) => r.status === "unread");
|
|
31628
|
-
const
|
|
31796
|
+
const alerts = parseAlerts(output);
|
|
31797
|
+
const payload = { box: found.box.name, ref: o.ref, mode: o.apply ? "apply" : "plan", files: results, drift, alerts, ok: drift === 0 && alerts.length === 0 };
|
|
31629
31798
|
if (o.json) console.log(JSON.stringify(payload, null, 2));
|
|
31630
|
-
else console.log(formatControl(payload.mode, found.box.name, results));
|
|
31631
|
-
process.exitCode = unread ? 2 : drift ? 1 : 0;
|
|
31799
|
+
else console.log([formatControl(payload.mode, found.box.name, results), ...alerts.map((a) => `ALERT ${a.name}: ${a.message}`)].join("\n"));
|
|
31800
|
+
process.exitCode = unread ? 2 : drift || alerts.length ? 1 : 0;
|
|
31632
31801
|
} catch (e) {
|
|
31633
31802
|
if (o.json) console.log(JSON.stringify({ box: name, ref: o.ref, ok: false, error: e.message }, null, 2));
|
|
31634
31803
|
else console.error(e.message);
|
package/package.json
CHANGED