@mutmutco/cli 4.3.42 → 4.3.44
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 +155 -51
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -15710,10 +15710,10 @@ var rollout_plan_default = {
|
|
|
15710
15710
|
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)."
|
|
15711
15711
|
},
|
|
15712
15712
|
baseline: {
|
|
15713
|
-
version: "4.3.
|
|
15714
|
-
tag: "v4.3.
|
|
15715
|
-
commit: "
|
|
15716
|
-
npm: "@mutmutco/cli@4.3.
|
|
15713
|
+
version: "4.3.44",
|
|
15714
|
+
tag: "v4.3.44",
|
|
15715
|
+
commit: "405d6e3283b9",
|
|
15716
|
+
npm: "@mutmutco/cli@4.3.44"
|
|
15717
15717
|
},
|
|
15718
15718
|
exitCriterion: "fleet-n-of-n",
|
|
15719
15719
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15730,14 +15730,14 @@ var rollout_plan_default = {
|
|
|
15730
15730
|
repo: "mutmutco/mmi-hub",
|
|
15731
15731
|
role: "canary",
|
|
15732
15732
|
schedule: "train",
|
|
15733
|
-
v3Target: "v4.3.
|
|
15733
|
+
v3Target: "v4.3.44"
|
|
15734
15734
|
}
|
|
15735
15735
|
],
|
|
15736
15736
|
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.",
|
|
15737
15737
|
rollback: {
|
|
15738
15738
|
independent: true,
|
|
15739
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15740
|
-
v3Target: "v4.3.
|
|
15739
|
+
mechanism: "npm dist-tag latest -> 4.3.44 and redeploy the Hub Lambda from tag v4.3.44 (405d6e3283b9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15740
|
+
v3Target: "v4.3.44 (@mutmutco/cli@4.3.44, tag commit 405d6e3283b9 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15741
15741
|
}
|
|
15742
15742
|
},
|
|
15743
15743
|
{
|
|
@@ -29159,7 +29159,7 @@ function registerDeployCommands(program3) {
|
|
|
29159
29159
|
}
|
|
29160
29160
|
|
|
29161
29161
|
// src/project-set.ts
|
|
29162
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary"];
|
|
29162
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary"];
|
|
29163
29163
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
29164
29164
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
29165
29165
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -29443,6 +29443,20 @@ function parsePublishDirVar(raw) {
|
|
|
29443
29443
|
}
|
|
29444
29444
|
return v;
|
|
29445
29445
|
}
|
|
29446
|
+
function parseReleaseChannelVar(raw) {
|
|
29447
|
+
const v = raw.trim();
|
|
29448
|
+
if (!/^[CG][A-Z0-9]{8,}$/.test(v)) {
|
|
29449
|
+
throw new Error("org project set: releaseChannel must be a Slack channel id (e.g. C0BE0BRGAT0); --unset releaseChannel disables release notes");
|
|
29450
|
+
}
|
|
29451
|
+
return v;
|
|
29452
|
+
}
|
|
29453
|
+
function parseReleaseLanguageVar(raw) {
|
|
29454
|
+
const v = raw.trim();
|
|
29455
|
+
if (!/^[a-z]{2}$/.test(v)) {
|
|
29456
|
+
throw new Error("org project set: releaseLanguage must be a 2-letter lowercase language code (e.g. tr); --unset releaseLanguage returns to en");
|
|
29457
|
+
}
|
|
29458
|
+
return v;
|
|
29459
|
+
}
|
|
29446
29460
|
function parseDsManifestPathVar(raw) {
|
|
29447
29461
|
const v = raw.trim();
|
|
29448
29462
|
if (v === "" || !/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(v) || /(^|\/)\.\.(\/|$)/.test(v)) {
|
|
@@ -29559,6 +29573,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
29559
29573
|
"oauth",
|
|
29560
29574
|
"publishRequired",
|
|
29561
29575
|
"publishDir",
|
|
29576
|
+
"releaseChannel",
|
|
29577
|
+
"releaseLanguage",
|
|
29562
29578
|
"dsManifestPath",
|
|
29563
29579
|
"fofuEnabled",
|
|
29564
29580
|
"consumesDesignSystem",
|
|
@@ -29586,6 +29602,8 @@ var SETTABLE_VAR_HINTS = {
|
|
|
29586
29602
|
projectNumber: "numeric",
|
|
29587
29603
|
publishRequired: "true|false",
|
|
29588
29604
|
publishDir: "relative subpath, e.g. packages/ui",
|
|
29605
|
+
releaseChannel: "Slack channel id, e.g. C0BE0BRGAT0",
|
|
29606
|
+
releaseLanguage: "2-letter language code for release notes, e.g. tr (default en)",
|
|
29589
29607
|
dsManifestPath: "relative path to a package.json, e.g. web/package.json",
|
|
29590
29608
|
fofuEnabled: "true|false",
|
|
29591
29609
|
consumesDesignSystem: '"fofu"',
|
|
@@ -29698,6 +29716,10 @@ function buildProjectSetPatch(input) {
|
|
|
29698
29716
|
patch[key] = parseConsumesDesignSystemVar(raw);
|
|
29699
29717
|
} else if (key === "publishDir") {
|
|
29700
29718
|
patch[key] = parsePublishDirVar(raw);
|
|
29719
|
+
} else if (key === "releaseChannel") {
|
|
29720
|
+
patch[key] = parseReleaseChannelVar(raw);
|
|
29721
|
+
} else if (key === "releaseLanguage") {
|
|
29722
|
+
patch[key] = parseReleaseLanguageVar(raw);
|
|
29701
29723
|
} else if (key === "dsManifestPath") {
|
|
29702
29724
|
patch[key] = parseDsManifestPathVar(raw);
|
|
29703
29725
|
} else if (key === "ci") {
|
|
@@ -42357,7 +42379,7 @@ var SSM_REGION = "eu-central-1";
|
|
|
42357
42379
|
var SSM_TOKEN_PARAM = "/mmi-future/_org/slack/SLACK_BOT_TOKEN";
|
|
42358
42380
|
var SSM_CHANNEL_PARAM = "/mmi-future/_org/slack/SLACK_ALERTS_CHANNEL";
|
|
42359
42381
|
var SLACK_TIMEOUT_MS = 1e4;
|
|
42360
|
-
async function
|
|
42382
|
+
async function readSsmParameter(deps, name, decrypt) {
|
|
42361
42383
|
const args = [
|
|
42362
42384
|
"ssm",
|
|
42363
42385
|
"get-parameter",
|
|
@@ -42375,18 +42397,24 @@ async function ssmParameter(deps, name, decrypt) {
|
|
|
42375
42397
|
if (!value || value === "None") throw new Error(`SSM parameter ${name} is empty`);
|
|
42376
42398
|
return value;
|
|
42377
42399
|
}
|
|
42378
|
-
async function
|
|
42379
|
-
const
|
|
42380
|
-
|
|
42400
|
+
async function postToChannel(deps, channel, text) {
|
|
42401
|
+
const resolved = channel.trim();
|
|
42402
|
+
if (!resolved || resolved === "None") throw new Error("no Slack channel resolved");
|
|
42403
|
+
const token = await readSsmParameter(deps, SSM_TOKEN_PARAM, true);
|
|
42381
42404
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
42382
42405
|
const res = await fetchImpl("https://slack.com/api/chat.postMessage", {
|
|
42383
42406
|
method: "POST",
|
|
42384
42407
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json; charset=utf-8" },
|
|
42385
|
-
body: JSON.stringify({ channel, text, unfurl_links: false }),
|
|
42408
|
+
body: JSON.stringify({ channel: resolved, text, unfurl_links: false }),
|
|
42386
42409
|
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
42387
42410
|
});
|
|
42388
42411
|
const json = await res.json().catch(() => ({}));
|
|
42389
42412
|
if (!json.ok) throw new Error(`slack postMessage failed: ${json.error ?? `http ${res.status}`}`);
|
|
42413
|
+
return { status: "posted", note: "posted" };
|
|
42414
|
+
}
|
|
42415
|
+
async function postToAlertsChannel(deps, text) {
|
|
42416
|
+
const channel = await readSsmParameter(deps, SSM_CHANNEL_PARAM, false);
|
|
42417
|
+
await postToChannel(deps, channel, text);
|
|
42390
42418
|
return { status: "posted", note: "posted to the alerts channel" };
|
|
42391
42419
|
}
|
|
42392
42420
|
|
|
@@ -42402,7 +42430,7 @@ function neutralizeHubProductBrands(text) {
|
|
|
42402
42430
|
function escapeMrkdwn(text) {
|
|
42403
42431
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
42404
42432
|
}
|
|
42405
|
-
function bulletToLine(raw) {
|
|
42433
|
+
function bulletToLine(raw, neutralize) {
|
|
42406
42434
|
const m = /^[*-]\s+(.*)$/.exec(raw.trim());
|
|
42407
42435
|
if (!m) return null;
|
|
42408
42436
|
let text = m[1].trim();
|
|
@@ -42417,9 +42445,9 @@ function bulletToLine(raw) {
|
|
|
42417
42445
|
text = text.slice(0, tail.index).trim();
|
|
42418
42446
|
}
|
|
42419
42447
|
if (!text) return null;
|
|
42420
|
-
return `\u2022 ${escapeMrkdwn(neutralizeHubProductBrands(text))}${link}`;
|
|
42448
|
+
return `\u2022 ${escapeMrkdwn(neutralize ? neutralizeHubProductBrands(text) : text)}${link}`;
|
|
42421
42449
|
}
|
|
42422
|
-
function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
|
|
42450
|
+
function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS, neutralize = true) {
|
|
42423
42451
|
const lines2 = [];
|
|
42424
42452
|
let bullets = 0;
|
|
42425
42453
|
let dropped = 0;
|
|
@@ -42437,7 +42465,7 @@ function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
|
|
|
42437
42465
|
continue;
|
|
42438
42466
|
}
|
|
42439
42467
|
if (/new contributors/i.test(section)) continue;
|
|
42440
|
-
const line = bulletToLine(raw);
|
|
42468
|
+
const line = bulletToLine(raw, neutralize);
|
|
42441
42469
|
if (!line) continue;
|
|
42442
42470
|
if (bullets >= maxBullets) {
|
|
42443
42471
|
dropped += 1;
|
|
@@ -42453,31 +42481,49 @@ function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
|
|
|
42453
42481
|
if (dropped > 0) lines2.push(`\u2026and ${dropped} more`);
|
|
42454
42482
|
return lines2;
|
|
42455
42483
|
}
|
|
42484
|
+
var ANNOUNCE_FRAMES = {
|
|
42485
|
+
en: { released: "released", notes: "Release notes" },
|
|
42486
|
+
tr: { released: "yay\u0131nda", notes: "S\xFCr\xFCm notlar\u0131" }
|
|
42487
|
+
};
|
|
42456
42488
|
function formatAnnouncement(args) {
|
|
42457
42489
|
const name = args.repo.split("/")[1] ?? args.repo;
|
|
42490
|
+
const code = args.language ?? "en";
|
|
42491
|
+
const frame = Object.hasOwn(ANNOUNCE_FRAMES, code) ? ANNOUNCE_FRAMES[code] : ANNOUNCE_FRAMES.en;
|
|
42458
42492
|
return [
|
|
42459
|
-
`:rocket: *${name} ${args.tag} released*`,
|
|
42493
|
+
`:rocket: *${name} ${args.tag} ${frame.released}*`,
|
|
42460
42494
|
"",
|
|
42461
42495
|
...args.lines,
|
|
42462
42496
|
"",
|
|
42463
|
-
`<${args.releaseUrl}
|
|
42497
|
+
`<${args.releaseUrl}|${frame.notes}>`
|
|
42464
42498
|
].join("\n");
|
|
42465
42499
|
}
|
|
42466
|
-
function summaryFileLines(content) {
|
|
42467
|
-
return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) =>
|
|
42500
|
+
function summaryFileLines(content, neutralize = true) {
|
|
42501
|
+
return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) => {
|
|
42502
|
+
const bare = l.replace(/^[•*-]\s*/, "");
|
|
42503
|
+
return escapeMrkdwn(neutralize ? neutralizeHubProductBrands(bare) : bare);
|
|
42504
|
+
}).filter(Boolean).map((l) => `\u2022 ${l}`);
|
|
42505
|
+
}
|
|
42506
|
+
async function resolveAnnounceTarget(deps, repo) {
|
|
42507
|
+
if (repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) return { kind: "alerts" };
|
|
42508
|
+
const target = deps.fetchReleaseTarget ? await deps.fetchReleaseTarget(slugOf(repo)) : { channel: null, language: "en" };
|
|
42509
|
+
return target.channel ? { kind: "channel", channel: target.channel, language: target.language } : { kind: "none" };
|
|
42510
|
+
}
|
|
42511
|
+
function missingChannelNote(repo) {
|
|
42512
|
+
const slug = slugOf(repo);
|
|
42513
|
+
return `no releaseChannel configured for ${slug} \u2014 enable release notes with \`mmi-cli oracle org project set --var releaseChannel=<slack channel id> ${repo}\``;
|
|
42468
42514
|
}
|
|
42469
42515
|
async function announceRelease(deps, args) {
|
|
42470
|
-
|
|
42471
|
-
return { status: "skipped", note: `announce is Hub-only \u2014 ${args.repo} skipped` };
|
|
42472
|
-
}
|
|
42516
|
+
const neutralize = args.repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase();
|
|
42473
42517
|
try {
|
|
42518
|
+
const target = await resolveAnnounceTarget(deps, args.repo);
|
|
42519
|
+
if (target.kind === "none") return { status: "skipped", note: missingChannelNote(args.repo) };
|
|
42474
42520
|
const viewRaw = await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body,url"]);
|
|
42475
42521
|
const view = JSON.parse(viewRaw);
|
|
42476
42522
|
const releaseUrl = view.url ?? `https://github.com/${args.repo}/releases/tag/${args.tag}`;
|
|
42477
42523
|
let lines2 = [];
|
|
42478
42524
|
if (args.summaryFile) {
|
|
42479
42525
|
if (!deps.readFile) throw new Error("summary file given but deps.readFile is missing");
|
|
42480
|
-
lines2 = summaryFileLines(await deps.readFile(args.summaryFile));
|
|
42526
|
+
lines2 = summaryFileLines(await deps.readFile(args.summaryFile), neutralize);
|
|
42481
42527
|
if (deps.removeFile) {
|
|
42482
42528
|
try {
|
|
42483
42529
|
await deps.removeFile(args.summaryFile);
|
|
@@ -42485,11 +42531,26 @@ async function announceRelease(deps, args) {
|
|
|
42485
42531
|
}
|
|
42486
42532
|
}
|
|
42487
42533
|
}
|
|
42488
|
-
if (lines2.length === 0)
|
|
42534
|
+
if (lines2.length === 0) {
|
|
42535
|
+
if (target.kind !== "alerts") {
|
|
42536
|
+
return { status: "skipped", note: "no --announce-summary-file given \u2014 raw release notes are never posted to a project channel" };
|
|
42537
|
+
}
|
|
42538
|
+
lines2 = curateReleaseNotes(view.body ?? "", MAX_BULLETS, neutralize);
|
|
42539
|
+
}
|
|
42489
42540
|
if (lines2.length === 0) lines2 = ["\u2022 maintenance release \u2014 see the notes for details"];
|
|
42490
|
-
const text = formatAnnouncement({
|
|
42491
|
-
|
|
42492
|
-
|
|
42541
|
+
const text = formatAnnouncement({
|
|
42542
|
+
repo: args.repo,
|
|
42543
|
+
tag: args.tag,
|
|
42544
|
+
lines: lines2,
|
|
42545
|
+
releaseUrl,
|
|
42546
|
+
...target.kind === "channel" ? { language: target.language } : {}
|
|
42547
|
+
});
|
|
42548
|
+
if (target.kind === "alerts") {
|
|
42549
|
+
await postToAlertsChannel(deps, text);
|
|
42550
|
+
return { status: "announced", note: `announced ${args.tag} to the alerts channel` };
|
|
42551
|
+
}
|
|
42552
|
+
await postToChannel(deps, target.channel, text);
|
|
42553
|
+
return { status: "announced", note: `announced ${args.tag} to the project release channel` };
|
|
42493
42554
|
} catch (e) {
|
|
42494
42555
|
return { status: "failed", note: `announce failed (release unaffected): ${e.message}` };
|
|
42495
42556
|
}
|
|
@@ -43842,6 +43903,18 @@ var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
|
43842
43903
|
var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
|
|
43843
43904
|
var NPM_TRAIN_TIMEOUT_MS = 6e4;
|
|
43844
43905
|
var JERV_GATEWAY_DEPLOY_TIMEOUT_MS = 20 * 6e4;
|
|
43906
|
+
async function announceSummaryRefusal(path2) {
|
|
43907
|
+
let lines2;
|
|
43908
|
+
try {
|
|
43909
|
+
lines2 = summaryFileLines(await (0, import_promises10.readFile)(path2, "utf8"));
|
|
43910
|
+
} catch (e) {
|
|
43911
|
+
return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
43912
|
+
}
|
|
43913
|
+
if (lines2.length < 3 || lines2.length > 6) {
|
|
43914
|
+
return `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
43915
|
+
}
|
|
43916
|
+
return null;
|
|
43917
|
+
}
|
|
43845
43918
|
function trainApplyDeps() {
|
|
43846
43919
|
return {
|
|
43847
43920
|
run: async (file, args) => {
|
|
@@ -43904,11 +43977,25 @@ function trainApplyDeps() {
|
|
|
43904
43977
|
// train lands the back-merge itself instead of leaving a manual step. Same enqueue path `pr land` uses,
|
|
43905
43978
|
// but method `--merge` (NOT squash): the misalignment guard needs the merge parentage to survive.
|
|
43906
43979
|
mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--merge"),
|
|
43907
|
-
// Slack release announcement (#883):
|
|
43980
|
+
// Slack release announcement (#883/#6521/#6523): the org alerts channel for the Hub, the project's own
|
|
43981
|
+
// registry releaseChannel META for a product repo — best-effort inside announceRelease itself.
|
|
43908
43982
|
announce: (args) => announceRelease({
|
|
43909
43983
|
run: async (file, cmdArgs) => (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
43910
43984
|
readFile: (path2) => (0, import_promises10.readFile)(path2, "utf8"),
|
|
43911
|
-
removeFile: (path2) => (0, import_promises10.unlink)(path2)
|
|
43985
|
+
removeFile: (path2) => (0, import_promises10.unlink)(path2),
|
|
43986
|
+
// #6521/#6523: the project's own release channel AND releaseLanguage, read from registry META in ONE
|
|
43987
|
+
// checked read. Same shape as fetchEdgeDomains below: a FAILED read refuses by name so the announce
|
|
43988
|
+
// note reports a failed read, never a project that configured nothing.
|
|
43989
|
+
fetchReleaseTarget: async (slug) => {
|
|
43990
|
+
const read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
|
|
43991
|
+
if (!read.ok) {
|
|
43992
|
+
throw new Error(`release announcement: Hub registry read FAILED for ${slug} (${read.error}) \u2014 releaseChannel is UNREAD, not unset`);
|
|
43993
|
+
}
|
|
43994
|
+
return {
|
|
43995
|
+
channel: read.project?.releaseChannel?.trim() || null,
|
|
43996
|
+
language: read.project?.releaseLanguage?.trim() || "en"
|
|
43997
|
+
};
|
|
43998
|
+
}
|
|
43912
43999
|
}, args),
|
|
43913
44000
|
// #4713 (I/O-boundary census): `null` used to mean BOTH "this project configures no edge domains"
|
|
43914
44001
|
// (a real answer) and "the registry read missed" — so a release verdict printed an environments block
|
|
@@ -44168,7 +44255,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44168
44255
|
for (const commandName of ["rcand", "release"]) {
|
|
44169
44256
|
const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
|
|
44170
44257
|
const RELEASE_ONLY_FLAGS = [
|
|
44171
|
-
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line
|
|
44258
|
+
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901) and for a product repo whose registry META sets `releaseChannel` (#6523), in its `releaseLanguage`; nothing is posted to a project channel without it" },
|
|
44172
44259
|
{ flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
|
|
44173
44260
|
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
|
|
44174
44261
|
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
|
|
@@ -44194,7 +44281,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44194
44281
|
return fail(`${commandName}: --dev applies only to release \u2014 it ships development -> main skipping rc, which rcand cannot do. Run: mmi-cli devops release --dev`);
|
|
44195
44282
|
}
|
|
44196
44283
|
if (o.announceSummaryFile && commandName !== "release") {
|
|
44197
|
-
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no
|
|
44284
|
+
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no release announcement. Run: mmi-cli devops release --announce-summary-file <path>`);
|
|
44198
44285
|
}
|
|
44199
44286
|
if (o.abort && commandName !== "release") {
|
|
44200
44287
|
return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished release tag. Run: mmi-cli devops release --abort --apply`);
|
|
@@ -44286,22 +44373,35 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44286
44373
|
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
|
|
44287
44374
|
if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
|
|
44288
44375
|
}
|
|
44289
|
-
if (o.apply && commandName === "release"
|
|
44290
|
-
|
|
44291
|
-
|
|
44292
|
-
|
|
44293
|
-
|
|
44294
|
-
|
|
44295
|
-
|
|
44296
|
-
|
|
44297
|
-
|
|
44298
|
-
|
|
44299
|
-
|
|
44376
|
+
if (o.apply && commandName === "release") {
|
|
44377
|
+
const applyingRepo = await resolveRepo();
|
|
44378
|
+
const slug = applyingRepo ? slugOf(applyingRepo) : "";
|
|
44379
|
+
const isHub = applyingRepo?.toLowerCase() === ANNOUNCE_REPO.toLowerCase();
|
|
44380
|
+
let announcesToChannel = isHub;
|
|
44381
|
+
let releaseLanguage2 = "en";
|
|
44382
|
+
if (!isHub && slug) {
|
|
44383
|
+
let read;
|
|
44384
|
+
try {
|
|
44385
|
+
read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
|
|
44386
|
+
} catch (e) {
|
|
44387
|
+
read = { ok: false, error: e.message };
|
|
44388
|
+
}
|
|
44389
|
+
if (!read.ok) {
|
|
44390
|
+
return fail(
|
|
44391
|
+
`release: Hub registry read failed for ${slug} (${read.error}) \u2014 whether ${slug} announces to a project release channel is UNREAD, so the --announce-summary-file requirement is unproven. Likely transient (cold start, network, or auth blip); retry shortly.`
|
|
44392
|
+
);
|
|
44393
|
+
}
|
|
44394
|
+
announcesToChannel = Boolean(read.project?.releaseChannel?.trim());
|
|
44395
|
+
releaseLanguage2 = read.project?.releaseLanguage?.trim() || "en";
|
|
44300
44396
|
}
|
|
44301
|
-
if (
|
|
44302
|
-
|
|
44303
|
-
|
|
44304
|
-
|
|
44397
|
+
if (announcesToChannel) {
|
|
44398
|
+
if (!o.announceSummaryFile) {
|
|
44399
|
+
return fail(
|
|
44400
|
+
isHub ? "release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing" : `release: ${slug} announces to its project release channel, so a new release requires --announce-summary-file <path> with 3-6 short plain-language lines in the project's releaseLanguage (${releaseLanguage2}); create the summary file, then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`
|
|
44401
|
+
);
|
|
44402
|
+
}
|
|
44403
|
+
const refusal = await announceSummaryRefusal(o.announceSummaryFile);
|
|
44404
|
+
if (refusal) return fail(refusal);
|
|
44305
44405
|
}
|
|
44306
44406
|
}
|
|
44307
44407
|
if (o.apply) {
|
|
@@ -44333,6 +44433,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44333
44433
|
const repo = o.repo ?? await resolveRepo();
|
|
44334
44434
|
const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
|
|
44335
44435
|
let releaseTrack;
|
|
44436
|
+
let releaseChannel = null;
|
|
44437
|
+
let releaseLanguage = "en";
|
|
44336
44438
|
if (repo) {
|
|
44337
44439
|
let read;
|
|
44338
44440
|
try {
|
|
@@ -44347,6 +44449,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44347
44449
|
}
|
|
44348
44450
|
const raw = read.project?.releaseTrack;
|
|
44349
44451
|
if (isReleaseTrack(raw)) releaseTrack = raw;
|
|
44452
|
+
releaseChannel = read.project?.releaseChannel?.trim() || null;
|
|
44453
|
+
releaseLanguage = read.project?.releaseLanguage?.trim() || "en";
|
|
44350
44454
|
}
|
|
44351
44455
|
const steps = trainPlan(commandName, { ...targets ?? {}, repo, dev: o.dev, releaseTrack });
|
|
44352
44456
|
let doctor;
|
|
@@ -44358,7 +44462,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44358
44462
|
}
|
|
44359
44463
|
}
|
|
44360
44464
|
console.log(
|
|
44361
|
-
o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps, ...doctor ? { doctor } : {} }, null, 2) : [renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps), ...doctor ? ["", formatTrainDoctor(doctor)] : []].join("\n")
|
|
44465
|
+
o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps, ...commandName === "release" ? { releaseChannel, releaseLanguage } : {}, ...doctor ? { doctor } : {} }, null, 2) : [renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps), ...doctor ? ["", formatTrainDoctor(doctor)] : []].join("\n")
|
|
44362
44466
|
);
|
|
44363
44467
|
});
|
|
44364
44468
|
}
|
|
@@ -44446,7 +44550,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44446
44550
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
|
|
44447
44551
|
});
|
|
44448
44552
|
hotfixCmd.command("start").description("cherry-pick one or more merged development PRs (or SHAs) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR number(s) or commit SHA(s) to cherry-pick, in pick order \u2014 one hotfix cycle carries as many fixes as you name (#4411)").option("--json", "machine-readable output").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o, renderHotfixStart));
|
|
44449
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line
|
|
44553
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => runHotfixSub("release", async () => {
|
|
44450
44554
|
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
|
|
44451
44555
|
const postReleaseJervDoctor = await runPostReleaseJervDoctor(result.repo);
|
|
44452
44556
|
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
@@ -45278,7 +45382,7 @@ projectDeploy.command("doctor").description("read-only estate scan for duplicate
|
|
|
45278
45382
|
if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
|
|
45279
45383
|
if (!report.ok && !report.error) process.exitCode = 1;
|
|
45280
45384
|
});
|
|
45281
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
45385
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
45282
45386
|
const cfg = await loadConfig();
|
|
45283
45387
|
let target;
|
|
45284
45388
|
try {
|
package/package.json
CHANGED