@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.
- package/dist/main.cjs +182 -75
- 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
|
|
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))
|
|
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.
|
|
17048
|
-
tag: "v4.5.
|
|
17049
|
-
commit: "
|
|
17050
|
-
npm: "@mutmutco/cli@4.5.
|
|
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.
|
|
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.
|
|
17074
|
-
v3Target: "v4.5.
|
|
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
|
|
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)
|
|
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) &&
|
|
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)
|
|
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 (!
|
|
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 (!
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
35060
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35292
|
-
|
|
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.
|
|
35299
|
-
|
|
35300
|
-
|
|
35301
|
-
|
|
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 (
|
|
35307
|
-
|
|
35308
|
-
|
|
35309
|
-
|
|
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")
|
|
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
|
-
|
|
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