@mutmutco/cli 4.5.59 → 4.5.60
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 +265 -130
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -8653,6 +8653,9 @@ async function upgradeMessage(res, body) {
|
|
|
8653
8653
|
if (res.status !== 426) return null;
|
|
8654
8654
|
return upgradeRequiredError(res, body ?? await readJsonBody(res));
|
|
8655
8655
|
}
|
|
8656
|
+
function safeServerReason(error) {
|
|
8657
|
+
return typeof error === "string" && error.length > 0 && error.length <= 200 && /^[\x20-\x7e]+$/.test(error) && error.includes(" ") && !/[A-Za-z0-9+/=_-]{32,}/.test(error);
|
|
8658
|
+
}
|
|
8656
8659
|
async function fetchSecretValue(deps, key, opts, diagnose = false) {
|
|
8657
8660
|
if (!isValidSecretKey(key)) return null;
|
|
8658
8661
|
const repo = await targetRepo(deps, opts);
|
|
@@ -8667,7 +8670,7 @@ async function fetchSecretValue(deps, key, opts, diagnose = false) {
|
|
|
8667
8670
|
if (diagnose) {
|
|
8668
8671
|
const body = await readJsonBody(res);
|
|
8669
8672
|
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";
|
|
8670
|
-
throw new Error(`${reason} (HTTP ${res.status}${
|
|
8673
|
+
throw new Error(`${reason} (HTTP ${res.status}${safeServerReason(body.error) ? `: ${body.error}` : ""})`);
|
|
8671
8674
|
}
|
|
8672
8675
|
return null;
|
|
8673
8676
|
}
|
|
@@ -14755,7 +14758,7 @@ function extractForwardRefs(markdown) {
|
|
|
14755
14758
|
});
|
|
14756
14759
|
return refs;
|
|
14757
14760
|
}
|
|
14758
|
-
function checkPins(root,
|
|
14761
|
+
function checkPins(root, readFile10, docs) {
|
|
14759
14762
|
const findings = [];
|
|
14760
14763
|
for (const [doc, markdown] of Object.entries(docs)) {
|
|
14761
14764
|
for (const pin of extractPins(markdown)) {
|
|
@@ -14763,7 +14766,7 @@ function checkPins(root, readFile9, docs) {
|
|
|
14763
14766
|
findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
|
|
14764
14767
|
continue;
|
|
14765
14768
|
}
|
|
14766
|
-
const source =
|
|
14769
|
+
const source = readFile10((0, import_node_path12.join)(root, pin.file));
|
|
14767
14770
|
if (source == null) {
|
|
14768
14771
|
findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
|
|
14769
14772
|
continue;
|
|
@@ -14980,7 +14983,7 @@ function defaultTrackedFirstSegments(root, firstSegments, exec2 = import_node_ch
|
|
|
14980
14983
|
}
|
|
14981
14984
|
}
|
|
14982
14985
|
function runDocRefs(root, deps = {}) {
|
|
14983
|
-
const
|
|
14986
|
+
const readFile10 = deps.readFile ?? readFileOrNull;
|
|
14984
14987
|
const exists = deps.exists ?? import_node_fs14.existsSync;
|
|
14985
14988
|
const listDocs = deps.listDocs ?? defaultListDocs;
|
|
14986
14989
|
const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
|
|
@@ -14989,11 +14992,11 @@ function runDocRefs(root, deps = {}) {
|
|
|
14989
14992
|
const walked = listDocs(root);
|
|
14990
14993
|
const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
|
|
14991
14994
|
const docs = Object.fromEntries(
|
|
14992
|
-
walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel,
|
|
14995
|
+
walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile10((0, import_node_path12.join)(root, rel))]).filter(([, body]) => body != null)
|
|
14993
14996
|
);
|
|
14994
14997
|
const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs);
|
|
14995
14998
|
const findings = [
|
|
14996
|
-
...checkPins(root,
|
|
14999
|
+
...checkPins(root, readFile10, docs).findings,
|
|
14997
15000
|
...refResult.findings
|
|
14998
15001
|
];
|
|
14999
15002
|
if (commandPaths == null) {
|
|
@@ -15772,22 +15775,22 @@ function renderBootstrapAncillaryPlan(labels, environments, exceptions = []) {
|
|
|
15772
15775
|
...exceptions.map(({ name, detail }) => ` OK environment ${name} (${detail})`)
|
|
15773
15776
|
].join("\n");
|
|
15774
15777
|
}
|
|
15775
|
-
function resolveSeedContent(seed, vars,
|
|
15776
|
-
if (seed.source === "self") return
|
|
15778
|
+
function resolveSeedContent(seed, vars, readFile10) {
|
|
15779
|
+
if (seed.source === "self") return readFile10(seed.target);
|
|
15777
15780
|
if (seed.source.startsWith("seed:")) {
|
|
15778
|
-
const tmpl =
|
|
15781
|
+
const tmpl = readFile10(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
15779
15782
|
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
15780
15783
|
}
|
|
15781
15784
|
return null;
|
|
15782
15785
|
}
|
|
15783
|
-
function resolveSeedWriteContent(seed, vars,
|
|
15786
|
+
function resolveSeedWriteContent(seed, vars, readFile10, remoteContent) {
|
|
15784
15787
|
if (!seed.managedBlock) {
|
|
15785
|
-
return { ok: true, content: resolveSeedContent(seed, vars,
|
|
15788
|
+
return { ok: true, content: resolveSeedContent(seed, vars, readFile10), managed: seed.source === "managed-block" };
|
|
15786
15789
|
}
|
|
15787
|
-
const base = remoteContent ?? resolveSeedContent(seed, vars,
|
|
15790
|
+
const base = remoteContent ?? resolveSeedContent(seed, vars, readFile10);
|
|
15788
15791
|
if (base == null) return { ok: true, content: null, managed: true };
|
|
15789
15792
|
const blockSeed = { ...seed, source: seed.managedBlock.source, managedBlock: void 0 };
|
|
15790
|
-
const desired = resolveSeedContent(blockSeed, vars,
|
|
15793
|
+
const desired = resolveSeedContent(blockSeed, vars, readFile10);
|
|
15791
15794
|
if (desired == null) return { ok: true, content: null, managed: true };
|
|
15792
15795
|
const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
|
|
15793
15796
|
return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
|
|
@@ -17148,10 +17151,10 @@ var rollout_plan_default = {
|
|
|
17148
17151
|
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)."
|
|
17149
17152
|
},
|
|
17150
17153
|
baseline: {
|
|
17151
|
-
version: "4.5.
|
|
17152
|
-
tag: "v4.5.
|
|
17153
|
-
commit: "
|
|
17154
|
-
npm: "@mutmutco/cli@4.5.
|
|
17154
|
+
version: "4.5.60",
|
|
17155
|
+
tag: "v4.5.60",
|
|
17156
|
+
commit: "18fb882893af",
|
|
17157
|
+
npm: "@mutmutco/cli@4.5.60"
|
|
17155
17158
|
},
|
|
17156
17159
|
exitCriterion: "fleet-n-of-n",
|
|
17157
17160
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17168,14 +17171,14 @@ var rollout_plan_default = {
|
|
|
17168
17171
|
repo: "mutmutco/mmi-hub",
|
|
17169
17172
|
role: "canary",
|
|
17170
17173
|
schedule: "train",
|
|
17171
|
-
v3Target: "v4.5.
|
|
17174
|
+
v3Target: "v4.5.60"
|
|
17172
17175
|
}
|
|
17173
17176
|
],
|
|
17174
17177
|
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.",
|
|
17175
17178
|
rollback: {
|
|
17176
17179
|
independent: true,
|
|
17177
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17178
|
-
v3Target: "v4.5.
|
|
17180
|
+
mechanism: "npm dist-tag latest -> 4.5.60 and redeploy the Hub Lambda from tag v4.5.60 (18fb882893af); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17181
|
+
v3Target: "v4.5.60 (@mutmutco/cli@4.5.60, tag commit 18fb882893af \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17179
17182
|
}
|
|
17180
17183
|
},
|
|
17181
17184
|
{
|
|
@@ -21362,7 +21365,7 @@ function renderTenantControl(r) {
|
|
|
21362
21365
|
|
|
21363
21366
|
// src/train-apply-phases.ts
|
|
21364
21367
|
var import_node_fs24 = require("node:fs");
|
|
21365
|
-
var
|
|
21368
|
+
var import_promises4 = require("node:fs/promises");
|
|
21366
21369
|
var import_node_path24 = require("node:path");
|
|
21367
21370
|
|
|
21368
21371
|
// src/actions-billing-preflight.ts
|
|
@@ -25049,6 +25052,59 @@ function formatTrainDoctor(v) {
|
|
|
25049
25052
|
return lines2.join("\n");
|
|
25050
25053
|
}
|
|
25051
25054
|
|
|
25055
|
+
// src/release-plain-words.ts
|
|
25056
|
+
var import_promises3 = require("node:fs/promises");
|
|
25057
|
+
var PLAIN_WORDS_HEADING = "## In plain words";
|
|
25058
|
+
var PLAIN_WORDS_MARKER = "<!-- mmi:big -->";
|
|
25059
|
+
function buildPlainWordsSection(lines2, big) {
|
|
25060
|
+
const [title, ...notes] = lines2;
|
|
25061
|
+
const body = [PLAIN_WORDS_HEADING, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
|
|
25062
|
+
if (big) body.push(PLAIN_WORDS_MARKER);
|
|
25063
|
+
return body.join("\n");
|
|
25064
|
+
}
|
|
25065
|
+
function noChangePlainWordsLine(suiteTag) {
|
|
25066
|
+
return `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}.`;
|
|
25067
|
+
}
|
|
25068
|
+
function upsertPlainWordsSection(body, section) {
|
|
25069
|
+
const lines2 = body.split(/\r?\n/);
|
|
25070
|
+
const at = lines2.findIndex((l) => l.trim() === PLAIN_WORDS_HEADING);
|
|
25071
|
+
let rest = body;
|
|
25072
|
+
if (at >= 0) {
|
|
25073
|
+
let end = lines2.length;
|
|
25074
|
+
for (let i = at + 1; i < lines2.length; i++) {
|
|
25075
|
+
if (lines2[i].trim().startsWith("## ")) {
|
|
25076
|
+
end = i;
|
|
25077
|
+
break;
|
|
25078
|
+
}
|
|
25079
|
+
}
|
|
25080
|
+
rest = [...lines2.slice(0, at), ...lines2.slice(end)].join("\n");
|
|
25081
|
+
}
|
|
25082
|
+
const trimmedRest = rest.trim();
|
|
25083
|
+
return trimmedRest ? `${section}
|
|
25084
|
+
|
|
25085
|
+
${trimmedRest}` : section;
|
|
25086
|
+
}
|
|
25087
|
+
function plainWordsSummaryLines(content) {
|
|
25088
|
+
return content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, 6);
|
|
25089
|
+
}
|
|
25090
|
+
async function writePlainWordsIfSuiteMember(deps, args) {
|
|
25091
|
+
if (args.meta?.suite !== "mm-work") return;
|
|
25092
|
+
try {
|
|
25093
|
+
const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag ?? args.tag)];
|
|
25094
|
+
const section = buildPlainWordsSection(lines2, args.big === true);
|
|
25095
|
+
const body = clean2(await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body", "-q", ".body"]));
|
|
25096
|
+
await deps.run("gh", ["release", "edit", args.tag, "--repo", args.repo, "--notes", upsertPlainWordsSection(body, section)]);
|
|
25097
|
+
} catch {
|
|
25098
|
+
}
|
|
25099
|
+
}
|
|
25100
|
+
async function writePlainWordsSection(deps, options, repo, meta, tag) {
|
|
25101
|
+
try {
|
|
25102
|
+
const summaryLines = options.announceSummaryFile ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFile)) : void 0;
|
|
25103
|
+
await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, big: options.big, suiteTag: options.suiteTag });
|
|
25104
|
+
} catch {
|
|
25105
|
+
}
|
|
25106
|
+
}
|
|
25107
|
+
|
|
25052
25108
|
// src/vercel-version-env.ts
|
|
25053
25109
|
var SKIPPED = { status: "skipped", note: "" };
|
|
25054
25110
|
function vercelVersionEnvAheadNote(key) {
|
|
@@ -25349,7 +25405,7 @@ async function waitForFoldIndexLock(deps, startBranch, resumeCommand) {
|
|
|
25349
25405
|
const deadline = now() + GIT_INDEX_LOCK_WAIT_MS;
|
|
25350
25406
|
for (; ; ) {
|
|
25351
25407
|
try {
|
|
25352
|
-
await (0,
|
|
25408
|
+
await (0, import_promises4.stat)(lockPath);
|
|
25353
25409
|
} catch (e) {
|
|
25354
25410
|
if (e.code === "ENOENT") return;
|
|
25355
25411
|
throw foldFailureGuidance(
|
|
@@ -25385,7 +25441,7 @@ async function clearFoldChildIndexLock(deps) {
|
|
|
25385
25441
|
"git index lock path"
|
|
25386
25442
|
);
|
|
25387
25443
|
try {
|
|
25388
|
-
await (0,
|
|
25444
|
+
await (0, import_promises4.unlink)(lockPath);
|
|
25389
25445
|
} catch (e) {
|
|
25390
25446
|
if (e.code === "ENOENT") return void 0;
|
|
25391
25447
|
throw e;
|
|
@@ -25984,6 +26040,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
25984
26040
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
25985
26041
|
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
25986
26042
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
26043
|
+
await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
|
|
25987
26044
|
await recordPhase(ledger, deps, ledgerAnchors, "githubRelease", phaseEntry({ state: "complete", sha: releaseSha, runUrl: releaseUrl, note: `verified GitHub Release ${tag} against ${releaseSha.slice(0, 12)}` }), { landed: "the immutable tag, the green required-check wall, and the origin/main fast-forward" });
|
|
25988
26045
|
let alignment = alignEarly ? await alignEarly() : void 0;
|
|
25989
26046
|
const prepared = prepareServedChannel(deps, { repo: ctx.repo, releaseSha, watch });
|
|
@@ -26543,6 +26600,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
26543
26600
|
steps.push(`created the GitHub Release for ${tag}`);
|
|
26544
26601
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
|
|
26545
26602
|
steps.push("verified the Release published against the tagged commit");
|
|
26603
|
+
await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
|
|
26546
26604
|
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, runUrl: releaseUrl, note: "verified the Release published against the tagged commit" }), { strict: ledgerMode === "strict", landed: "the immutable tag, the re-proven wall, and the origin/main push" });
|
|
26547
26605
|
let devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
26548
26606
|
let rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
@@ -27280,6 +27338,45 @@ var REQUIRED_CONTEXTS_EMITTED_LABEL = "required check contexts match PR workflow
|
|
|
27280
27338
|
var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
|
|
27281
27339
|
var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
|
|
27282
27340
|
var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
|
|
27341
|
+
var DEV_STAGE_FRESH_LABEL = "tenant dev stage runs the development tip (#7536)";
|
|
27342
|
+
var DEV_STAGE_STALE_TOLERANCE_MS = 30 * 60 * 1e3;
|
|
27343
|
+
function devStageFreshnessCheck(input) {
|
|
27344
|
+
const { repo, deployedTag, tipSha, tipCommittedAt, nowMs } = input;
|
|
27345
|
+
const label = DEV_STAGE_FRESH_LABEL;
|
|
27346
|
+
const cannotAudit = (why) => ({ ok: true, label, detail: `cannot audit: ${why}` });
|
|
27347
|
+
if (!tipSha) return cannotAudit("development tip unreadable");
|
|
27348
|
+
if (!deployedTag) return cannotAudit("deployed dev SHA unreadable or never stamped");
|
|
27349
|
+
if (deployedTag.toLowerCase() === tipSha.toLowerCase()) return { ok: true, label };
|
|
27350
|
+
const ageMs = nowMs - Date.parse(tipCommittedAt ?? "");
|
|
27351
|
+
if (!Number.isFinite(ageMs)) return cannotAudit("development tip commit time unreadable");
|
|
27352
|
+
const minutes = Math.max(0, Math.round(ageMs / 6e4));
|
|
27353
|
+
const behind = `dev runs ${deployedTag.slice(0, 7)}, development is at ${tipSha.slice(0, 7)} (${minutes} min old)`;
|
|
27354
|
+
if (ageMs <= DEV_STAGE_STALE_TOLERANCE_MS) return { ok: true, label, detail: `deploy pending: ${behind}` };
|
|
27355
|
+
return {
|
|
27356
|
+
ok: false,
|
|
27357
|
+
label,
|
|
27358
|
+
detail: behind,
|
|
27359
|
+
remediation: `mmi-cli devops runtime tenant redeploy ${repo} dev --watch \u2014 then read the Hub's "dev-auto-deploy ${repo}" log lines for why the merge did not deploy`
|
|
27360
|
+
};
|
|
27361
|
+
}
|
|
27362
|
+
async function auditDevStageFreshness(repo, deps) {
|
|
27363
|
+
const facts = await deps.getDeployFacts(slugFromRepo(repo)).catch(() => null);
|
|
27364
|
+
const dev = facts?.stages.dev;
|
|
27365
|
+
if (facts && !dev?.present) return void 0;
|
|
27366
|
+
let tip;
|
|
27367
|
+
try {
|
|
27368
|
+
tip = await deps.client.rest("GET", `repos/${repo}/commits/development`);
|
|
27369
|
+
} catch {
|
|
27370
|
+
tip = void 0;
|
|
27371
|
+
}
|
|
27372
|
+
return devStageFreshnessCheck({
|
|
27373
|
+
repo,
|
|
27374
|
+
deployedTag: dev?.deployedTag,
|
|
27375
|
+
tipSha: tip?.sha,
|
|
27376
|
+
tipCommittedAt: tip?.commit?.committer?.date,
|
|
27377
|
+
nowMs: Date.now()
|
|
27378
|
+
});
|
|
27379
|
+
}
|
|
27283
27380
|
var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
|
|
27284
27381
|
var RECONCILE_SEED_BRANCH_PREFIX = "bootstrap-seed-ruleset-ref";
|
|
27285
27382
|
var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
|
|
@@ -27919,6 +28016,10 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
27919
28016
|
const workflows = await getBranchWorkflows();
|
|
27920
28017
|
if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
|
|
27921
28018
|
}
|
|
28019
|
+
if (meta?.deployModel === "tenant-container" && deps.getDeployFacts) {
|
|
28020
|
+
const fresh = await auditDevStageFreshness(repo, deps);
|
|
28021
|
+
if (fresh) checks.push(fresh);
|
|
28022
|
+
}
|
|
27922
28023
|
const failure = unauditedReason();
|
|
27923
28024
|
if (failure) {
|
|
27924
28025
|
return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
|
|
@@ -30369,7 +30470,7 @@ init_hub_auth();
|
|
|
30369
30470
|
init_github_client();
|
|
30370
30471
|
|
|
30371
30472
|
// src/project-set.ts
|
|
30372
|
-
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv"];
|
|
30473
|
+
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "suiteHold"];
|
|
30373
30474
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
30374
30475
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
30375
30476
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -30869,6 +30970,7 @@ var SETTABLE_VAR_KEYS = [
|
|
|
30869
30970
|
"branch",
|
|
30870
30971
|
"vaultPath",
|
|
30871
30972
|
"suite",
|
|
30973
|
+
"suiteHold",
|
|
30872
30974
|
"repos",
|
|
30873
30975
|
"oauth",
|
|
30874
30976
|
"publishRequired",
|
|
@@ -30903,6 +31005,7 @@ var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
|
30903
31005
|
var SETTABLE_VAR_HINTS = {
|
|
30904
31006
|
projectNumber: "numeric",
|
|
30905
31007
|
suite: "lowercase kebab slug, e.g. mm-work",
|
|
31008
|
+
suiteHold: 'JSON {reason, since?} \u2014 cuts the member out of the MM-Work suite train (e.g. {"reason":"frozen pending go-live proof"}); clear with --unset suiteHold (#7547)',
|
|
30906
31009
|
publishRequired: "true|false",
|
|
30907
31010
|
publishDir: "relative subpath, e.g. packages/ui",
|
|
30908
31011
|
releaseChannel: "Slack channel id, e.g. C0BE0BRGAT0",
|
|
@@ -30981,6 +31084,20 @@ function clearSecretsConfirmationError(unsets, existing, confirmed) {
|
|
|
30981
31084
|
if (confirmed) return null;
|
|
30982
31085
|
return `org project set: --unset secrets clears the WHOLE catalog for ${existing?.slug ?? "this project"} (${count} declared entr${count === 1 ? "y" : "ies"} would be removed). To drop ONE entry use --unset-secret <KEY>; re-run with --confirm-clear-secrets to wipe all ${count}.`;
|
|
30983
31086
|
}
|
|
31087
|
+
function parseSuiteHoldVar(raw) {
|
|
31088
|
+
let parsed;
|
|
31089
|
+
try {
|
|
31090
|
+
parsed = JSON.parse(raw);
|
|
31091
|
+
} catch {
|
|
31092
|
+
throw new Error('org project set: suiteHold must be a JSON object {"reason":"...","since":"..."}');
|
|
31093
|
+
}
|
|
31094
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error('org project set: suiteHold must be a JSON object {"reason":"...","since":"..."}');
|
|
31095
|
+
const hold = parsed;
|
|
31096
|
+
if (typeof hold.reason !== "string" || !hold.reason.trim()) throw new Error("org project set: suiteHold.reason must be a non-empty string naming the freeze");
|
|
31097
|
+
if (hold.since !== void 0 && (typeof hold.since !== "string" || !hold.since.trim())) throw new Error('org project set: suiteHold.since must be a non-empty string (e.g. "2026-09-28")');
|
|
31098
|
+
if (Object.keys(hold).some((k) => k !== "reason" && k !== "since")) throw new Error("org project set: suiteHold accepts only reason and since");
|
|
31099
|
+
return { reason: hold.reason, ...typeof hold.since === "string" ? { since: hold.since } : {} };
|
|
31100
|
+
}
|
|
30984
31101
|
function buildProjectSetPatch(input) {
|
|
30985
31102
|
const patch = {};
|
|
30986
31103
|
if (input.class) {
|
|
@@ -31058,6 +31175,8 @@ function buildProjectSetPatch(input) {
|
|
|
31058
31175
|
} else if (key === "suite") {
|
|
31059
31176
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(raw)) throw new Error("org project set: suite must be a lowercase kebab slug (e.g. mm-work)");
|
|
31060
31177
|
patch[key] = raw;
|
|
31178
|
+
} else if (key === "suiteHold") {
|
|
31179
|
+
patch[key] = parseSuiteHoldVar(raw);
|
|
31061
31180
|
} else if (key === "releaseChannel") {
|
|
31062
31181
|
patch[key] = parseReleaseChannelVar(raw);
|
|
31063
31182
|
} else if (key === "releaseLanguage") {
|
|
@@ -33984,7 +34103,7 @@ function registerBootstrapCommands(program3) {
|
|
|
33984
34103
|
}
|
|
33985
34104
|
const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
|
|
33986
34105
|
const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
|
|
33987
|
-
const
|
|
34106
|
+
const readFile10 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
|
|
33988
34107
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
33989
34108
|
const putSeed = async (target, content, ref, sha) => {
|
|
33990
34109
|
const tmp = (0, import_node_path28.join)((0, import_node_os17.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
@@ -34140,7 +34259,7 @@ function registerBootstrapCommands(program3) {
|
|
|
34140
34259
|
registryMeta?.requiredCheckBranches,
|
|
34141
34260
|
registryMeta?.requiredChecks
|
|
34142
34261
|
);
|
|
34143
|
-
const rendered = resolveSeedContent(gateSeed, gateVars,
|
|
34262
|
+
const rendered = resolveSeedContent(gateSeed, gateVars, readFile10);
|
|
34144
34263
|
if (rendered === null || missingPlaceholders(rendered).length) return failGraceful("bootstrap apply: registry gate template is unreadable or has unfilled placeholders");
|
|
34145
34264
|
const base = (await readTargetedSeedFiles(repo, scopedPlan, onlyTarget, scopedPlan.baseSha, controlClient)).base?.content ?? null;
|
|
34146
34265
|
configGateAction = planGateFromConfig(base, rendered);
|
|
@@ -34244,7 +34363,7 @@ function registerBootstrapCommands(program3) {
|
|
|
34244
34363
|
...vars,
|
|
34245
34364
|
...registryMeta?.requiredChecks?.length ? { GATE_RULESET_CONTEXTS_JSON: rulesetVars.GATE_RULESET_CONTEXTS_JSON } : {},
|
|
34246
34365
|
...registryMeta?.requiredCheckBranches?.length ? { GATE_RULESET_BRANCH_REFS_JSON: rulesetVars.GATE_RULESET_BRANCH_REFS_JSON } : {}
|
|
34247
|
-
} : vars,
|
|
34366
|
+
} : vars, readFile10, preservedContent);
|
|
34248
34367
|
if (!writeContent.ok) {
|
|
34249
34368
|
return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
34250
34369
|
}
|
|
@@ -34399,7 +34518,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
34399
34518
|
}
|
|
34400
34519
|
const rulesetSeed = repo.toLowerCase() === "mutmutco/mmi-hub" ? void 0 : manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
34401
34520
|
if (rulesetSeed) {
|
|
34402
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
34521
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile10);
|
|
34403
34522
|
if (rulesetContent) {
|
|
34404
34523
|
try {
|
|
34405
34524
|
const client = controlClient;
|
|
@@ -35260,6 +35379,16 @@ function pathOf(url) {
|
|
|
35260
35379
|
}
|
|
35261
35380
|
}
|
|
35262
35381
|
var bundlesOf = (members) => Object.fromEntries(members.map((m) => [m.id, m.bundles]));
|
|
35382
|
+
async function holdSplit(deps, modules) {
|
|
35383
|
+
const pairs = await Promise.all(modules.map(async (m) => [m, await deps.hold(m)]));
|
|
35384
|
+
return {
|
|
35385
|
+
active: pairs.filter(([, hold]) => !hold).map(([m]) => m),
|
|
35386
|
+
held: pairs.flatMap(([m, hold]) => hold ? [{ id: m.id, repo: m.repo, reason: hold.reason, ...hold.since ? { since: hold.since } : {} }] : [])
|
|
35387
|
+
};
|
|
35388
|
+
}
|
|
35389
|
+
function heldMatches(recorded, now) {
|
|
35390
|
+
return recorded.length === now.length && now.every((h) => recorded.some((r) => r.id === h.id && r.repo === h.repo));
|
|
35391
|
+
}
|
|
35263
35392
|
function suiteReleaseTag(rc) {
|
|
35264
35393
|
if (!RC_TAG.test(rc)) throw new Error(`invalid rc tag ${rc}`);
|
|
35265
35394
|
return rc.replace(/-rc\.\d+$/, "");
|
|
@@ -35278,7 +35407,7 @@ function readSuiteCut(path2) {
|
|
|
35278
35407
|
} catch {
|
|
35279
35408
|
throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
|
|
35280
35409
|
}
|
|
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)))
|
|
35410
|
+
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.heldMembers !== void 0 && (!Array.isArray(record.heldMembers) || record.heldMembers.some((h) => !h.id || !h.repo || typeof h.reason !== "string" || !h.reason.trim() || h.since !== void 0 && typeof h.since !== "string" || record.members.some((m) => m.id === h.id))) || 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)))
|
|
35282
35411
|
throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
|
|
35283
35412
|
return record;
|
|
35284
35413
|
}
|
|
@@ -35333,8 +35462,10 @@ async function suiteRcand(deps, opts) {
|
|
|
35333
35462
|
if (!opts.ownerGo) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
|
|
35334
35463
|
const members = suiteMembers(await deps.registry());
|
|
35335
35464
|
const modules = members.filter((m) => m.repo !== SUITE_REPO);
|
|
35465
|
+
const { active, held } = await holdSplit(deps, modules);
|
|
35466
|
+
const heldById = new Map(held.map((h) => [h.id, h]));
|
|
35336
35467
|
const plan = [
|
|
35337
|
-
...modules.map((m) => `${m.repo}: normal rcand + module-rc-publish.yml (once)`),
|
|
35468
|
+
...modules.map((m) => heldById.has(m.id) ? `${m.repo}: HELD: ${heldById.get(m.id).reason}` : `${m.repo}: normal rcand + module-rc-publish.yml (once)`),
|
|
35338
35469
|
`${SUITE_REPO}: one PR for both rc/main pins, wait for merge`,
|
|
35339
35470
|
`${SUITE_REPO}: rcand`
|
|
35340
35471
|
];
|
|
@@ -35354,7 +35485,7 @@ async function suiteRcand(deps, opts) {
|
|
|
35354
35485
|
const [leadDevelopment, leadPreviousRc, snapshots] = await Promise.all([
|
|
35355
35486
|
deps.head(SUITE_REPO, "development"),
|
|
35356
35487
|
deps.head(SUITE_REPO, "rc"),
|
|
35357
|
-
Promise.all(
|
|
35488
|
+
Promise.all(active.map(async (m) => {
|
|
35358
35489
|
const [backend, development, rcHead] = await Promise.all([deps.backend(m), deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
|
|
35359
35490
|
const previousRc = rcHead ?? await deps.head(m.repo, "main");
|
|
35360
35491
|
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`);
|
|
@@ -35362,12 +35493,14 @@ async function suiteRcand(deps, opts) {
|
|
|
35362
35493
|
}))
|
|
35363
35494
|
]);
|
|
35364
35495
|
if (!leadDevelopment || !leadPreviousRc || !SHA2.test(leadDevelopment) || !SHA2.test(leadPreviousRc)) throw new Error("MM-Work development/rc SHA unproven; no cut started");
|
|
35365
|
-
const created = { schema: SCHEMA, lead: SUITE_REPO, leadDevelopment, leadPreviousRc, members: snapshots };
|
|
35496
|
+
const created = { schema: SCHEMA, lead: SUITE_REPO, leadDevelopment, leadPreviousRc, members: snapshots, ...held.length ? { heldMembers: held } : {} };
|
|
35366
35497
|
writeSuiteCut(deps.recordPath, created);
|
|
35367
35498
|
record = created;
|
|
35368
35499
|
}
|
|
35369
35500
|
if (!record) throw new Error("suite cut record unavailable after initialization");
|
|
35370
|
-
if (
|
|
35501
|
+
if (!heldMatches(record.heldMembers ?? [], held))
|
|
35502
|
+
throw new Error("suite hold changed since cut began; finish or abandon this cut, then re-cut with the new holds");
|
|
35503
|
+
if (record.members.length !== active.length || active.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id)))
|
|
35371
35504
|
throw new Error("suite roster changed since cut began; refuse to omit or add a member");
|
|
35372
35505
|
const quiet = [];
|
|
35373
35506
|
for (const member of record.members) {
|
|
@@ -35437,7 +35570,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
|
|
|
35437
35570
|
}
|
|
35438
35571
|
const pins = suitePins(record, bundlesOf(modules));
|
|
35439
35572
|
await deps.verifyLeadCut(record.leadDevelopment);
|
|
35440
|
-
const pr = await deps.pin({ cut: record.leadDevelopment, pins });
|
|
35573
|
+
const pr = await deps.pin({ cut: record.leadDevelopment, pins, heldIds: record.heldMembers?.map((h) => h.id) });
|
|
35441
35574
|
await deps.verifyLeadCut(record.leadDevelopment);
|
|
35442
35575
|
if (record.pinPr && record.pinPr !== pr.pr) throw new Error(`pin PR changed from ${record.pinPr} to ${pr.pr}; stop`);
|
|
35443
35576
|
record.pinPr = pr.pr;
|
|
@@ -35454,12 +35587,14 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
|
|
|
35454
35587
|
}
|
|
35455
35588
|
return { plan, record };
|
|
35456
35589
|
}
|
|
35457
|
-
async function suiteRelease(deps, ownerGo) {
|
|
35590
|
+
async function suiteRelease(deps, ownerGo, big = false) {
|
|
35458
35591
|
if (!ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
|
|
35459
35592
|
const record = readSuiteCut(deps.recordPath);
|
|
35460
35593
|
if (!record?.pinMerged || !record.leadRcTag) throw new Error("no completed suite rc cut and merged pin PR; release refused");
|
|
35461
35594
|
const members = suiteMembers(await deps.registry()).filter((m) => m.repo !== SUITE_REPO);
|
|
35462
|
-
|
|
35595
|
+
const { active, held } = await holdSplit(deps, members);
|
|
35596
|
+
if (!heldMatches(record.heldMembers ?? [], held)) throw new Error("suite hold changed after the cut; release refused");
|
|
35597
|
+
if (active.length !== record.members.length || active.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id))) throw new Error("suite roster changed after the cut; release refused");
|
|
35463
35598
|
await deps.verifyPins(suitePins(record, bundlesOf(members)), record.leadRcTag);
|
|
35464
35599
|
for (const member of record.members) {
|
|
35465
35600
|
if (member.carried) {
|
|
@@ -35505,7 +35640,7 @@ Each member's live release must serve <data>/main/module.js at its main pin path
|
|
|
35505
35640
|
console.warn(`${member.repo}: released as ${member.releasedAs ?? suiteReleaseTag(member.tag)} and hotfixed to ${member.hotfixedTo}, which is live; recorded as released (#7482)`);
|
|
35506
35641
|
} else {
|
|
35507
35642
|
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);
|
|
35643
|
+
await deps.release(member.repo, member.releasedAs ?? suiteReleaseTag(member.tag), member.sha, { suiteTag: suiteReleaseTag(record.leadRcTag) });
|
|
35509
35644
|
}
|
|
35510
35645
|
member.released = true;
|
|
35511
35646
|
writeSuiteCut(deps.recordPath, record);
|
|
@@ -35517,7 +35652,7 @@ Each member's live release must serve <data>/main/module.js at its main pin path
|
|
|
35517
35652
|
}
|
|
35518
35653
|
}
|
|
35519
35654
|
if (!record.leadReleased) {
|
|
35520
|
-
await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha);
|
|
35655
|
+
await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha, { big });
|
|
35521
35656
|
record.leadReleased = true;
|
|
35522
35657
|
writeSuiteCut(deps.recordPath, record);
|
|
35523
35658
|
}
|
|
@@ -35545,14 +35680,14 @@ var import_node_path43 = require("node:path");
|
|
|
35545
35680
|
init_cli_shared();
|
|
35546
35681
|
|
|
35547
35682
|
// src/command-register-train-operations.ts
|
|
35548
|
-
var
|
|
35683
|
+
var import_promises9 = require("node:fs/promises");
|
|
35549
35684
|
init_clean_exit();
|
|
35550
35685
|
init_cli_shared();
|
|
35551
35686
|
init_client_version();
|
|
35552
35687
|
init_house_map();
|
|
35553
35688
|
|
|
35554
35689
|
// src/hotfix-apply.ts
|
|
35555
|
-
var
|
|
35690
|
+
var import_promises5 = require("node:fs/promises");
|
|
35556
35691
|
|
|
35557
35692
|
// src/release-announce.ts
|
|
35558
35693
|
var ANNOUNCE_REPO = "mutmutco/MMI-Hub";
|
|
@@ -36521,7 +36656,7 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
|
|
|
36521
36656
|
}
|
|
36522
36657
|
let lines2;
|
|
36523
36658
|
try {
|
|
36524
|
-
lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0,
|
|
36659
|
+
lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0, import_promises5.readFile)(p, "utf8")))(options.announceSummaryFile));
|
|
36525
36660
|
} catch (e) {
|
|
36526
36661
|
throw new Error(`could not read --announce-summary-file ${options.announceSummaryFile}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
36527
36662
|
}
|
|
@@ -36572,6 +36707,7 @@ ${decision.note}`);
|
|
|
36572
36707
|
await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", "main", "--generate-notes", "--latest"]);
|
|
36573
36708
|
releaseNote = `Release ${tag} created (target main)`;
|
|
36574
36709
|
}
|
|
36710
|
+
await writePlainWordsSection(deps, options, ctx.repo, meta, tag);
|
|
36575
36711
|
await recordHotfixPhase(ledger, anchors, "githubRelease", { state: "complete", sha: mergedSha, note: releaseNote }, "the immutable tag, the green wall, and origin/main");
|
|
36576
36712
|
const prepared = prepareServedChannel(deps, { repo: ctx.repo, releaseSha: mergedSha, watch: true });
|
|
36577
36713
|
const runs = [];
|
|
@@ -37896,8 +38032,8 @@ function annotateChangeMeaning(changed, policy, read) {
|
|
|
37896
38032
|
function isMeaningfulRow(file) {
|
|
37897
38033
|
return file.meaningful !== false;
|
|
37898
38034
|
}
|
|
37899
|
-
function loadPolicy(root,
|
|
37900
|
-
const raw =
|
|
38035
|
+
function loadPolicy(root, readFile10 = readFileOrNull2) {
|
|
38036
|
+
const raw = readFile10((0, import_node_path30.join)(root, POLICY_FILE));
|
|
37901
38037
|
if (raw == null) return { mandatory: [], declared: false };
|
|
37902
38038
|
return parsePolicy(raw, POLICY_FILE);
|
|
37903
38039
|
}
|
|
@@ -38845,12 +38981,16 @@ function releaseProgressVerdict(status, subject = "Release") {
|
|
|
38845
38981
|
}
|
|
38846
38982
|
|
|
38847
38983
|
// src/suite-pin.ts
|
|
38848
|
-
var
|
|
38984
|
+
var import_promises6 = require("node:fs/promises");
|
|
38849
38985
|
var import_node_path34 = require("node:path");
|
|
38850
38986
|
async function openSuiteCutPinPr(deps, input) {
|
|
38851
38987
|
if (!/^[a-f0-9]{40}$/.test(input.cut) || !input.pins.length) throw new Error("suite pin PR needs an anchored cut and at least one module");
|
|
38852
38988
|
const ids = input.pins.map((p) => p.module);
|
|
38853
38989
|
if (new Set(ids).size !== ids.length) throw new Error("suite pin PR contains a duplicate module");
|
|
38990
|
+
const heldIds = input.heldIds ?? [];
|
|
38991
|
+
if (new Set(heldIds).size !== heldIds.length) throw new Error("suite pin PR contains a duplicate held member");
|
|
38992
|
+
const covered = [...ids, ...heldIds];
|
|
38993
|
+
if (new Set(covered).size !== covered.length) throw new Error("suite pin PR holds a member it also pins");
|
|
38854
38994
|
const branch = `suite-pin/cut-${input.cut.slice(0, 16)}`;
|
|
38855
38995
|
const registry2 = async (ref = "development") => {
|
|
38856
38996
|
const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`]));
|
|
@@ -38861,7 +39001,7 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
38861
39001
|
try {
|
|
38862
39002
|
const rows = await registry2();
|
|
38863
39003
|
const members = suiteMembers(rows).filter((m) => m.repo !== SUITE_REPO);
|
|
38864
|
-
if (members.length !==
|
|
39004
|
+
if (members.length !== covered.length || members.some((m) => !covered.includes(m.id))) throw new Error("cut pins do not match the complete MM-Work roster");
|
|
38865
39005
|
for (const pin of input.pins) {
|
|
38866
39006
|
for (const url of [pin.rc, pin.main]) {
|
|
38867
39007
|
const parsed = new URL(url);
|
|
@@ -38883,8 +39023,8 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
38883
39023
|
});
|
|
38884
39024
|
if (prs.length && matches(await registry2(branch))) return { status: "pending", pr: prs[0].url };
|
|
38885
39025
|
if (!prs.length && matches(rows)) throw new Error("pins already match without a merged cut PR \u2014 provenance unverified");
|
|
38886
|
-
await (0,
|
|
38887
|
-
scratch = await (0,
|
|
39026
|
+
await (0, import_promises6.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
|
|
39027
|
+
scratch = await (0, import_promises6.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
|
|
38888
39028
|
const run = (file, args) => deps.run(file, args, scratch);
|
|
38889
39029
|
await run("git", ["clone", "--quiet", "--branch", prs.length ? branch : "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
|
|
38890
39030
|
if (prs.length) {
|
|
@@ -38897,7 +39037,7 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
38897
39037
|
}
|
|
38898
39038
|
for (const stage of ["rc", "main"]) {
|
|
38899
39039
|
const path2 = (0, import_node_path34.join)(scratch, `${stage}-pins.json`);
|
|
38900
|
-
await (0,
|
|
39040
|
+
await (0, import_promises6.writeFile)(path2, JSON.stringify(input.pins.map((p) => ({ module: p.module, url: p[stage] }))));
|
|
38901
39041
|
await run("node", ["scripts/pin-modules.mjs", "--stage", stage, "--pins", path2]);
|
|
38902
39042
|
}
|
|
38903
39043
|
await run("git", ["add", "seed/modules.json"]);
|
|
@@ -38927,7 +39067,7 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
38927
39067
|
} catch (e) {
|
|
38928
39068
|
throw new Error(`suite cut pin PR FAILED: ${e.message}; merge/verify the single ${SUITE_REPO} development pin PR, then rerun the cut (never recut member tags)`);
|
|
38929
39069
|
} finally {
|
|
38930
|
-
if (scratch) await (0,
|
|
39070
|
+
if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
|
|
38931
39071
|
}
|
|
38932
39072
|
}
|
|
38933
39073
|
async function openSuitePinPr(deps, input) {
|
|
@@ -38946,8 +39086,8 @@ async function openSuitePinPr(deps, input) {
|
|
|
38946
39086
|
url.searchParams.set("v", tag);
|
|
38947
39087
|
const pin = url.toString();
|
|
38948
39088
|
const branch = `suite-pin/${stage}-${member.id}-${tag}`;
|
|
38949
|
-
await (0,
|
|
38950
|
-
scratch = await (0,
|
|
39089
|
+
await (0, import_promises6.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
|
|
39090
|
+
scratch = await (0, import_promises6.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
|
|
38951
39091
|
const run = (file, args) => deps.run(file, args, scratch);
|
|
38952
39092
|
await run("git", ["clone", "--quiet", "--branch", "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
|
|
38953
39093
|
const existing = JSON.parse(await run("gh", ["pr", "list", "--repo", SUITE_REPO, "--head", branch, "--state", "all", "--json", "url,state"]));
|
|
@@ -38985,14 +39125,14 @@ async function openSuitePinPr(deps, input) {
|
|
|
38985
39125
|
} catch (e) {
|
|
38986
39126
|
throw new Error(`suite pin PR FAILED for ${repo} ${tag}: ${e.message}; remedy: ${remedy}`);
|
|
38987
39127
|
} finally {
|
|
38988
|
-
if (scratch) await (0,
|
|
39128
|
+
if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
|
|
38989
39129
|
}
|
|
38990
39130
|
}
|
|
38991
39131
|
|
|
38992
39132
|
// src/command-register-collaboration.ts
|
|
38993
39133
|
var import_node_child_process17 = require("node:child_process");
|
|
38994
39134
|
var import_node_fs42 = require("node:fs");
|
|
38995
|
-
var
|
|
39135
|
+
var import_promises8 = require("node:fs/promises");
|
|
38996
39136
|
init_clean_exit();
|
|
38997
39137
|
init_cli_shared();
|
|
38998
39138
|
init_error_codes();
|
|
@@ -40265,7 +40405,7 @@ function postMergeReconWarnings(input) {
|
|
|
40265
40405
|
}
|
|
40266
40406
|
|
|
40267
40407
|
// src/pr-commands.ts
|
|
40268
|
-
var
|
|
40408
|
+
var import_promises7 = require("node:fs/promises");
|
|
40269
40409
|
init_cli_shared();
|
|
40270
40410
|
init_error_codes();
|
|
40271
40411
|
var GC_GH_TIMEOUT_MS2 = 2e4;
|
|
@@ -40419,10 +40559,10 @@ function registerPrLifecycleCommands(program3) {
|
|
|
40419
40559
|
let body;
|
|
40420
40560
|
try {
|
|
40421
40561
|
if (o.title || o.titleFile) {
|
|
40422
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
40562
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
40423
40563
|
}
|
|
40424
40564
|
if (o.body || o.bodyFile) {
|
|
40425
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
40565
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
40426
40566
|
}
|
|
40427
40567
|
} catch (e) {
|
|
40428
40568
|
return fail(`pr edit: ${e.message}`);
|
|
@@ -40452,7 +40592,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
40452
40592
|
}
|
|
40453
40593
|
let body;
|
|
40454
40594
|
try {
|
|
40455
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
40595
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
40456
40596
|
} catch (e) {
|
|
40457
40597
|
return fail(`pr comment: ${e.message}`);
|
|
40458
40598
|
}
|
|
@@ -42813,34 +42953,6 @@ function renderResidueSweepLines(report) {
|
|
|
42813
42953
|
return lines2;
|
|
42814
42954
|
}
|
|
42815
42955
|
|
|
42816
|
-
// src/pr-deploy-dev.ts
|
|
42817
|
-
function slugOfRepo(repo) {
|
|
42818
|
-
return repo.split("/")[1]?.toLowerCase() ?? "";
|
|
42819
|
-
}
|
|
42820
|
-
async function planDevDeployOnDevelopmentMerge(repo, baseRef, deps) {
|
|
42821
|
-
if (baseRef !== "development") return { applicable: false, reason: "not-development-base" };
|
|
42822
|
-
const slug = slugOfRepo(repo);
|
|
42823
|
-
if (!slug) return { applicable: false, reason: "unreadable" };
|
|
42824
|
-
let project2;
|
|
42825
|
-
try {
|
|
42826
|
-
project2 = await fetchProjectBySlug(slug, deps);
|
|
42827
|
-
} catch {
|
|
42828
|
-
return { applicable: false, reason: "unreadable" };
|
|
42829
|
-
}
|
|
42830
|
-
const model = project2?.deployModel;
|
|
42831
|
-
if (!model || !isCentralDispatchModel(model)) return { applicable: false, reason: "not-tenant" };
|
|
42832
|
-
return {
|
|
42833
|
-
applicable: true,
|
|
42834
|
-
// The issue's fallback ask, verbatim-shaped: the gap must be visible and actionable at the moment it opens.
|
|
42835
|
-
manualPointer: `dev not redeployed \u2014 run \`mmi-cli devops runtime tenant redeploy ${repo} dev\``
|
|
42836
|
-
};
|
|
42837
|
-
}
|
|
42838
|
-
async function dispatchDevDeploy(repo, deps) {
|
|
42839
|
-
const res = await tenantDeploy({ repo, stage: "dev" }, deps);
|
|
42840
|
-
if (res.ok) return { ok: true };
|
|
42841
|
-
return { ok: false, detail: res.body?.error ?? res.error ?? `HTTP ${res.status}` };
|
|
42842
|
-
}
|
|
42843
|
-
|
|
42844
42956
|
// src/command-register-collaboration.ts
|
|
42845
42957
|
var DEFAULT_PRIORITY = "medium";
|
|
42846
42958
|
function argvWantsJson2() {
|
|
@@ -43038,7 +43150,7 @@ function registerCollaborationCommands(program3) {
|
|
|
43038
43150
|
try {
|
|
43039
43151
|
title = await resolveIssueTitle(
|
|
43040
43152
|
{ title: opts.title, titleFile: opts.titleFile },
|
|
43041
|
-
{ readFile:
|
|
43153
|
+
{ readFile: import_promises8.readFile, readStdin }
|
|
43042
43154
|
);
|
|
43043
43155
|
} catch (e) {
|
|
43044
43156
|
return fail(
|
|
@@ -43092,8 +43204,8 @@ function registerCollaborationCommands(program3) {
|
|
|
43092
43204
|
let surfaceInferred;
|
|
43093
43205
|
try {
|
|
43094
43206
|
issueType = resolveCreateType(o.type, "issue create", o.label);
|
|
43095
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
43096
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
43207
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43208
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43097
43209
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
43098
43210
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
43099
43211
|
extraLabels = [...o.label ?? []];
|
|
@@ -43369,7 +43481,7 @@ function registerCollaborationCommands(program3) {
|
|
|
43369
43481
|
}
|
|
43370
43482
|
let body;
|
|
43371
43483
|
try {
|
|
43372
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
43484
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43373
43485
|
} catch (e) {
|
|
43374
43486
|
return fail(`issue comment: ${e.message}`);
|
|
43375
43487
|
}
|
|
@@ -43429,8 +43541,8 @@ ${list}`);
|
|
|
43429
43541
|
let title;
|
|
43430
43542
|
const sourceRepo = o.repo ?? await resolveRepo(void 0);
|
|
43431
43543
|
try {
|
|
43432
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
43433
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
43544
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43545
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43434
43546
|
priority = resolveCreatePriority(o.priority, "report");
|
|
43435
43547
|
if (!ISSUE_TYPES.includes(o.type)) {
|
|
43436
43548
|
throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
@@ -43484,8 +43596,8 @@ ${list}`);
|
|
|
43484
43596
|
let args;
|
|
43485
43597
|
try {
|
|
43486
43598
|
skill = assertSkillName(o.skill);
|
|
43487
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
43488
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
43599
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43600
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43489
43601
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
43490
43602
|
priority = resolveCreatePriority(o.priority, "skill-lesson");
|
|
43491
43603
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -43546,8 +43658,8 @@ ${list}`);
|
|
|
43546
43658
|
let body;
|
|
43547
43659
|
let title;
|
|
43548
43660
|
try {
|
|
43549
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
43550
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
43661
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43662
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
43551
43663
|
} catch (e) {
|
|
43552
43664
|
return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
43553
43665
|
}
|
|
@@ -44082,8 +44194,6 @@ ${list}`);
|
|
|
44082
44194
|
const baseRef = prMeta.base;
|
|
44083
44195
|
const headRefOid = (prMeta.oid ?? "").trim() || void 0;
|
|
44084
44196
|
if (!repoForPostCleanup) throw new Error("pr merge: cannot resolve target repository");
|
|
44085
|
-
const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
|
|
44086
|
-
const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
|
|
44087
44197
|
const closingGuardInput = await withAlreadyClosedCommitTargets(
|
|
44088
44198
|
await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
|
|
44089
44199
|
async (n) => {
|
|
@@ -44313,7 +44423,6 @@ ${list}`);
|
|
|
44313
44423
|
const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
|
|
44314
44424
|
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
44315
44425
|
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
|
|
44316
|
-
if (devDeployPlan.applicable) console.warn(`pr merge: ${devDeployPlan.manualPointer} once the merge lands (auto-merge does not redeploy the dev stage).`);
|
|
44317
44426
|
if (upgradedToAuto && !o.auto) {
|
|
44318
44427
|
console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
44319
44428
|
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
@@ -44423,8 +44532,6 @@ ${list}`);
|
|
|
44423
44532
|
} else if (repoForPostCleanup) {
|
|
44424
44533
|
clearPostMergeReconRecovery(process.cwd(), repoForPostCleanup, number);
|
|
44425
44534
|
}
|
|
44426
|
-
const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
|
|
44427
|
-
const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
|
|
44428
44535
|
let observedMethod;
|
|
44429
44536
|
if (remoteNotAttemptedReason === "pr-already-merged" && repoForPostCleanup) {
|
|
44430
44537
|
try {
|
|
@@ -44459,14 +44566,10 @@ ${list}`);
|
|
|
44459
44566
|
...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
|
|
44460
44567
|
boardAdvanceStatus: boardAdvance.status,
|
|
44461
44568
|
...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
|
|
44462
|
-
...recovery ? { postMergeReconRecovery: recovery } : {}
|
|
44463
|
-
...devDeploy ? { devDeploy: devDeploy.ok ? { dispatched: true } : { dispatched: false, manual: devDeployManual } } : {}
|
|
44569
|
+
...recovery ? { postMergeReconRecovery: recovery } : {}
|
|
44464
44570
|
}));
|
|
44465
44571
|
for (const line of renderPrMergeCleanupLines(localCleanup)) console.warn(line);
|
|
44466
44572
|
for (const line of renderResidueSweepLines(residueSweep)) console.warn(line);
|
|
44467
|
-
if (devDeploy && !devDeploy.ok) {
|
|
44468
|
-
console.error(`pr merge: ${devDeployManual} (${devDeploy.detail})`);
|
|
44469
|
-
}
|
|
44470
44573
|
for (const line of postMergeReconWarnings({
|
|
44471
44574
|
context: "pr merge",
|
|
44472
44575
|
boardAdvance,
|
|
@@ -44530,7 +44633,7 @@ function trainGitArgs(args) {
|
|
|
44530
44633
|
async function announceSummaryRefusal(path2, isHub = false) {
|
|
44531
44634
|
let lines2;
|
|
44532
44635
|
try {
|
|
44533
|
-
lines2 = summaryFileLines(await (0,
|
|
44636
|
+
lines2 = summaryFileLines(await (0, import_promises9.readFile)(path2, "utf8"));
|
|
44534
44637
|
} catch (e) {
|
|
44535
44638
|
return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
44536
44639
|
}
|
|
@@ -44638,8 +44741,8 @@ function trainApplyDeps() {
|
|
|
44638
44741
|
}
|
|
44639
44742
|
return body.status;
|
|
44640
44743
|
},
|
|
44641
|
-
readFile: (path2) => (0,
|
|
44642
|
-
removeFile: (path2) => (0,
|
|
44744
|
+
readFile: (path2) => (0, import_promises9.readFile)(path2, "utf8"),
|
|
44745
|
+
removeFile: (path2) => (0, import_promises9.unlink)(path2),
|
|
44643
44746
|
// #6521/#6523: the project's own release channel AND releaseLanguage, read from registry META in ONE
|
|
44644
44747
|
// checked read. Same shape as fetchEdgeDomains below: a FAILED read refuses by name so the announce
|
|
44645
44748
|
// note reports a failed read, never a project that configured nothing.
|
|
@@ -44967,7 +45070,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44967
45070
|
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
|
|
44968
45071
|
{ flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553); combined with --supersede-publish it closes BOTH content-caused legs in one approved closure (#6752)" },
|
|
44969
45072
|
{ flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 a deferred Jerv-Hub operator-host Gateway deploy leg is run and verified as part of the closure \u2014 the next patch train then carries the fix (#6638/#6742); combined with --supersede-deploy it closes BOTH content-caused legs in one approved closure (#6752)" },
|
|
44970
|
-
{ flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
|
|
45073
|
+
{ flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" },
|
|
45074
|
+
{ flags: "--big", description: "marks this release big for the mm-work suite `## In plain words` section even though its patch tag alone would not read as one (#7510); automatic for a minor/major bump \u2014 this is only for a patch a project-admin wants surfaced as big" }
|
|
44971
45075
|
];
|
|
44972
45076
|
for (const f of RELEASE_ONLY_FLAGS) {
|
|
44973
45077
|
if (commandName === "release") {
|
|
@@ -44995,6 +45099,9 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44995
45099
|
if (o.announceSummaryFile && commandName !== "release") {
|
|
44996
45100
|
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no release announcement. Run: mmi-cli devops release --announce-summary-file <path>`);
|
|
44997
45101
|
}
|
|
45102
|
+
if (o.big && commandName !== "release") {
|
|
45103
|
+
return fail(`${commandName}: --big applies only to release \u2014 it marks the mm-work suite plain-words section, which only a Release carries. Run: mmi-cli devops release --big`);
|
|
45104
|
+
}
|
|
44998
45105
|
if (o.abort && commandName !== "release") {
|
|
44999
45106
|
return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished release tag. Run: mmi-cli devops release --abort --apply`);
|
|
45000
45107
|
}
|
|
@@ -45037,7 +45144,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45037
45144
|
if (o.abort) return fail("release: --supersede-deploy --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
|
|
45038
45145
|
if (o.retryPublish) return fail("release: --supersede-deploy --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
|
|
45039
45146
|
if (!o.apply) return fail("release: --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
45040
|
-
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
45147
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
|
|
45041
45148
|
return fail("release: --supersede-deploy --supersede-publish accepts only --apply, --repo, --json and --out; it closes ledger legs and dispatches nothing");
|
|
45042
45149
|
}
|
|
45043
45150
|
if (o.repo) {
|
|
@@ -45054,7 +45161,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45054
45161
|
if (o.retryPublish) {
|
|
45055
45162
|
if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
|
|
45056
45163
|
if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
|
|
45057
|
-
if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo, --json and --out");
|
|
45164
|
+
if (o.announceSummaryFile || o.ack || o.dev || o.big) return fail("release: --retry-publish accepts only --apply, --watch, --repo, --json and --out");
|
|
45058
45165
|
if (o.repo) {
|
|
45059
45166
|
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
|
|
45060
45167
|
if (!guard.ok) return fail(`release: ${guard.message}`);
|
|
@@ -45078,7 +45185,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45078
45185
|
if (o.abort) {
|
|
45079
45186
|
if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
|
|
45080
45187
|
if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
|
|
45081
|
-
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
45188
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
|
|
45082
45189
|
return fail("release: --abort accepts only --apply, --repo, --json and --out; promotion flags cannot be combined with rollback");
|
|
45083
45190
|
}
|
|
45084
45191
|
if (o.repo) {
|
|
@@ -45097,7 +45204,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45097
45204
|
if (o.abort) return fail("release: --supersede-deploy and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
|
|
45098
45205
|
if (o.retryPublish) return fail("release: --supersede-deploy cannot be combined with --retry-publish");
|
|
45099
45206
|
if (!o.apply) return fail("release: --supersede-deploy requires --apply after explicit master approval; nothing was written");
|
|
45100
|
-
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
45207
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
|
|
45101
45208
|
return fail("release: --supersede-deploy accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
|
|
45102
45209
|
}
|
|
45103
45210
|
if (o.repo) {
|
|
@@ -45116,7 +45223,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45116
45223
|
if (o.abort) return fail("release: --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PROMOTED release, abort only ever deletes a proven unpublished candidate");
|
|
45117
45224
|
if (o.retryPublish) return fail("release: --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
|
|
45118
45225
|
if (!o.apply) return fail("release: --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
45119
|
-
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
45226
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev || o.big) {
|
|
45120
45227
|
return fail("release: --supersede-publish accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
|
|
45121
45228
|
}
|
|
45122
45229
|
if (o.repo) {
|
|
@@ -45144,7 +45251,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45144
45251
|
const raw = await withReleaseProgress(
|
|
45145
45252
|
trainApplyDeps(),
|
|
45146
45253
|
"Checking the existing release",
|
|
45147
|
-
(deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, suiteSolo: o.suiteSolo })
|
|
45254
|
+
(deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, suiteSolo: o.suiteSolo, big: o.big })
|
|
45148
45255
|
);
|
|
45149
45256
|
const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
|
|
45150
45257
|
const projectInfoSync = await withReleaseProgress(
|
|
@@ -45208,7 +45315,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45208
45315
|
if (o.apply) {
|
|
45209
45316
|
try {
|
|
45210
45317
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
45211
|
-
const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev, suiteSolo: o.suiteSolo });
|
|
45318
|
+
const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev, suiteSolo: o.suiteSolo, big: o.big });
|
|
45212
45319
|
const raw = await withReleaseProgress(trainApplyDeps(), commandName === "release" ? "Preparing release" : "Preparing release candidate", apply);
|
|
45213
45320
|
const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
|
|
45214
45321
|
let projectInfoSync;
|
|
@@ -45459,7 +45566,10 @@ function rcRunCauseFromLog(log) {
|
|
|
45459
45566
|
const causes = [...new Set([...log.matchAll(/##\[error\](?!Process completed with exit code)([^\r\n]+)/g)].map((m) => m[1].trim()).filter(Boolean))];
|
|
45460
45567
|
return causes.length ? `: ${causes.join("; ")}` : "";
|
|
45461
45568
|
}
|
|
45462
|
-
function
|
|
45569
|
+
function memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles) {
|
|
45570
|
+
return repo === SUITE_REPO ? summaryFile : memberSummaryFiles[repo];
|
|
45571
|
+
}
|
|
45572
|
+
function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
|
|
45463
45573
|
const recordPath = repoRuntimeStatePath(root, "suite", "cut.json");
|
|
45464
45574
|
const base = (0, import_node_path43.join)(repoRuntimeStatePath(root, "suite"), "checkouts");
|
|
45465
45575
|
const checkout = async (repo) => {
|
|
@@ -45507,6 +45617,17 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45507
45617
|
if (!health.ok) throw new Error(`${member.repo}: live backend health could not be proved at ${url}; stop`);
|
|
45508
45618
|
return { version: live.deployedTag, host };
|
|
45509
45619
|
};
|
|
45620
|
+
const hold = async (member) => {
|
|
45621
|
+
const slug = member.repo.split("/")[1].toLowerCase();
|
|
45622
|
+
const read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
|
|
45623
|
+
if (!read.ok) throw new Error(`${member.repo}: registry META unreadable for suiteHold (${read.error}); stop`);
|
|
45624
|
+
const value = read.project?.suiteHold;
|
|
45625
|
+
if (value === void 0 || value === null) return null;
|
|
45626
|
+
const raw = value;
|
|
45627
|
+
if (typeof raw.reason !== "string" || !raw.reason.trim())
|
|
45628
|
+
throw new Error(`${member.repo}: registry META suiteHold is malformed; set it as {"reason":"...","since":"..."} or clear it with --unset suiteHold; stop`);
|
|
45629
|
+
return { reason: raw.reason, ...typeof raw.since === "string" && raw.since ? { since: raw.since } : {} };
|
|
45630
|
+
};
|
|
45510
45631
|
const tagSha = async (repo, tag) => {
|
|
45511
45632
|
try {
|
|
45512
45633
|
return await gh(["api", `repos/${repo}/commits/${encodeURIComponent(tag)}`, "--jq", ".sha"]);
|
|
@@ -45553,8 +45674,9 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45553
45674
|
return { tag: result.tag, sha: result.tagSha };
|
|
45554
45675
|
}
|
|
45555
45676
|
});
|
|
45556
|
-
const release = async (repo, expected, rcSha) => inTrain(repo, async (deps) => {
|
|
45677
|
+
const release = async (repo, expected, rcSha, opts) => inTrain(repo, async (deps) => {
|
|
45557
45678
|
await prepareRcCheckout((args) => deps.run("git", args), repo);
|
|
45679
|
+
const plainWords = { announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles), big: opts?.big, suiteTag: opts?.suiteTag };
|
|
45558
45680
|
const current = await tagSha(repo, expected);
|
|
45559
45681
|
if (current) {
|
|
45560
45682
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
@@ -45563,10 +45685,10 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45563
45685
|
} catch {
|
|
45564
45686
|
throw new Error(`${repo}: existing ${expected} does not carry this suite's rc SHA ${rcSha}; refuse stale release`);
|
|
45565
45687
|
}
|
|
45566
|
-
const result = await runReleaseResume(deps, { watch: true });
|
|
45688
|
+
const result = await runReleaseResume(deps, { watch: true, ...plainWords });
|
|
45567
45689
|
if (result.tag !== expected || result.state !== "aligned") throw new Error(`${repo}: release resume not complete at ${expected} (${result.tag}/${result.state})`);
|
|
45568
45690
|
} else {
|
|
45569
|
-
const result = await withEnv("MMI_RELEASE_VERSION", expected.replace(/^v/, ""), () => runTrainApply("release", deps, { watch: true }));
|
|
45691
|
+
const result = await withEnv("MMI_RELEASE_VERSION", expected.replace(/^v/, ""), () => runTrainApply("release", deps, { watch: true, ...plainWords }));
|
|
45570
45692
|
if (result.tag !== expected || result.deployStatus !== "success") throw new Error(`${repo}: release is not green at expected ${expected} (${result.tag}/${result.deployStatus})`);
|
|
45571
45693
|
}
|
|
45572
45694
|
});
|
|
@@ -45654,6 +45776,7 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45654
45776
|
recordPath,
|
|
45655
45777
|
registry: registry2,
|
|
45656
45778
|
backend,
|
|
45779
|
+
hold,
|
|
45657
45780
|
head: async (repo, branch) => {
|
|
45658
45781
|
try {
|
|
45659
45782
|
return await gh(["api", `repos/${repo}/git/ref/heads/${branch}`, "--jq", ".object.sha"]);
|
|
@@ -45717,6 +45840,15 @@ function suiteTrainIo(root, summaryFile) {
|
|
|
45717
45840
|
// src/suite-commands.ts
|
|
45718
45841
|
init_cli_shared();
|
|
45719
45842
|
var import_node_fs44 = require("node:fs");
|
|
45843
|
+
function parseMemberSummaryFiles(entries) {
|
|
45844
|
+
const memberSummaryFiles = {};
|
|
45845
|
+
for (const entry of entries) {
|
|
45846
|
+
const eq = entry.indexOf("=");
|
|
45847
|
+
if (eq < 0) throw new Error(`--member-summary-file expects <owner/repo>=<path>, got ${entry}`);
|
|
45848
|
+
memberSummaryFiles[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
45849
|
+
}
|
|
45850
|
+
return memberSummaryFiles;
|
|
45851
|
+
}
|
|
45720
45852
|
function registerSuiteCommands(program3) {
|
|
45721
45853
|
const suite = program3.command("suite").description("MM-Work suite status, candidate cut and release (#7262/#7263)");
|
|
45722
45854
|
const leadRoot = async () => {
|
|
@@ -45764,23 +45896,25 @@ function registerSuiteCommands(program3) {
|
|
|
45764
45896
|
return fail(`suite abandon: ${e.message}`);
|
|
45765
45897
|
}
|
|
45766
45898
|
});
|
|
45767
|
-
suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train").option("--json", "structured receipt").action(async (o) => {
|
|
45899
|
+
suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train; also this cut's plain-words summary for MM-Work itself").option("--member-summary-file <owner/repo=path>", "repeatable: a member's OWN plain-words summary (#7510) \u2014 never the suite lead's summary reused; a member with no entry still ships, with the coordinator-ruled no-change line", (val, prev = []) => [...prev, val], []).option("--big", "marks MM-Work's own release big for the plain-words section (#7510); a member is never marked big through this flag \u2014 it is big only via the read route's automatic minor/major detection").option("--json", "structured receipt").action(async (o) => {
|
|
45768
45900
|
try {
|
|
45769
45901
|
if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
|
|
45770
45902
|
if (!o.announceSummaryFile) throw new Error("suite release requires --announce-summary-file before promoting any member");
|
|
45771
45903
|
const lines2 = (0, import_node_fs44.readFileSync)(o.announceSummaryFile, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
45772
45904
|
if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
|
|
45905
|
+
const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
|
|
45773
45906
|
const root = await leadRoot();
|
|
45774
45907
|
const unlock = await acquireTrainWriterLock(root, "suite release");
|
|
45775
45908
|
let result;
|
|
45776
45909
|
try {
|
|
45777
|
-
result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile), true);
|
|
45910
|
+
result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles), true, o.big === true);
|
|
45778
45911
|
} finally {
|
|
45779
45912
|
await unlock();
|
|
45780
45913
|
}
|
|
45781
45914
|
console.log(o.json ? JSON.stringify(result, null, 2) : [
|
|
45782
45915
|
`suite release: ${result.leadRcTag} \u2192 main`,
|
|
45783
45916
|
...result.members.map((m) => m.carried ? ` - ${m.repo}: carried, untouched` : ` - ${m.repo}: ${m.tag} \u2192 main ${m.released ? "released" : "pending"}`),
|
|
45917
|
+
...result.heldMembers?.length ? ["held (frozen live release, untouched):", ...result.heldMembers.map((h) => ` - ${h.repo}: HELD: ${h.reason}`)] : [],
|
|
45784
45918
|
` - MM-Work: ${result.leadReleased ? "released" : "pending"}; announcement: ${result.announcement}`
|
|
45785
45919
|
].join("\n"));
|
|
45786
45920
|
} catch (e) {
|
|
@@ -48812,7 +48946,7 @@ function renderTrackToDirectResult(result) {
|
|
|
48812
48946
|
}
|
|
48813
48947
|
|
|
48814
48948
|
// src/schedules-commands.ts
|
|
48815
|
-
var
|
|
48949
|
+
var import_promises10 = require("node:fs/promises");
|
|
48816
48950
|
init_clean_exit();
|
|
48817
48951
|
init_github_client();
|
|
48818
48952
|
init_cli_shared();
|
|
@@ -49006,9 +49140,9 @@ function registerSchedulesCommands(program3) {
|
|
|
49006
49140
|
await failGraceful("org schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
|
|
49007
49141
|
return;
|
|
49008
49142
|
}
|
|
49009
|
-
const docText = await (0,
|
|
49143
|
+
const docText = await (0, import_promises10.readFile)(o.doc, "utf8");
|
|
49010
49144
|
const spliced = spliceDoc(docText, renderDocSection(entries, now.toISOString()));
|
|
49011
|
-
await (0,
|
|
49145
|
+
await (0, import_promises10.writeFile)(o.doc, spliced, "utf8");
|
|
49012
49146
|
reportParked(parkedLines);
|
|
49013
49147
|
reportDrift(drift);
|
|
49014
49148
|
console.log(`org schedules: wrote ${entries.length} entries into ${o.doc}`);
|
|
@@ -49746,7 +49880,7 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
49746
49880
|
// src/tenant-artifact.ts
|
|
49747
49881
|
var import_node_crypto15 = require("node:crypto");
|
|
49748
49882
|
var import_node_fs48 = require("node:fs");
|
|
49749
|
-
var
|
|
49883
|
+
var import_promises11 = require("node:fs/promises");
|
|
49750
49884
|
var import_node_path46 = require("node:path");
|
|
49751
49885
|
var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
49752
49886
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
@@ -49758,7 +49892,7 @@ async function sha256File(path2) {
|
|
|
49758
49892
|
async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
49759
49893
|
if (!["dev", "rc", "main"].includes(stage)) throw new Error("tenant artifact put: <stage> must be dev, rc, or main");
|
|
49760
49894
|
const path2 = (0, import_node_path46.resolve)(inputPath);
|
|
49761
|
-
const info = await (0,
|
|
49895
|
+
const info = await (0, import_promises11.stat)(path2);
|
|
49762
49896
|
if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
|
|
49763
49897
|
if (!Number.isSafeInteger(info.size) || info.size < 1 || info.size > MAX_BYTES) throw new Error(`tenant artifact put: file must be 1..${MAX_BYTES} bytes`);
|
|
49764
49898
|
const sha2562 = await sha256File(path2);
|
|
@@ -50240,7 +50374,7 @@ init_github_client();
|
|
|
50240
50374
|
init_hub_auth();
|
|
50241
50375
|
|
|
50242
50376
|
// src/schedules-lift-command.ts
|
|
50243
|
-
var
|
|
50377
|
+
var import_promises12 = require("node:fs/promises");
|
|
50244
50378
|
var import_node_path49 = require("node:path");
|
|
50245
50379
|
init_clean_exit();
|
|
50246
50380
|
init_cli_shared();
|
|
@@ -50263,14 +50397,14 @@ var RegistryUnreachableError = class extends Error {
|
|
|
50263
50397
|
async function readWorkflowFiles(dir) {
|
|
50264
50398
|
let names;
|
|
50265
50399
|
try {
|
|
50266
|
-
names = await (0,
|
|
50400
|
+
names = await (0, import_promises12.readdir)(dir);
|
|
50267
50401
|
} catch {
|
|
50268
50402
|
return [];
|
|
50269
50403
|
}
|
|
50270
50404
|
const files = [];
|
|
50271
50405
|
for (const name of names.sort()) {
|
|
50272
50406
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
50273
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0,
|
|
50407
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises12.readFile)((0, import_node_path49.join)(dir, name), "utf8") });
|
|
50274
50408
|
}
|
|
50275
50409
|
return files;
|
|
50276
50410
|
}
|
|
@@ -51997,6 +52131,7 @@ function ciAuditDeps2() {
|
|
|
51997
52131
|
client: defaultGitHubClient(),
|
|
51998
52132
|
listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
|
|
51999
52133
|
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
|
|
52134
|
+
getDeployFacts: async (slug) => fetchDeployFactsBySlug(slug, registryClientDeps(await cfgPromise)),
|
|
52000
52135
|
// Continuous CI delivery (#1550): the gate re-seed renders from the Hub's on-disk seed templates.
|
|
52001
52136
|
// Resolve from the installed package root so the reconcile works from any org repo checkout; when the
|
|
52002
52137
|
// Hub root is not locatable (e.g. a global npm install without a local checkout), return null so the
|
package/package.json
CHANGED