@mutmutco/cli 4.5.56 → 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 +182 -75
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -7130,12 +7130,37 @@ function extractOnBlock(yaml) {
7130
7130
  }
7131
7131
  return block.length ? block.join("\n") : null;
7132
7132
  }
7133
- function collectPullRequestWorkflowContexts(workflows) {
7133
+ function jobUsesWorkflow(job) {
7134
+ const match = job.body.match(/^\s+uses:\s*['"]?([^'"\s#]*\.github\/workflows\/[^'"\s#@]+\.ya?ml(?:@[^'"\s#]+)?)['"]?\s*(?:#.*)?$/m);
7135
+ return match ? match[1] : null;
7136
+ }
7137
+ function reusableWorkflowRefs(workflows) {
7138
+ const refs = /* @__PURE__ */ new Set();
7139
+ for (const wf of workflows) {
7140
+ if (!workflowTriggersPullRequest(wf.body)) continue;
7141
+ for (const job of parseWorkflowJobs(wf.body)) {
7142
+ const uses = jobUsesWorkflow(job);
7143
+ if (uses && jobReportsPullRequestCheck(job)) refs.add(uses);
7144
+ }
7145
+ }
7146
+ return [...refs];
7147
+ }
7148
+ function parseReusableWorkflowRef(uses) {
7149
+ const local = uses.match(/^\.\/(\.github\/workflows\/[^@]+)$/);
7150
+ if (local) return { path: local[1] };
7151
+ const remote = uses.match(/^([^/]+\/[^/]+)\/(\.github\/workflows\/[^@]+)@(.+)$/);
7152
+ return remote ? { repo: remote[1], path: remote[2], ref: remote[3] } : null;
7153
+ }
7154
+ function collectPullRequestWorkflowContexts(workflows, called = {}) {
7134
7155
  const contexts = /* @__PURE__ */ new Set();
7135
7156
  for (const wf of workflows) {
7136
7157
  if (!workflowTriggersPullRequest(wf.body)) continue;
7137
7158
  for (const job of parseWorkflowJobs(wf.body)) {
7138
- if (jobReportsPullRequestCheck(job)) contexts.add(job.id);
7159
+ if (!jobReportsPullRequestCheck(job)) continue;
7160
+ const uses = jobUsesWorkflow(job);
7161
+ const body = uses ? called[uses] : void 0;
7162
+ if (body) for (const inner of parseWorkflowJobs(body)) contexts.add(`${job.id} / ${inner.id}`);
7163
+ else contexts.add(job.id);
7139
7164
  }
7140
7165
  }
7141
7166
  return [...contexts].sort((a, b) => a.localeCompare(b));
@@ -15672,11 +15697,12 @@ function planBootstrapLabels(labels, live) {
15672
15697
  ...labelsToPrune(labels.map((label) => label.name)).filter((name) => existing.has(name)).map((name) => ({ name, action: "DELETE" }))
15673
15698
  ];
15674
15699
  }
15675
- function renderBootstrapAncillaryPlan(labels, environments) {
15700
+ function renderBootstrapAncillaryPlan(labels, environments, exceptions = []) {
15676
15701
  return [
15677
15702
  "bootstrap apply \u2014 labels and environments (dry-run; no mutations):",
15678
15703
  ...labels.map(({ action, name }) => ` ${action} label ${name}`),
15679
- ...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})`)
15680
15706
  ].join("\n");
15681
15707
  }
15682
15708
  function resolveSeedContent(seed, vars, readFile9) {
@@ -17044,10 +17070,10 @@ var rollout_plan_default = {
17044
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)."
17045
17071
  },
17046
17072
  baseline: {
17047
- version: "4.5.56",
17048
- tag: "v4.5.56",
17049
- commit: "7502269d51cd",
17050
- npm: "@mutmutco/cli@4.5.56"
17073
+ version: "4.5.58",
17074
+ tag: "v4.5.58",
17075
+ commit: "05d087d1fcb7",
17076
+ npm: "@mutmutco/cli@4.5.58"
17051
17077
  },
17052
17078
  exitCriterion: "fleet-n-of-n",
17053
17079
  hubOnlyShortcut: "forbidden",
@@ -17064,14 +17090,14 @@ var rollout_plan_default = {
17064
17090
  repo: "mutmutco/mmi-hub",
17065
17091
  role: "canary",
17066
17092
  schedule: "train",
17067
- v3Target: "v4.5.56"
17093
+ v3Target: "v4.5.58"
17068
17094
  }
17069
17095
  ],
17070
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.",
17071
17097
  rollback: {
17072
17098
  independent: true,
17073
- mechanism: "npm dist-tag latest -> 4.5.56 and redeploy the Hub Lambda from tag v4.5.56 (7502269d51cd); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17074
- v3Target: "v4.5.56 (@mutmutco/cli@4.5.56, tag commit 7502269d51cd \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)"
17075
17101
  }
17076
17102
  },
17077
17103
  {
@@ -20069,9 +20095,14 @@ function validSuiteCutContext(repo, context) {
20069
20095
  function authorisedSuiteCut(repo, meta, context) {
20070
20096
  return validSuiteCutContext(repo, context) && !!meta?.suite && context.lead.toLowerCase() === suiteLeadRepo(repo, meta.suite).toLowerCase();
20071
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
+ }
20072
20103
  function suiteMemberRefusal(repo, meta, lane, solo = false, cut) {
20073
20104
  const suite = meta?.suite;
20074
- if (!suite || repo.split("/")[1]?.toLowerCase() === suite.toLowerCase()) return null;
20105
+ if (!suite || !isBundleOnlySuiteMember(repo, suite)) return null;
20075
20106
  if (authorisedSuiteCut(repo, meta, cut)) return null;
20076
20107
  if (lane !== "rcand" && solo) return null;
20077
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.`;
@@ -20676,7 +20707,7 @@ async function appendServedChannelLegs(deps, input, dispatch) {
20676
20707
  const publishStatus = input.publish?.state === "complete" ? "success" : "failure";
20677
20708
  const prepared = await (input.prepared ?? prepareServedChannel(deps, input));
20678
20709
  if (!isHubControlRepo(input.repo)) {
20679
- const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut) && input.repo.split("/")[1]?.toLowerCase() !== "mmi-katip";
20710
+ const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut);
20680
20711
  const [home2, proof2] = await Promise.all([
20681
20712
  appendInstallHomePublish(deps, { repo: input.repo, installer: input.installer, publishStatus, watch: input.watch }, dispatch),
20682
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)
@@ -20946,7 +20977,7 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
20946
20977
  return extra;
20947
20978
  }
20948
20979
  async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
20949
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip" && stage !== "dev") {
20980
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut) && stage !== "dev") {
20950
20981
  return { note: "suite bundle-only: live backend not deployed", deployStatus: "success" };
20951
20982
  }
20952
20983
  if (isCentralDispatchModel(model)) {
@@ -21073,7 +21104,7 @@ async function attemptRetire(deps, ctx) {
21073
21104
  return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
21074
21105
  }
21075
21106
  async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
21076
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip")
21107
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut))
21077
21108
  return { status: "not-applicable", note: "bundle-only suite member has no rc runtime to retire" };
21078
21109
  if (model !== "tenant-container") {
21079
21110
  return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
@@ -24693,7 +24724,7 @@ async function runTrainDoctor(input) {
24693
24724
  }
24694
24725
  }
24695
24726
  let preflightTimeouts = 0;
24696
- if (!(repo.split("/")[1]?.toLowerCase() !== "mmi-katip" && authorisedSuiteCut(repo, meta, input.suiteCut))) for (; ; ) {
24727
+ if (!authorisedSuiteCut(repo, meta, input.suiteCut)) for (; ; ) {
24697
24728
  try {
24698
24729
  await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
24699
24730
  break;
@@ -25136,7 +25167,7 @@ async function preflight(deps, ctx, stage, meta, lane) {
25136
25167
  if (model === "none") {
25137
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`);
25138
25169
  }
25139
- if (!(ctx.slug !== "mmi-katip" && authorisedSuiteCut(ctx.repo, meta, deps.suiteCut)))
25170
+ if (!authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
25140
25171
  await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
25141
25172
  await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
25142
25173
  await assertActionsJobsCanStart(deps, ctx.repo);
@@ -25735,7 +25766,7 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
25735
25766
  );
25736
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;
25737
25768
  const unverifiable = !isCentralDispatchModel(deployModel) && dispatch.deployStatus === "pending" && dispatch.runId == null && !dispatch.workflowRuns?.length;
25738
- 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 });
25739
25770
  await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployEntry), { ...opts, landed: "the immutable rc tag, the green wall, and origin/rc" });
25740
25771
  await recordPhase(ledger, deps, anchors, "publish", phaseEntry({ state: "skipped", sha, note: "rcand publishes nothing \u2014 the publish leg rides the release" }), { ...opts, landed });
25741
25772
  await recordPhase(ledger, deps, anchors, "alignment", phaseEntry({ state: "skipped", sha, note: "rcand aligns no branch \u2014 alignment rides the release" }), { ...opts, landed });
@@ -25898,7 +25929,7 @@ ${recovery.note}`), recoveryInput);
25898
25929
  }
25899
25930
  );
25900
25931
  const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch), versionEnv);
25901
- 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 }));
25902
25933
  await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
25903
25934
  landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
25904
25935
  });
@@ -26321,7 +26352,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
26321
26352
  dispatch2.deployStatus = "success";
26322
26353
  dispatch2.note = `historical ${deployModel} deploy/publish status: success (re-verified by the recorded run)`;
26323
26354
  }
26324
- if (validSuiteCutContext(ctx.repo, deps.suiteCut) && ctx.slug !== "mmi-katip") {
26355
+ if (validSuiteCutContext(ctx.repo, deps.suiteCut)) {
26325
26356
  phaseInputs.deploy = { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" };
26326
26357
  if (dispatch2.deployStatus !== "failure" && (phaseInputs.publish.state === "complete" || phaseInputs.publish.state === "skipped")) {
26327
26358
  dispatch2.deployStatus = "success";
@@ -26456,7 +26487,7 @@ ${recovery.note}`), recoveryInput);
26456
26487
  );
26457
26488
  const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch), versionEnv);
26458
26489
  steps.push(`dispatched the ${deployModel} deploy path`);
26459
- 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 }));
26460
26491
  if (persisted?.phases.deploy.superseded !== true) {
26461
26492
  await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
26462
26493
  strict: ledgerMode === "strict",
@@ -27279,7 +27310,17 @@ function gateWorkflowFiles(prWorkflowPaths) {
27279
27310
  }
27280
27311
  async function resolveEmittedPrContexts(deps, repo, branch) {
27281
27312
  const workflows = await resolveBranchWorkflows(deps, repo, branch);
27282
- return workflows === void 0 ? void 0 : collectPullRequestWorkflowContexts(workflows);
27313
+ return workflows === void 0 ? void 0 : collectPullRequestWorkflowContexts(workflows, await resolveCalledWorkflows(deps, repo, branch, workflows));
27314
+ }
27315
+ async function resolveCalledWorkflows(deps, repo, branch, workflows) {
27316
+ const called = {};
27317
+ for (const uses of reusableWorkflowRefs(workflows)) {
27318
+ const ref = parseReusableWorkflowRef(uses);
27319
+ if (!ref) continue;
27320
+ const body = await fetchFileContent(deps, ref.repo ?? repo, ref.ref ?? branch, ref.path);
27321
+ if (body) called[uses] = body;
27322
+ }
27323
+ return called;
27283
27324
  }
27284
27325
  async function resolveBranchWorkflows(deps, repo, branch) {
27285
27326
  const paths = await listWorkflowPaths(deps, repo, branch);
@@ -27527,7 +27568,7 @@ async function auditRepoCi(repo, deps) {
27527
27568
  };
27528
27569
  const getEmittedPrContexts = async () => {
27529
27570
  const workflows = await getBranchWorkflows();
27530
- return workflows === void 0 ? void 0 : collectPullRequestWorkflowContexts(workflows);
27571
+ return workflows === void 0 ? void 0 : collectPullRequestWorkflowContexts(workflows, await resolveCalledWorkflows(deps, repo, baseBranch, workflows));
27531
27572
  };
27532
27573
  const committedReferenceRaw = deployableGated ? await fetchFileContent(deps, repo, baseBranch, PRODUCT_RULESET_REF) : null;
27533
27574
  let authoritativeRuleset = null;
@@ -32321,11 +32362,12 @@ function environmentNamesForTrack(track) {
32321
32362
  const branches = new Set(branchesForTrack(track));
32322
32363
  return REPO_ENVIRONMENTS.filter((environment) => branches.has(ENVIRONMENT_BRANCHES[environment]));
32323
32364
  }
32324
- function repoEnvironmentSpecs(_repo, releaseTrack = "full") {
32365
+ function repoEnvironmentSpecs(repo, releaseTrack = "full", suite) {
32325
32366
  const included = new Set(environmentNamesForTrack(releaseTrack));
32326
32367
  const trackExceptions = releaseTrack === "full" ? void 0 : TRACK_ENVIRONMENT_EXCEPTIONS[releaseTrack];
32368
+ const bundleOnlyRc = isBundleOnlySuiteMember(repo, suite);
32327
32369
  return REPO_ENVIRONMENTS.map((name) => {
32328
- 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;
32329
32371
  return {
32330
32372
  name,
32331
32373
  branch: ENVIRONMENT_BRANCHES[name],
@@ -32336,8 +32378,8 @@ function repoEnvironmentSpecs(_repo, releaseTrack = "full") {
32336
32378
  };
32337
32379
  });
32338
32380
  }
32339
- function provisionedEnvironmentSpecs(repo, releaseTrack = "full") {
32340
- 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);
32341
32383
  }
32342
32384
  function asEnvironmentList(value) {
32343
32385
  if (Array.isArray(value)) return value;
@@ -32377,10 +32419,10 @@ var PLAN_GATED_PROTECTION_RE = /billing plan/i;
32377
32419
  function isPlanGatedProtectionError(error) {
32378
32420
  return PLAN_GATED_PROTECTION_RE.test(String(error?.message ?? error));
32379
32421
  }
32380
- async function reconcileRepoEnvironments(repo, releaseTrack, client) {
32422
+ async function reconcileRepoEnvironments(repo, releaseTrack, client, suite) {
32381
32423
  const raw = await client.rest("GET", `repos/${repo}/environments`);
32382
32424
  const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
32383
- const planned = provisionedEnvironmentSpecs(repo, releaseTrack);
32425
+ const planned = provisionedEnvironmentSpecs(repo, releaseTrack, suite);
32384
32426
  const created = [];
32385
32427
  const updated = [];
32386
32428
  const skipped = [];
@@ -32413,10 +32455,10 @@ async function reconcileRepoEnvironments(repo, releaseTrack, client) {
32413
32455
  }
32414
32456
  return { repo, planned, created, updated, skipped, planGated };
32415
32457
  }
32416
- async function verifyRepoEnvironments(repo, releaseTrack, client) {
32458
+ async function verifyRepoEnvironments(repo, releaseTrack, client, suite) {
32417
32459
  const raw = await client.rest("GET", `repos/${repo}/environments`);
32418
32460
  const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
32419
- return provisionedEnvironmentSpecs(repo, releaseTrack).map((spec) => {
32461
+ return provisionedEnvironmentSpecs(repo, releaseTrack, suite).map((spec) => {
32420
32462
  const drift = environmentDrift(live.get(spec.name), spec);
32421
32463
  return { environment: spec.name, missing: !live.has(spec.name), problems: drift.problems, notes: drift.notes };
32422
32464
  });
@@ -32534,7 +32576,17 @@ function planBootstrapControlPlane(input) {
32534
32576
  }
32535
32577
  }
32536
32578
  const deployStages = deployStagesForBootstrap(input.deployModel, input.releaseTrack);
32579
+ const bundleOnlyRc = isBundleOnlySuiteMember(input.repo, input.suite);
32537
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
+ }
32538
32590
  const present = input.deployFacts?.stages?.[stage]?.present === true;
32539
32591
  const command = bootstrapSetDeployCommand(input.repo, stage, input.deployModel);
32540
32592
  rows.push(present && stage === "main" ? {
@@ -32924,10 +32976,13 @@ function centralContainerPortRangeCheck(deployModel, portRange, repo) {
32924
32976
  detail: ok ? void 0 : `${deployModel} needs PROJECT# META.portRange for mmi-cli stage \u2014 assign with: mmi-cli stage port-range ${repo}`
32925
32977
  };
32926
32978
  }
32927
- function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead) {
32979
+ function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead, suite) {
32928
32980
  const stages = deployStagesForBootstrap(deployModel, releaseTrack);
32929
32981
  if (!stages.length) return [];
32930
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
+ }
32931
32986
  const ok = factsRead && facts?.stages?.[stage]?.present === true;
32932
32987
  return {
32933
32988
  ok,
@@ -32988,7 +33043,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
32988
33043
  }
32989
33044
  let environmentDrift2 = [];
32990
33045
  try {
32991
- environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client);
33046
+ environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client, deps.projectMeta?.suite);
32992
33047
  } catch (e) {
32993
33048
  checks.push({
32994
33049
  ok: false,
@@ -33003,7 +33058,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33003
33058
  detail: drift.problems.length ? drift.problems.join("; ") : drift.notes.length ? `advisory: ${drift.notes.join("; ")}` : void 0
33004
33059
  });
33005
33060
  }
33006
- 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)) {
33007
33062
  checks.push({ ok: true, label: `repository environment exception: ${spec.name}`, detail: spec.exception });
33008
33063
  }
33009
33064
  const strayTrainBranches = TRAIN_BRANCHES2.filter((b) => !branchesWanted.includes(b) && branchNames.has(b));
@@ -33077,7 +33132,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
33077
33132
  deps.deployModel,
33078
33133
  effectiveTrack,
33079
33134
  deps.deployFacts,
33080
- deps.deployFactsRead === true
33135
+ deps.deployFactsRead === true,
33136
+ deps.projectMeta?.suite
33081
33137
  ));
33082
33138
  const readme = await contentText(deps, repo, baseBranch, "README.md");
33083
33139
  checks.push({
@@ -33672,6 +33728,7 @@ function registerBootstrapCommands(program3) {
33672
33728
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(meta, cls), repo),
33673
33729
  requiredChecks: meta?.requiredChecks ?? void 0,
33674
33730
  deployModel: meta?.deployModel,
33731
+ suite: meta?.suite,
33675
33732
  owners: controlOwners,
33676
33733
  github,
33677
33734
  deployFacts,
@@ -33738,31 +33795,39 @@ function registerBootstrapCommands(program3) {
33738
33795
  return fail(`bootstrap apply: ${e.message}`);
33739
33796
  }
33740
33797
  let effectiveTrack = bootstrapReleaseTrack;
33798
+ let suiteSlug;
33741
33799
  if (!o.releaseTrack) {
33742
33800
  const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
33743
33801
  if (read.ok) {
33744
33802
  effectiveTrack = resolveReleaseTrack(metaWithDeclaredClass(read.project, o.class), void 0, repo);
33803
+ suiteSlug = read.project?.suite;
33745
33804
  } else if (o.execute) {
33746
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`);
33747
33806
  } else {
33748
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`);
33749
33808
  }
33809
+ } else {
33810
+ const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
33811
+ if (read.ok) suiteSlug = read.project?.suite;
33750
33812
  }
33751
33813
  if (o.only.trim() === "environments") {
33752
33814
  const client = defaultGitHubClient();
33753
33815
  const envJson = { repo, track: effectiveTrack };
33754
33816
  if (!o.execute) {
33755
- const drift = await verifyRepoEnvironments(repo, effectiveTrack, client);
33817
+ const drift = await verifyRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
33756
33818
  const classified = drift.map(classifyEnvironmentDrift);
33757
33819
  envJson.plan = classified;
33758
33820
  if (o.json) console.log(JSON.stringify(envJson, null, 2));
33759
33821
  else {
33760
33822
  console.log(`bootstrap apply --only environments \u2014 dry-run (${repo}, track ${effectiveTrack}); no writes:`);
33761
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
+ }
33762
33827
  console.log(" (plan-gated protections \u2014 wait_timer/reviewers the org plan rejects \u2014 are reported at --execute)");
33763
33828
  }
33764
33829
  } else {
33765
- const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client);
33830
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
33766
33831
  const lines2 = [];
33767
33832
  if (envResult.created.length) lines2.push(`environments created: ${envResult.created.join(", ")}`);
33768
33833
  if (envResult.updated.length) lines2.push(`environments updated: ${envResult.updated.join(", ")}`);
@@ -33770,6 +33835,9 @@ function registerBootstrapCommands(program3) {
33770
33835
  for (const gated of envResult.planGated) {
33771
33836
  lines2.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
33772
33837
  }
33838
+ for (const spec of repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug)) {
33839
+ if (spec.exception) lines2.push(`OK environment ${spec.name} (${spec.exception})`);
33840
+ }
33773
33841
  envJson.result = { created: envResult.created, updated: envResult.updated, skipped: envResult.skipped, planGated: envResult.planGated };
33774
33842
  if (o.json) console.log(JSON.stringify(envJson, null, 2));
33775
33843
  else {
@@ -33891,6 +33959,7 @@ function registerBootstrapCommands(program3) {
33891
33959
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
33892
33960
  requiredChecks: registryMeta?.requiredChecks ?? void 0,
33893
33961
  deployModel: applyDeployModel,
33962
+ suite: suiteSlug,
33894
33963
  owners: controlOwners,
33895
33964
  github,
33896
33965
  deployFacts: controlDeployFacts,
@@ -34235,7 +34304,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34235
34304
  }
34236
34305
  if (o.execute && !onlyTarget) {
34237
34306
  try {
34238
- const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient());
34307
+ const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient(), suiteSlug);
34239
34308
  if (envResult.created.length) applied.push(`environments created: ${envResult.created.join(", ")}`);
34240
34309
  if (envResult.updated.length) applied.push(`environments updated: ${envResult.updated.join(", ")}`);
34241
34310
  if (envResult.skipped.length) applied.push(`environments current: ${envResult.skipped.join(", ")}`);
@@ -34332,6 +34401,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34332
34401
  requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
34333
34402
  requiredChecks: registryMeta?.requiredChecks ?? void 0,
34334
34403
  deployModel: applyDeployModel,
34404
+ suite: suiteSlug,
34335
34405
  owners: controlOwners,
34336
34406
  github,
34337
34407
  deployFacts: controlDeployFacts,
@@ -34375,7 +34445,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34375
34445
  const liveLabels = JSON.parse((await gh2(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"])).stdout);
34376
34446
  if (!Array.isArray(liveLabels)) throw new Error("label list is not an array");
34377
34447
  ancillaryPlan = planBootstrapLabels(manifest.labels, liveLabels);
34378
- environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient)).map(classifyEnvironmentDrift);
34448
+ environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient, suiteSlug)).map(classifyEnvironmentDrift);
34379
34449
  } catch (e) {
34380
34450
  return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
34381
34451
  }
@@ -34386,7 +34456,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
34386
34456
  if (controlRows.length) console.log(`
34387
34457
  ${renderBootstrapControlPlan(controlRows)}`);
34388
34458
  if (ancillaryPlan && environmentPlan) console.log(`
34389
- ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan)}`);
34459
+ ${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan, repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug).filter((spec) => spec.exception).map((spec) => ({ name: spec.name, detail: spec.exception })))}`);
34390
34460
  if (o.execute) console.log(`
34391
34461
  LIVE apply to ${repo}:
34392
34462
  ${applied.join("\n ")}`);
@@ -35051,13 +35121,15 @@ var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
35051
35121
  function suitePins(record, bundles) {
35052
35122
  return record.members.map((m) => m.carried ? { module: m.id, rc: m.rcPin, main: m.mainPin } : {
35053
35123
  module: m.id,
35054
- 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}`,
35055
35125
  main: mainPinFor(m.host, bundles[m.id], suiteReleaseTag(m.tag))
35056
35126
  });
35057
35127
  }
35058
35128
  function mainPinFor(host, bundles, tag) {
35059
- const path2 = [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
35060
- 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";
35061
35133
  }
35062
35134
  function pathOf(url) {
35063
35135
  try {
@@ -35085,7 +35157,7 @@ function readSuiteCut(path2) {
35085
35157
  } catch {
35086
35158
  throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
35087
35159
  }
35088
- 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)))
35089
35161
  throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
35090
35162
  return record;
35091
35163
  }
@@ -35141,13 +35213,13 @@ async function suiteRcand(deps, opts) {
35141
35213
  const members = suiteMembers(await deps.registry());
35142
35214
  const modules = members.filter((m) => m.repo !== SUITE_REPO);
35143
35215
  const plan = [
35144
- ...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)`),
35145
35217
  `${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
35146
35218
  `${SUITE_REPO}: rcand`
35147
35219
  ];
35148
35220
  if (opts.dryRun) return { plan };
35149
35221
  let record = readSuiteCut(deps.recordPath);
35150
- if (record?.announcement === "sent" && record.leadReleased) {
35222
+ if ((record?.announcement === "sent" || record?.announcement === "skipped") && record.leadReleased) {
35151
35223
  const archive = `${deps.recordPath}.${record.leadRcTag}.json`;
35152
35224
  if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`suite cut archive already exists at ${archive}; inspect before a new cut`);
35153
35225
  (0, import_node_fs29.renameSync)(deps.recordPath, archive);
@@ -35165,7 +35237,7 @@ async function suiteRcand(deps, opts) {
35165
35237
  const [backend, development, rcHead] = await Promise.all([deps.backend(m), deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
35166
35238
  const previousRc = rcHead ?? await deps.head(m.repo, "main");
35167
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`);
35168
- 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 } : {} };
35169
35241
  }))
35170
35242
  ]);
35171
35243
  if (!leadDevelopment || !leadPreviousRc || !SHA2.test(leadDevelopment) || !SHA2.test(leadPreviousRc)) throw new Error("MM-Work development/rc SHA unproven; no cut started");
@@ -35198,7 +35270,7 @@ async function suiteRcand(deps, opts) {
35198
35270
  }
35199
35271
  const refusals = [];
35200
35272
  for (const member of record.members) {
35201
- if (member.published || member.id === "katip") continue;
35273
+ if (member.published) continue;
35202
35274
  try {
35203
35275
  await deps.preflight(member.repo);
35204
35276
  } catch (e) {
@@ -35216,7 +35288,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35216
35288
  if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
35217
35289
  throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
35218
35290
  if (member.carried) {
35219
- if (member.id !== "katip") await deps.publish(member.repo, pinVersion(member.rcPin));
35291
+ await deps.publish(member.repo, pinVersion(member.rcPin));
35220
35292
  continue;
35221
35293
  }
35222
35294
  if (!member.tag) {
@@ -35229,7 +35301,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35229
35301
  }
35230
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`);
35231
35303
  if (!member.published) {
35232
- if (member.id !== "katip") await deps.publish(member.repo, member.tag);
35304
+ await deps.publish(member.repo, member.tag);
35233
35305
  member.published = true;
35234
35306
  writeSuiteCut(deps.recordPath, record);
35235
35307
  }
@@ -35288,43 +35360,59 @@ async function suiteRelease(deps, ownerGo) {
35288
35360
  if (hotfix) member.hotfixedTo = hotfix;
35289
35361
  }
35290
35362
  if (member.hotfixedTo && !member.released) continue;
35291
- const katipReleaseSha = member.id === "katip" ? await deps.tagSha(member.repo, planned) : void 0;
35292
- const expectedBackend = member.id === "katip" && member.released ? member.backendAfterRelease ?? katipReleaseSha : member.backend;
35293
- if (live.version !== expectedBackend && !(member.id === "katip" && !member.released && katipReleaseSha && live.version === katipReleaseSha))
35294
- 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`);
35295
35365
  }
35296
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 = [];
35297
35369
  for (const member of record.members) {
35298
- if (member.released || member.carried) continue;
35299
- if (member.hotfixedTo) {
35300
- console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
35301
- member.backendAfterRelease = (await deps.backend({ repo: member.repo, id: member.id, bundles: {} })).version;
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.`);
35380
+ for (const member of record.members) {
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
+ }
35302
35389
  member.released = true;
35303
35390
  writeSuiteCut(deps.recordPath, record);
35304
- continue;
35305
35391
  }
35306
- 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)`);
35307
- await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha);
35308
- if (member.id === "katip") {
35309
- const expected = await deps.tagSha(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag));
35310
- const live = await deps.backend({ repo: member.repo, id: member.id, bundles: {} });
35311
- if (!expected || live.version !== expected) throw new Error(`${member.repo}: normal main deploy not stamped at released tag SHA; resume after verifying live backend`);
35312
- 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);
35313
35396
  }
35314
- member.released = true;
35315
- writeSuiteCut(deps.recordPath, record);
35316
35397
  }
35317
35398
  if (!record.leadReleased) {
35318
35399
  await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha);
35319
35400
  record.leadReleased = true;
35320
35401
  writeSuiteCut(deps.recordPath, record);
35321
35402
  }
35322
- 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
+ }
35323
35410
  if (!record.announcement) {
35324
35411
  record.announcement = "pending";
35325
35412
  writeSuiteCut(deps.recordPath, record);
35326
- await deps.announce(record);
35327
- record.announcement = "sent";
35413
+ const { skipped } = await deps.announce(record);
35414
+ record.announcement = skipped ? "skipped" : "sent";
35415
+ if (skipped) record.announcementNote = skipped;
35328
35416
  writeSuiteCut(deps.recordPath, record);
35329
35417
  }
35330
35418
  return record;
@@ -45296,10 +45384,7 @@ function suiteTrainIo(root, summaryFile) {
45296
45384
  throw new Error(`${member.repo}: healthy live backend stamp and edgeDomains.main must agree with DEPLOY#main; stop`);
45297
45385
  const health = await probePublicHealth(url);
45298
45386
  if (!health.ok) throw new Error(`${member.repo}: live backend health could not be proved at ${url}; stop`);
45299
- const rcHost = edgeDomains?.rc;
45300
- if (member.id === "katip" && (!rcHost || facts?.stages.rc?.domain !== rcHost))
45301
- throw new Error(`${member.repo}: normal Katip rc host must agree with DEPLOY#rc; stop`);
45302
- return { version: live.deployedTag, host, ...rcHost ? { rcHost } : {} };
45387
+ return { version: live.deployedTag, host };
45303
45388
  };
45304
45389
  const tagSha = async (repo, tag) => {
45305
45390
  try {
@@ -45414,10 +45499,22 @@ function suiteTrainIo(root, summaryFile) {
45414
45499
  const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`], existing[0]);
45415
45500
  if (run.conclusion !== "success") throw new Error(`${repo}: module rc publish run ${run.id} not green${await rcRunCause(run.id)}`);
45416
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
+ };
45417
45510
  const preflight2 = async (repo) => {
45418
45511
  const run = await watchRcRun(`module-rc-preflight ${repo}`, [`repo=${repo}`, "preflight=true"], void 0);
45419
45512
  if (run.conclusion !== "success") throw new Error(`rc serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
45420
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
+ };
45421
45518
  const rcRunCause = async (id) => {
45422
45519
  try {
45423
45520
  return rcRunCauseFromLog(await exec("gh", ["run", "view", String(id), "--repo", "mutmutco/MMI-Hub", "--log-failed"]));
@@ -45458,6 +45555,8 @@ function suiteTrainIo(root, summaryFile) {
45458
45555
  rcand: runRcand,
45459
45556
  publish,
45460
45557
  preflight: preflight2,
45558
+ publishMain,
45559
+ preflightMain,
45461
45560
  pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
45462
45561
  verifyLeadCut: async (sha) => {
45463
45562
  const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
@@ -45481,7 +45580,15 @@ function suiteTrainIo(root, summaryFile) {
45481
45580
  announce: async (record) => {
45482
45581
  if (!summaryFile) throw new Error("suite announcement requires --announce-summary-file; outcome was not posted");
45483
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 };
45484
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`;
45485
45592
  }
45486
45593
  };
45487
45594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.56",
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",