@mutmutco/cli 4.5.57 → 4.5.58

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 +143 -71
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -15697,11 +15697,12 @@ function planBootstrapLabels(labels, live) {
15697
15697
  ...labelsToPrune(labels.map((label) => label.name)).filter((name) => existing.has(name)).map((name) => ({ name, action: "DELETE" }))
15698
15698
  ];
15699
15699
  }
15700
- function renderBootstrapAncillaryPlan(labels, environments) {
15700
+ function renderBootstrapAncillaryPlan(labels, environments, exceptions = []) {
15701
15701
  return [
15702
15702
  "bootstrap apply \u2014 labels and environments (dry-run; no mutations):",
15703
15703
  ...labels.map(({ action, name }) => ` ${action} label ${name}`),
15704
- ...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`)
15704
+ ...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`),
15705
+ ...exceptions.map(({ name, detail }) => ` OK environment ${name} (${detail})`)
15705
15706
  ].join("\n");
15706
15707
  }
15707
15708
  function resolveSeedContent(seed, vars, readFile9) {
@@ -17069,10 +17070,10 @@ var rollout_plan_default = {
17069
17070
  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)."
17070
17071
  },
17071
17072
  baseline: {
17072
- version: "4.5.57",
17073
- tag: "v4.5.57",
17074
- commit: "d8aff7ba4689",
17075
- npm: "@mutmutco/cli@4.5.57"
17073
+ version: "4.5.58",
17074
+ tag: "v4.5.58",
17075
+ commit: "05d087d1fcb7",
17076
+ npm: "@mutmutco/cli@4.5.58"
17076
17077
  },
17077
17078
  exitCriterion: "fleet-n-of-n",
17078
17079
  hubOnlyShortcut: "forbidden",
@@ -17089,14 +17090,14 @@ var rollout_plan_default = {
17089
17090
  repo: "mutmutco/mmi-hub",
17090
17091
  role: "canary",
17091
17092
  schedule: "train",
17092
- v3Target: "v4.5.57"
17093
+ v3Target: "v4.5.58"
17093
17094
  }
17094
17095
  ],
17095
17096
  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.",
17096
17097
  rollback: {
17097
17098
  independent: true,
17098
- mechanism: "npm dist-tag latest -> 4.5.57 and redeploy the Hub Lambda from tag v4.5.57 (d8aff7ba4689); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17099
- v3Target: "v4.5.57 (@mutmutco/cli@4.5.57, tag commit d8aff7ba4689 \u2014 last known-good release carrying the repo-index v4-only contract)"
17099
+ mechanism: "npm dist-tag latest -> 4.5.58 and redeploy the Hub Lambda from tag v4.5.58 (05d087d1fcb7); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17100
+ v3Target: "v4.5.58 (@mutmutco/cli@4.5.58, tag commit 05d087d1fcb7 \u2014 last known-good release carrying the repo-index v4-only contract)"
17100
17101
  }
17101
17102
  },
17102
17103
  {
@@ -20094,9 +20095,14 @@ function validSuiteCutContext(repo, context) {
20094
20095
  function authorisedSuiteCut(repo, meta, context) {
20095
20096
  return validSuiteCutContext(repo, context) && !!meta?.suite && context.lead.toLowerCase() === suiteLeadRepo(repo, meta.suite).toLowerCase();
20096
20097
  }
20098
+ var BUNDLE_ONLY_RC_EXCEPTION = "suite member: rc is bundle-only (no rc runtime)";
20099
+ function isBundleOnlySuiteMember(repo, suite) {
20100
+ if (!suite) return false;
20101
+ return repo.split("/")[1]?.toLowerCase() !== suite.toLowerCase();
20102
+ }
20097
20103
  function suiteMemberRefusal(repo, meta, lane, solo = false, cut) {
20098
20104
  const suite = meta?.suite;
20099
- if (!suite || repo.split("/")[1]?.toLowerCase() === suite.toLowerCase()) return null;
20105
+ if (!suite || !isBundleOnlySuiteMember(repo, suite)) return null;
20100
20106
  if (authorisedSuiteCut(repo, meta, cut)) return null;
20101
20107
  if (lane !== "rcand" && solo) return null;
20102
20108
  return `${repo} is part of the ${suite} suite; run the suite cut/release from ${suiteLeadRepo(repo, suite)} (mmi-cli devops suite rcand|release). A solo backend release needs --suite-solo --owner-go.`;
@@ -20701,7 +20707,7 @@ async function appendServedChannelLegs(deps, input, dispatch) {
20701
20707
  const publishStatus = input.publish?.state === "complete" ? "success" : "failure";
20702
20708
  const prepared = await (input.prepared ?? prepareServedChannel(deps, input));
20703
20709
  if (!isHubControlRepo(input.repo)) {
20704
- const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut) && input.repo.split("/")[1]?.toLowerCase() !== "mmi-katip";
20710
+ const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut);
20705
20711
  const [home2, proof2] = await Promise.all([
20706
20712
  appendInstallHomePublish(deps, { repo: input.repo, installer: input.installer, publishStatus, watch: input.watch }, dispatch),
20707
20713
  bundleOnly && servedChannelFor(input.repo, input.installer) ? Promise.resolve({ ...dispatch, note: `${dispatch.note}; served channel not probed: a suite bundle-only release does not deploy the backend that serves it (#7468)` }) : appendServedChannelProof(deps, { repo: input.repo, tag: input.tag, shipped: shipLegsGreen(input.deploy, input.publish), installer: input.installer }, dispatch)
@@ -20971,7 +20977,7 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
20971
20977
  return extra;
20972
20978
  }
20973
20979
  async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
20974
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" && stage !== "dev") {
20980
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut) && stage !== "dev") {
20975
20981
  return { note: "suite bundle-only: live backend not deployed", deployStatus: "success" };
20976
20982
  }
20977
20983
  if (isCentralDispatchModel(model)) {
@@ -21098,7 +21104,7 @@ async function attemptRetire(deps, ctx) {
21098
21104
  return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
21099
21105
  }
21100
21106
  async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
21101
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip")
21107
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut))
21102
21108
  return { status: "not-applicable", note: "bundle-only suite member has no rc runtime to retire" };
21103
21109
  if (model !== "tenant-container") {
21104
21110
  return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
@@ -24718,7 +24724,7 @@ async function runTrainDoctor(input) {
24718
24724
  }
24719
24725
  }
24720
24726
  let preflightTimeouts = 0;
24721
- if (!(repo.split("/")[1]?.toLowerCase() !== "mmi-katip" && authorisedSuiteCut(repo, meta, input.suiteCut))) for (; ; ) {
24727
+ if (!authorisedSuiteCut(repo, meta, input.suiteCut)) for (; ; ) {
24722
24728
  try {
24723
24729
  await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
24724
24730
  break;
@@ -25161,7 +25167,7 @@ async function preflight(deps, ctx, stage, meta, lane) {
25161
25167
  if (model === "none") {
25162
25168
  throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
25163
25169
  }
25164
- if (!(ctx.slug !== "mmi-katip" && authorisedSuiteCut(ctx.repo, meta, deps.suiteCut)))
25170
+ if (!authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
25165
25171
  await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
25166
25172
  await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
25167
25173
  await assertActionsJobsCanStart(deps, ctx.repo);
@@ -25760,7 +25766,7 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
25760
25766
  );
25761
25767
  const dispatch = recovered.deployStatus === "failure" && !recovered.note.includes(RCAND_RETRY_DEPLOY_COMMAND) ? { ...recovered, note: `once the cause is fixed, retry this rc deploy with \`${RCAND_RETRY_DEPLOY_COMMAND}\` (same tag and SHA, never re-cut): ${recovered.note}` } : recovered;
25762
25768
  const unverifiable = !isCentralDispatchModel(deployModel) && dispatch.deployStatus === "pending" && dispatch.runId == null && !dispatch.workflowRuns?.length;
25763
- const deployEntry = validSuiteCutContext(anchors.repo, deps.suiteCut) && anchors.repo.split("/")[1]?.toLowerCase() !== "mmi-katip" ? { state: "skipped", sha, note: "suite bundle-only: rc backend not deployed (#7262)" } : unverifiable ? { state: "skipped", sha, note: `${dispatch.note} \u2014 no run to verify on rc; recorded skipped, never pending (#6068)` } : deployPhaseInput(deployModel, dispatch, { releaseSha: sha });
25769
+ const deployEntry = validSuiteCutContext(anchors.repo, deps.suiteCut) ? { state: "skipped", sha, note: "suite bundle-only: rc backend not deployed (#7262)" } : unverifiable ? { state: "skipped", sha, note: `${dispatch.note} \u2014 no run to verify on rc; recorded skipped, never pending (#6068)` } : deployPhaseInput(deployModel, dispatch, { releaseSha: sha });
25764
25770
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployEntry), { ...opts, landed: "the immutable rc tag, the green wall, and origin/rc" });
25765
25771
  await recordPhase(ledger, deps, anchors, "publish", phaseEntry({ state: "skipped", sha, note: "rcand publishes nothing \u2014 the publish leg rides the release" }), { ...opts, landed });
25766
25772
  await recordPhase(ledger, deps, anchors, "alignment", phaseEntry({ state: "skipped", sha, note: "rcand aligns no branch \u2014 alignment rides the release" }), { ...opts, landed });
@@ -25923,7 +25929,7 @@ ${recovery.note}`), recoveryInput);
25923
25929
  }
25924
25930
  );
25925
25931
  const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch), versionEnv);
25926
- const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: releaseSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha }));
25932
+ const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) ? { state: "skipped", sha: releaseSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha }));
25927
25933
  await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
25928
25934
  landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
25929
25935
  });
@@ -26346,7 +26352,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
26346
26352
  dispatch2.deployStatus = "success";
26347
26353
  dispatch2.note = `historical ${deployModel} deploy/publish status: success (re-verified by the recorded run)`;
26348
26354
  }
26349
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip") {
26355
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut)) {
26350
26356
  phaseInputs.deploy = { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" };
26351
26357
  if (dispatch2.deployStatus !== "failure" && (phaseInputs.publish.state === "complete" || phaseInputs.publish.state === "skipped")) {
26352
26358
  dispatch2.deployStatus = "success";
@@ -26481,7 +26487,7 @@ ${recovery.note}`), recoveryInput);
26481
26487
  );
26482
26488
  const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch), versionEnv);
26483
26489
  steps.push(`dispatched the ${deployModel} deploy path`);
26484
- const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" ? { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
26490
+ const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) ? { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
26485
26491
  if (persisted?.phases.deploy.superseded !== true) {
26486
26492
  await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
26487
26493
  strict: ledgerMode === "strict",
@@ -32356,11 +32362,12 @@ function environmentNamesForTrack(track) {
32356
32362
  const branches = new Set(branchesForTrack(track));
32357
32363
  return REPO_ENVIRONMENTS.filter((environment) => branches.has(ENVIRONMENT_BRANCHES[environment]));
32358
32364
  }
32359
- function repoEnvironmentSpecs(_repo, releaseTrack = "full") {
32365
+ function repoEnvironmentSpecs(repo, releaseTrack = "full", suite) {
32360
32366
  const included = new Set(environmentNamesForTrack(releaseTrack));
32361
32367
  const trackExceptions = releaseTrack === "full" ? void 0 : TRACK_ENVIRONMENT_EXCEPTIONS[releaseTrack];
32368
+ const bundleOnlyRc = isBundleOnlySuiteMember(repo, suite);
32362
32369
  return REPO_ENVIRONMENTS.map((name) => {
32363
- const exception = name in included ? void 0 : trackExceptions?.[name];
32370
+ const exception = !included.has(name) ? trackExceptions?.[name] : name === "rc" && bundleOnlyRc ? BUNDLE_ONLY_RC_EXCEPTION : void 0;
32364
32371
  return {
32365
32372
  name,
32366
32373
  branch: ENVIRONMENT_BRANCHES[name],
@@ -32371,8 +32378,8 @@ function repoEnvironmentSpecs(_repo, releaseTrack = "full") {
32371
32378
  };
32372
32379
  });
32373
32380
  }
32374
- function provisionedEnvironmentSpecs(repo, releaseTrack = "full") {
32375
- return repoEnvironmentSpecs(repo, releaseTrack).filter((spec) => spec.exception == null);
32381
+ function provisionedEnvironmentSpecs(repo, releaseTrack = "full", suite) {
32382
+ return repoEnvironmentSpecs(repo, releaseTrack, suite).filter((spec) => spec.exception == null);
32376
32383
  }
32377
32384
  function asEnvironmentList(value) {
32378
32385
  if (Array.isArray(value)) return value;
@@ -32412,10 +32419,10 @@ var PLAN_GATED_PROTECTION_RE = /billing plan/i;
32412
32419
  function isPlanGatedProtectionError(error) {
32413
32420
  return PLAN_GATED_PROTECTION_RE.test(String(error?.message ?? error));
32414
32421
  }
32415
- async function reconcileRepoEnvironments(repo, releaseTrack, client) {
32422
+ async function reconcileRepoEnvironments(repo, releaseTrack, client, suite) {
32416
32423
  const raw = await client.rest("GET", `repos/${repo}/environments`);
32417
32424
  const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
32418
- const planned = provisionedEnvironmentSpecs(repo, releaseTrack);
32425
+ const planned = provisionedEnvironmentSpecs(repo, releaseTrack, suite);
32419
32426
  const created = [];
32420
32427
  const updated = [];
32421
32428
  const skipped = [];
@@ -32448,10 +32455,10 @@ async function reconcileRepoEnvironments(repo, releaseTrack, client) {
32448
32455
  }
32449
32456
  return { repo, planned, created, updated, skipped, planGated };
32450
32457
  }
32451
- async function verifyRepoEnvironments(repo, releaseTrack, client) {
32458
+ async function verifyRepoEnvironments(repo, releaseTrack, client, suite) {
32452
32459
  const raw = await client.rest("GET", `repos/${repo}/environments`);
32453
32460
  const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
32454
- return provisionedEnvironmentSpecs(repo, releaseTrack).map((spec) => {
32461
+ return provisionedEnvironmentSpecs(repo, releaseTrack, suite).map((spec) => {
32455
32462
  const drift = environmentDrift(live.get(spec.name), spec);
32456
32463
  return { environment: spec.name, missing: !live.has(spec.name), problems: drift.problems, notes: drift.notes };
32457
32464
  });
@@ -32569,7 +32576,17 @@ function planBootstrapControlPlane(input) {
32569
32576
  }
32570
32577
  }
32571
32578
  const deployStages = deployStagesForBootstrap(input.deployModel, input.releaseTrack);
32579
+ const bundleOnlyRc = isBundleOnlySuiteMember(input.repo, input.suite);
32572
32580
  for (const stage of deployStages) {
32581
+ if (stage === "rc" && bundleOnlyRc) {
32582
+ rows.push({
32583
+ kind: "deploy-row",
32584
+ target: "DEPLOY#rc",
32585
+ action: "skip",
32586
+ detail: BUNDLE_ONLY_RC_EXCEPTION
32587
+ });
32588
+ continue;
32589
+ }
32573
32590
  const present = input.deployFacts?.stages?.[stage]?.present === true;
32574
32591
  const command = bootstrapSetDeployCommand(input.repo, stage, input.deployModel);
32575
32592
  rows.push(present && stage === "main" ? {
@@ -32959,10 +32976,13 @@ function centralContainerPortRangeCheck(deployModel, portRange, repo) {
32959
32976
  detail: ok ? void 0 : `${deployModel} needs PROJECT# META.portRange for mmi-cli stage \u2014 assign with: mmi-cli stage port-range ${repo}`
32960
32977
  };
32961
32978
  }
32962
- function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead) {
32979
+ function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead, suite) {
32963
32980
  const stages = deployStagesForBootstrap(deployModel, releaseTrack);
32964
32981
  if (!stages.length) return [];
32965
32982
  return stages.map((stage) => {
32983
+ if (stage === "rc" && isBundleOnlySuiteMember(repo, suite)) {
32984
+ return { ok: true, label: "DEPLOY#rc row exception", detail: BUNDLE_ONLY_RC_EXCEPTION };
32985
+ }
32966
32986
  const ok = factsRead && facts?.stages?.[stage]?.present === true;
32967
32987
  return {
32968
32988
  ok,
@@ -33023,7 +33043,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33023
33043
  }
33024
33044
  let environmentDrift2 = [];
33025
33045
  try {
33026
- environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client);
33046
+ environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client, deps.projectMeta?.suite);
33027
33047
  } catch (e) {
33028
33048
  checks.push({
33029
33049
  ok: false,
@@ -33038,7 +33058,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33038
33058
  detail: drift.problems.length ? drift.problems.join("; ") : drift.notes.length ? `advisory: ${drift.notes.join("; ")}` : void 0
33039
33059
  });
33040
33060
  }
33041
- for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full")).filter((entry) => entry.exception)) {
33061
+ for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full"), deps.projectMeta?.suite).filter((entry) => entry.exception)) {
33042
33062
  checks.push({ ok: true, label: `repository environment exception: ${spec.name}`, detail: spec.exception });
33043
33063
  }
33044
33064
  const strayTrainBranches = TRAIN_BRANCHES2.filter((b) => !branchesWanted.includes(b) && branchNames.has(b));
@@ -33112,7 +33132,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33112
33132
  deps.deployModel,
33113
33133
  effectiveTrack,
33114
33134
  deps.deployFacts,
33115
- deps.deployFactsRead === true
33135
+ deps.deployFactsRead === true,
33136
+ deps.projectMeta?.suite
33116
33137
  ));
33117
33138
  const readme = await contentText(deps, repo, baseBranch, "README.md");
33118
33139
  checks.push({
@@ -33707,6 +33728,7 @@ function registerBootstrapCommands(program3) {
33707
33728
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(meta, cls), repo),
33708
33729
  requiredChecks: meta?.requiredChecks ?? void 0,
33709
33730
  deployModel: meta?.deployModel,
33731
+ suite: meta?.suite,
33710
33732
  owners: controlOwners,
33711
33733
  github,
33712
33734
  deployFacts,
@@ -33773,31 +33795,39 @@ function registerBootstrapCommands(program3) {
33773
33795
  return fail(`bootstrap apply: ${e.message}`);
33774
33796
  }
33775
33797
  let effectiveTrack = bootstrapReleaseTrack;
33798
+ let suiteSlug;
33776
33799
  if (!o.releaseTrack) {
33777
33800
  const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
33778
33801
  if (read.ok) {
33779
33802
  effectiveTrack = resolveReleaseTrack(metaWithDeclaredClass(read.project, o.class), void 0, repo);
33803
+ suiteSlug = read.project?.suite;
33780
33804
  } else if (o.execute) {
33781
33805
  return failGraceful(`bootstrap apply: registry META unreadable (${read.error}) and no --release-track given \u2014 refusing to assume the ${bootstrapReleaseTrack} track for live writes`);
33782
33806
  } else {
33783
33807
  console.error(`bootstrap apply: registry META unreadable (${read.error}) \u2014 release track ${bootstrapReleaseTrack} is ASSUMED from the class default for this dry-run; --execute will refuse`);
33784
33808
  }
33809
+ } else {
33810
+ const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
33811
+ if (read.ok) suiteSlug = read.project?.suite;
33785
33812
  }
33786
33813
  if (o.only.trim() === "environments") {
33787
33814
  const client = defaultGitHubClient();
33788
33815
  const envJson = { repo, track: effectiveTrack };
33789
33816
  if (!o.execute) {
33790
- const drift = await verifyRepoEnvironments(repo, effectiveTrack, client);
33817
+ const drift = await verifyRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
33791
33818
  const classified = drift.map(classifyEnvironmentDrift);
33792
33819
  envJson.plan = classified;
33793
33820
  if (o.json) console.log(JSON.stringify(envJson, null, 2));
33794
33821
  else {
33795
33822
  console.log(`bootstrap apply --only environments \u2014 dry-run (${repo}, track ${effectiveTrack}); no writes:`);
33796
33823
  for (const c of classified) console.log(` ${c.action} ${c.environment} \u2014 ${c.reason}`);
33824
+ for (const spec of repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug)) {
33825
+ if (spec.exception) console.log(` OK environment ${spec.name} (${spec.exception})`);
33826
+ }
33797
33827
  console.log(" (plan-gated protections \u2014 wait_timer/reviewers the org plan rejects \u2014 are reported at --execute)");
33798
33828
  }
33799
33829
  } else {
33800
- const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client);
33830
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
33801
33831
  const lines2 = [];
33802
33832
  if (envResult.created.length) lines2.push(`environments created: ${envResult.created.join(", ")}`);
33803
33833
  if (envResult.updated.length) lines2.push(`environments updated: ${envResult.updated.join(", ")}`);
@@ -33805,6 +33835,9 @@ function registerBootstrapCommands(program3) {
33805
33835
  for (const gated of envResult.planGated) {
33806
33836
  lines2.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
33807
33837
  }
33838
+ for (const spec of repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug)) {
33839
+ if (spec.exception) lines2.push(`OK environment ${spec.name} (${spec.exception})`);
33840
+ }
33808
33841
  envJson.result = { created: envResult.created, updated: envResult.updated, skipped: envResult.skipped, planGated: envResult.planGated };
33809
33842
  if (o.json) console.log(JSON.stringify(envJson, null, 2));
33810
33843
  else {
@@ -33926,6 +33959,7 @@ function registerBootstrapCommands(program3) {
33926
33959
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
33927
33960
  requiredChecks: registryMeta?.requiredChecks ?? void 0,
33928
33961
  deployModel: applyDeployModel,
33962
+ suite: suiteSlug,
33929
33963
  owners: controlOwners,
33930
33964
  github,
33931
33965
  deployFacts: controlDeployFacts,
@@ -34270,7 +34304,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34270
34304
  }
34271
34305
  if (o.execute && !onlyTarget) {
34272
34306
  try {
34273
- const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient());
34307
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient(), suiteSlug);
34274
34308
  if (envResult.created.length) applied.push(`environments created: ${envResult.created.join(", ")}`);
34275
34309
  if (envResult.updated.length) applied.push(`environments updated: ${envResult.updated.join(", ")}`);
34276
34310
  if (envResult.skipped.length) applied.push(`environments current: ${envResult.skipped.join(", ")}`);
@@ -34367,6 +34401,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34367
34401
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
34368
34402
  requiredChecks: registryMeta?.requiredChecks ?? void 0,
34369
34403
  deployModel: applyDeployModel,
34404
+ suite: suiteSlug,
34370
34405
  owners: controlOwners,
34371
34406
  github,
34372
34407
  deployFacts: controlDeployFacts,
@@ -34410,7 +34445,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34410
34445
  const liveLabels = JSON.parse((await gh2(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"])).stdout);
34411
34446
  if (!Array.isArray(liveLabels)) throw new Error("label list is not an array");
34412
34447
  ancillaryPlan = planBootstrapLabels(manifest.labels, liveLabels);
34413
- environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient)).map(classifyEnvironmentDrift);
34448
+ environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient, suiteSlug)).map(classifyEnvironmentDrift);
34414
34449
  } catch (e) {
34415
34450
  return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
34416
34451
  }
@@ -34421,7 +34456,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34421
34456
  if (controlRows.length) console.log(`
34422
34457
  ${renderBootstrapControlPlan(controlRows)}`);
34423
34458
  if (ancillaryPlan && environmentPlan) console.log(`
34424
- ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan)}`);
34459
+ ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan, repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug).filter((spec) => spec.exception).map((spec) => ({ name: spec.name, detail: spec.exception })))}`);
34425
34460
  if (o.execute) console.log(`
34426
34461
  LIVE apply to ${repo}:
34427
34462
  ${applied.join("\n ")}`);
@@ -35086,13 +35121,15 @@ var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
35086
35121
  function suitePins(record, bundles) {
35087
35122
  return record.members.map((m) => m.carried ? { module: m.id, rc: m.rcPin, main: m.mainPin } : {
35088
35123
  module: m.id,
35089
- rc: `https://${m.id === "katip" ? m.rcHost ?? m.host : m.host}/${m.id === "katip" ? "" : "rc/"}module.js?v=${m.tag}`,
35124
+ rc: `https://${m.host}/rc/module.js?v=${m.tag}`,
35090
35125
  main: mainPinFor(m.host, bundles[m.id], suiteReleaseTag(m.tag))
35091
35126
  });
35092
35127
  }
35093
35128
  function mainPinFor(host, bundles, tag) {
35094
- const path2 = [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
35095
- return `https://${host}${path2}?v=${tag}`;
35129
+ return `https://${host}${mainBundlePath(bundles)}?v=${tag}`;
35130
+ }
35131
+ function mainBundlePath(bundles) {
35132
+ return [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
35096
35133
  }
35097
35134
  function pathOf(url) {
35098
35135
  try {
@@ -35120,7 +35157,7 @@ function readSuiteCut(path2) {
35120
35157
  } catch {
35121
35158
  throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
35122
35159
  }
35123
- if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent"].includes(record.announcement) || !record.leadReleased) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
35160
+ if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
35124
35161
  throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
35125
35162
  return record;
35126
35163
  }
@@ -35176,13 +35213,13 @@ async function suiteRcand(deps, opts) {
35176
35213
  const members = suiteMembers(await deps.registry());
35177
35214
  const modules = members.filter((m) => m.repo !== SUITE_REPO);
35178
35215
  const plan = [
35179
- ...modules.map((m) => `${m.repo}: normal rcand${m.id === "katip" ? " + rc server" : " + module-rc-publish.yml"} (once)`),
35216
+ ...modules.map((m) => `${m.repo}: normal rcand + module-rc-publish.yml (once)`),
35180
35217
  `${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
35181
35218
  `${SUITE_REPO}: rcand`
35182
35219
  ];
35183
35220
  if (opts.dryRun) return { plan };
35184
35221
  let record = readSuiteCut(deps.recordPath);
35185
- if (record?.announcement === "sent" && record.leadReleased) {
35222
+ if ((record?.announcement === "sent" || record?.announcement === "skipped") && record.leadReleased) {
35186
35223
  const archive = `${deps.recordPath}.${record.leadRcTag}.json`;
35187
35224
  if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`suite cut archive already exists at ${archive}; inspect before a new cut`);
35188
35225
  (0, import_node_fs29.renameSync)(deps.recordPath, archive);
@@ -35200,7 +35237,7 @@ async function suiteRcand(deps, opts) {
35200
35237
  const [backend, development, rcHead] = await Promise.all([deps.backend(m), deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
35201
35238
  const previousRc = rcHead ?? await deps.head(m.repo, "main");
35202
35239
  if (!backend.version || !backend.host || !development || !SHA2.test(development) || !previousRc || !SHA2.test(previousRc)) throw new Error(`${m.repo}: live backend or branch snapshot is unproven; no cut started`);
35203
- return { repo: m.repo, id: m.id, backend: backend.version, host: backend.host, rcHost: backend.rcHost, development, previousRc, ...rcHead === null ? { rcWasAbsent: true } : {} };
35240
+ return { repo: m.repo, id: m.id, backend: backend.version, host: backend.host, development, previousRc, ...rcHead === null ? { rcWasAbsent: true } : {} };
35204
35241
  }))
35205
35242
  ]);
35206
35243
  if (!leadDevelopment || !leadPreviousRc || !SHA2.test(leadDevelopment) || !SHA2.test(leadPreviousRc)) throw new Error("MM-Work development/rc SHA unproven; no cut started");
@@ -35233,7 +35270,7 @@ async function suiteRcand(deps, opts) {
35233
35270
  }
35234
35271
  const refusals = [];
35235
35272
  for (const member of record.members) {
35236
- if (member.published || member.id === "katip") continue;
35273
+ if (member.published) continue;
35237
35274
  try {
35238
35275
  await deps.preflight(member.repo);
35239
35276
  } catch (e) {
@@ -35251,7 +35288,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35251
35288
  if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
35252
35289
  throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
35253
35290
  if (member.carried) {
35254
- if (member.id !== "katip") await deps.publish(member.repo, pinVersion(member.rcPin));
35291
+ await deps.publish(member.repo, pinVersion(member.rcPin));
35255
35292
  continue;
35256
35293
  }
35257
35294
  if (!member.tag) {
@@ -35264,7 +35301,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35264
35301
  }
35265
35302
  if ((await deps.tagSha(member.repo, member.tag))?.toLowerCase() !== member.sha.toLowerCase()) throw new Error(`${member.repo}: rc tag ${member.tag} moved or is unreadable; stop`);
35266
35303
  if (!member.published) {
35267
- if (member.id !== "katip") await deps.publish(member.repo, member.tag);
35304
+ await deps.publish(member.repo, member.tag);
35268
35305
  member.published = true;
35269
35306
  writeSuiteCut(deps.recordPath, record);
35270
35307
  }
@@ -35323,43 +35360,59 @@ async function suiteRelease(deps, ownerGo) {
35323
35360
  if (hotfix) member.hotfixedTo = hotfix;
35324
35361
  }
35325
35362
  if (member.hotfixedTo && !member.released) continue;
35326
- const katipReleaseSha = member.id === "katip" ? await deps.tagSha(member.repo, planned) : void 0;
35327
- const expectedBackend = member.id === "katip" && member.released ? member.backendAfterRelease ?? katipReleaseSha : member.backend;
35328
- if (live.version !== expectedBackend && !(member.id === "katip" && !member.released && katipReleaseSha && live.version === katipReleaseSha))
35329
- throw new Error(`${member.repo}: live backend changed after the cut (${expectedBackend} \u2192 ${live.version}); release refused`);
35363
+ if (live.version !== member.backend && !(member.hotfixedTo && await deps.hotfixOver(member.repo, planned, live.version) === member.hotfixedTo))
35364
+ throw new Error(`${member.repo}: live backend changed after the cut (${member.backend} \u2192 ${live.version}); release refused`);
35330
35365
  }
35331
35366
  if ((await deps.tagSha(SUITE_REPO, record.leadRcTag))?.toLowerCase() !== record.leadRcSha?.toLowerCase()) throw new Error("MM-Work rc tag changed after the cut; release refused");
35367
+ const publishesMain = !record.leadReleased;
35368
+ const refusals = [];
35369
+ for (const member of record.members) {
35370
+ if (!publishesMain || member.carried || member.released) continue;
35371
+ try {
35372
+ await deps.preflightMain(member.repo, mainBundlePath(members.find((m) => m.repo === member.repo)?.bundles));
35373
+ } catch (e) {
35374
+ refusals.push(`${member.repo}: ${e.message}`);
35375
+ }
35376
+ }
35377
+ if (refusals.length) throw new Error(`module main serving is not ready; no member was released:
35378
+ ${refusals.join("\n")}
35379
+ Each member's live release must serve <data>/main/module.js at its main pin path when <data>/main exists.`);
35332
35380
  for (const member of record.members) {
35333
- if (member.released || member.carried) continue;
35334
- if (member.hotfixedTo) {
35335
- console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
35336
- member.backendAfterRelease = (await deps.backend({ repo: member.repo, id: member.id, bundles: {} })).version;
35381
+ if (member.carried) continue;
35382
+ if (!member.released) {
35383
+ if (member.hotfixedTo) {
35384
+ console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
35385
+ } else {
35386
+ if (member.releasedAs && member.releasedAs !== suiteReleaseTag(member.tag)) console.warn(`${member.repo}: its rc already shipped as ${member.releasedAs}, not ${suiteReleaseTag(member.tag)}; finishing that release (#7464)`);
35387
+ await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha);
35388
+ }
35337
35389
  member.released = true;
35338
35390
  writeSuiteCut(deps.recordPath, record);
35339
- continue;
35340
35391
  }
35341
- if (member.releasedAs && member.releasedAs !== suiteReleaseTag(member.tag)) console.warn(`${member.repo}: its rc already shipped as ${member.releasedAs}, not ${suiteReleaseTag(member.tag)}; finishing that release (#7464)`);
35342
- await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha);
35343
- if (member.id === "katip") {
35344
- const expected = await deps.tagSha(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag));
35345
- const live = await deps.backend({ repo: member.repo, id: member.id, bundles: {} });
35346
- if (!expected || live.version !== expected) throw new Error(`${member.repo}: normal main deploy not stamped at released tag SHA; resume after verifying live backend`);
35347
- member.backendAfterRelease = live.version;
35392
+ if (publishesMain && !member.mainPublished) {
35393
+ await deps.publishMain(member.repo, member.hotfixedTo ?? member.releasedAs ?? suiteReleaseTag(member.tag), mainBundlePath(members.find((m) => m.repo === member.repo)?.bundles));
35394
+ member.mainPublished = true;
35395
+ writeSuiteCut(deps.recordPath, record);
35348
35396
  }
35349
- member.released = true;
35350
- writeSuiteCut(deps.recordPath, record);
35351
35397
  }
35352
35398
  if (!record.leadReleased) {
35353
35399
  await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha);
35354
35400
  record.leadReleased = true;
35355
35401
  writeSuiteCut(deps.recordPath, record);
35356
35402
  }
35357
- if (record.announcement === "pending") throw new Error("suite announcement outcome ambiguous; verify it before retrying (never post twice)");
35403
+ if (record.announcement === "pending") {
35404
+ const reason = await deps.announceSkipReason(record);
35405
+ if (!reason) throw new Error("suite announcement outcome ambiguous; verify it before retrying (never post twice)");
35406
+ record.announcement = "skipped";
35407
+ record.announcementNote = reason;
35408
+ writeSuiteCut(deps.recordPath, record);
35409
+ }
35358
35410
  if (!record.announcement) {
35359
35411
  record.announcement = "pending";
35360
35412
  writeSuiteCut(deps.recordPath, record);
35361
- await deps.announce(record);
35362
- record.announcement = "sent";
35413
+ const { skipped } = await deps.announce(record);
35414
+ record.announcement = skipped ? "skipped" : "sent";
35415
+ if (skipped) record.announcementNote = skipped;
35363
35416
  writeSuiteCut(deps.recordPath, record);
35364
35417
  }
35365
35418
  return record;
@@ -45331,10 +45384,7 @@ function suiteTrainIo(root, summaryFile) {
45331
45384
  throw new Error(`${member.repo}: healthy live backend stamp and edgeDomains.main must agree with DEPLOY#main; stop`);
45332
45385
  const health = await probePublicHealth(url);
45333
45386
  if (!health.ok) throw new Error(`${member.repo}: live backend health could not be proved at ${url}; stop`);
45334
- const rcHost = edgeDomains?.rc;
45335
- if (member.id === "katip" && (!rcHost || facts?.stages.rc?.domain !== rcHost))
45336
- throw new Error(`${member.repo}: normal Katip rc host must agree with DEPLOY#rc; stop`);
45337
- return { version: live.deployedTag, host, ...rcHost ? { rcHost } : {} };
45387
+ return { version: live.deployedTag, host };
45338
45388
  };
45339
45389
  const tagSha = async (repo, tag) => {
45340
45390
  try {
@@ -45449,10 +45499,22 @@ function suiteTrainIo(root, summaryFile) {
45449
45499
  const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`], existing[0]);
45450
45500
  if (run.conclusion !== "success") throw new Error(`${repo}: module rc publish run ${run.id} not green${await rcRunCause(run.id)}`);
45451
45501
  };
45502
+ const publishMain = async (repo, tag, path2) => {
45503
+ const title = `module-main-publish ${repo} @ ${tag}`;
45504
+ const existing = (await rcRuns()).filter((r) => r.displayTitle === title).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
45505
+ if (existing[0]?.conclusion === "success") return;
45506
+ if (existing[0]?.conclusion === "failure") throw new Error(`${repo}: prior module-main-publish ${tag} failed; inspect the run and retry it explicitly before continuing`);
45507
+ const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`, "channel=main", `path=${path2}`], existing[0]);
45508
+ if (run.conclusion !== "success") throw new Error(`${repo}: module main publish run ${run.id} not green${await rcRunCause(run.id)}`);
45509
+ };
45452
45510
  const preflight2 = async (repo) => {
45453
45511
  const run = await watchRcRun(`module-rc-preflight ${repo}`, [`repo=${repo}`, "preflight=true"], void 0);
45454
45512
  if (run.conclusion !== "success") throw new Error(`rc serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
45455
45513
  };
45514
+ const preflightMain = async (repo, path2) => {
45515
+ const run = await watchRcRun(`module-main-preflight ${repo}`, [`repo=${repo}`, "channel=main", `path=${path2}`, "preflight=true"], void 0);
45516
+ if (run.conclusion !== "success") throw new Error(`main serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
45517
+ };
45456
45518
  const rcRunCause = async (id) => {
45457
45519
  try {
45458
45520
  return rcRunCauseFromLog(await exec("gh", ["run", "view", String(id), "--repo", "mutmutco/MMI-Hub", "--log-failed"]));
@@ -45493,6 +45555,8 @@ function suiteTrainIo(root, summaryFile) {
45493
45555
  rcand: runRcand,
45494
45556
  publish,
45495
45557
  preflight: preflight2,
45558
+ publishMain,
45559
+ preflightMain,
45496
45560
  pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
45497
45561
  verifyLeadCut: async (sha) => {
45498
45562
  const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
@@ -45516,7 +45580,15 @@ function suiteTrainIo(root, summaryFile) {
45516
45580
  announce: async (record) => {
45517
45581
  if (!summaryFile) throw new Error("suite announcement requires --announce-summary-file; outcome was not posted");
45518
45582
  const result = await trainApplyDeps().announce?.({ repo: SUITE_REPO, tag: record.leadRcTag.replace(/-rc\.\d+$/, ""), summaryFile });
45583
+ if (result?.status === "skipped") return { skipped: result.note };
45519
45584
  if (result?.status !== "announced") throw new Error(`suite announcement unverified: ${result?.note ?? "no announcement seam"}`);
45585
+ return {};
45586
+ },
45587
+ announceSkipReason: async () => {
45588
+ const slug = slugOf(SUITE_REPO);
45589
+ const read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
45590
+ if (!read.ok) throw new Error(`suite announcement: registry read failed for ${slug} (${read.error}); outcome stays ambiguous`);
45591
+ return read.project?.releaseChannel?.trim() ? null : `no releaseChannel configured for ${slug}; nothing could have been posted`;
45520
45592
  }
45521
45593
  };
45522
45594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.57",
3
+ "version": "4.5.58",
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",