@mutmutco/cli 4.5.57 → 4.5.59
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 +291 -98
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5825,11 +5825,14 @@ function waitReadFailureMessage(e) {
|
|
|
5825
5825
|
return (stderr || err?.message || String(e)).replace(/\s+/g, " ").slice(0, 300);
|
|
5826
5826
|
}
|
|
5827
5827
|
var PR_CHECKS_POLL_MS = 3e4;
|
|
5828
|
+
function pollIntervalMs(elapsedMs) {
|
|
5829
|
+
return Math.min(4 * PR_CHECKS_POLL_MS, PR_CHECKS_POLL_MS * 2 ** Math.floor(Math.max(0, elapsedMs) / 3e5));
|
|
5830
|
+
}
|
|
5828
5831
|
var PR_CHECKS_TIMEOUT_MS = 30 * 6e4;
|
|
5829
5832
|
var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
|
|
5830
5833
|
var PR_CHECKS_CONFIRM_MS = 5e3;
|
|
5831
5834
|
var PR_CHECKS_FAILURE_CONFIRMATIONS = 2;
|
|
5832
|
-
var PR_CHECKS_RATELIMIT_FLOOR =
|
|
5835
|
+
var PR_CHECKS_RATELIMIT_FLOOR = 1e3;
|
|
5833
5836
|
var PR_CHECKS_RATELIMIT_JITTER_MAX_MS = 5e3;
|
|
5834
5837
|
var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
|
|
5835
5838
|
var PR_CHECKS_ZERO_RUNS_GRACE_MS = 2 * 6e4;
|
|
@@ -5965,7 +5968,7 @@ async function waitForPrChecks(deps) {
|
|
|
5965
5968
|
}
|
|
5966
5969
|
if (state === "failing") {
|
|
5967
5970
|
lastDetail = "failing (a check failed; others still running)";
|
|
5968
|
-
await deps.sleep(
|
|
5971
|
+
await deps.sleep(pollIntervalMs(now() - started));
|
|
5969
5972
|
continue;
|
|
5970
5973
|
}
|
|
5971
5974
|
if (state === "success") {
|
|
@@ -6003,11 +6006,11 @@ async function waitForPrChecks(deps) {
|
|
|
6003
6006
|
} else {
|
|
6004
6007
|
zeroRunsStreak = 0;
|
|
6005
6008
|
}
|
|
6006
|
-
await deps.sleep(
|
|
6009
|
+
await deps.sleep(pollIntervalMs(now() - started));
|
|
6007
6010
|
continue;
|
|
6008
6011
|
}
|
|
6009
6012
|
lastDetail = state;
|
|
6010
|
-
await deps.sleep(
|
|
6013
|
+
await deps.sleep(pollIntervalMs(now() - started));
|
|
6011
6014
|
}
|
|
6012
6015
|
return { policy, status: "timeout", reason, detail: lastDetail, waitedMs: now() - started };
|
|
6013
6016
|
}
|
|
@@ -6044,15 +6047,38 @@ async function pollToken() {
|
|
|
6044
6047
|
pollTokenOnce ??= pollTokenProvider().catch(() => void 0);
|
|
6045
6048
|
return pollTokenOnce;
|
|
6046
6049
|
}
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6050
|
+
var INCLUDED_HEADER_BLOCK = /^(?:\r?\n)?HTTP\/\d(?:\.\d)? (\d{3})[^\n]*\n(?:[^\n]+\n)*?\r?\n/gm;
|
|
6051
|
+
function parseIncludedOutput(out) {
|
|
6052
|
+
let status;
|
|
6053
|
+
let etag;
|
|
6054
|
+
let pages = 0;
|
|
6055
|
+
const body = out.replace(INCLUDED_HEADER_BLOCK, (block, code) => {
|
|
6056
|
+
if (pages++ === 0) {
|
|
6057
|
+
status = Number(code);
|
|
6058
|
+
etag = /^etag:\s*(.+?)\s*$/im.exec(block)?.[1];
|
|
6059
|
+
}
|
|
6060
|
+
return "";
|
|
6054
6061
|
});
|
|
6055
|
-
return
|
|
6062
|
+
return { body, status, etag, pages };
|
|
6063
|
+
}
|
|
6064
|
+
var etagCache = /* @__PURE__ */ new Map();
|
|
6065
|
+
async function defaultGhApi(args) {
|
|
6066
|
+
const key = args.join("\0");
|
|
6067
|
+
const cached = etagCache.get(key);
|
|
6068
|
+
let stdout;
|
|
6069
|
+
try {
|
|
6070
|
+
({ stdout } = await execFileP("gh", ["api", "--include", ...cached ? ["-H", `If-None-Match: ${cached.etag}`] : [], ...args], {
|
|
6071
|
+
timeout: REST_GH_TIMEOUT_MS
|
|
6072
|
+
}));
|
|
6073
|
+
} catch (e) {
|
|
6074
|
+
const out = e.stdout;
|
|
6075
|
+
if (cached && typeof out === "string" && parseIncludedOutput(out).status === 304) return cached.body;
|
|
6076
|
+
throw e;
|
|
6077
|
+
}
|
|
6078
|
+
const { body, etag, pages } = parseIncludedOutput(stdout);
|
|
6079
|
+
if (etag && pages === 1) etagCache.set(key, { etag, body });
|
|
6080
|
+
else etagCache.delete(key);
|
|
6081
|
+
return body;
|
|
6056
6082
|
}
|
|
6057
6083
|
function classifyCheckRun(run) {
|
|
6058
6084
|
if (run.status !== "completed") return "pending";
|
|
@@ -8475,6 +8501,9 @@ function isValidSecretKey(key) {
|
|
|
8475
8501
|
if (key.includes("..") || key.startsWith("/") || key.includes("*")) return false;
|
|
8476
8502
|
return KEY_RE.test(key);
|
|
8477
8503
|
}
|
|
8504
|
+
function stripTrailingCRLF(value) {
|
|
8505
|
+
return value.replace(/\r\n$|\r$|\n$/, "");
|
|
8506
|
+
}
|
|
8478
8507
|
function classifyTier(_slug, key) {
|
|
8479
8508
|
const slash2 = key.indexOf("/");
|
|
8480
8509
|
if (slash2 === -1) return "project";
|
|
@@ -8637,7 +8666,7 @@ async function fetchSecretValue(deps, key, opts, diagnose = false) {
|
|
|
8637
8666
|
if (!res.ok) {
|
|
8638
8667
|
if (diagnose) {
|
|
8639
8668
|
const body = await readJsonBody(res);
|
|
8640
|
-
const reason = res.status === 403 ? "authority refused" : res.status === 429 ? "rate limited" : "Hub route refused";
|
|
8669
|
+
const reason = res.status === 403 ? "authority refused" : res.status === 429 ? "rate limited" : body.code === "github-rate-limited" ? "Hub GitHub App allowance used up, not a denial" : "Hub route refused";
|
|
8641
8670
|
throw new Error(`${reason} (HTTP ${res.status}${typeof body.error === "string" && /^(forbidden|unauthorized|rate limit|too many requests|master-admin only)$/i.test(body.error) ? `: ${body.error}` : ""})`);
|
|
8642
8671
|
}
|
|
8643
8672
|
return null;
|
|
@@ -9918,6 +9947,10 @@ async function secretsVerify(deps, key, opts) {
|
|
|
9918
9947
|
}
|
|
9919
9948
|
const hasProviderVerifier = Boolean(providerForSecretKey(key));
|
|
9920
9949
|
const value = await fetchSecretValue(deps, key, opts);
|
|
9950
|
+
if (value && value !== stripTrailingCRLF(value)) {
|
|
9951
|
+
deps.err(`${key}: stored value ends in a CR or LF character \u2014 likely corrupted by a piped write; re-set it with \`mmi-cli vault secrets set ${key}\``);
|
|
9952
|
+
return false;
|
|
9953
|
+
}
|
|
9921
9954
|
if (!hasProviderVerifier) {
|
|
9922
9955
|
if (!value) {
|
|
9923
9956
|
deps.err(`${key}: ABSENT or unreachable at this coordinate \u2014 no value could be read`);
|
|
@@ -10105,6 +10138,40 @@ async function secretsDeclare(deps, key, opts) {
|
|
|
10105
10138
|
deps.log(`declared ${key} (project tier, ${slug}) \u2014 catalog metadata only; ask master (secrets request) to wire it into requiredRuntimeSecrets`);
|
|
10106
10139
|
return true;
|
|
10107
10140
|
}
|
|
10141
|
+
async function secretsUndeclare(deps, key, opts) {
|
|
10142
|
+
if (!DECLARE_KEY_RE.test(key.split("@")[0] ?? "")) {
|
|
10143
|
+
deps.err(`secrets undeclare: invalid key ${JSON.stringify(key)} \u2014 must be an env-style name (UPPER_SNAKE convention), optionally @dev|rc|main`);
|
|
10144
|
+
return false;
|
|
10145
|
+
}
|
|
10146
|
+
const repo = await targetRepo(deps, opts);
|
|
10147
|
+
const slug = await vaultSlug(deps, opts);
|
|
10148
|
+
let res;
|
|
10149
|
+
try {
|
|
10150
|
+
res = await deps.fetch(`${deps.apiUrl}/projects/${slug}/secrets/undeclare`, {
|
|
10151
|
+
method: "POST",
|
|
10152
|
+
headers: await deps.headers({ "content-type": "application/json" }),
|
|
10153
|
+
body: JSON.stringify({ repo, key }),
|
|
10154
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
10155
|
+
});
|
|
10156
|
+
} catch (e) {
|
|
10157
|
+
deps.err(`secrets undeclare: ${e.message}`);
|
|
10158
|
+
return false;
|
|
10159
|
+
}
|
|
10160
|
+
const body = await readJsonBody(res);
|
|
10161
|
+
if (!res.ok) {
|
|
10162
|
+
deps.err(await upgradeMessage(res, body) ?? `secrets undeclare failed: HTTP ${res.status}${errorDetail(body)}`);
|
|
10163
|
+
return false;
|
|
10164
|
+
}
|
|
10165
|
+
const undeclared = body?.undeclared !== false;
|
|
10166
|
+
if (opts.json) {
|
|
10167
|
+
deps.log(JSON.stringify({ ok: true, key, undeclared }));
|
|
10168
|
+
return true;
|
|
10169
|
+
}
|
|
10170
|
+
deps.log(
|
|
10171
|
+
undeclared ? `undeclared ${key} (project tier, ${slug}) \u2014 catalog entry removed; the vault value was already absent` : `${key} was not declared in ${slug}'s catalog \u2014 nothing to undeclare`
|
|
10172
|
+
);
|
|
10173
|
+
return true;
|
|
10174
|
+
}
|
|
10108
10175
|
function parseRailsCredentialMapping(raw, stage) {
|
|
10109
10176
|
const eq = raw.indexOf("=");
|
|
10110
10177
|
if (eq <= 0 || eq === raw.length - 1) return null;
|
|
@@ -15697,11 +15764,12 @@ function planBootstrapLabels(labels, live) {
|
|
|
15697
15764
|
...labelsToPrune(labels.map((label) => label.name)).filter((name) => existing.has(name)).map((name) => ({ name, action: "DELETE" }))
|
|
15698
15765
|
];
|
|
15699
15766
|
}
|
|
15700
|
-
function renderBootstrapAncillaryPlan(labels, environments) {
|
|
15767
|
+
function renderBootstrapAncillaryPlan(labels, environments, exceptions = []) {
|
|
15701
15768
|
return [
|
|
15702
15769
|
"bootstrap apply \u2014 labels and environments (dry-run; no mutations):",
|
|
15703
15770
|
...labels.map(({ action, name }) => ` ${action} label ${name}`),
|
|
15704
|
-
...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`)
|
|
15771
|
+
...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`),
|
|
15772
|
+
...exceptions.map(({ name, detail }) => ` OK environment ${name} (${detail})`)
|
|
15705
15773
|
].join("\n");
|
|
15706
15774
|
}
|
|
15707
15775
|
function resolveSeedContent(seed, vars, readFile9) {
|
|
@@ -16301,6 +16369,17 @@ function isHubControlRepo(repo) {
|
|
|
16301
16369
|
function isJervHubRepo(repo) {
|
|
16302
16370
|
return repo.toLowerCase() === JERV_HUB_REPO.toLowerCase();
|
|
16303
16371
|
}
|
|
16372
|
+
var WINDOWS_GIT_BASH_MSYSTEM = /^(?:MINGW(?:32|64)|UCRT64|CLANG(?:64|ARM64))$/u;
|
|
16373
|
+
function isWindowsGitBash(msystem) {
|
|
16374
|
+
return WINDOWS_GIT_BASH_MSYSTEM.test(msystem ?? "");
|
|
16375
|
+
}
|
|
16376
|
+
function assertGitBashOnWindowsForJervHubRelease(repo, platform2 = process.platform, env = process.env) {
|
|
16377
|
+
if (platform2 !== "win32" || !isJervHubRepo(repo)) return;
|
|
16378
|
+
if (isWindowsGitBash(env.MSYSTEM)) return;
|
|
16379
|
+
throw new Error(
|
|
16380
|
+
`${repo}: Windows Git Bash required \u2014 the release train's Jerv Gateway deploy leg refuses outside it, and the current shell is not Git Bash (MSYSTEM=${env.MSYSTEM ?? "(unset)"}). Run the release from Git Bash instead, e.g.: "C:/Program Files/Git/bin/bash.exe" -lc "mmi-cli devops release --apply"`
|
|
16381
|
+
);
|
|
16382
|
+
}
|
|
16304
16383
|
|
|
16305
16384
|
// src/train-apply-correlation.ts
|
|
16306
16385
|
var import_node_fs18 = require("node:fs");
|
|
@@ -17069,10 +17148,10 @@ var rollout_plan_default = {
|
|
|
17069
17148
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
17070
17149
|
},
|
|
17071
17150
|
baseline: {
|
|
17072
|
-
version: "4.5.
|
|
17073
|
-
tag: "v4.5.
|
|
17074
|
-
commit: "
|
|
17075
|
-
npm: "@mutmutco/cli@4.5.
|
|
17151
|
+
version: "4.5.59",
|
|
17152
|
+
tag: "v4.5.59",
|
|
17153
|
+
commit: "ea079366d96e",
|
|
17154
|
+
npm: "@mutmutco/cli@4.5.59"
|
|
17076
17155
|
},
|
|
17077
17156
|
exitCriterion: "fleet-n-of-n",
|
|
17078
17157
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17089,14 +17168,14 @@ var rollout_plan_default = {
|
|
|
17089
17168
|
repo: "mutmutco/mmi-hub",
|
|
17090
17169
|
role: "canary",
|
|
17091
17170
|
schedule: "train",
|
|
17092
|
-
v3Target: "v4.5.
|
|
17171
|
+
v3Target: "v4.5.59"
|
|
17093
17172
|
}
|
|
17094
17173
|
],
|
|
17095
17174
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
17096
17175
|
rollback: {
|
|
17097
17176
|
independent: true,
|
|
17098
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17099
|
-
v3Target: "v4.5.
|
|
17177
|
+
mechanism: "npm dist-tag latest -> 4.5.59 and redeploy the Hub Lambda from tag v4.5.59 (ea079366d96e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17178
|
+
v3Target: "v4.5.59 (@mutmutco/cli@4.5.59, tag commit ea079366d96e \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17100
17179
|
}
|
|
17101
17180
|
},
|
|
17102
17181
|
{
|
|
@@ -20094,9 +20173,14 @@ function validSuiteCutContext(repo, context) {
|
|
|
20094
20173
|
function authorisedSuiteCut(repo, meta, context) {
|
|
20095
20174
|
return validSuiteCutContext(repo, context) && !!meta?.suite && context.lead.toLowerCase() === suiteLeadRepo(repo, meta.suite).toLowerCase();
|
|
20096
20175
|
}
|
|
20176
|
+
var BUNDLE_ONLY_RC_EXCEPTION = "suite member: rc is bundle-only (no rc runtime)";
|
|
20177
|
+
function isBundleOnlySuiteMember(repo, suite) {
|
|
20178
|
+
if (!suite) return false;
|
|
20179
|
+
return repo.split("/")[1]?.toLowerCase() !== suite.toLowerCase();
|
|
20180
|
+
}
|
|
20097
20181
|
function suiteMemberRefusal(repo, meta, lane, solo = false, cut) {
|
|
20098
20182
|
const suite = meta?.suite;
|
|
20099
|
-
if (!suite || repo
|
|
20183
|
+
if (!suite || !isBundleOnlySuiteMember(repo, suite)) return null;
|
|
20100
20184
|
if (authorisedSuiteCut(repo, meta, cut)) return null;
|
|
20101
20185
|
if (lane !== "rcand" && solo) return null;
|
|
20102
20186
|
return `${repo} is part of the ${suite} suite; run the suite cut/release from ${suiteLeadRepo(repo, suite)} (mmi-cli devops suite rcand|release). A solo backend release needs --suite-solo --owner-go.`;
|
|
@@ -20701,7 +20785,7 @@ async function appendServedChannelLegs(deps, input, dispatch) {
|
|
|
20701
20785
|
const publishStatus = input.publish?.state === "complete" ? "success" : "failure";
|
|
20702
20786
|
const prepared = await (input.prepared ?? prepareServedChannel(deps, input));
|
|
20703
20787
|
if (!isHubControlRepo(input.repo)) {
|
|
20704
|
-
const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut)
|
|
20788
|
+
const bundleOnly = validSuiteCutContext(input.repo, deps.suiteCut);
|
|
20705
20789
|
const [home2, proof2] = await Promise.all([
|
|
20706
20790
|
appendInstallHomePublish(deps, { repo: input.repo, installer: input.installer, publishStatus, watch: input.watch }, dispatch),
|
|
20707
20791
|
bundleOnly && servedChannelFor(input.repo, input.installer) ? Promise.resolve({ ...dispatch, note: `${dispatch.note}; served channel not probed: a suite bundle-only release does not deploy the backend that serves it (#7468)` }) : appendServedChannelProof(deps, { repo: input.repo, tag: input.tag, shipped: shipLegsGreen(input.deploy, input.publish), installer: input.installer }, dispatch)
|
|
@@ -20971,7 +21055,7 @@ async function discoverShaWorkflowRuns(deps, repo, headSha, seenRunIds) {
|
|
|
20971
21055
|
return extra;
|
|
20972
21056
|
}
|
|
20973
21057
|
async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
20974
|
-
if (validSuiteCutContext(ctx.repo, deps.suiteCut) &&
|
|
21058
|
+
if (validSuiteCutContext(ctx.repo, deps.suiteCut) && stage !== "dev") {
|
|
20975
21059
|
return { note: "suite bundle-only: live backend not deployed", deployStatus: "success" };
|
|
20976
21060
|
}
|
|
20977
21061
|
if (isCentralDispatchModel(model)) {
|
|
@@ -21098,7 +21182,7 @@ async function attemptRetire(deps, ctx) {
|
|
|
21098
21182
|
return { status: "failed", category: r.category ?? "transport-failed", note: r.note };
|
|
21099
21183
|
}
|
|
21100
21184
|
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
21101
|
-
if (validSuiteCutContext(ctx.repo, deps.suiteCut)
|
|
21185
|
+
if (validSuiteCutContext(ctx.repo, deps.suiteCut))
|
|
21102
21186
|
return { status: "not-applicable", note: "bundle-only suite member has no rc runtime to retire" };
|
|
21103
21187
|
if (model !== "tenant-container") {
|
|
21104
21188
|
return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
|
|
@@ -24718,7 +24802,7 @@ async function runTrainDoctor(input) {
|
|
|
24718
24802
|
}
|
|
24719
24803
|
}
|
|
24720
24804
|
let preflightTimeouts = 0;
|
|
24721
|
-
if (!
|
|
24805
|
+
if (!authorisedSuiteCut(repo, meta, input.suiteCut)) for (; ; ) {
|
|
24722
24806
|
try {
|
|
24723
24807
|
await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
|
|
24724
24808
|
break;
|
|
@@ -25151,6 +25235,7 @@ function enforceGateBudget(deps, repo) {
|
|
|
25151
25235
|
}
|
|
25152
25236
|
}
|
|
25153
25237
|
async function preflight(deps, ctx, stage, meta, lane) {
|
|
25238
|
+
assertGitBashOnWindowsForJervHubRelease(ctx.repo);
|
|
25154
25239
|
deps.warn?.("Release: Checking release prerequisites");
|
|
25155
25240
|
if (deps.suiteCut && !authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
|
|
25156
25241
|
throw new Error(`${ctx.repo}: suite cut context does not match registry META; no deployment or tag was changed`);
|
|
@@ -25161,7 +25246,7 @@ async function preflight(deps, ctx, stage, meta, lane) {
|
|
|
25161
25246
|
if (model === "none") {
|
|
25162
25247
|
throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
|
|
25163
25248
|
}
|
|
25164
|
-
if (!
|
|
25249
|
+
if (!authorisedSuiteCut(ctx.repo, meta, deps.suiteCut))
|
|
25165
25250
|
await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
|
|
25166
25251
|
await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
|
|
25167
25252
|
await assertActionsJobsCanStart(deps, ctx.repo);
|
|
@@ -25760,7 +25845,7 @@ async function recordRcandDeployLegs(ledger, deps, anchors, deployModel, d, opts
|
|
|
25760
25845
|
);
|
|
25761
25846
|
const dispatch = recovered.deployStatus === "failure" && !recovered.note.includes(RCAND_RETRY_DEPLOY_COMMAND) ? { ...recovered, note: `once the cause is fixed, retry this rc deploy with \`${RCAND_RETRY_DEPLOY_COMMAND}\` (same tag and SHA, never re-cut): ${recovered.note}` } : recovered;
|
|
25762
25847
|
const unverifiable = !isCentralDispatchModel(deployModel) && dispatch.deployStatus === "pending" && dispatch.runId == null && !dispatch.workflowRuns?.length;
|
|
25763
|
-
const deployEntry = validSuiteCutContext(anchors.repo, deps.suiteCut)
|
|
25848
|
+
const deployEntry = validSuiteCutContext(anchors.repo, deps.suiteCut) ? { state: "skipped", sha, note: "suite bundle-only: rc backend not deployed (#7262)" } : unverifiable ? { state: "skipped", sha, note: `${dispatch.note} \u2014 no run to verify on rc; recorded skipped, never pending (#6068)` } : deployPhaseInput(deployModel, dispatch, { releaseSha: sha });
|
|
25764
25849
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployEntry), { ...opts, landed: "the immutable rc tag, the green wall, and origin/rc" });
|
|
25765
25850
|
await recordPhase(ledger, deps, anchors, "publish", phaseEntry({ state: "skipped", sha, note: "rcand publishes nothing \u2014 the publish leg rides the release" }), { ...opts, landed });
|
|
25766
25851
|
await recordPhase(ledger, deps, anchors, "alignment", phaseEntry({ state: "skipped", sha, note: "rcand aligns no branch \u2014 alignment rides the release" }), { ...opts, landed });
|
|
@@ -25923,7 +26008,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
25923
26008
|
}
|
|
25924
26009
|
);
|
|
25925
26010
|
const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch), versionEnv);
|
|
25926
|
-
const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut)
|
|
26011
|
+
const deployLeg = phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) ? { state: "skipped", sha: releaseSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha }));
|
|
25927
26012
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", deployLeg, {
|
|
25928
26013
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
25929
26014
|
});
|
|
@@ -26346,7 +26431,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
26346
26431
|
dispatch2.deployStatus = "success";
|
|
26347
26432
|
dispatch2.note = `historical ${deployModel} deploy/publish status: success (re-verified by the recorded run)`;
|
|
26348
26433
|
}
|
|
26349
|
-
if (validSuiteCutContext(ctx.repo, deps.suiteCut)
|
|
26434
|
+
if (validSuiteCutContext(ctx.repo, deps.suiteCut)) {
|
|
26350
26435
|
phaseInputs.deploy = { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" };
|
|
26351
26436
|
if (dispatch2.deployStatus !== "failure" && (phaseInputs.publish.state === "complete" || phaseInputs.publish.state === "skipped")) {
|
|
26352
26437
|
dispatch2.deployStatus = "success";
|
|
@@ -26481,7 +26566,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26481
26566
|
);
|
|
26482
26567
|
const deployDispatch = annotateVersionEnvDispatch(await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch), versionEnv);
|
|
26483
26568
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
26484
|
-
const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut)
|
|
26569
|
+
const deployLeg = persisted?.phases.deploy.superseded === true ? persisted.phases.deploy : phaseEntry(validSuiteCutContext(ctx.repo, deps.suiteCut) ? { state: "skipped", sha: tagSha, note: "suite bundle-only: live backend not deployed (#7263)" } : deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha }));
|
|
26485
26570
|
if (persisted?.phases.deploy.superseded !== true) {
|
|
26486
26571
|
await recordPhase(ledger, deps, anchors, "deploy", deployLeg, {
|
|
26487
26572
|
strict: ledgerMode === "strict",
|
|
@@ -27461,15 +27546,16 @@ async function branchPresence(deps, repo, branch) {
|
|
|
27461
27546
|
}
|
|
27462
27547
|
}
|
|
27463
27548
|
function collectRegistryRepos(projects) {
|
|
27464
|
-
const seen = /* @__PURE__ */ new
|
|
27549
|
+
const seen = /* @__PURE__ */ new Map();
|
|
27465
27550
|
for (const project2 of projects) {
|
|
27466
27551
|
for (const raw of project2.repos ?? []) {
|
|
27467
27552
|
const repo = raw.includes("/") ? raw : `mutmutco/${raw}`;
|
|
27468
|
-
|
|
27553
|
+
const key = repo.toLowerCase();
|
|
27554
|
+
if (!seen.has(key)) seen.set(key, repo);
|
|
27469
27555
|
}
|
|
27470
27556
|
}
|
|
27471
|
-
if (!
|
|
27472
|
-
return [...seen].sort((a, b) => a.localeCompare(b));
|
|
27557
|
+
if (!seen.has(HUB_REPO4.toLowerCase())) seen.set(HUB_REPO4.toLowerCase(), HUB_REPO4);
|
|
27558
|
+
return [...seen.values()].sort((a, b) => a.localeCompare(b));
|
|
27473
27559
|
}
|
|
27474
27560
|
async function prTriggeredWorkflowsOnRef(deps, repo, branch, repoClass) {
|
|
27475
27561
|
const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, branch, PRODUCT_GATE_PATH);
|
|
@@ -27516,7 +27602,29 @@ async function filterPullRequestTriggered(deps, repo, branch, paths) {
|
|
|
27516
27602
|
}
|
|
27517
27603
|
return kept;
|
|
27518
27604
|
}
|
|
27519
|
-
|
|
27605
|
+
function trackUnauditableReads(deps) {
|
|
27606
|
+
let reason;
|
|
27607
|
+
const record = (e) => {
|
|
27608
|
+
if (reason) return;
|
|
27609
|
+
const err = e;
|
|
27610
|
+
if (err?.rateLimited) reason = `GitHub App rate-limited: ${err.message ?? "rate limit exceeded"}`;
|
|
27611
|
+
else if (err?.status === 403 || err?.status === 429) reason = `HTTP ${err.status}${err.message ? `: ${err.message}` : ""}`;
|
|
27612
|
+
};
|
|
27613
|
+
const client = {
|
|
27614
|
+
...deps.client,
|
|
27615
|
+
rest: async (method, path2, init) => {
|
|
27616
|
+
try {
|
|
27617
|
+
return await deps.client.rest(method, path2, init);
|
|
27618
|
+
} catch (e) {
|
|
27619
|
+
record(e);
|
|
27620
|
+
throw e;
|
|
27621
|
+
}
|
|
27622
|
+
}
|
|
27623
|
+
};
|
|
27624
|
+
return { deps: { ...deps, client }, reason: () => reason };
|
|
27625
|
+
}
|
|
27626
|
+
async function auditRepoCi(repo, callerDeps) {
|
|
27627
|
+
const { deps, reason: unauditedReason } = trackUnauditableReads(callerDeps);
|
|
27520
27628
|
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
27521
27629
|
const repoClass = classifyRepo(repo, meta);
|
|
27522
27630
|
const checks = [];
|
|
@@ -27811,6 +27919,10 @@ async function auditRepoCi(repo, deps) {
|
|
|
27811
27919
|
const workflows = await getBranchWorkflows();
|
|
27812
27920
|
if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
|
|
27813
27921
|
}
|
|
27922
|
+
const failure = unauditedReason();
|
|
27923
|
+
if (failure) {
|
|
27924
|
+
return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
|
|
27925
|
+
}
|
|
27814
27926
|
const ok = checks.every((c) => c.ok);
|
|
27815
27927
|
return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
|
|
27816
27928
|
}
|
|
@@ -27852,8 +27964,8 @@ function renderCiAuditMarkdown(report) {
|
|
|
27852
27964
|
"|------|-------|--------|----|---------|"
|
|
27853
27965
|
];
|
|
27854
27966
|
for (const r of report.repos) {
|
|
27855
|
-
const gap = r.checks.find((c) => !c.ok);
|
|
27856
|
-
lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap
|
|
27967
|
+
const gap = r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
|
|
27968
|
+
lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap} |`);
|
|
27857
27969
|
}
|
|
27858
27970
|
return lines2.join("\n");
|
|
27859
27971
|
}
|
|
@@ -27863,8 +27975,13 @@ function renderCiAuditText(report) {
|
|
|
27863
27975
|
renderCiAuditScopeLine(report)
|
|
27864
27976
|
];
|
|
27865
27977
|
for (const r of report.repos) {
|
|
27978
|
+
const verdict = r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
|
|
27866
27979
|
lines2.push(`
|
|
27867
|
-
${r.repo} (${r.class}, policy=${r.mergePolicy}) ${
|
|
27980
|
+
${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
|
|
27981
|
+
if (r.unauditedReason) {
|
|
27982
|
+
lines2.push(` UNAUDITED \u2014 ${r.unauditedReason}`);
|
|
27983
|
+
continue;
|
|
27984
|
+
}
|
|
27868
27985
|
for (const c of r.checks) {
|
|
27869
27986
|
lines2.push(` ${c.ok ? "OK" : "FAIL"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
27870
27987
|
}
|
|
@@ -31350,7 +31467,7 @@ async function readSecretStdin() {
|
|
|
31350
31467
|
}
|
|
31351
31468
|
const chunks = [];
|
|
31352
31469
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
31353
|
-
return Buffer.concat(chunks).toString("utf8")
|
|
31470
|
+
return stripTrailingCRLF(Buffer.concat(chunks).toString("utf8"));
|
|
31354
31471
|
}
|
|
31355
31472
|
function makeSecretsDeps(cfg) {
|
|
31356
31473
|
return {
|
|
@@ -31502,6 +31619,10 @@ function registerSecretsCommands(program3) {
|
|
|
31502
31619
|
});
|
|
31503
31620
|
if (!ok) process.exitCode = 1;
|
|
31504
31621
|
}));
|
|
31622
|
+
secrets.command("undeclare <key>").description("project-admin self-undeclare: retire a secrets-catalog entry for your own repo (catalog metadata only \u2014 never a value); declare's exact inverse (MMI-Hub#7524). Refuses (409) while a live vault value still exists for the key at any declared stage \u2014 delete the value first (`secrets rm`), then undeclare. A key that is not declared is a no-op, not an error.").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((key, o) => withSecrets(async (d) => {
|
|
31623
|
+
const ok = await secretsUndeclare(d, key, { repo: o.repo, json: o.json });
|
|
31624
|
+
if (!ok) process.exitCode = 1;
|
|
31625
|
+
}));
|
|
31505
31626
|
secrets.command("verify <key>").description("validate a known provider secret without printing its value; a key with no configured provider verifier falls back to a value-free PRESENT/ABSENT reachability check (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").action((key, o) => withSecrets(async (d) => {
|
|
31506
31627
|
const ok = await secretsVerify(d, key, o);
|
|
31507
31628
|
if (!ok) process.exitCode = 1;
|
|
@@ -32356,11 +32477,12 @@ function environmentNamesForTrack(track) {
|
|
|
32356
32477
|
const branches = new Set(branchesForTrack(track));
|
|
32357
32478
|
return REPO_ENVIRONMENTS.filter((environment) => branches.has(ENVIRONMENT_BRANCHES[environment]));
|
|
32358
32479
|
}
|
|
32359
|
-
function repoEnvironmentSpecs(
|
|
32480
|
+
function repoEnvironmentSpecs(repo, releaseTrack = "full", suite) {
|
|
32360
32481
|
const included = new Set(environmentNamesForTrack(releaseTrack));
|
|
32361
32482
|
const trackExceptions = releaseTrack === "full" ? void 0 : TRACK_ENVIRONMENT_EXCEPTIONS[releaseTrack];
|
|
32483
|
+
const bundleOnlyRc = isBundleOnlySuiteMember(repo, suite);
|
|
32362
32484
|
return REPO_ENVIRONMENTS.map((name) => {
|
|
32363
|
-
const exception = name
|
|
32485
|
+
const exception = !included.has(name) ? trackExceptions?.[name] : name === "rc" && bundleOnlyRc ? BUNDLE_ONLY_RC_EXCEPTION : void 0;
|
|
32364
32486
|
return {
|
|
32365
32487
|
name,
|
|
32366
32488
|
branch: ENVIRONMENT_BRANCHES[name],
|
|
@@ -32371,8 +32493,8 @@ function repoEnvironmentSpecs(_repo, releaseTrack = "full") {
|
|
|
32371
32493
|
};
|
|
32372
32494
|
});
|
|
32373
32495
|
}
|
|
32374
|
-
function provisionedEnvironmentSpecs(repo, releaseTrack = "full") {
|
|
32375
|
-
return repoEnvironmentSpecs(repo, releaseTrack).filter((spec) => spec.exception == null);
|
|
32496
|
+
function provisionedEnvironmentSpecs(repo, releaseTrack = "full", suite) {
|
|
32497
|
+
return repoEnvironmentSpecs(repo, releaseTrack, suite).filter((spec) => spec.exception == null);
|
|
32376
32498
|
}
|
|
32377
32499
|
function asEnvironmentList(value) {
|
|
32378
32500
|
if (Array.isArray(value)) return value;
|
|
@@ -32412,10 +32534,10 @@ var PLAN_GATED_PROTECTION_RE = /billing plan/i;
|
|
|
32412
32534
|
function isPlanGatedProtectionError(error) {
|
|
32413
32535
|
return PLAN_GATED_PROTECTION_RE.test(String(error?.message ?? error));
|
|
32414
32536
|
}
|
|
32415
|
-
async function reconcileRepoEnvironments(repo, releaseTrack, client) {
|
|
32537
|
+
async function reconcileRepoEnvironments(repo, releaseTrack, client, suite) {
|
|
32416
32538
|
const raw = await client.rest("GET", `repos/${repo}/environments`);
|
|
32417
32539
|
const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
|
|
32418
|
-
const planned = provisionedEnvironmentSpecs(repo, releaseTrack);
|
|
32540
|
+
const planned = provisionedEnvironmentSpecs(repo, releaseTrack, suite);
|
|
32419
32541
|
const created = [];
|
|
32420
32542
|
const updated = [];
|
|
32421
32543
|
const skipped = [];
|
|
@@ -32448,10 +32570,10 @@ async function reconcileRepoEnvironments(repo, releaseTrack, client) {
|
|
|
32448
32570
|
}
|
|
32449
32571
|
return { repo, planned, created, updated, skipped, planGated };
|
|
32450
32572
|
}
|
|
32451
|
-
async function verifyRepoEnvironments(repo, releaseTrack, client) {
|
|
32573
|
+
async function verifyRepoEnvironments(repo, releaseTrack, client, suite) {
|
|
32452
32574
|
const raw = await client.rest("GET", `repos/${repo}/environments`);
|
|
32453
32575
|
const live = new Map(asEnvironmentList(raw).flatMap((entry) => entry.name ? [[entry.name, entry]] : []));
|
|
32454
|
-
return provisionedEnvironmentSpecs(repo, releaseTrack).map((spec) => {
|
|
32576
|
+
return provisionedEnvironmentSpecs(repo, releaseTrack, suite).map((spec) => {
|
|
32455
32577
|
const drift = environmentDrift(live.get(spec.name), spec);
|
|
32456
32578
|
return { environment: spec.name, missing: !live.has(spec.name), problems: drift.problems, notes: drift.notes };
|
|
32457
32579
|
});
|
|
@@ -32569,7 +32691,17 @@ function planBootstrapControlPlane(input) {
|
|
|
32569
32691
|
}
|
|
32570
32692
|
}
|
|
32571
32693
|
const deployStages = deployStagesForBootstrap(input.deployModel, input.releaseTrack);
|
|
32694
|
+
const bundleOnlyRc = isBundleOnlySuiteMember(input.repo, input.suite);
|
|
32572
32695
|
for (const stage of deployStages) {
|
|
32696
|
+
if (stage === "rc" && bundleOnlyRc) {
|
|
32697
|
+
rows.push({
|
|
32698
|
+
kind: "deploy-row",
|
|
32699
|
+
target: "DEPLOY#rc",
|
|
32700
|
+
action: "skip",
|
|
32701
|
+
detail: BUNDLE_ONLY_RC_EXCEPTION
|
|
32702
|
+
});
|
|
32703
|
+
continue;
|
|
32704
|
+
}
|
|
32573
32705
|
const present = input.deployFacts?.stages?.[stage]?.present === true;
|
|
32574
32706
|
const command = bootstrapSetDeployCommand(input.repo, stage, input.deployModel);
|
|
32575
32707
|
rows.push(present && stage === "main" ? {
|
|
@@ -32959,10 +33091,13 @@ function centralContainerPortRangeCheck(deployModel, portRange, repo) {
|
|
|
32959
33091
|
detail: ok ? void 0 : `${deployModel} needs PROJECT# META.portRange for mmi-cli stage \u2014 assign with: mmi-cli stage port-range ${repo}`
|
|
32960
33092
|
};
|
|
32961
33093
|
}
|
|
32962
|
-
function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead) {
|
|
33094
|
+
function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead, suite) {
|
|
32963
33095
|
const stages = deployStagesForBootstrap(deployModel, releaseTrack);
|
|
32964
33096
|
if (!stages.length) return [];
|
|
32965
33097
|
return stages.map((stage) => {
|
|
33098
|
+
if (stage === "rc" && isBundleOnlySuiteMember(repo, suite)) {
|
|
33099
|
+
return { ok: true, label: "DEPLOY#rc row exception", detail: BUNDLE_ONLY_RC_EXCEPTION };
|
|
33100
|
+
}
|
|
32966
33101
|
const ok = factsRead && facts?.stages?.[stage]?.present === true;
|
|
32967
33102
|
return {
|
|
32968
33103
|
ok,
|
|
@@ -33023,7 +33158,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
33023
33158
|
}
|
|
33024
33159
|
let environmentDrift2 = [];
|
|
33025
33160
|
try {
|
|
33026
|
-
environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client);
|
|
33161
|
+
environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client, deps.projectMeta?.suite);
|
|
33027
33162
|
} catch (e) {
|
|
33028
33163
|
checks.push({
|
|
33029
33164
|
ok: false,
|
|
@@ -33038,7 +33173,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
33038
33173
|
detail: drift.problems.length ? drift.problems.join("; ") : drift.notes.length ? `advisory: ${drift.notes.join("; ")}` : void 0
|
|
33039
33174
|
});
|
|
33040
33175
|
}
|
|
33041
|
-
for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full")).filter((entry) => entry.exception)) {
|
|
33176
|
+
for (const spec of repoEnvironmentSpecs(repo, releaseTrack ?? (repoClass === "content" ? "trunk" : "full"), deps.projectMeta?.suite).filter((entry) => entry.exception)) {
|
|
33042
33177
|
checks.push({ ok: true, label: `repository environment exception: ${spec.name}`, detail: spec.exception });
|
|
33043
33178
|
}
|
|
33044
33179
|
const strayTrainBranches = TRAIN_BRANCHES2.filter((b) => !branchesWanted.includes(b) && branchNames.has(b));
|
|
@@ -33112,7 +33247,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
33112
33247
|
deps.deployModel,
|
|
33113
33248
|
effectiveTrack,
|
|
33114
33249
|
deps.deployFacts,
|
|
33115
|
-
deps.deployFactsRead === true
|
|
33250
|
+
deps.deployFactsRead === true,
|
|
33251
|
+
deps.projectMeta?.suite
|
|
33116
33252
|
));
|
|
33117
33253
|
const readme = await contentText(deps, repo, baseBranch, "README.md");
|
|
33118
33254
|
checks.push({
|
|
@@ -33349,9 +33485,13 @@ function applyWaivers(checks, waivers) {
|
|
|
33349
33485
|
const set = new Set(waivers);
|
|
33350
33486
|
return checks.map((c) => !c.ok && set.has(c.label) ? { ...c, waived: true } : c);
|
|
33351
33487
|
}
|
|
33488
|
+
var laneReasons = (doctor) => doctor.findings.filter((f) => (f.severity === "blocker" || f.code === "origin-unverified") && f.code !== SUITE_GUARD_CODE);
|
|
33489
|
+
function onlySuiteLeadOwned(doctor) {
|
|
33490
|
+
return !doctor.ready && laneReasons(doctor).length === 0 && doctor.findings.some((f) => f.code === SUITE_GUARD_CODE);
|
|
33491
|
+
}
|
|
33352
33492
|
function laneNotEvaluatedFromThisBranch(doctor) {
|
|
33353
33493
|
if (doctor.ready) return void 0;
|
|
33354
|
-
const reasons = doctor
|
|
33494
|
+
const reasons = laneReasons(doctor);
|
|
33355
33495
|
const only = reasons.length === 1 ? reasons[0] : void 0;
|
|
33356
33496
|
return only?.code === "branch-mismatch" ? only.title : void 0;
|
|
33357
33497
|
}
|
|
@@ -33362,7 +33502,7 @@ function attachTrainDoctorVerdicts(report, trainDoctors) {
|
|
|
33362
33502
|
});
|
|
33363
33503
|
return {
|
|
33364
33504
|
...report,
|
|
33365
|
-
ok: report.ok && marked.every((doctor) => doctor.ready || doctor.notEvaluatedFromThisBranch !== void 0),
|
|
33505
|
+
ok: report.ok && marked.every((doctor) => doctor.ready || doctor.notEvaluatedFromThisBranch !== void 0 || onlySuiteLeadOwned(doctor)),
|
|
33366
33506
|
...marked.length ? { trainDoctors: marked } : {}
|
|
33367
33507
|
};
|
|
33368
33508
|
}
|
|
@@ -33376,6 +33516,8 @@ function renderBootstrapVerifyReport(report) {
|
|
|
33376
33516
|
lines2.push("", formatTrainDoctor(doctor));
|
|
33377
33517
|
if (doctor.notEvaluatedFromThisBranch) {
|
|
33378
33518
|
lines2.push(` NOT EVALUATED from this branch \u2014 ${doctor.notEvaluatedFromThisBranch}; one checkout stands on one branch, so this lane does not fail the verify (#6677). Run \`mmi-cli devops train doctor --lane ${doctor.lane}\` from that branch for its own verdict.`);
|
|
33519
|
+
} else if (onlySuiteLeadOwned(doctor)) {
|
|
33520
|
+
lines2.push(" SUITE MEMBER: this lane is run from the suite lead (mmi-cli devops suite rcand|release), so suite-member-solo does not fail the verify (#7511).");
|
|
33379
33521
|
}
|
|
33380
33522
|
}
|
|
33381
33523
|
return lines2.join("\n");
|
|
@@ -33707,6 +33849,7 @@ function registerBootstrapCommands(program3) {
|
|
|
33707
33849
|
requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(meta, cls), repo),
|
|
33708
33850
|
requiredChecks: meta?.requiredChecks ?? void 0,
|
|
33709
33851
|
deployModel: meta?.deployModel,
|
|
33852
|
+
suite: meta?.suite,
|
|
33710
33853
|
owners: controlOwners,
|
|
33711
33854
|
github,
|
|
33712
33855
|
deployFacts,
|
|
@@ -33773,31 +33916,39 @@ function registerBootstrapCommands(program3) {
|
|
|
33773
33916
|
return fail(`bootstrap apply: ${e.message}`);
|
|
33774
33917
|
}
|
|
33775
33918
|
let effectiveTrack = bootstrapReleaseTrack;
|
|
33919
|
+
let suiteSlug;
|
|
33776
33920
|
if (!o.releaseTrack) {
|
|
33777
33921
|
const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
|
|
33778
33922
|
if (read.ok) {
|
|
33779
33923
|
effectiveTrack = resolveReleaseTrack(metaWithDeclaredClass(read.project, o.class), void 0, repo);
|
|
33924
|
+
suiteSlug = read.project?.suite;
|
|
33780
33925
|
} else if (o.execute) {
|
|
33781
33926
|
return failGraceful(`bootstrap apply: registry META unreadable (${read.error}) and no --release-track given \u2014 refusing to assume the ${bootstrapReleaseTrack} track for live writes`);
|
|
33782
33927
|
} else {
|
|
33783
33928
|
console.error(`bootstrap apply: registry META unreadable (${read.error}) \u2014 release track ${bootstrapReleaseTrack} is ASSUMED from the class default for this dry-run; --execute will refuse`);
|
|
33784
33929
|
}
|
|
33930
|
+
} else {
|
|
33931
|
+
const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
|
|
33932
|
+
if (read.ok) suiteSlug = read.project?.suite;
|
|
33785
33933
|
}
|
|
33786
33934
|
if (o.only.trim() === "environments") {
|
|
33787
33935
|
const client = defaultGitHubClient();
|
|
33788
33936
|
const envJson = { repo, track: effectiveTrack };
|
|
33789
33937
|
if (!o.execute) {
|
|
33790
|
-
const drift = await verifyRepoEnvironments(repo, effectiveTrack, client);
|
|
33938
|
+
const drift = await verifyRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
|
|
33791
33939
|
const classified = drift.map(classifyEnvironmentDrift);
|
|
33792
33940
|
envJson.plan = classified;
|
|
33793
33941
|
if (o.json) console.log(JSON.stringify(envJson, null, 2));
|
|
33794
33942
|
else {
|
|
33795
33943
|
console.log(`bootstrap apply --only environments \u2014 dry-run (${repo}, track ${effectiveTrack}); no writes:`);
|
|
33796
33944
|
for (const c of classified) console.log(` ${c.action} ${c.environment} \u2014 ${c.reason}`);
|
|
33945
|
+
for (const spec of repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug)) {
|
|
33946
|
+
if (spec.exception) console.log(` OK environment ${spec.name} (${spec.exception})`);
|
|
33947
|
+
}
|
|
33797
33948
|
console.log(" (plan-gated protections \u2014 wait_timer/reviewers the org plan rejects \u2014 are reported at --execute)");
|
|
33798
33949
|
}
|
|
33799
33950
|
} else {
|
|
33800
|
-
const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client);
|
|
33951
|
+
const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, client, suiteSlug);
|
|
33801
33952
|
const lines2 = [];
|
|
33802
33953
|
if (envResult.created.length) lines2.push(`environments created: ${envResult.created.join(", ")}`);
|
|
33803
33954
|
if (envResult.updated.length) lines2.push(`environments updated: ${envResult.updated.join(", ")}`);
|
|
@@ -33805,6 +33956,9 @@ function registerBootstrapCommands(program3) {
|
|
|
33805
33956
|
for (const gated of envResult.planGated) {
|
|
33806
33957
|
lines2.push(`environment ${gated.environment}: plan-gated protections (${gated.protections.join(", ")} not settable on this org plan \u2014 policy-only provisioned)`);
|
|
33807
33958
|
}
|
|
33959
|
+
for (const spec of repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug)) {
|
|
33960
|
+
if (spec.exception) lines2.push(`OK environment ${spec.name} (${spec.exception})`);
|
|
33961
|
+
}
|
|
33808
33962
|
envJson.result = { created: envResult.created, updated: envResult.updated, skipped: envResult.skipped, planGated: envResult.planGated };
|
|
33809
33963
|
if (o.json) console.log(JSON.stringify(envJson, null, 2));
|
|
33810
33964
|
else {
|
|
@@ -33926,6 +34080,7 @@ function registerBootstrapCommands(program3) {
|
|
|
33926
34080
|
requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
|
|
33927
34081
|
requiredChecks: registryMeta?.requiredChecks ?? void 0,
|
|
33928
34082
|
deployModel: applyDeployModel,
|
|
34083
|
+
suite: suiteSlug,
|
|
33929
34084
|
owners: controlOwners,
|
|
33930
34085
|
github,
|
|
33931
34086
|
deployFacts: controlDeployFacts,
|
|
@@ -34270,7 +34425,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34270
34425
|
}
|
|
34271
34426
|
if (o.execute && !onlyTarget) {
|
|
34272
34427
|
try {
|
|
34273
|
-
const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient());
|
|
34428
|
+
const envResult = await reconcileRepoEnvironments(repo, effectiveTrack, defaultGitHubClient(), suiteSlug);
|
|
34274
34429
|
if (envResult.created.length) applied.push(`environments created: ${envResult.created.join(", ")}`);
|
|
34275
34430
|
if (envResult.updated.length) applied.push(`environments updated: ${envResult.updated.join(", ")}`);
|
|
34276
34431
|
if (envResult.skipped.length) applied.push(`environments current: ${envResult.skipped.join(", ")}`);
|
|
@@ -34367,6 +34522,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34367
34522
|
requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
|
|
34368
34523
|
requiredChecks: registryMeta?.requiredChecks ?? void 0,
|
|
34369
34524
|
deployModel: applyDeployModel,
|
|
34525
|
+
suite: suiteSlug,
|
|
34370
34526
|
owners: controlOwners,
|
|
34371
34527
|
github,
|
|
34372
34528
|
deployFacts: controlDeployFacts,
|
|
@@ -34410,7 +34566,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34410
34566
|
const liveLabels = JSON.parse((await gh2(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"])).stdout);
|
|
34411
34567
|
if (!Array.isArray(liveLabels)) throw new Error("label list is not an array");
|
|
34412
34568
|
ancillaryPlan = planBootstrapLabels(manifest.labels, liveLabels);
|
|
34413
|
-
environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient)).map(classifyEnvironmentDrift);
|
|
34569
|
+
environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient, suiteSlug)).map(classifyEnvironmentDrift);
|
|
34414
34570
|
} catch (e) {
|
|
34415
34571
|
return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
|
|
34416
34572
|
}
|
|
@@ -34421,7 +34577,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34421
34577
|
if (controlRows.length) console.log(`
|
|
34422
34578
|
${renderBootstrapControlPlan(controlRows)}`);
|
|
34423
34579
|
if (ancillaryPlan && environmentPlan) console.log(`
|
|
34424
|
-
${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan)}`);
|
|
34580
|
+
${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan, repoEnvironmentSpecs(repo, effectiveTrack, suiteSlug).filter((spec) => spec.exception).map((spec) => ({ name: spec.name, detail: spec.exception })))}`);
|
|
34425
34581
|
if (o.execute) console.log(`
|
|
34426
34582
|
LIVE apply to ${repo}:
|
|
34427
34583
|
${applied.join("\n ")}`);
|
|
@@ -35086,13 +35242,15 @@ var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
|
|
|
35086
35242
|
function suitePins(record, bundles) {
|
|
35087
35243
|
return record.members.map((m) => m.carried ? { module: m.id, rc: m.rcPin, main: m.mainPin } : {
|
|
35088
35244
|
module: m.id,
|
|
35089
|
-
rc: `https://${m.
|
|
35245
|
+
rc: `https://${m.host}/rc/module.js?v=${m.tag}`,
|
|
35090
35246
|
main: mainPinFor(m.host, bundles[m.id], suiteReleaseTag(m.tag))
|
|
35091
35247
|
});
|
|
35092
35248
|
}
|
|
35093
35249
|
function mainPinFor(host, bundles, tag) {
|
|
35094
|
-
|
|
35095
|
-
|
|
35250
|
+
return `https://${host}${mainBundlePath(bundles)}?v=${tag}`;
|
|
35251
|
+
}
|
|
35252
|
+
function mainBundlePath(bundles) {
|
|
35253
|
+
return [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
|
|
35096
35254
|
}
|
|
35097
35255
|
function pathOf(url) {
|
|
35098
35256
|
try {
|
|
@@ -35120,7 +35278,7 @@ function readSuiteCut(path2) {
|
|
|
35120
35278
|
} catch {
|
|
35121
35279
|
throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
|
|
35122
35280
|
}
|
|
35123
|
-
if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent"].includes(record.announcement) || !record.leadReleased) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
|
|
35281
|
+
if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
|
|
35124
35282
|
throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
|
|
35125
35283
|
return record;
|
|
35126
35284
|
}
|
|
@@ -35176,13 +35334,13 @@ async function suiteRcand(deps, opts) {
|
|
|
35176
35334
|
const members = suiteMembers(await deps.registry());
|
|
35177
35335
|
const modules = members.filter((m) => m.repo !== SUITE_REPO);
|
|
35178
35336
|
const plan = [
|
|
35179
|
-
...modules.map((m) => `${m.repo}: normal rcand
|
|
35337
|
+
...modules.map((m) => `${m.repo}: normal rcand + module-rc-publish.yml (once)`),
|
|
35180
35338
|
`${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
|
|
35181
35339
|
`${SUITE_REPO}: rcand`
|
|
35182
35340
|
];
|
|
35183
35341
|
if (opts.dryRun) return { plan };
|
|
35184
35342
|
let record = readSuiteCut(deps.recordPath);
|
|
35185
|
-
if (record?.announcement === "sent" && record.leadReleased) {
|
|
35343
|
+
if ((record?.announcement === "sent" || record?.announcement === "skipped") && record.leadReleased) {
|
|
35186
35344
|
const archive = `${deps.recordPath}.${record.leadRcTag}.json`;
|
|
35187
35345
|
if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`suite cut archive already exists at ${archive}; inspect before a new cut`);
|
|
35188
35346
|
(0, import_node_fs29.renameSync)(deps.recordPath, archive);
|
|
@@ -35200,7 +35358,7 @@ async function suiteRcand(deps, opts) {
|
|
|
35200
35358
|
const [backend, development, rcHead] = await Promise.all([deps.backend(m), deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
|
|
35201
35359
|
const previousRc = rcHead ?? await deps.head(m.repo, "main");
|
|
35202
35360
|
if (!backend.version || !backend.host || !development || !SHA2.test(development) || !previousRc || !SHA2.test(previousRc)) throw new Error(`${m.repo}: live backend or branch snapshot is unproven; no cut started`);
|
|
35203
|
-
return { repo: m.repo, id: m.id, backend: backend.version, host: backend.host,
|
|
35361
|
+
return { repo: m.repo, id: m.id, backend: backend.version, host: backend.host, development, previousRc, ...rcHead === null ? { rcWasAbsent: true } : {} };
|
|
35204
35362
|
}))
|
|
35205
35363
|
]);
|
|
35206
35364
|
if (!leadDevelopment || !leadPreviousRc || !SHA2.test(leadDevelopment) || !SHA2.test(leadPreviousRc)) throw new Error("MM-Work development/rc SHA unproven; no cut started");
|
|
@@ -35233,7 +35391,7 @@ async function suiteRcand(deps, opts) {
|
|
|
35233
35391
|
}
|
|
35234
35392
|
const refusals = [];
|
|
35235
35393
|
for (const member of record.members) {
|
|
35236
|
-
if (member.published
|
|
35394
|
+
if (member.published) continue;
|
|
35237
35395
|
try {
|
|
35238
35396
|
await deps.preflight(member.repo);
|
|
35239
35397
|
} catch (e) {
|
|
@@ -35251,7 +35409,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
|
|
|
35251
35409
|
if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
|
|
35252
35410
|
throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
|
|
35253
35411
|
if (member.carried) {
|
|
35254
|
-
|
|
35412
|
+
await deps.publish(member.repo, pinVersion(member.rcPin));
|
|
35255
35413
|
continue;
|
|
35256
35414
|
}
|
|
35257
35415
|
if (!member.tag) {
|
|
@@ -35264,7 +35422,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
|
|
|
35264
35422
|
}
|
|
35265
35423
|
if ((await deps.tagSha(member.repo, member.tag))?.toLowerCase() !== member.sha.toLowerCase()) throw new Error(`${member.repo}: rc tag ${member.tag} moved or is unreadable; stop`);
|
|
35266
35424
|
if (!member.published) {
|
|
35267
|
-
|
|
35425
|
+
await deps.publish(member.repo, member.tag);
|
|
35268
35426
|
member.published = true;
|
|
35269
35427
|
writeSuiteCut(deps.recordPath, record);
|
|
35270
35428
|
}
|
|
@@ -35323,43 +35481,59 @@ async function suiteRelease(deps, ownerGo) {
|
|
|
35323
35481
|
if (hotfix) member.hotfixedTo = hotfix;
|
|
35324
35482
|
}
|
|
35325
35483
|
if (member.hotfixedTo && !member.released) continue;
|
|
35326
|
-
|
|
35327
|
-
|
|
35328
|
-
if (live.version !== expectedBackend && !(member.id === "katip" && !member.released && katipReleaseSha && live.version === katipReleaseSha))
|
|
35329
|
-
throw new Error(`${member.repo}: live backend changed after the cut (${expectedBackend} \u2192 ${live.version}); release refused`);
|
|
35484
|
+
if (live.version !== member.backend && !(member.hotfixedTo && await deps.hotfixOver(member.repo, planned, live.version) === member.hotfixedTo))
|
|
35485
|
+
throw new Error(`${member.repo}: live backend changed after the cut (${member.backend} \u2192 ${live.version}); release refused`);
|
|
35330
35486
|
}
|
|
35331
35487
|
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");
|
|
35488
|
+
const publishesMain = !record.leadReleased;
|
|
35489
|
+
const refusals = [];
|
|
35490
|
+
for (const member of record.members) {
|
|
35491
|
+
if (!publishesMain || member.carried || member.released) continue;
|
|
35492
|
+
try {
|
|
35493
|
+
await deps.preflightMain(member.repo, mainBundlePath(members.find((m) => m.repo === member.repo)?.bundles));
|
|
35494
|
+
} catch (e) {
|
|
35495
|
+
refusals.push(`${member.repo}: ${e.message}`);
|
|
35496
|
+
}
|
|
35497
|
+
}
|
|
35498
|
+
if (refusals.length) throw new Error(`module main serving is not ready; no member was released:
|
|
35499
|
+
${refusals.join("\n")}
|
|
35500
|
+
Each member's live release must serve <data>/main/module.js at its main pin path when <data>/main exists.`);
|
|
35332
35501
|
for (const member of record.members) {
|
|
35333
|
-
if (member.
|
|
35334
|
-
if (member.
|
|
35335
|
-
|
|
35336
|
-
|
|
35502
|
+
if (member.carried) continue;
|
|
35503
|
+
if (!member.released) {
|
|
35504
|
+
if (member.hotfixedTo) {
|
|
35505
|
+
console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
|
|
35506
|
+
} else {
|
|
35507
|
+
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)`);
|
|
35508
|
+
await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha);
|
|
35509
|
+
}
|
|
35337
35510
|
member.released = true;
|
|
35338
35511
|
writeSuiteCut(deps.recordPath, record);
|
|
35339
|
-
continue;
|
|
35340
35512
|
}
|
|
35341
|
-
if (
|
|
35342
|
-
|
|
35343
|
-
|
|
35344
|
-
|
|
35345
|
-
const live = await deps.backend({ repo: member.repo, id: member.id, bundles: {} });
|
|
35346
|
-
if (!expected || live.version !== expected) throw new Error(`${member.repo}: normal main deploy not stamped at released tag SHA; resume after verifying live backend`);
|
|
35347
|
-
member.backendAfterRelease = live.version;
|
|
35513
|
+
if (publishesMain && !member.mainPublished) {
|
|
35514
|
+
await deps.publishMain(member.repo, member.hotfixedTo ?? member.releasedAs ?? suiteReleaseTag(member.tag), mainBundlePath(members.find((m) => m.repo === member.repo)?.bundles));
|
|
35515
|
+
member.mainPublished = true;
|
|
35516
|
+
writeSuiteCut(deps.recordPath, record);
|
|
35348
35517
|
}
|
|
35349
|
-
member.released = true;
|
|
35350
|
-
writeSuiteCut(deps.recordPath, record);
|
|
35351
35518
|
}
|
|
35352
35519
|
if (!record.leadReleased) {
|
|
35353
35520
|
await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha);
|
|
35354
35521
|
record.leadReleased = true;
|
|
35355
35522
|
writeSuiteCut(deps.recordPath, record);
|
|
35356
35523
|
}
|
|
35357
|
-
if (record.announcement === "pending")
|
|
35524
|
+
if (record.announcement === "pending") {
|
|
35525
|
+
const reason = await deps.announceSkipReason(record);
|
|
35526
|
+
if (!reason) throw new Error("suite announcement outcome ambiguous; verify it before retrying (never post twice)");
|
|
35527
|
+
record.announcement = "skipped";
|
|
35528
|
+
record.announcementNote = reason;
|
|
35529
|
+
writeSuiteCut(deps.recordPath, record);
|
|
35530
|
+
}
|
|
35358
35531
|
if (!record.announcement) {
|
|
35359
35532
|
record.announcement = "pending";
|
|
35360
35533
|
writeSuiteCut(deps.recordPath, record);
|
|
35361
|
-
await deps.announce(record);
|
|
35362
|
-
record.announcement = "sent";
|
|
35534
|
+
const { skipped } = await deps.announce(record);
|
|
35535
|
+
record.announcement = skipped ? "skipped" : "sent";
|
|
35536
|
+
if (skipped) record.announcementNote = skipped;
|
|
35363
35537
|
writeSuiteCut(deps.recordPath, record);
|
|
35364
35538
|
}
|
|
35365
35539
|
return record;
|
|
@@ -42059,6 +42233,7 @@ function unlinkEscapingReparsePoints(root, primaryRoot) {
|
|
|
42059
42233
|
try {
|
|
42060
42234
|
entries = (0, import_node_fs40.readdirSync)(dir, { withFileTypes: true });
|
|
42061
42235
|
} catch (e) {
|
|
42236
|
+
if (e.code === "ENOENT") continue;
|
|
42062
42237
|
return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
|
|
42063
42238
|
}
|
|
42064
42239
|
for (const entry of entries) {
|
|
@@ -43877,7 +44052,7 @@ ${list}`);
|
|
|
43877
44052
|
console.error(line);
|
|
43878
44053
|
}
|
|
43879
44054
|
}
|
|
43880
|
-
const credential =
|
|
44055
|
+
const credential = "user";
|
|
43881
44056
|
if (o.json) printLine(JSON.stringify({ ...result, credential }));
|
|
43882
44057
|
else printLine(
|
|
43883
44058
|
`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
|
|
@@ -44392,9 +44567,8 @@ function trainApplyDeps() {
|
|
|
44392
44567
|
trainWriter: (verb, cwd) => acquireTrainWriterLock(cwd, `mmi-cli devops ${verb}`),
|
|
44393
44568
|
run: async (file, args) => {
|
|
44394
44569
|
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_TIMEOUT_MS : file === "npm" && (args[0] === "version" || args[0] === "ci" || args[0] === "install") ? NPM_FOLD_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file === "bash" && /jerv-gateway-release-deploy\.sh$/.test(args[0] ?? "") ? JERV_GATEWAY_DEPLOY_TIMEOUT_MS : file === "git" && args[0] === "commit" ? GIT_COMMIT_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
|
|
44395
|
-
const watchToken = file === "gh" && args[0] === "run" && args[1] === "watch" ? await pollToken() : void 0;
|
|
44396
44570
|
try {
|
|
44397
|
-
return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, file === "git" ? trainGitArgs(args) : args, { timeout
|
|
44571
|
+
return isWin3 && file === "npm" ? (await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP(file, file === "git" ? trainGitArgs(args) : args, { timeout })).stdout;
|
|
44398
44572
|
} catch (e) {
|
|
44399
44573
|
throw surfaceTrainSubprocessFailure(e, file, args);
|
|
44400
44574
|
}
|
|
@@ -45331,10 +45505,7 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45331
45505
|
throw new Error(`${member.repo}: healthy live backend stamp and edgeDomains.main must agree with DEPLOY#main; stop`);
|
|
45332
45506
|
const health = await probePublicHealth(url);
|
|
45333
45507
|
if (!health.ok) throw new Error(`${member.repo}: live backend health could not be proved at ${url}; stop`);
|
|
45334
|
-
|
|
45335
|
-
if (member.id === "katip" && (!rcHost || facts?.stages.rc?.domain !== rcHost))
|
|
45336
|
-
throw new Error(`${member.repo}: normal Katip rc host must agree with DEPLOY#rc; stop`);
|
|
45337
|
-
return { version: live.deployedTag, host, ...rcHost ? { rcHost } : {} };
|
|
45508
|
+
return { version: live.deployedTag, host };
|
|
45338
45509
|
};
|
|
45339
45510
|
const tagSha = async (repo, tag) => {
|
|
45340
45511
|
try {
|
|
@@ -45449,10 +45620,22 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45449
45620
|
const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`], existing[0]);
|
|
45450
45621
|
if (run.conclusion !== "success") throw new Error(`${repo}: module rc publish run ${run.id} not green${await rcRunCause(run.id)}`);
|
|
45451
45622
|
};
|
|
45623
|
+
const publishMain = async (repo, tag, path2) => {
|
|
45624
|
+
const title = `module-main-publish ${repo} @ ${tag}`;
|
|
45625
|
+
const existing = (await rcRuns()).filter((r) => r.displayTitle === title).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
|
|
45626
|
+
if (existing[0]?.conclusion === "success") return;
|
|
45627
|
+
if (existing[0]?.conclusion === "failure") throw new Error(`${repo}: prior module-main-publish ${tag} failed; inspect the run and retry it explicitly before continuing`);
|
|
45628
|
+
const run = await watchRcRun(title, [`repo=${repo}`, `tag=${tag}`, "channel=main", `path=${path2}`], existing[0]);
|
|
45629
|
+
if (run.conclusion !== "success") throw new Error(`${repo}: module main publish run ${run.id} not green${await rcRunCause(run.id)}`);
|
|
45630
|
+
};
|
|
45452
45631
|
const preflight2 = async (repo) => {
|
|
45453
45632
|
const run = await watchRcRun(`module-rc-preflight ${repo}`, [`repo=${repo}`, "preflight=true"], void 0);
|
|
45454
45633
|
if (run.conclusion !== "success") throw new Error(`rc serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
|
|
45455
45634
|
};
|
|
45635
|
+
const preflightMain = async (repo, path2) => {
|
|
45636
|
+
const run = await watchRcRun(`module-main-preflight ${repo}`, [`repo=${repo}`, "channel=main", `path=${path2}`, "preflight=true"], void 0);
|
|
45637
|
+
if (run.conclusion !== "success") throw new Error(`main serving preflight run ${run.id} failed${await rcRunCause(run.id)}`);
|
|
45638
|
+
};
|
|
45456
45639
|
const rcRunCause = async (id) => {
|
|
45457
45640
|
try {
|
|
45458
45641
|
return rcRunCauseFromLog(await exec("gh", ["run", "view", String(id), "--repo", "mutmutco/MMI-Hub", "--log-failed"]));
|
|
@@ -45493,6 +45676,8 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45493
45676
|
rcand: runRcand,
|
|
45494
45677
|
publish,
|
|
45495
45678
|
preflight: preflight2,
|
|
45679
|
+
publishMain,
|
|
45680
|
+
preflightMain,
|
|
45496
45681
|
pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
|
|
45497
45682
|
verifyLeadCut: async (sha) => {
|
|
45498
45683
|
const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
|
|
@@ -45516,7 +45701,15 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45516
45701
|
announce: async (record) => {
|
|
45517
45702
|
if (!summaryFile) throw new Error("suite announcement requires --announce-summary-file; outcome was not posted");
|
|
45518
45703
|
const result = await trainApplyDeps().announce?.({ repo: SUITE_REPO, tag: record.leadRcTag.replace(/-rc\.\d+$/, ""), summaryFile });
|
|
45704
|
+
if (result?.status === "skipped") return { skipped: result.note };
|
|
45519
45705
|
if (result?.status !== "announced") throw new Error(`suite announcement unverified: ${result?.note ?? "no announcement seam"}`);
|
|
45706
|
+
return {};
|
|
45707
|
+
},
|
|
45708
|
+
announceSkipReason: async () => {
|
|
45709
|
+
const slug = slugOf(SUITE_REPO);
|
|
45710
|
+
const read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
|
|
45711
|
+
if (!read.ok) throw new Error(`suite announcement: registry read failed for ${slug} (${read.error}); outcome stays ambiguous`);
|
|
45712
|
+
return read.project?.releaseChannel?.trim() ? null : `no releaseChannel configured for ${slug}; nothing could have been posted`;
|
|
45520
45713
|
}
|
|
45521
45714
|
};
|
|
45522
45715
|
}
|
package/package.json
CHANGED