@mutmutco/cli 4.3.42 → 4.3.43

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.
Files changed (2) hide show
  1. package/dist/main.cjs +66 -29
  2. 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.42",
15714
- tag: "v4.3.42",
15715
- commit: "d890700b4096",
15716
- npm: "@mutmutco/cli@4.3.42"
15713
+ version: "4.3.43",
15714
+ tag: "v4.3.43",
15715
+ commit: "120c58230cfc",
15716
+ npm: "@mutmutco/cli@4.3.43"
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.42"
15733
+ v3Target: "v4.3.43"
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.42 and redeploy the Hub Lambda from tag v4.3.42 (d890700b4096); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15740
- v3Target: "v4.3.42 (@mutmutco/cli@4.3.42, tag commit d890700b4096 \u2014 last known-good release carrying the repo-index v4-only contract)"
15739
+ mechanism: "npm dist-tag latest -> 4.3.43 and redeploy the Hub Lambda from tag v4.3.43 (120c58230cfc); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15740
+ v3Target: "v4.3.43 (@mutmutco/cli@4.3.43, tag commit 120c58230cfc \u2014 last known-good release carrying the repo-index v4-only contract)"
15741
15741
  }
15742
15742
  },
15743
15743
  {
@@ -42357,7 +42357,7 @@ var SSM_REGION = "eu-central-1";
42357
42357
  var SSM_TOKEN_PARAM = "/mmi-future/_org/slack/SLACK_BOT_TOKEN";
42358
42358
  var SSM_CHANNEL_PARAM = "/mmi-future/_org/slack/SLACK_ALERTS_CHANNEL";
42359
42359
  var SLACK_TIMEOUT_MS = 1e4;
42360
- async function ssmParameter(deps, name, decrypt) {
42360
+ async function readSsmParameter(deps, name, decrypt) {
42361
42361
  const args = [
42362
42362
  "ssm",
42363
42363
  "get-parameter",
@@ -42375,23 +42375,30 @@ async function ssmParameter(deps, name, decrypt) {
42375
42375
  if (!value || value === "None") throw new Error(`SSM parameter ${name} is empty`);
42376
42376
  return value;
42377
42377
  }
42378
- async function postToAlertsChannel(deps, text) {
42379
- const token = await ssmParameter(deps, SSM_TOKEN_PARAM, true);
42380
- const channel = await ssmParameter(deps, SSM_CHANNEL_PARAM, false);
42378
+ async function postToChannel(deps, channel, text) {
42379
+ const resolved = channel.trim();
42380
+ if (!resolved || resolved === "None") throw new Error("no Slack channel resolved");
42381
+ const token = await readSsmParameter(deps, SSM_TOKEN_PARAM, true);
42381
42382
  const fetchImpl = deps.fetchImpl ?? fetch;
42382
42383
  const res = await fetchImpl("https://slack.com/api/chat.postMessage", {
42383
42384
  method: "POST",
42384
42385
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json; charset=utf-8" },
42385
- body: JSON.stringify({ channel, text, unfurl_links: false }),
42386
+ body: JSON.stringify({ channel: resolved, text, unfurl_links: false }),
42386
42387
  signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
42387
42388
  });
42388
42389
  const json = await res.json().catch(() => ({}));
42389
42390
  if (!json.ok) throw new Error(`slack postMessage failed: ${json.error ?? `http ${res.status}`}`);
42391
+ return { status: "posted", note: "posted" };
42392
+ }
42393
+ async function postToAlertsChannel(deps, text) {
42394
+ const channel = await readSsmParameter(deps, SSM_CHANNEL_PARAM, false);
42395
+ await postToChannel(deps, channel, text);
42390
42396
  return { status: "posted", note: "posted to the alerts channel" };
42391
42397
  }
42392
42398
 
42393
42399
  // src/release-announce.ts
42394
42400
  var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
42401
+ var RELEASE_CHANNEL_KEY = "SLACK_RELEASE_CHANNEL";
42395
42402
  var MAX_BULLETS = 6;
42396
42403
  var MAX_SUMMARY_LINES = 8;
42397
42404
  function neutralizeHubProductBrands(text) {
@@ -42399,10 +42406,17 @@ function neutralizeHubProductBrands(text) {
42399
42406
  s = s.replace(/\bdesign-system\s+design-system\b/gi, "design-system");
42400
42407
  return s.replace(/\s{2,}/g, " ").trim();
42401
42408
  }
42409
+ function releaseChannelParam(repo) {
42410
+ return `/mmi-future/${slugOf(repo)}/${RELEASE_CHANNEL_KEY}`;
42411
+ }
42412
+ function isAbsentParameter(e) {
42413
+ const message2 = e instanceof Error ? e.message : String(e);
42414
+ return /ParameterNotFound/.test(message2) || /is empty$/.test(message2);
42415
+ }
42402
42416
  function escapeMrkdwn(text) {
42403
42417
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42404
42418
  }
42405
- function bulletToLine(raw) {
42419
+ function bulletToLine(raw, neutralize) {
42406
42420
  const m = /^[*-]\s+(.*)$/.exec(raw.trim());
42407
42421
  if (!m) return null;
42408
42422
  let text = m[1].trim();
@@ -42417,9 +42431,9 @@ function bulletToLine(raw) {
42417
42431
  text = text.slice(0, tail.index).trim();
42418
42432
  }
42419
42433
  if (!text) return null;
42420
- return `\u2022 ${escapeMrkdwn(neutralizeHubProductBrands(text))}${link}`;
42434
+ return `\u2022 ${escapeMrkdwn(neutralize ? neutralizeHubProductBrands(text) : text)}${link}`;
42421
42435
  }
42422
- function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
42436
+ function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS, neutralize = true) {
42423
42437
  const lines2 = [];
42424
42438
  let bullets = 0;
42425
42439
  let dropped = 0;
@@ -42437,7 +42451,7 @@ function curateReleaseNotes(notesMd, maxBullets = MAX_BULLETS) {
42437
42451
  continue;
42438
42452
  }
42439
42453
  if (/new contributors/i.test(section)) continue;
42440
- const line = bulletToLine(raw);
42454
+ const line = bulletToLine(raw, neutralize);
42441
42455
  if (!line) continue;
42442
42456
  if (bullets >= maxBullets) {
42443
42457
  dropped += 1;
@@ -42463,21 +42477,39 @@ function formatAnnouncement(args) {
42463
42477
  `<${args.releaseUrl}|Release notes>`
42464
42478
  ].join("\n");
42465
42479
  }
42466
- function summaryFileLines(content) {
42467
- return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) => escapeMrkdwn(neutralizeHubProductBrands(l.replace(/^[•*-]\s*/, "")))).filter(Boolean).map((l) => `\u2022 ${l}`);
42480
+ function summaryFileLines(content, neutralize = true) {
42481
+ return content.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, MAX_SUMMARY_LINES).map((l) => {
42482
+ const bare = l.replace(/^[•*-]\s*/, "");
42483
+ return escapeMrkdwn(neutralize ? neutralizeHubProductBrands(bare) : bare);
42484
+ }).filter(Boolean).map((l) => `\u2022 ${l}`);
42468
42485
  }
42469
- async function announceRelease(deps, args) {
42470
- if (args.repo !== ANNOUNCE_REPO) {
42471
- return { status: "skipped", note: `announce is Hub-only \u2014 ${args.repo} skipped` };
42486
+ async function resolveAnnounceTarget(deps, repo) {
42487
+ if (repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) return { kind: "alerts" };
42488
+ const param = releaseChannelParam(repo);
42489
+ try {
42490
+ const channel = await readSsmParameter(deps, param, false);
42491
+ return { kind: "channel", channel };
42492
+ } catch (e) {
42493
+ if (isAbsentParameter(e)) return { kind: "none", param };
42494
+ throw e;
42472
42495
  }
42496
+ }
42497
+ function missingChannelNote(repo) {
42498
+ const slug = slugOf(repo);
42499
+ return `no ${RELEASE_CHANNEL_KEY} declared for ${slug} \u2014 declare it with \`mmi-cli vault secrets declare ${RELEASE_CHANNEL_KEY} --group slack --consumers agent --purpose \u2026\` then \`mmi-cli vault secrets set ${RELEASE_CHANNEL_KEY}\``;
42500
+ }
42501
+ async function announceRelease(deps, args) {
42502
+ const neutralize = args.repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase();
42473
42503
  try {
42504
+ const target = await resolveAnnounceTarget(deps, args.repo);
42505
+ if (target.kind === "none") return { status: "skipped", note: missingChannelNote(args.repo) };
42474
42506
  const viewRaw = await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body,url"]);
42475
42507
  const view = JSON.parse(viewRaw);
42476
42508
  const releaseUrl = view.url ?? `https://github.com/${args.repo}/releases/tag/${args.tag}`;
42477
42509
  let lines2 = [];
42478
42510
  if (args.summaryFile) {
42479
42511
  if (!deps.readFile) throw new Error("summary file given but deps.readFile is missing");
42480
- lines2 = summaryFileLines(await deps.readFile(args.summaryFile));
42512
+ lines2 = summaryFileLines(await deps.readFile(args.summaryFile), neutralize);
42481
42513
  if (deps.removeFile) {
42482
42514
  try {
42483
42515
  await deps.removeFile(args.summaryFile);
@@ -42485,11 +42517,15 @@ async function announceRelease(deps, args) {
42485
42517
  }
42486
42518
  }
42487
42519
  }
42488
- if (lines2.length === 0) lines2 = curateReleaseNotes(view.body ?? "");
42520
+ if (lines2.length === 0) lines2 = curateReleaseNotes(view.body ?? "", MAX_BULLETS, neutralize);
42489
42521
  if (lines2.length === 0) lines2 = ["\u2022 maintenance release \u2014 see the notes for details"];
42490
42522
  const text = formatAnnouncement({ repo: args.repo, tag: args.tag, lines: lines2, releaseUrl });
42491
- await postToAlertsChannel(deps, text);
42492
- return { status: "announced", note: `announced ${args.tag} to the alerts channel` };
42523
+ if (target.kind === "alerts") {
42524
+ await postToAlertsChannel(deps, text);
42525
+ return { status: "announced", note: `announced ${args.tag} to the alerts channel` };
42526
+ }
42527
+ await postToChannel(deps, target.channel, text);
42528
+ return { status: "announced", note: `announced ${args.tag} to the project release channel` };
42493
42529
  } catch (e) {
42494
42530
  return { status: "failed", note: `announce failed (release unaffected): ${e.message}` };
42495
42531
  }
@@ -43904,7 +43940,8 @@ function trainApplyDeps() {
43904
43940
  // train lands the back-merge itself instead of leaving a manual step. Same enqueue path `pr land` uses,
43905
43941
  // but method `--merge` (NOT squash): the misalignment guard needs the merge parentage to survive.
43906
43942
  mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--merge"),
43907
- // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
43943
+ // Slack release announcement (#883/#6517): the org alerts channel for the Hub, the project's own
43944
+ // SLACK_RELEASE_CHANNEL for a product repo — best-effort inside announceRelease itself.
43908
43945
  announce: (args) => announceRelease({
43909
43946
  run: async (file, cmdArgs) => (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
43910
43947
  readFile: (path2) => (0, import_promises10.readFile)(path2, "utf8"),
@@ -44168,7 +44205,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44168
44205
  for (const commandName of ["rcand", "release"]) {
44169
44206
  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
44207
  const RELEASE_ONLY_FLAGS = [
44171
- { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
44208
+ { flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901), optional for a product repo announcing to its project release channel (#6517)" },
44172
44209
  { 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
44210
  { 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
44211
  { 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 +44231,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44194
44231
  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
44232
  }
44196
44233
  if (o.announceSummaryFile && commandName !== "release") {
44197
- return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no Hub Slack announcement. Run: mmi-cli devops release --announce-summary-file <path>`);
44234
+ 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
44235
  }
44199
44236
  if (o.abort && commandName !== "release") {
44200
44237
  return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished release tag. Run: mmi-cli devops release --abort --apply`);
@@ -44446,7 +44483,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44446
44483
  console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
44447
44484
  });
44448
44485
  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 Hub Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068)").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 () => {
44486
+ 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
44487
  const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
44451
44488
  const postReleaseJervDoctor = await runPostReleaseJervDoctor(result.repo);
44452
44489
  const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.42",
3
+ "version": "4.3.43",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",