@mutmutco/cli 4.5.83 → 4.5.85
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 +688 -100
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3967,7 +3967,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3967
3967
|
const negativeNumberArg = (arg) => {
|
|
3968
3968
|
if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(arg)) return false;
|
|
3969
3969
|
return !this._getCommandAndAncestors().some(
|
|
3970
|
-
(cmd) => cmd.options.map((opt) => opt.short).some((
|
|
3970
|
+
(cmd) => cmd.options.map((opt) => opt.short).some((short3) => /^-\d$/.test(short3))
|
|
3971
3971
|
);
|
|
3972
3972
|
};
|
|
3973
3973
|
let activeVariadicOption = null;
|
|
@@ -17268,10 +17268,10 @@ var rollout_plan_default = {
|
|
|
17268
17268
|
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)."
|
|
17269
17269
|
},
|
|
17270
17270
|
baseline: {
|
|
17271
|
-
version: "4.5.
|
|
17272
|
-
tag: "v4.5.
|
|
17273
|
-
commit: "
|
|
17274
|
-
npm: "@mutmutco/cli@4.5.
|
|
17271
|
+
version: "4.5.85",
|
|
17272
|
+
tag: "v4.5.85",
|
|
17273
|
+
commit: "a1ce2f136846",
|
|
17274
|
+
npm: "@mutmutco/cli@4.5.85"
|
|
17275
17275
|
},
|
|
17276
17276
|
exitCriterion: "fleet-n-of-n",
|
|
17277
17277
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17288,14 +17288,14 @@ var rollout_plan_default = {
|
|
|
17288
17288
|
repo: "mutmutco/mmi-hub",
|
|
17289
17289
|
role: "canary",
|
|
17290
17290
|
schedule: "train",
|
|
17291
|
-
v3Target: "v4.5.
|
|
17291
|
+
v3Target: "v4.5.85"
|
|
17292
17292
|
}
|
|
17293
17293
|
],
|
|
17294
17294
|
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.",
|
|
17295
17295
|
rollback: {
|
|
17296
17296
|
independent: true,
|
|
17297
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17298
|
-
v3Target: "v4.5.
|
|
17297
|
+
mechanism: "npm dist-tag latest -> 4.5.85 and redeploy the Hub Lambda from tag v4.5.85 (a1ce2f136846); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17298
|
+
v3Target: "v4.5.85 (@mutmutco/cli@4.5.85, tag commit a1ce2f136846 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17299
17299
|
}
|
|
17300
17300
|
},
|
|
17301
17301
|
{
|
|
@@ -19607,6 +19607,7 @@ var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state,id:.id
|
|
|
19607
19607
|
var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
19608
19608
|
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
19609
19609
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
19610
|
+
var TRAIN_QUEUED_CHECK_ATTEMPTS = 80;
|
|
19610
19611
|
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
19611
19612
|
var TRAIN_PR_ONLY_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done", "guard", "verdict"]);
|
|
19612
19613
|
var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
|
|
@@ -20011,10 +20012,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20011
20012
|
const sleep2 = resolveSleep(deps);
|
|
20012
20013
|
let lastStatus = "not checked";
|
|
20013
20014
|
let lastError;
|
|
20015
|
+
let workflowRunRead = "not-read";
|
|
20016
|
+
let workflowRunNote = "";
|
|
20017
|
+
let workflowRunEverSeen = false;
|
|
20018
|
+
let hadQueuedWorkflowRun = false;
|
|
20014
20019
|
const everObserved = /* @__PURE__ */ new Set();
|
|
20015
20020
|
const autoSatisfied = /* @__PURE__ */ new Set();
|
|
20016
20021
|
let fleetForeignStreak = 0;
|
|
20017
|
-
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
20022
|
+
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS || hadQueuedWorkflowRun && attempt < TRAIN_QUEUED_CHECK_ATTEMPTS; attempt++) {
|
|
20018
20023
|
if (attempt > 0) await sleep2(TRAIN_CHECK_DELAY_MS);
|
|
20019
20024
|
let checkRuns;
|
|
20020
20025
|
let statuses;
|
|
@@ -20032,6 +20037,40 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20032
20037
|
lastError = e.message || String(e);
|
|
20033
20038
|
continue;
|
|
20034
20039
|
}
|
|
20040
|
+
try {
|
|
20041
|
+
const workflowOut = await deps.run("gh", [
|
|
20042
|
+
"run",
|
|
20043
|
+
"list",
|
|
20044
|
+
"--repo",
|
|
20045
|
+
ctx.repo,
|
|
20046
|
+
"--commit",
|
|
20047
|
+
sha,
|
|
20048
|
+
"--limit",
|
|
20049
|
+
"100",
|
|
20050
|
+
"--json",
|
|
20051
|
+
"databaseId,status,conclusion,headSha"
|
|
20052
|
+
]);
|
|
20053
|
+
const parsedWorkflowRuns = JSON.parse(workflowOut);
|
|
20054
|
+
if (!Array.isArray(parsedWorkflowRuns)) throw new Error("workflow-runs response was not an array");
|
|
20055
|
+
const matchingRuns = parsedWorkflowRuns.filter((run) => !run.headSha || run.headSha === sha);
|
|
20056
|
+
if (matchingRuns.length) {
|
|
20057
|
+
workflowRunRead = "found";
|
|
20058
|
+
workflowRunEverSeen = true;
|
|
20059
|
+
const activeRun = matchingRuns.find((run) => run.status === "queued" || run.status === "in_progress");
|
|
20060
|
+
if (activeRun) {
|
|
20061
|
+
if (activeRun.status === "queued") hadQueuedWorkflowRun = true;
|
|
20062
|
+
workflowRunNote = activeRun.status === "queued" ? "Actions run is queued behind busy runners" : "Actions run is in progress";
|
|
20063
|
+
} else {
|
|
20064
|
+
workflowRunNote = "Actions run exists but has not produced the required check-run";
|
|
20065
|
+
}
|
|
20066
|
+
} else {
|
|
20067
|
+
workflowRunRead = "empty";
|
|
20068
|
+
workflowRunNote = "";
|
|
20069
|
+
}
|
|
20070
|
+
} catch (e) {
|
|
20071
|
+
workflowRunRead = "failed";
|
|
20072
|
+
workflowRunNote = `workflow-run lookup failed: ${e.message || String(e)}`;
|
|
20073
|
+
}
|
|
20035
20074
|
for (const c of required) {
|
|
20036
20075
|
if (checkRuns.some((r) => r.name === c) || statuses.some((s) => s.context === c)) everObserved.add(c);
|
|
20037
20076
|
}
|
|
@@ -20069,11 +20108,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20069
20108
|
return `required checks passed: ${pending.join(", ") || "(none pending)"}${satisfiedNote}${fleetNote}`;
|
|
20070
20109
|
}
|
|
20071
20110
|
}
|
|
20072
|
-
const
|
|
20111
|
+
const attempts = hadQueuedWorkflowRun ? TRAIN_QUEUED_CHECK_ATTEMPTS : TRAIN_CHECK_ATTEMPTS;
|
|
20112
|
+
const waitedMin = Math.round((attempts - 1) * TRAIN_CHECK_DELAY_MS / 6e4);
|
|
20073
20113
|
const neverMaterialized = required.filter((c) => !everObserved.has(c) && !autoSatisfied.has(c));
|
|
20074
|
-
const
|
|
20114
|
+
const noWorkflowRun = workflowRunRead === "empty" && !workflowRunEverSeen;
|
|
20115
|
+
const neverNote = neverMaterialized.length && noWorkflowRun ? ` No workflow run was found on ${sha} after a successful lookup, and no check-run or commit status appeared: ${neverMaterialized.join(", ")}.` : "";
|
|
20116
|
+
const runNote = workflowRunNote ? ` ${workflowRunNote}.` : "";
|
|
20075
20117
|
throw new Error(
|
|
20076
|
-
`timed out after ~${waitedMin}m (${
|
|
20118
|
+
`timed out after ~${waitedMin}m (${attempts} attempts) waiting for required train checks on ${sha}.${neverNote}${runNote}${lastError ? ` Last read error: ${lastError}` : ` Last observed: ${lastStatus}`}`
|
|
20077
20119
|
);
|
|
20078
20120
|
}
|
|
20079
20121
|
async function probeRemoteTag(deps, tag) {
|
|
@@ -20287,6 +20329,10 @@ var SUITE_CUT_TOKEN = /* @__PURE__ */ Symbol("suite-cut");
|
|
|
20287
20329
|
function suiteCutContext(repo, lead) {
|
|
20288
20330
|
return { repo, lead, token: SUITE_CUT_TOKEN };
|
|
20289
20331
|
}
|
|
20332
|
+
var SUITE_LEAD_TOKEN = /* @__PURE__ */ Symbol("suite-lead");
|
|
20333
|
+
function suiteLeadContext(lead) {
|
|
20334
|
+
return { repo: lead, lead, token: SUITE_LEAD_TOKEN };
|
|
20335
|
+
}
|
|
20290
20336
|
function validSuiteCutContext(repo, context) {
|
|
20291
20337
|
return !!context && context.token === SUITE_CUT_TOKEN && context.repo.toLowerCase() === repo.toLowerCase();
|
|
20292
20338
|
}
|
|
@@ -20300,7 +20346,12 @@ function isBundleOnlySuiteMember(repo, suite) {
|
|
|
20300
20346
|
}
|
|
20301
20347
|
function suiteMemberRefusal(repo, meta, lane, solo = false, cut) {
|
|
20302
20348
|
const suite = meta?.suite;
|
|
20303
|
-
if (!suite
|
|
20349
|
+
if (!suite) return null;
|
|
20350
|
+
if (!isBundleOnlySuiteMember(repo, suite)) {
|
|
20351
|
+
if (solo || cut?.token === SUITE_LEAD_TOKEN && cut.repo.toLowerCase() === repo.toLowerCase()) return null;
|
|
20352
|
+
const route = lane === "hotfix" ? "hotfix the affected member from its own checkout, then run mmi-cli devops suite release --owner-go" : `run mmi-cli devops suite ${lane} --owner-go`;
|
|
20353
|
+
return `${repo} is the ${suite} suite lead; a plain ${lane} would cut it without its members: ${route}. A lead-only ${lane} needs --suite-solo --owner-go.`;
|
|
20354
|
+
}
|
|
20304
20355
|
if (authorisedSuiteCut(repo, meta, cut)) return null;
|
|
20305
20356
|
if (lane !== "rcand" && solo) return null;
|
|
20306
20357
|
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.`;
|
|
@@ -21738,7 +21789,7 @@ function partialTrainRecoveryError(cause, input) {
|
|
|
21738
21789
|
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
21739
21790
|
const branch = input.stage;
|
|
21740
21791
|
const releaseState = input.stage === "rc" ? "GitHub Release n/a" : "GitHub Release not created";
|
|
21741
|
-
const resumeCommand = input.stage === "rc" ? "mmi-cli devops rcand --resume" :
|
|
21792
|
+
const resumeCommand = input.stage === "rc" ? "mmi-cli devops rcand --resume" : `mmi-cli devops release --resume${input.suiteSolo ? " --suite-solo --owner-go" : ""}`;
|
|
21742
21793
|
const tenant2 = input.deployModel === "tenant-container";
|
|
21743
21794
|
const deployLine = tenant2 ? ` - deploy: dispatched by the train after the branch push; recover with \`mmi-cli devops runtime tenant redeploy ${input.repo} ${input.stage} --watch\` once the branch and release exist.` : ` - deploy: this repo is \`${input.deployModel ?? "not tenant-container"}\` \u2014 it has NO tenant runtime. Its own release/main-push workflows publish and deploy once the branch and Release exist. Do NOT run the tenant redeployer.`;
|
|
21744
21795
|
const stateBlock = `partial train state for ${input.tag}:
|
|
@@ -26145,7 +26196,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
26145
26196
|
}
|
|
26146
26197
|
let tagPush;
|
|
26147
26198
|
let tagLedgerStarted = false;
|
|
26148
|
-
const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
|
|
26199
|
+
const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, suiteSolo: options.suiteSolo, meta, branchHints };
|
|
26149
26200
|
try {
|
|
26150
26201
|
await assertComputedReleaseTagStillNext(deps, tag, preOneZeroInForce(meta));
|
|
26151
26202
|
if (deployModel === "hub-serverless") {
|
|
@@ -26736,7 +26787,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
26736
26787
|
}
|
|
26737
26788
|
const steps = [];
|
|
26738
26789
|
let requiredChecks;
|
|
26739
|
-
const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, meta, branchHints };
|
|
26790
|
+
const recoveryInput = { repo: ctx.repo, tag, stage: "main", deployModel, suiteSolo: options.suiteSolo, meta, branchHints };
|
|
26740
26791
|
try {
|
|
26741
26792
|
requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
26742
26793
|
} catch (e) {
|
|
@@ -35595,6 +35646,7 @@ init_github_client();
|
|
|
35595
35646
|
init_cli_shared();
|
|
35596
35647
|
|
|
35597
35648
|
// src/suite-status.ts
|
|
35649
|
+
var import_node_crypto13 = require("node:crypto");
|
|
35598
35650
|
var SUITE_REPO = "mutmutco/MM-Work";
|
|
35599
35651
|
var REPOS = {
|
|
35600
35652
|
"mmi-scout": "mutmutco/MMI-Scout",
|
|
@@ -35604,6 +35656,54 @@ var REPOS = {
|
|
|
35604
35656
|
katip: "mutmutco/MMI-Katip"
|
|
35605
35657
|
};
|
|
35606
35658
|
var STAGES3 = ["dev", "rc", "main"];
|
|
35659
|
+
var MAPPING_MARKER = /<!-- mmi-suite-members (\{[^\n]*\}) -->/;
|
|
35660
|
+
function suiteMappingBlock(tag, mapping) {
|
|
35661
|
+
const rows = Object.entries(mapping).map(([id, v]) => `- ${id}: ${v}`).join("\n");
|
|
35662
|
+
return `<!-- mmi-suite-members ${JSON.stringify(mapping)} -->
|
|
35663
|
+
## Suite ${tag} members
|
|
35664
|
+
${rows}
|
|
35665
|
+
`;
|
|
35666
|
+
}
|
|
35667
|
+
function parseSuiteMapping(body) {
|
|
35668
|
+
const raw = MAPPING_MARKER.exec(body ?? "")?.[1];
|
|
35669
|
+
if (!raw) return void 0;
|
|
35670
|
+
try {
|
|
35671
|
+
const v = JSON.parse(raw);
|
|
35672
|
+
return Object.values(v).every((x) => typeof x === "string") ? v : void 0;
|
|
35673
|
+
} catch {
|
|
35674
|
+
return void 0;
|
|
35675
|
+
}
|
|
35676
|
+
}
|
|
35677
|
+
var sha256 = (b) => (0, import_node_crypto13.createHash)("sha256").update(b).digest("hex");
|
|
35678
|
+
async function proveBundle(url, tag, fetchUrl) {
|
|
35679
|
+
const get = async (u) => {
|
|
35680
|
+
try {
|
|
35681
|
+
return await fetchUrl(u);
|
|
35682
|
+
} catch (e) {
|
|
35683
|
+
return `unreachable (${e.message})`;
|
|
35684
|
+
}
|
|
35685
|
+
};
|
|
35686
|
+
const bundle = await get(url);
|
|
35687
|
+
if (typeof bundle === "string") return { state: "unprovable", detail: `bundle ${bundle}` };
|
|
35688
|
+
if (bundle.status !== 200) return { state: "unprovable", detail: `bundle HTTP ${bundle.status}` };
|
|
35689
|
+
const served = sha256(bundle.body);
|
|
35690
|
+
const recordUrl = new URL(url);
|
|
35691
|
+
recordUrl.search = "";
|
|
35692
|
+
recordUrl.pathname = recordUrl.pathname.replace(/[^/]*$/, "module.json");
|
|
35693
|
+
const rec = await get(recordUrl.toString());
|
|
35694
|
+
if (typeof rec === "string") return { state: "unprovable", detail: `build record ${rec}; served ${served.slice(0, 12)}` };
|
|
35695
|
+
if (rec.status !== 200) return { state: "unprovable", detail: `build record not served (HTTP ${rec.status}); served ${served.slice(0, 12)}` };
|
|
35696
|
+
let record;
|
|
35697
|
+
try {
|
|
35698
|
+
record = JSON.parse(Buffer.from(rec.body).toString("utf8"));
|
|
35699
|
+
} catch {
|
|
35700
|
+
return { state: "unprovable", detail: `build record unreadable; served ${served.slice(0, 12)}` };
|
|
35701
|
+
}
|
|
35702
|
+
if (typeof record.tag !== "string" || typeof record.sha256 !== "string") return { state: "unprovable", detail: `build record malformed; served ${served.slice(0, 12)}` };
|
|
35703
|
+
if (record.tag !== tag) return { state: "mismatch", detail: `host serves ${record.tag}, pinned ${tag}` };
|
|
35704
|
+
if (record.sha256.toLowerCase() !== served) return { state: "mismatch", detail: `served ${served.slice(0, 12)}, pinned ${record.sha256.slice(0, 12)}` };
|
|
35705
|
+
return { state: "proven", detail: `sha ${served.slice(0, 12)}` };
|
|
35706
|
+
}
|
|
35607
35707
|
function suiteMembers(raw) {
|
|
35608
35708
|
if (!Array.isArray(raw)) throw new Error("MM-Work seed/modules.json is unreadable");
|
|
35609
35709
|
const members = [{ repo: SUITE_REPO, id: "mm-work", bundles: {} }];
|
|
@@ -35639,6 +35739,7 @@ async function stageStatus(member, stage, deps) {
|
|
|
35639
35739
|
let served = "unknown";
|
|
35640
35740
|
let source = "unknown";
|
|
35641
35741
|
let commit;
|
|
35742
|
+
let note;
|
|
35642
35743
|
let deployStatus;
|
|
35643
35744
|
const pin = stage !== "dev" ? bundleTag(member.bundles[stage]) : void 0;
|
|
35644
35745
|
if (pin) {
|
|
@@ -35660,47 +35761,157 @@ async function stageStatus(member, stage, deps) {
|
|
|
35660
35761
|
else {
|
|
35661
35762
|
try {
|
|
35662
35763
|
commit = await deps.tagCommit(member.repo, served);
|
|
35764
|
+
if (!commit && stage === "main") note = `pin names no release (tag ${served} absent)`;
|
|
35663
35765
|
} catch {
|
|
35664
35766
|
}
|
|
35665
35767
|
}
|
|
35666
35768
|
}
|
|
35667
|
-
|
|
35769
|
+
let lag = commit && head !== "unknown" ? commit.toLowerCase() !== head.toLowerCase() : null;
|
|
35770
|
+
let proof;
|
|
35771
|
+
if (pin && stage !== "dev" && deps.fetchUrl) {
|
|
35772
|
+
proof = await proveBundle(member.bundles[stage], pin, deps.fetchUrl);
|
|
35773
|
+
if (proof.state === "mismatch") lag = true;
|
|
35774
|
+
}
|
|
35668
35775
|
return {
|
|
35669
35776
|
stage,
|
|
35670
35777
|
head,
|
|
35671
35778
|
served,
|
|
35672
35779
|
source,
|
|
35673
|
-
lag: source === "registry-pin" && lag === false ? null : lag,
|
|
35780
|
+
lag: source === "registry-pin" && lag === false && proof?.state !== "proven" ? null : lag,
|
|
35781
|
+
...proof ? { proof } : {},
|
|
35782
|
+
...note ? { note } : {},
|
|
35674
35783
|
...deployStatus ? { deployStatus } : {}
|
|
35675
35784
|
};
|
|
35676
35785
|
}
|
|
35677
35786
|
async function readSuiteStatus(deps) {
|
|
35678
35787
|
const members = suiteMembers(await deps.registry());
|
|
35679
|
-
|
|
35680
|
-
|
|
35681
|
-
|
|
35682
|
-
|
|
35683
|
-
|
|
35684
|
-
|
|
35685
|
-
|
|
35686
|
-
|
|
35788
|
+
const rows = await Promise.all(members.map(async (member) => ({
|
|
35789
|
+
repo: member.repo,
|
|
35790
|
+
id: member.id,
|
|
35791
|
+
stages: await Promise.all(STAGES3.map((stage) => stageStatus(member, stage, deps)))
|
|
35792
|
+
})));
|
|
35793
|
+
if (!deps.leadTags || !deps.registryAt) return { suite: "mm-work", members: rows };
|
|
35794
|
+
const tags = await deps.leadTags().catch(() => []);
|
|
35795
|
+
const latest = (rc) => tags.filter((t) => /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(t) && t.includes("-rc.") === rc).sort((a, b) => compareVersions(b, a))[0];
|
|
35796
|
+
const leadTag = { rc: latest(true), main: latest(false) };
|
|
35797
|
+
const lead = rows[0].stages;
|
|
35798
|
+
for (const stage of ["rc", "main"]) {
|
|
35799
|
+
const tag = leadTag[stage];
|
|
35800
|
+
if (!tag) continue;
|
|
35801
|
+
let pins = [];
|
|
35802
|
+
try {
|
|
35803
|
+
pins = suiteMembers(await deps.registryAt(tag));
|
|
35804
|
+
} catch {
|
|
35805
|
+
}
|
|
35806
|
+
for (const row of rows.slice(1)) {
|
|
35807
|
+
const st = row.stages.find((s) => s.stage === stage);
|
|
35808
|
+
st.suitePin = { suite: tag, pin: bundleTag(pins.find((m) => m.id === row.id)?.bundles[stage]) };
|
|
35809
|
+
}
|
|
35810
|
+
}
|
|
35811
|
+
const recorded = leadTag.main && deps.leadReleaseBody ? parseSuiteMapping(await deps.leadReleaseBody(leadTag.main).catch(() => null)) : void 0;
|
|
35812
|
+
return { suite: "mm-work", lead: { dev: lead[0].head, rc: leadTag.rc ?? "n/a", main: leadTag.main ?? "unknown", ...recorded ? { members: recorded } : {} }, members: rows };
|
|
35813
|
+
}
|
|
35814
|
+
var short = (v) => /^[a-f0-9]{40}$/i.test(v) ? v.slice(0, 12) : v;
|
|
35815
|
+
var proofText = (p) => p ? p.state === "proven" ? "proven" : p.state === "mismatch" ? `mismatch (${p.detail})` : `unprovable (${p.detail})` : "";
|
|
35816
|
+
function formatSuiteMarkdown(report) {
|
|
35817
|
+
const cell = (st) => !st || st.source === "n/a" ? "n/a" : short(st.served);
|
|
35818
|
+
const by = (st, name) => st?.suitePin ? `${name} ${st.suitePin.suite}${st.suitePin.pin && st.suitePin.pin !== st.served ? ` (pins ${st.suitePin.pin})` : st.suitePin.pin ? "" : " (no pin)"}` : "";
|
|
35819
|
+
const lines2 = ["| Member | dev | rc | main | Pinned by suite |", "| --- | --- | --- | --- | --- |"];
|
|
35820
|
+
if (report.lead) lines2.push(`| MM-Work (suite) | ${short(report.lead.dev)} | ${report.lead.rc} | ${report.lead.main} | - |`);
|
|
35821
|
+
for (const m of report.members.slice(report.lead ? 1 : 0)) {
|
|
35822
|
+
const [dev, rc, main] = ["dev", "rc", "main"].map((s) => m.stages.find((x) => x.stage === s));
|
|
35823
|
+
lines2.push(`| ${m.repo.split("/")[1]} | ${cell(dev)} | ${cell(rc)} | ${cell(main)} | ${[by(rc, "rc"), by(main, "main")].filter(Boolean).join(", ") || "-"} |`);
|
|
35824
|
+
}
|
|
35825
|
+
return lines2.join("\n");
|
|
35687
35826
|
}
|
|
35688
35827
|
function formatSuiteStatus(report) {
|
|
35689
35828
|
const lines2 = [
|
|
35690
35829
|
"suite mm-work \u2014 registry pins are declarations, tenant versions require a healthy probe",
|
|
35830
|
+
...report.lead ? [`suite version: dev ${short(report.lead.dev)} rc ${report.lead.rc} main ${report.lead.main}`] : [],
|
|
35831
|
+
...report.lead?.members ? [`suite ${report.lead.main} = members ${Object.entries(report.lead.members).map(([id, v]) => `${id} ${v}`).join(", ")} (recorded by suite hotfix, #7738)`] : [],
|
|
35691
35832
|
"member stage head served source lag"
|
|
35692
35833
|
];
|
|
35693
35834
|
for (const member of report.members) for (const row of member.stages) {
|
|
35694
35835
|
const served = /^[a-f0-9]{40}$/i.test(row.served) ? row.served.slice(0, 12) : row.served;
|
|
35695
|
-
lines2.push(`${member.repo.split("/")[1].padEnd(18)} ${row.stage.padEnd(6)} ${row.head.slice(0, 12).padEnd(13)} ${served.padEnd(18)} ${row.source.padEnd(15)} ${row.source === "n/a" ? "n/a" : row.lag === null ? "?" : row.lag ? "LAG" : "ok"}`);
|
|
35836
|
+
lines2.push(`${member.repo.split("/")[1].padEnd(18)} ${row.stage.padEnd(6)} ${row.head.slice(0, 12).padEnd(13)} ${served.padEnd(18)} ${row.source.padEnd(15)} ${row.source === "n/a" ? "n/a" : row.lag === null ? row.note ?? "?" : row.lag ? "LAG" : "ok"}${row.proof ? ` ${proofText(row.proof)}` : ""}${row.suitePin ? ` by suite ${row.suitePin.suite}` : ""}`);
|
|
35696
35837
|
}
|
|
35697
35838
|
return lines2.join("\n");
|
|
35698
35839
|
}
|
|
35840
|
+
async function setSuiteHold(deps, member, hold) {
|
|
35841
|
+
const wanted = member.toLowerCase();
|
|
35842
|
+
const found = suiteMembers(await deps.registry()).find((m) => m.repo.toLowerCase() === wanted || m.repo.split("/")[1].toLowerCase() === wanted || m.id === wanted);
|
|
35843
|
+
if (!found) throw new Error(`${member} is not an MM-Work suite member; nothing was written`);
|
|
35844
|
+
if (found.repo === SUITE_REPO) throw new Error(`${SUITE_REPO} is the suite lead and cannot be held; nothing was written`);
|
|
35845
|
+
const slug = found.repo.split("/")[1].toLowerCase();
|
|
35846
|
+
const before = await deps.read(slug);
|
|
35847
|
+
if (!before.ok || !before.project) throw new Error(`${found.repo}: registry META unreadable${before.ok ? "" : ` (${before.error})`}; nothing was written`);
|
|
35848
|
+
const track = before.project.releaseTrack;
|
|
35849
|
+
const res = await deps.write(slug, { suiteHold: hold ? parseSuiteHoldVar(JSON.stringify(hold)) : null, repo: found.repo, ...track ? { releaseTrack: track } : {} });
|
|
35850
|
+
if (!res.ok) throw new Error(`${found.repo}: registry write failed (HTTP ${res.status}${res.error ? `, ${res.error}` : ""})`);
|
|
35851
|
+
const after = await deps.read(slug);
|
|
35852
|
+
if (!after.ok || !after.project) throw new Error(`${found.repo}: write sent but read-back failed; verify with org project get`);
|
|
35853
|
+
const got = after.project.suiteHold ?? null;
|
|
35854
|
+
if (JSON.stringify(got) !== JSON.stringify(hold)) throw new Error(`${found.repo}: read-back suiteHold ${JSON.stringify(got)} does not match what was written`);
|
|
35855
|
+
if (track && after.project.releaseTrack !== track) throw new Error(`${found.repo}: releaseTrack changed from ${track} to ${after.project.releaseTrack ?? "unset"} by the write; restore it with org project set --release-track ${track}`);
|
|
35856
|
+
return hold ? `${found.repo}: HELD (${hold.reason})` : `${found.repo}: hold cleared`;
|
|
35857
|
+
}
|
|
35858
|
+
|
|
35859
|
+
// src/suite-dev.ts
|
|
35860
|
+
var WALK = 20;
|
|
35861
|
+
var short2 = (s) => s && /^[a-f0-9]{40}$/i.test(s) ? s.slice(0, 12) : s ?? "-";
|
|
35862
|
+
var same = (a, b) => !!a && !!b && a.toLowerCase() === b.toLowerCase();
|
|
35863
|
+
async function newestGreen(deps, repo) {
|
|
35864
|
+
const checks = await deps.requiredChecks(repo);
|
|
35865
|
+
for (const sha of await deps.commits(repo, WALK)) {
|
|
35866
|
+
const runs = await deps.checkRuns(repo, sha);
|
|
35867
|
+
if (checks.every((name) => {
|
|
35868
|
+
const latest = runs.filter((r) => r.name === name).sort((a, b) => b.id - a.id)[0];
|
|
35869
|
+
return latest?.status === "completed" && latest.conclusion === "success";
|
|
35870
|
+
})) return sha;
|
|
35871
|
+
}
|
|
35872
|
+
return void 0;
|
|
35873
|
+
}
|
|
35874
|
+
async function readSuiteDev(deps) {
|
|
35875
|
+
const report = await deps.status();
|
|
35876
|
+
return Promise.all(report.members.map(async (m) => {
|
|
35877
|
+
const st = m.stages.find((s) => s.stage === "dev");
|
|
35878
|
+
const row = { repo: m.repo, head: st.head, served: st.served };
|
|
35879
|
+
if (st.source === "n/a") return { ...row, state: "converged", note: "no dev stage" };
|
|
35880
|
+
if (st.lag === false) return { ...row, state: "converged", note: "dev serves the development head" };
|
|
35881
|
+
let green;
|
|
35882
|
+
try {
|
|
35883
|
+
green = await newestGreen(deps, m.repo);
|
|
35884
|
+
} catch (e) {
|
|
35885
|
+
return { ...row, state: "unknown", note: `checks unreadable (${e.message})` };
|
|
35886
|
+
}
|
|
35887
|
+
const last = st.deployStatus?.lastRun;
|
|
35888
|
+
if (st.served === "unknown") return { ...row, green, state: "unknown", note: `dev version unproven (health probe failed or no deploy record)${last?.url ? `; last deploy ${last.conclusion ?? last.status}: ${last.url}` : ""}` };
|
|
35889
|
+
if (same(st.served, green)) return { ...row, green, state: "converged", note: `dev serves the newest green SHA; head ${short2(st.head)} is not green yet` };
|
|
35890
|
+
if (!green) return { ...row, state: "blocked", note: `no green SHA in the last ${WALK} development commits` };
|
|
35891
|
+
if (last?.conclusion === "failure") return { ...row, green, state: "blocked", note: `last dev deploy failed: ${last.url ?? "no run url"}` };
|
|
35892
|
+
return { ...row, green, state: "lagging", note: `newest green ${short2(green)} not deployed yet; the dev auto-deploy sweep (every 15 min) picks it up` };
|
|
35893
|
+
}));
|
|
35894
|
+
}
|
|
35895
|
+
function formatSuiteDev(rows) {
|
|
35896
|
+
return [
|
|
35897
|
+
"member head served green state note",
|
|
35898
|
+
...rows.map((r) => `${r.repo.split("/")[1].padEnd(18)} ${short2(r.head).padEnd(13)} ${short2(r.served).padEnd(13)} ${short2(r.green).padEnd(13)} ${r.state.padEnd(10)} ${r.note}`)
|
|
35899
|
+
].join("\n");
|
|
35900
|
+
}
|
|
35901
|
+
async function waitSuiteDev(deps, opts) {
|
|
35902
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
35903
|
+
for (let waited = 0; ; waited += opts.intervalMs) {
|
|
35904
|
+
const rows = await readSuiteDev(deps);
|
|
35905
|
+
opts.onTick?.(rows);
|
|
35906
|
+
if (!rows.some((r) => r.state === "lagging") || waited + opts.intervalMs > opts.timeoutMs) return rows;
|
|
35907
|
+
await sleep2(opts.intervalMs);
|
|
35908
|
+
}
|
|
35909
|
+
}
|
|
35699
35910
|
|
|
35700
35911
|
// src/suite-train.ts
|
|
35701
35912
|
var import_node_fs29 = require("node:fs");
|
|
35702
35913
|
var import_node_path29 = require("node:path");
|
|
35703
|
-
var
|
|
35914
|
+
var import_node_crypto14 = require("node:crypto");
|
|
35704
35915
|
|
|
35705
35916
|
// src/kit-version.ts
|
|
35706
35917
|
var parse = (v) => /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(typeof v === "string" ? v.trim() : "");
|
|
@@ -35791,7 +36002,7 @@ function readSuiteCut(path2) {
|
|
|
35791
36002
|
}
|
|
35792
36003
|
function writeSuiteCut(path2, record) {
|
|
35793
36004
|
(0, import_node_fs29.mkdirSync)((0, import_node_path29.dirname)(path2), { recursive: true });
|
|
35794
|
-
const temp = `${path2}.${(0,
|
|
36005
|
+
const temp = `${path2}.${(0, import_node_crypto14.randomBytes)(8).toString("hex")}.tmp`;
|
|
35795
36006
|
try {
|
|
35796
36007
|
(0, import_node_fs29.writeFileSync)(temp, JSON.stringify(record, null, 2), { flag: "wx" });
|
|
35797
36008
|
(0, import_node_fs29.renameSync)(temp, path2);
|
|
@@ -35836,8 +36047,41 @@ function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Dat
|
|
|
35836
36047
|
(0, import_node_fs29.renameSync)(recordPath, archive);
|
|
35837
36048
|
return { archive, orphanTags: record.members.filter((m) => m.tag).map((m) => `${m.repo}@${m.tag}`) };
|
|
35838
36049
|
}
|
|
36050
|
+
async function suiteRestoreMainPins(deps) {
|
|
36051
|
+
const record = readSuiteCut(deps.recordPath);
|
|
36052
|
+
if (!record?.pinPr || record.leadReleased || record.members.some((m) => m.released)) return void 0;
|
|
36053
|
+
const pins = [];
|
|
36054
|
+
for (const m of record.members) {
|
|
36055
|
+
if (m.carried || !m.tag || !m.priorMain) continue;
|
|
36056
|
+
const planned = suiteReleaseTag(m.tag);
|
|
36057
|
+
if (!await deps.tagSha(m.repo, planned)) pins.push({ module: m.id, planned, prior: m.priorMain });
|
|
36058
|
+
}
|
|
36059
|
+
return pins.length ? deps.restoreMain({ cut: record.leadDevelopment, pins }) : void 0;
|
|
36060
|
+
}
|
|
36061
|
+
async function dryRunStatus(deps, m) {
|
|
36062
|
+
const [dev, rc] = await Promise.all([deps.head(m.repo, "development"), deps.head(m.repo, "rc")]);
|
|
36063
|
+
const previousRc = rc ?? await deps.head(m.repo, "main");
|
|
36064
|
+
if (!dev || !previousRc) return "branch heads unreadable";
|
|
36065
|
+
const verdict = await deps.contains(m.repo, dev, previousRc) ? "carried (no change on development since its last rc tag)" : "new candidate";
|
|
36066
|
+
let pending = "";
|
|
36067
|
+
try {
|
|
36068
|
+
const why = await pendingBackend(deps, m, dev);
|
|
36069
|
+
if (why) pending = `; pending backend release (${why})`;
|
|
36070
|
+
} catch (e) {
|
|
36071
|
+
pending = `; live backend unreadable (${e.message})`;
|
|
36072
|
+
}
|
|
36073
|
+
return verdict + pending;
|
|
36074
|
+
}
|
|
36075
|
+
var NOT_BACKEND = /^(module|docs|\.github)\/|\.md$/;
|
|
36076
|
+
async function pendingBackend(deps, m, dev) {
|
|
36077
|
+
const { version } = await deps.backend(m);
|
|
36078
|
+
const live = await deps.tagSha(m.repo, version);
|
|
36079
|
+
if (!live || await deps.contains(m.repo, dev, live)) return "";
|
|
36080
|
+
const files = (await deps.changedPaths(m.repo, live, dev)).filter((f) => !NOT_BACKEND.test(f));
|
|
36081
|
+
return files.length ? `${files.length} backend file(s) changed since live ${version}, e.g. ${files[0]}` : "";
|
|
36082
|
+
}
|
|
35839
36083
|
async function suiteRcand(deps, opts) {
|
|
35840
|
-
if (!opts.ownerGo) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
|
|
36084
|
+
if (!opts.ownerGo && !opts.dryRun) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
|
|
35841
36085
|
const members = suiteMembers(await deps.registry());
|
|
35842
36086
|
const modules = members.filter((m) => m.repo !== SUITE_REPO);
|
|
35843
36087
|
const { active, held } = await holdSplit(deps, modules);
|
|
@@ -35849,7 +36093,10 @@ async function suiteRcand(deps, opts) {
|
|
|
35849
36093
|
];
|
|
35850
36094
|
const lead = await deps.head(SUITE_REPO, "development");
|
|
35851
36095
|
plan.push(await kitCheck(deps, lead, active.map((m) => ({ id: m.id, url: m.bundles.dev })), "dev bundles"));
|
|
35852
|
-
if (opts.dryRun)
|
|
36096
|
+
if (opts.dryRun) {
|
|
36097
|
+
plan.push(...await Promise.all(modules.map(async (m) => `${m.repo}: ${heldById.has(m.id) ? "HELD" : await dryRunStatus(deps, m)}`)));
|
|
36098
|
+
return { plan };
|
|
36099
|
+
}
|
|
35853
36100
|
let record = readSuiteCut(deps.recordPath);
|
|
35854
36101
|
if (record?.leadReleased && record.announcement === "pending") {
|
|
35855
36102
|
const reason = await deps.announceSkipReason(record);
|
|
@@ -35872,6 +36119,12 @@ async function suiteRcand(deps, opts) {
|
|
|
35872
36119
|
record = void 0;
|
|
35873
36120
|
}
|
|
35874
36121
|
if (!record) {
|
|
36122
|
+
const pending = (await Promise.all(active.map(async (m) => {
|
|
36123
|
+
const dev = await deps.head(m.repo, "development");
|
|
36124
|
+
const why = dev ? await pendingBackend(deps, m, dev) : "";
|
|
36125
|
+
return why ? `${m.repo} (${why})` : "";
|
|
36126
|
+
}))).filter(Boolean);
|
|
36127
|
+
if (pending.length) throw new Error(`member backends not live yet: ${pending.join("; ")}. Release each from its own checkout first (mmi-cli devops release --suite-solo --owner-go), or hold it (suite hold set), then re-run suite rcand; nothing was cut`);
|
|
35875
36128
|
const [leadDevelopment, leadPreviousRc, snapshots] = await Promise.all([
|
|
35876
36129
|
deps.head(SUITE_REPO, "development"),
|
|
35877
36130
|
deps.head(SUITE_REPO, "rc"),
|
|
@@ -35972,6 +36225,11 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
|
|
|
35972
36225
|
const pins = suitePins(record, bundlesOf(modules));
|
|
35973
36226
|
await kitCheck(deps, record.leadDevelopment, pins.map((p) => ({ id: p.module, url: p.rc })), "rc bundles");
|
|
35974
36227
|
await deps.verifyLeadCut(record.leadDevelopment);
|
|
36228
|
+
for (const member of record.members) {
|
|
36229
|
+
const prior = modules.find((m) => m.id === member.id)?.bundles.main;
|
|
36230
|
+
if (!member.carried && !member.priorMain && prior && pinVersion(prior) !== suiteReleaseTag(member.tag)) member.priorMain = prior;
|
|
36231
|
+
}
|
|
36232
|
+
writeSuiteCut(deps.recordPath, record);
|
|
35975
36233
|
const pr = await deps.pin({ cut: record.leadDevelopment, pins, heldIds: record.heldMembers?.map((h) => h.id) });
|
|
35976
36234
|
await deps.verifyLeadCut(record.leadDevelopment);
|
|
35977
36235
|
if (record.pinPr && record.pinPr !== pr.pr) throw new Error(`pin PR changed from ${record.pinPr} to ${pr.pr}; stop`);
|
|
@@ -36075,6 +36333,112 @@ Each member's live release must serve <data>/main/module.js at its main pin path
|
|
|
36075
36333
|
}
|
|
36076
36334
|
return record;
|
|
36077
36335
|
}
|
|
36336
|
+
var hotfixRecordPath = (deps) => (0, import_node_path29.join)((0, import_node_path29.dirname)(deps.recordPath), "hotfix.json");
|
|
36337
|
+
function readSuiteHotfix(path2) {
|
|
36338
|
+
let raw;
|
|
36339
|
+
try {
|
|
36340
|
+
raw = (0, import_node_fs29.readFileSync)(path2, "utf8");
|
|
36341
|
+
} catch (e) {
|
|
36342
|
+
if (e.code === "ENOENT") return void 0;
|
|
36343
|
+
throw e;
|
|
36344
|
+
}
|
|
36345
|
+
let r;
|
|
36346
|
+
try {
|
|
36347
|
+
r = JSON.parse(raw);
|
|
36348
|
+
} catch {
|
|
36349
|
+
throw new Error(`suite hotfix record unreadable at ${path2}; repair it, never restart`);
|
|
36350
|
+
}
|
|
36351
|
+
if (r.schema !== "mmi-suite-hotfix/v1" || !r.repo || !r.member || !r.from || r.tag !== void 0 && !RELEASE_TAG.test(r.tag) || r.released && !r.tag || r.pinMerged && !r.released || r.leadTag !== void 0 && (!RELEASE_TAG.test(r.leadTag) || !r.pinMerged) || r.leadReleased && !r.leadTag || r.mapped && !r.leadReleased)
|
|
36352
|
+
throw new Error(`suite hotfix record has unknown or invalid state at ${path2}; repair it, never restart`);
|
|
36353
|
+
return r;
|
|
36354
|
+
}
|
|
36355
|
+
function writeSuiteHotfix(path2, record) {
|
|
36356
|
+
writeSuiteCut(path2, record);
|
|
36357
|
+
}
|
|
36358
|
+
async function suiteHotfix(deps, opts) {
|
|
36359
|
+
if (!opts.ownerGo && !opts.dryRun) throw new Error("suite hotfix requires --owner-go (it ships a member and MM-Work to production); nothing was written");
|
|
36360
|
+
const wanted = opts.member.toLowerCase();
|
|
36361
|
+
const found = suiteMembers(await deps.registry()).find((m) => m.repo.toLowerCase() === wanted || m.repo.split("/")[1].toLowerCase() === wanted || m.id === wanted);
|
|
36362
|
+
if (!found) throw new Error(`${opts.member} is not an MM-Work suite member; nothing was written`);
|
|
36363
|
+
if (found.repo === SUITE_REPO) throw new Error(`${SUITE_REPO} is the suite lead; hotfix a member, this command cuts the lead; nothing was written`);
|
|
36364
|
+
const held = await deps.hold(found);
|
|
36365
|
+
if (held) throw new Error(`${found.repo} is HELD (${held.reason}); clear it with suite hold clear before a suite hotfix; nothing was written`);
|
|
36366
|
+
const plan = [
|
|
36367
|
+
`${found.repo}: hotfix start --from ${opts.from} + release (suite-solo, in-process)`,
|
|
36368
|
+
`${SUITE_REPO}: main pin PR for ${found.id}, wait for merge`,
|
|
36369
|
+
`${SUITE_REPO}: hotfix (patch) carrying only the merged pin PR onto main`,
|
|
36370
|
+
`${SUITE_REPO}: record suite version -> member versions on its release`
|
|
36371
|
+
];
|
|
36372
|
+
if (opts.dryRun) return { plan };
|
|
36373
|
+
const cut = readSuiteCut(deps.recordPath);
|
|
36374
|
+
if (cut && !cut.leadReleased) throw new Error(`an unfinished suite cut (${cut.leadRcTag ?? "no lead tag"}) is in flight; finish it with suite release or retire it with suite abandon first`);
|
|
36375
|
+
const path2 = hotfixRecordPath(deps);
|
|
36376
|
+
let record = readSuiteHotfix(path2);
|
|
36377
|
+
if (record?.mapped) {
|
|
36378
|
+
(0, import_node_fs29.renameSync)(path2, `${path2}.${record.leadTag}.json`);
|
|
36379
|
+
record = void 0;
|
|
36380
|
+
}
|
|
36381
|
+
if (record && record.repo !== found.repo) throw new Error(`an unfinished suite hotfix for ${record.repo} is recorded; finish it (rerun with that member) first`);
|
|
36382
|
+
const rec = record ?? { schema: "mmi-suite-hotfix/v1", member: found.id, repo: found.repo, from: opts.from, ...opts.ack?.length ? { ack: opts.ack } : {} };
|
|
36383
|
+
const save = () => writeSuiteHotfix(path2, rec);
|
|
36384
|
+
save();
|
|
36385
|
+
const resume = `rerun suite hotfix ${found.id} --from ${rec.from} --owner-go to resume`;
|
|
36386
|
+
if (!rec.tag) {
|
|
36387
|
+
const started = await deps.hotfixStart(found.repo, { from: rec.from, ack: rec.ack });
|
|
36388
|
+
if (!RELEASE_TAG.test(started.tag)) throw new Error(`${found.repo}: hotfix start returned no vX.Y.Z tag`);
|
|
36389
|
+
rec.tag = started.tag;
|
|
36390
|
+
save();
|
|
36391
|
+
}
|
|
36392
|
+
if (!rec.released) {
|
|
36393
|
+
let out;
|
|
36394
|
+
try {
|
|
36395
|
+
out = await deps.hotfixRelease(found.repo, rec.tag);
|
|
36396
|
+
} catch (e) {
|
|
36397
|
+
throw new Error(`${e.message}; the hotfix ${rec.tag} is recorded: ${resume}`);
|
|
36398
|
+
}
|
|
36399
|
+
if (out.tag !== rec.tag) throw new Error(`${found.repo}: released ${out.tag}, expected ${rec.tag}; stop`);
|
|
36400
|
+
rec.released = true;
|
|
36401
|
+
save();
|
|
36402
|
+
}
|
|
36403
|
+
if (!rec.pinMerged) {
|
|
36404
|
+
const pin = await deps.soloPin(found.repo, rec.tag);
|
|
36405
|
+
if (pin.status === "superseded") throw new Error(`${found.id} pin at ${rec.tag} is superseded by ${pin.pr}; a newer ${found.id} release owns the pin; stop`);
|
|
36406
|
+
rec.pinPr = pin.pr;
|
|
36407
|
+
save();
|
|
36408
|
+
if (pin.status !== "merged") throw new Error(`pin PR ${pin.pr} is pending; merge it, then ${resume} (the hotfix is not repeated)`);
|
|
36409
|
+
rec.pinMerged = true;
|
|
36410
|
+
save();
|
|
36411
|
+
}
|
|
36412
|
+
if (!rec.leadTag) {
|
|
36413
|
+
const prNumber = /\/pull\/(\d+)$/.exec(rec.pinPr ?? "")?.[1];
|
|
36414
|
+
if (!prNumber) throw new Error(`pin PR ${rec.pinPr ?? "(none)"} has no PR number; MM-Work not hotfixed`);
|
|
36415
|
+
const started = await deps.hotfixStart(SUITE_REPO, { from: prNumber });
|
|
36416
|
+
if (!RELEASE_TAG.test(started.tag)) throw new Error(`${SUITE_REPO}: hotfix start returned no vX.Y.Z tag`);
|
|
36417
|
+
rec.leadTag = started.tag;
|
|
36418
|
+
save();
|
|
36419
|
+
}
|
|
36420
|
+
const leadTag = rec.leadTag;
|
|
36421
|
+
if (!rec.leadReleased) {
|
|
36422
|
+
let out;
|
|
36423
|
+
try {
|
|
36424
|
+
out = await deps.hotfixRelease(SUITE_REPO, leadTag);
|
|
36425
|
+
} catch (e) {
|
|
36426
|
+
throw new Error(`${e.message}; the MM-Work hotfix ${leadTag} is recorded: ${resume}`);
|
|
36427
|
+
}
|
|
36428
|
+
if (out.tag !== leadTag) throw new Error(`${SUITE_REPO}: released ${out.tag}, expected ${leadTag}; stop`);
|
|
36429
|
+
rec.leadReleased = true;
|
|
36430
|
+
save();
|
|
36431
|
+
}
|
|
36432
|
+
if (!rec.mapped) {
|
|
36433
|
+
const mapping = await deps.suiteMapping(leadTag);
|
|
36434
|
+
if (mapping[found.id] !== rec.tag) throw new Error(`MM-Work ${leadTag} pins ${found.id} at ${mapping[found.id] ?? "nothing"}, not the hotfix ${rec.tag}; mapping not recorded`);
|
|
36435
|
+
await deps.recordMapping(leadTag, mapping);
|
|
36436
|
+
rec.mapping = mapping;
|
|
36437
|
+
rec.mapped = true;
|
|
36438
|
+
save();
|
|
36439
|
+
}
|
|
36440
|
+
return { plan, record: rec };
|
|
36441
|
+
}
|
|
36078
36442
|
|
|
36079
36443
|
// src/suite-rc-data.ts
|
|
36080
36444
|
var RC_DATA_VAULT_REPO = "mutmutco/MM-Work";
|
|
@@ -36593,8 +36957,8 @@ async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
|
|
|
36593
36957
|
if (sources.length === 0) return { failures: [], skipped: [] };
|
|
36594
36958
|
const { mkdtemp: mkdtemp2, rm: rm2 } = await import("node:fs/promises");
|
|
36595
36959
|
const { tmpdir: tmpdir11 } = await import("node:os");
|
|
36596
|
-
const { join:
|
|
36597
|
-
const dir = await mkdtemp2(
|
|
36960
|
+
const { join: join46 } = await import("node:path");
|
|
36961
|
+
const dir = await mkdtemp2(join46(tmpdir11(), "mmi-hotfix-preflight-"));
|
|
36598
36962
|
const wt = { run: (file, args) => deps.run(file, file === "git" ? ["-C", dir, ...args] : args) };
|
|
36599
36963
|
const failures = [];
|
|
36600
36964
|
const skipped = [];
|
|
@@ -39767,6 +40131,59 @@ async function openSuiteCutPinPr(deps, input) {
|
|
|
39767
40131
|
if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
|
|
39768
40132
|
}
|
|
39769
40133
|
}
|
|
40134
|
+
async function openSuiteRestorePinPr(deps, input) {
|
|
40135
|
+
if (!/^[a-f0-9]{40}$/.test(input.cut)) throw new Error("suite restore PR needs an anchored cut");
|
|
40136
|
+
const branch = `suite-pin/restore-${input.cut.slice(0, 16)}`;
|
|
40137
|
+
let scratch = "";
|
|
40138
|
+
try {
|
|
40139
|
+
const prs = JSON.parse(await deps.run("gh", ["pr", "list", "--repo", SUITE_REPO, "--head", branch, "--state", "all", "--json", "url,state"]));
|
|
40140
|
+
if (prs.some((p) => p.state === "CLOSED")) throw new Error(`${branch} was closed without landing`);
|
|
40141
|
+
if (prs.length) return { status: prs.some((p) => p.state === "MERGED") ? "merged" : "pending", pr: prs[0].url };
|
|
40142
|
+
const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
|
|
40143
|
+
if (!get.content) throw new Error("MM-Work seed/modules.json is unreadable");
|
|
40144
|
+
const rows = JSON.parse(Buffer.from(get.content, "base64").toString("utf8"));
|
|
40145
|
+
const todo = input.pins.filter((p) => {
|
|
40146
|
+
try {
|
|
40147
|
+
return new URL(rows.find((r) => r.id === p.module)?.bundles?.main ?? "").searchParams.get("v") === p.planned;
|
|
40148
|
+
} catch {
|
|
40149
|
+
return false;
|
|
40150
|
+
}
|
|
40151
|
+
});
|
|
40152
|
+
if (!todo.length) return void 0;
|
|
40153
|
+
await (0, import_promises6.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
|
|
40154
|
+
scratch = await (0, import_promises6.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
|
|
40155
|
+
const run = (file, args) => deps.run(file, args, scratch);
|
|
40156
|
+
await run("git", ["clone", "--quiet", "--branch", "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
|
|
40157
|
+
await run("git", ["switch", "-c", branch]);
|
|
40158
|
+
const path2 = (0, import_node_path34.join)(scratch, "main-pins.json");
|
|
40159
|
+
await (0, import_promises6.writeFile)(path2, JSON.stringify(todo.map((p) => ({ module: p.module, url: p.prior }))));
|
|
40160
|
+
await run("node", ["scripts/pin-modules.mjs", "--stage", "main", "--pins", path2]);
|
|
40161
|
+
await run("git", ["add", "seed/modules.json"]);
|
|
40162
|
+
await run("git", ["commit", "-m", `Restore main pins of abandoned suite cut ${input.cut.slice(0, 12)} (#7758)`]);
|
|
40163
|
+
await run("git", ["push", "origin", branch]);
|
|
40164
|
+
const pr = (await run("gh", [
|
|
40165
|
+
"pr",
|
|
40166
|
+
"create",
|
|
40167
|
+
"--repo",
|
|
40168
|
+
SUITE_REPO,
|
|
40169
|
+
"--base",
|
|
40170
|
+
"development",
|
|
40171
|
+
"--head",
|
|
40172
|
+
branch,
|
|
40173
|
+
"--title",
|
|
40174
|
+
"Restore main pins of abandoned suite cut",
|
|
40175
|
+
"--body",
|
|
40176
|
+
`Abandoned cut ${input.cut.slice(0, 12)} pinned main to releases that never shipped (${todo.map((p) => `${p.module} ${p.planned}`).join(", ")}); restore the pre-cut pins (#7758).`
|
|
40177
|
+
])).trim();
|
|
40178
|
+
if (!pr.includes("/pull/")) throw new Error(`gh pr create returned no PR URL: ${pr}`);
|
|
40179
|
+
await run("gh", ["pr", "merge", pr, "--repo", SUITE_REPO, "--auto", "--squash"]);
|
|
40180
|
+
return { status: "pending", pr };
|
|
40181
|
+
} catch (e) {
|
|
40182
|
+
throw new Error(`suite restore pin PR FAILED: ${e.message}; rerun suite abandon --owner-go (the record is kept until the restore PR exists)`);
|
|
40183
|
+
} finally {
|
|
40184
|
+
if (scratch) await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
|
|
40185
|
+
}
|
|
40186
|
+
}
|
|
39770
40187
|
async function openSuitePinPr(deps, input) {
|
|
39771
40188
|
const { repo, tag, stage } = input;
|
|
39772
40189
|
const remedy = `open/merge the ${SUITE_REPO} development pin PR for ${repo} ${tag}, then rerun the same train command; do not recut the tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`;
|
|
@@ -39978,7 +40395,7 @@ init_github_quota();
|
|
|
39978
40395
|
|
|
39979
40396
|
// src/issue-commands.ts
|
|
39980
40397
|
var import_node_fs34 = require("node:fs");
|
|
39981
|
-
var
|
|
40398
|
+
var import_node_crypto15 = require("node:crypto");
|
|
39982
40399
|
init_cli_shared();
|
|
39983
40400
|
init_clean_exit();
|
|
39984
40401
|
init_error_codes();
|
|
@@ -40451,7 +40868,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
40451
40868
|
const identity = `${spec.type}
|
|
40452
40869
|
${spec.title.trim()}
|
|
40453
40870
|
${spec.body ?? ""}`;
|
|
40454
|
-
const hash = (0,
|
|
40871
|
+
const hash = (0, import_node_crypto15.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
40455
40872
|
return `${batchKey}:${hash}`;
|
|
40456
40873
|
}
|
|
40457
40874
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -45839,7 +46256,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45839
46256
|
async function pinSoloMember(repo, tag) {
|
|
45840
46257
|
const read = await fetchProjectBySlugChecked(slugOf(repo), registryClientDeps(await loadConfig()));
|
|
45841
46258
|
if (!read.ok || !read.project) throw new Error(`suite pin PR FAILED: registry META unreadable for ${repo} (${read.ok ? "missing" : read.error}); rerun the same train command after restoring registry access \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`);
|
|
45842
|
-
if (!
|
|
46259
|
+
if (!isBundleOnlySuiteMember(repo, read.project.suite)) return void 0;
|
|
45843
46260
|
const root = (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
45844
46261
|
return openSuitePinPr({ root, run: async (file, args, cwd) => (await execFileP(file, args, { cwd, timeout: GIT_TIMEOUT_MS })).stdout }, { repo, tag, stage: "main" });
|
|
45845
46262
|
}
|
|
@@ -45874,8 +46291,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
45874
46291
|
} catch (e) {
|
|
45875
46292
|
return fail(`${commandName}: ${e.message}`);
|
|
45876
46293
|
}
|
|
45877
|
-
if (o.suiteSolo !== o.ownerGo
|
|
45878
|
-
return fail(`${commandName}:
|
|
46294
|
+
if (o.suiteSolo !== o.ownerGo)
|
|
46295
|
+
return fail(`${commandName}: --suite-solo and --owner-go must be passed together (a solo member release/hotfix, or a lead-only cut; a member rcand is always refused)`);
|
|
45879
46296
|
if (o.ack && commandName !== "release") {
|
|
45880
46297
|
return fail(`${commandName}: --ack applies only to release \u2014 it overrides the rc -> main hotfix-coverage guard, which rcand does not run. Run: mmi-cli devops release --ack <shas>`);
|
|
45881
46298
|
}
|
|
@@ -46354,6 +46771,17 @@ async function prepareLaneCheckout(git3, repo, branch, lane) {
|
|
|
46354
46771
|
const [head, origin] = (await git3(["rev-parse", "HEAD", `origin/${branch}`])).trim().split(/\s+/);
|
|
46355
46772
|
if (!head || head !== origin) throw new Error(`${repo}: local ${branch} ${head?.slice(0, 12)} is not origin/${branch} ${origin?.slice(0, 12)}; stop`);
|
|
46356
46773
|
}
|
|
46774
|
+
async function refreshSuiteCheckout(git3) {
|
|
46775
|
+
await git3(["fetch", "--prune", "origin"]);
|
|
46776
|
+
if ((await git3(["branch", "--remotes", "--list", "origin/rc"])).trim()) return;
|
|
46777
|
+
if (!(await git3(["branch", "--list", "rc"])).trim()) return;
|
|
46778
|
+
const upstream = (await git3(["for-each-ref", "--format=%(upstream:short)", "refs/heads/rc"])).trim();
|
|
46779
|
+
if (upstream !== "origin/rc") return;
|
|
46780
|
+
const sha = (await git3(["rev-parse", "--short=12", "refs/heads/rc"])).trim();
|
|
46781
|
+
let stale = `stale-rc-${sha}`;
|
|
46782
|
+
for (let suffix = 1; (await git3(["branch", "--list", stale])).trim(); suffix++) stale = `stale-rc-${sha}-${suffix}`;
|
|
46783
|
+
await git3(["branch", "--move", "rc", stale]);
|
|
46784
|
+
}
|
|
46357
46785
|
function rcRunCauseFromLog(log) {
|
|
46358
46786
|
const causes = [...new Set([...log.matchAll(/##\[error\](?!Process completed with exit code)([^\r\n]+)/g)].map((m) => m[1].trim()).filter(Boolean))];
|
|
46359
46787
|
return causes.length ? `: ${causes.join("; ")}` : "";
|
|
@@ -46375,18 +46803,21 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
|
|
|
46375
46803
|
const origin = await exec("git", ["remote", "get-url", "origin"], target);
|
|
46376
46804
|
if (!origin.toLowerCase().endsWith(`/${repo.toLowerCase()}.git`) && !origin.toLowerCase().endsWith(`/${repo.toLowerCase()}`))
|
|
46377
46805
|
throw new Error(`${repo}: suite checkout origin mismatch at ${target}; stop`);
|
|
46806
|
+
await refreshSuiteCheckout((args) => exec("git", args, target, WATCH_TIMEOUT));
|
|
46378
46807
|
return target;
|
|
46379
46808
|
};
|
|
46380
|
-
const inTrain = async (repo, action) => {
|
|
46809
|
+
const inTrain = async (repo, action, solo = false) => {
|
|
46381
46810
|
const target = await checkout(repo);
|
|
46382
46811
|
const before = process.cwd();
|
|
46383
46812
|
process.chdir(target);
|
|
46384
46813
|
try {
|
|
46385
46814
|
const deps = trainApplyDeps();
|
|
46386
|
-
if (
|
|
46815
|
+
if (solo) {
|
|
46816
|
+
} else if (repo !== SUITE_REPO) {
|
|
46387
46817
|
deps.suiteCut = suiteCutContext(repo, SUITE_REPO);
|
|
46388
46818
|
deps.announce = async () => ({ status: "skipped", note: "suite announcement belongs to MM-Work" });
|
|
46389
46819
|
} else {
|
|
46820
|
+
deps.suiteCut = suiteLeadContext(SUITE_REPO);
|
|
46390
46821
|
deps.announce = async () => ({ status: "skipped", note: "suite announcement after all members" });
|
|
46391
46822
|
}
|
|
46392
46823
|
return await action(deps);
|
|
@@ -46595,12 +47026,18 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
|
|
|
46595
47026
|
return false;
|
|
46596
47027
|
}
|
|
46597
47028
|
},
|
|
47029
|
+
changedPaths: async (repo, base2, head) => {
|
|
47030
|
+
const target = await checkout(repo);
|
|
47031
|
+
await exec("git", ["fetch", "--quiet", "origin", "--tags"], target, WATCH_TIMEOUT);
|
|
47032
|
+
return (await exec("git", ["diff", "--name-only", base2, head], target)).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
47033
|
+
},
|
|
46598
47034
|
rcand: runRcand,
|
|
46599
47035
|
publish,
|
|
46600
47036
|
preflight: preflight2,
|
|
46601
47037
|
publishMain,
|
|
46602
47038
|
preflightMain,
|
|
46603
47039
|
pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
|
|
47040
|
+
restoreMain: (input) => openSuiteRestorePinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
|
|
46604
47041
|
verifyLeadCut: async (sha) => {
|
|
46605
47042
|
const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
|
|
46606
47043
|
if (current.toLowerCase() === sha.toLowerCase()) return;
|
|
@@ -46632,6 +47069,39 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
|
|
|
46632
47069
|
return void 0;
|
|
46633
47070
|
}
|
|
46634
47071
|
},
|
|
47072
|
+
// #7738: a member hotfixes as suite-solo; the lead hotfixes under its suite-lead capability (inTrain sets it).
|
|
47073
|
+
hotfixStart: (repo, o) => inTrain(repo, async (deps) => {
|
|
47074
|
+
const r = await runHotfixStart(deps, { from: o.from, ack: o.ack, suiteSolo: repo !== SUITE_REPO });
|
|
47075
|
+
return { tag: r.tag };
|
|
47076
|
+
}, repo !== SUITE_REPO),
|
|
47077
|
+
hotfixRelease: (repo, tag) => inTrain(repo, async (deps) => ({ tag: (await runHotfixRelease(deps, tag, { suiteSolo: repo !== SUITE_REPO })).tag }), repo !== SUITE_REPO),
|
|
47078
|
+
// #7738: the #7714 solo pin leg, polled until the auto-merge lands (30 s x 40 = 20 min), then the caller decides.
|
|
47079
|
+
soloPin: async (repo, tag) => {
|
|
47080
|
+
for (let i = 0; ; i++) {
|
|
47081
|
+
const pin = await openSuitePinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, { repo, tag, stage: "main" });
|
|
47082
|
+
if (pin.status !== "pending" || i >= 40) return pin;
|
|
47083
|
+
await new Promise((resolve7) => setTimeout(resolve7, 3e4));
|
|
47084
|
+
}
|
|
47085
|
+
},
|
|
47086
|
+
suiteMapping: async (leadTag) => {
|
|
47087
|
+
const rows = suiteMembers(await registry2(leadTag)).filter((m) => m.repo !== SUITE_REPO);
|
|
47088
|
+
return Object.fromEntries(rows.map((m) => {
|
|
47089
|
+
const v = bundleTag(m.bundles.main);
|
|
47090
|
+
if (!v) throw new Error(`${m.id}: MM-Work ${leadTag} has no versioned main pin; mapping not recorded`);
|
|
47091
|
+
return [m.id, v];
|
|
47092
|
+
}));
|
|
47093
|
+
},
|
|
47094
|
+
recordMapping: async (leadTag, mapping) => {
|
|
47095
|
+
const body = await gh(["release", "view", leadTag, "--repo", SUITE_REPO, "--json", "body", "--jq", ".body"]);
|
|
47096
|
+
if (parseSuiteMapping(body)) return;
|
|
47097
|
+
const dir = repoRuntimeStatePath(root, "suite");
|
|
47098
|
+
(0, import_node_fs43.mkdirSync)(dir, { recursive: true });
|
|
47099
|
+
const file = (0, import_node_path43.join)(dir, `notes-${leadTag}.md`);
|
|
47100
|
+
(0, import_node_fs43.writeFileSync)(file, `${body}
|
|
47101
|
+
|
|
47102
|
+
${suiteMappingBlock(leadTag, mapping)}`);
|
|
47103
|
+
await gh(["release", "edit", leadTag, "--repo", SUITE_REPO, "--notes-file", file]);
|
|
47104
|
+
},
|
|
46635
47105
|
release,
|
|
46636
47106
|
shippedTag,
|
|
46637
47107
|
sameContent: sameContent2,
|
|
@@ -46688,9 +47158,9 @@ function registerSuiteCommands(program3) {
|
|
|
46688
47158
|
if (repo?.toLowerCase() !== SUITE_REPO.toLowerCase()) throw new Error(`suite commands run only from ${SUITE_REPO} (got ${repo ?? "unknown"})`);
|
|
46689
47159
|
return (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
46690
47160
|
};
|
|
46691
|
-
suite.command("rcand").description("cut all module candidates, publish bundle-only rc, merge one two-stage pin PR, then cut MM-Work").option("--owner-go", "explicit approval: rc tests on live data").option("--dry-run", "
|
|
47161
|
+
suite.command("rcand").description("cut all module candidates, publish bundle-only rc, merge one two-stage pin PR, then cut MM-Work").option("--owner-go", "explicit approval: rc tests on live data (not needed for --dry-run)").option("--dry-run", "read-only: per member HELD / carried / new candidate, flagging a pending backend release; no --owner-go needed. Shows the roster and sequence without writing").option("--json", "structured receipt").action(async (o) => {
|
|
46692
47162
|
try {
|
|
46693
|
-
if (!o.ownerGo) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
|
|
47163
|
+
if (!o.ownerGo && !o.dryRun) throw new Error("suite rcand requires --owner-go (rc uses live data); nothing was written");
|
|
46694
47164
|
const root = await leadRoot();
|
|
46695
47165
|
const unlock = o.dryRun ? async () => void 0 : await acquireTrainWriterLock(root, "suite rcand");
|
|
46696
47166
|
let result;
|
|
@@ -46709,19 +47179,39 @@ function registerSuiteCommands(program3) {
|
|
|
46709
47179
|
return fail(`suite rcand: ${e.message}`);
|
|
46710
47180
|
}
|
|
46711
47181
|
});
|
|
47182
|
+
const holdCmd = suite.command("hold").description("freeze or unfreeze a member live release in the suite train (registry META suiteHold, #7735)");
|
|
47183
|
+
const holdWrite = async (member, hold) => {
|
|
47184
|
+
try {
|
|
47185
|
+
const root = await leadRoot();
|
|
47186
|
+
const reg = registryClientDeps(await loadConfig());
|
|
47187
|
+
console.log(await setSuiteHold({
|
|
47188
|
+
registry: () => suiteTrainIo(root).registry(),
|
|
47189
|
+
read: (slug) => fetchProjectBySlugChecked(slug, reg),
|
|
47190
|
+
write: (slug, patch) => upsertProject(slug, patch, reg)
|
|
47191
|
+
}, member, hold));
|
|
47192
|
+
} catch (e) {
|
|
47193
|
+
return fail(`suite hold: ${e.message}`);
|
|
47194
|
+
}
|
|
47195
|
+
};
|
|
47196
|
+
holdCmd.command("set <member-repo>").description("cut the member out of the suite train until cleared").requiredOption("--reason <text>", "why the live release is frozen").option("--since <date>", "when the freeze began (e.g. 2026-09-28)").action((member, o) => holdWrite(member, { reason: o.reason, ...o.since ? { since: o.since } : {} }));
|
|
47197
|
+
holdCmd.command("clear <member-repo>").description("put the member back in the suite train").action((member) => holdWrite(member, null));
|
|
46712
47198
|
suite.command("abandon").description("retire an unfinished cut: archive the record; member rc tags stay (#7396). A cut whose pin PR landed but released nothing needs --owner-go (#7450)").option("--owner-go", "explicit owner approval to retire a cut whose pin PR already landed").option("--json", "structured receipt").action(async (o) => {
|
|
46713
47199
|
try {
|
|
46714
47200
|
const root = await leadRoot();
|
|
46715
47201
|
const unlock = await acquireTrainWriterLock(root, "suite abandon");
|
|
46716
47202
|
let result;
|
|
47203
|
+
let restored;
|
|
46717
47204
|
try {
|
|
46718
|
-
|
|
47205
|
+
const io = suiteTrainIo(root);
|
|
47206
|
+
restored = o.ownerGo === true ? await suiteRestoreMainPins(io) : void 0;
|
|
47207
|
+
result = suiteAbandon(io.recordPath, o.ownerGo === true);
|
|
46719
47208
|
} finally {
|
|
46720
47209
|
await unlock();
|
|
46721
47210
|
}
|
|
46722
47211
|
console.log(o.json ? JSON.stringify(result, null, 2) : [
|
|
46723
47212
|
`suite abandon: archived the cut record to ${result.archive}`,
|
|
46724
47213
|
...result.orphanTags.length ? [" member rc tags left in place (immutable; the next cut reuses one still unreleased on its rc head, #7450):", ...result.orphanTags.map((t) => ` - ${t}`)] : [" no member rc tag was minted"],
|
|
47214
|
+
...restored ? [` main pins the cut planned for unreleased tags are restored by ${restored.pr} (${restored.status}, #7758)`] : [],
|
|
46725
47215
|
" the next suite rcand --owner-go re-snapshots every member"
|
|
46726
47216
|
].join("\n"));
|
|
46727
47217
|
} catch (e) {
|
|
@@ -46753,6 +47243,26 @@ function registerSuiteCommands(program3) {
|
|
|
46753
47243
|
return fail(`suite release: ${e.message}`);
|
|
46754
47244
|
}
|
|
46755
47245
|
});
|
|
47246
|
+
suite.command("hotfix <member>").description("member hotfix, main pin PR, MM-Work patch release, and the recorded suite version -> member versions mapping (#7738)").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR(s) or SHA(s) the member hotfix carries").option("--ack <shas>", "dev shas verified as not needed on main (see hotfix start --ack)").option("--owner-go", "explicit owner approval: ships the member and MM-Work to production (not needed for --dry-run)").option("--dry-run", "read-only: print the ordered plan; writes nothing").option("--json", "structured receipt").action(async (member, o) => {
|
|
47247
|
+
try {
|
|
47248
|
+
if (!o.ownerGo && !o.dryRun) throw new Error("suite hotfix requires --owner-go (it ships to production); nothing was written. --dry-run prints the plan");
|
|
47249
|
+
const root = await leadRoot();
|
|
47250
|
+
const unlock = o.dryRun ? async () => void 0 : await acquireTrainWriterLock(root, "suite hotfix");
|
|
47251
|
+
let result;
|
|
47252
|
+
try {
|
|
47253
|
+
result = await suiteHotfix(suiteTrainIo(root), { member, from: o.from, ack: o.ack?.split(",").map((s) => s.trim()).filter(Boolean), ownerGo: o.ownerGo, dryRun: o.dryRun });
|
|
47254
|
+
} finally {
|
|
47255
|
+
await unlock();
|
|
47256
|
+
}
|
|
47257
|
+
console.log(o.json ? JSON.stringify(result, null, 2) : [
|
|
47258
|
+
`suite hotfix ${o.dryRun ? "dry-run" : "receipt"}:`,
|
|
47259
|
+
...result.plan.map((line) => ` - ${line}`),
|
|
47260
|
+
...result.record?.mapping ? [` - MM-Work ${result.record.leadTag} = members ${Object.entries(result.record.mapping).map(([id, v]) => `${id} ${v}`).join(", ")}`] : []
|
|
47261
|
+
].join("\n"));
|
|
47262
|
+
} catch (e) {
|
|
47263
|
+
return fail(`suite hotfix: ${e.message}`);
|
|
47264
|
+
}
|
|
47265
|
+
});
|
|
46756
47266
|
suite.command("rc-data").description("switch the rc Preview envs between the live and rc data sets as one bound switch (#7588)").option("--to <direction>", "switch source: live or rc").option("--apply", "write the switch (default is a dry-run plan)").option("--owner-go", "explicit approval: touches the live data key").option("--json", "structured receipt").action(async (o) => {
|
|
46757
47267
|
try {
|
|
46758
47268
|
if (o.to !== "live" && o.to !== "rc") throw new Error(`--to must be live or rc (got ${o.to ?? "nothing"}); nothing was written`);
|
|
@@ -46778,56 +47288,101 @@ function registerSuiteCommands(program3) {
|
|
|
46778
47288
|
return fail(`suite rc-data: ${e.message}`);
|
|
46779
47289
|
}
|
|
46780
47290
|
});
|
|
46781
|
-
suite.command("status").description("compare served versions with development, rc and main branch heads across MM-Work modules").option("--suite <name>", "suite name (mm-work)", "mm-work").option("--json", "structured output").action(async (o) => {
|
|
47291
|
+
suite.command("status").description("compare served versions with development, rc and main branch heads across MM-Work modules").option("--suite <name>", "suite name (mm-work)", "mm-work").option("--json", "structured output").option("--markdown", "print a markdown docs table of member versions per stage and the suite tag that pins each").action(async (o) => {
|
|
46782
47292
|
if (o.suite !== "mm-work") return fail("suite status: only mm-work is supported");
|
|
46783
47293
|
const github = defaultGitHubClient();
|
|
46784
47294
|
const cfg = await loadConfig();
|
|
46785
47295
|
const registry2 = registryClientDeps(cfg);
|
|
46786
47296
|
try {
|
|
46787
|
-
const report = await readSuiteStatus(
|
|
46788
|
-
|
|
46789
|
-
const file = await github.rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`);
|
|
46790
|
-
if (!file?.content) throw new Error("MM-Work module registry is unavailable");
|
|
46791
|
-
return JSON.parse(decodeGitHubContents(file.content));
|
|
46792
|
-
},
|
|
46793
|
-
head: async (repo, branch) => {
|
|
46794
|
-
try {
|
|
46795
|
-
const ref = await github.rest("GET", `repos/${repo}/git/ref/heads/${branch}`);
|
|
46796
|
-
return ref?.object?.sha ?? "";
|
|
46797
|
-
} catch (e) {
|
|
46798
|
-
if (e instanceof GitHubApiError && e.status === 404) return null;
|
|
46799
|
-
throw e;
|
|
46800
|
-
}
|
|
46801
|
-
},
|
|
46802
|
-
tagCommit: async (repo, tag) => {
|
|
46803
|
-
const commit = await github.rest("GET", `repos/${repo}/commits/${encodeURIComponent(tag)}`);
|
|
46804
|
-
return commit?.sha;
|
|
46805
|
-
},
|
|
46806
|
-
deploy: async (repo, stage) => {
|
|
46807
|
-
const slug = repo.split("/")[1].toLowerCase();
|
|
46808
|
-
const facts = await fetchDeployFactsBySlug(slug, registry2);
|
|
46809
|
-
if (!facts) return void 0;
|
|
46810
|
-
const url = publicUrlFromDeployFact(facts.stages[stage]);
|
|
46811
|
-
const [health, lastRun] = await Promise.all([
|
|
46812
|
-
url ? probePublicHealth(url) : Promise.resolve(void 0),
|
|
46813
|
-
fetchLastTenantDeployRun(slug, stage)
|
|
46814
|
-
]);
|
|
46815
|
-
return buildDeployStatusReport({
|
|
46816
|
-
repo,
|
|
46817
|
-
slug,
|
|
46818
|
-
stage,
|
|
46819
|
-
deployFacts: facts,
|
|
46820
|
-
lastRun: lastRun.run,
|
|
46821
|
-
lastRunAbsence: lastRun.absence,
|
|
46822
|
-
health
|
|
46823
|
-
});
|
|
46824
|
-
}
|
|
46825
|
-
});
|
|
46826
|
-
console.log(o.json ? JSON.stringify(report, null, 2) : formatSuiteStatus(report));
|
|
47297
|
+
const report = await readSuiteStatus(suiteStatusDeps(github, registry2));
|
|
47298
|
+
console.log(o.json ? JSON.stringify(report, null, 2) : o.markdown ? formatSuiteMarkdown(report) : formatSuiteStatus(report));
|
|
46827
47299
|
} catch (e) {
|
|
46828
47300
|
return fail(`suite status: ${e.message}`);
|
|
46829
47301
|
}
|
|
46830
47302
|
});
|
|
47303
|
+
suite.command("dev").description("per member: development head, the version dev serves and the newest green SHA; --wait until none is lagging (#7739)").option("--wait", "poll every 60s until no member is lagging (blocked or unknown members end the wait)").option("--timeout <minutes>", "how long --wait polls", "30").option("--json", "structured output").action(async (o) => {
|
|
47304
|
+
const github = defaultGitHubClient();
|
|
47305
|
+
const registry2 = registryClientDeps(await loadConfig());
|
|
47306
|
+
const status = suiteStatusDeps(github, registry2);
|
|
47307
|
+
const deps = {
|
|
47308
|
+
status: () => readSuiteStatus(status),
|
|
47309
|
+
commits: async (repo, limit) => (await github.rest("GET", `repos/${repo}/commits?sha=development&per_page=${limit}`) ?? []).map((c) => c.sha),
|
|
47310
|
+
checkRuns: async (repo, sha) => (await github.rest("GET", `repos/${repo}/commits/${sha}/check-runs?per_page=100`))?.check_runs ?? [],
|
|
47311
|
+
requiredChecks: async (repo) => {
|
|
47312
|
+
const meta = await fetchProjectBySlugChecked(repo.split("/")[1].toLowerCase(), registry2);
|
|
47313
|
+
if (!meta.ok) throw new Error(`${repo} META unreadable: ${meta.error}`);
|
|
47314
|
+
return meta.project?.requiredChecks ?? ["gate"];
|
|
47315
|
+
}
|
|
47316
|
+
};
|
|
47317
|
+
try {
|
|
47318
|
+
const minutes = Number(o.timeout);
|
|
47319
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return fail("suite dev: --timeout must be a positive number of minutes");
|
|
47320
|
+
const rows = o.wait ? await waitSuiteDev(deps, { timeoutMs: minutes * 6e4, intervalMs: 6e4, onTick: (r) => {
|
|
47321
|
+
if (!o.json) console.error(`suite dev: ${r.filter((x) => x.state === "lagging").length} lagging`);
|
|
47322
|
+
} }) : await readSuiteDev(deps);
|
|
47323
|
+
console.log(o.json ? JSON.stringify(rows, null, 2) : formatSuiteDev(rows));
|
|
47324
|
+
if (rows.some((r) => r.state !== "converged")) process.exitCode = 1;
|
|
47325
|
+
} catch (e) {
|
|
47326
|
+
return fail(`suite dev: ${e.message}`);
|
|
47327
|
+
}
|
|
47328
|
+
});
|
|
47329
|
+
}
|
|
47330
|
+
function suiteStatusDeps(github, registry2) {
|
|
47331
|
+
return {
|
|
47332
|
+
registry: async () => {
|
|
47333
|
+
const file = await github.rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`);
|
|
47334
|
+
if (!file?.content) throw new Error("MM-Work module registry is unavailable");
|
|
47335
|
+
return JSON.parse(decodeGitHubContents(file.content));
|
|
47336
|
+
},
|
|
47337
|
+
head: async (repo, branch) => {
|
|
47338
|
+
try {
|
|
47339
|
+
const ref = await github.rest("GET", `repos/${repo}/git/ref/heads/${branch}`);
|
|
47340
|
+
return ref?.object?.sha ?? "";
|
|
47341
|
+
} catch (e) {
|
|
47342
|
+
if (e instanceof GitHubApiError && e.status === 404) return null;
|
|
47343
|
+
throw e;
|
|
47344
|
+
}
|
|
47345
|
+
},
|
|
47346
|
+
tagCommit: async (repo, tag) => {
|
|
47347
|
+
try {
|
|
47348
|
+
const commit = await github.rest("GET", `repos/${repo}/commits/${encodeURIComponent(tag)}`);
|
|
47349
|
+
return commit?.sha;
|
|
47350
|
+
} catch (e) {
|
|
47351
|
+
if (e instanceof GitHubApiError && (e.status === 404 || e.status === 422)) return void 0;
|
|
47352
|
+
throw e;
|
|
47353
|
+
}
|
|
47354
|
+
},
|
|
47355
|
+
fetchUrl: async (url) => {
|
|
47356
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1e4), redirect: "follow" });
|
|
47357
|
+
return { status: res.status, body: new Uint8Array(await res.arrayBuffer()) };
|
|
47358
|
+
},
|
|
47359
|
+
leadTags: async () => (await github.rest("GET", `repos/${SUITE_REPO}/tags?per_page=100`) ?? []).map((t) => t.name),
|
|
47360
|
+
leadReleaseBody: async (tag) => (await github.rest("GET", `repos/${SUITE_REPO}/releases/tags/${encodeURIComponent(tag)}`))?.body ?? null,
|
|
47361
|
+
registryAt: async (ref) => {
|
|
47362
|
+
const file = await github.rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`);
|
|
47363
|
+
if (!file?.content) throw new Error(`MM-Work seed/modules.json at ${ref} is unavailable`);
|
|
47364
|
+
return JSON.parse(decodeGitHubContents(file.content));
|
|
47365
|
+
},
|
|
47366
|
+
deploy: async (repo, stage) => {
|
|
47367
|
+
const slug = repo.split("/")[1].toLowerCase();
|
|
47368
|
+
const facts = await fetchDeployFactsBySlug(slug, registry2);
|
|
47369
|
+
if (!facts) return void 0;
|
|
47370
|
+
const url = publicUrlFromDeployFact(facts.stages[stage]);
|
|
47371
|
+
const [health, lastRun] = await Promise.all([
|
|
47372
|
+
url ? probePublicHealth(url) : Promise.resolve(void 0),
|
|
47373
|
+
fetchLastTenantDeployRun(slug, stage)
|
|
47374
|
+
]);
|
|
47375
|
+
return buildDeployStatusReport({
|
|
47376
|
+
repo,
|
|
47377
|
+
slug,
|
|
47378
|
+
stage,
|
|
47379
|
+
deployFacts: facts,
|
|
47380
|
+
lastRun: lastRun.run,
|
|
47381
|
+
lastRunAbsence: lastRun.absence,
|
|
47382
|
+
health
|
|
47383
|
+
});
|
|
47384
|
+
}
|
|
47385
|
+
};
|
|
46831
47386
|
}
|
|
46832
47387
|
|
|
46833
47388
|
// src/discovery-commands.ts
|
|
@@ -47265,6 +47820,16 @@ var surfaces_default = {
|
|
|
47265
47820
|
"mmi-cli.cmd"
|
|
47266
47821
|
]
|
|
47267
47822
|
},
|
|
47823
|
+
{
|
|
47824
|
+
mode: "files",
|
|
47825
|
+
sourcePath: "cli",
|
|
47826
|
+
targetPath: "packages/claude-plugin/cli",
|
|
47827
|
+
include: [
|
|
47828
|
+
"dist/index.cjs",
|
|
47829
|
+
"dist/main.cjs",
|
|
47830
|
+
"package.json"
|
|
47831
|
+
]
|
|
47832
|
+
},
|
|
47268
47833
|
{
|
|
47269
47834
|
mode: "files",
|
|
47270
47835
|
sourcePath: ".claude-plugin",
|
|
@@ -47340,6 +47905,16 @@ var surfaces_default = {
|
|
|
47340
47905
|
"mmi-cli.cmd"
|
|
47341
47906
|
]
|
|
47342
47907
|
},
|
|
47908
|
+
{
|
|
47909
|
+
mode: "files",
|
|
47910
|
+
sourcePath: "cli",
|
|
47911
|
+
targetPath: "packages/codex-plugin/cli",
|
|
47912
|
+
include: [
|
|
47913
|
+
"dist/index.cjs",
|
|
47914
|
+
"dist/main.cjs",
|
|
47915
|
+
"package.json"
|
|
47916
|
+
]
|
|
47917
|
+
},
|
|
47343
47918
|
{
|
|
47344
47919
|
mode: "files",
|
|
47345
47920
|
sourcePath: ".codex-plugin",
|
|
@@ -47415,6 +47990,16 @@ var surfaces_default = {
|
|
|
47415
47990
|
"mmi-cli.cmd"
|
|
47416
47991
|
]
|
|
47417
47992
|
},
|
|
47993
|
+
{
|
|
47994
|
+
mode: "files",
|
|
47995
|
+
sourcePath: "cli",
|
|
47996
|
+
targetPath: "packages/cursor-plugin/cli",
|
|
47997
|
+
include: [
|
|
47998
|
+
"dist/index.cjs",
|
|
47999
|
+
"dist/main.cjs",
|
|
48000
|
+
"package.json"
|
|
48001
|
+
]
|
|
48002
|
+
},
|
|
47418
48003
|
{
|
|
47419
48004
|
mode: "files",
|
|
47420
48005
|
sourcePath: ".cursor-plugin",
|
|
@@ -47631,7 +48216,7 @@ var surfaces_default = {
|
|
|
47631
48216
|
ownerPath: "packages/claude-plugin/package.json",
|
|
47632
48217
|
deliveryPath: "packages/claude-plugin/package.json",
|
|
47633
48218
|
delivery: "npm",
|
|
47634
|
-
applicability: "Claude Code plugin; npm package @mutmutco/claude-plugin carrying the plugin manifest, skills, hooks, gate scripts, and launchers. The thin git catalog stays in .claude-plugin/marketplace.json (#4945).",
|
|
48219
|
+
applicability: "Claude Code plugin; npm package @mutmutco/claude-plugin carrying the plugin manifest, skills, hooks, gate scripts, launchers, and the bundled mmi-cli engine the launchers run. The thin git catalog stays in .claude-plugin/marketplace.json (#4945).",
|
|
47635
48220
|
versionCoordinated: true,
|
|
47636
48221
|
surfaceToken: "claude",
|
|
47637
48222
|
versionPaths: [
|
|
@@ -47647,7 +48232,8 @@ var surfaces_default = {
|
|
|
47647
48232
|
additionalPaths: [
|
|
47648
48233
|
"packages/claude-plugin/.claude-plugin",
|
|
47649
48234
|
"packages/claude-plugin/skills",
|
|
47650
|
-
"packages/claude-plugin/bin"
|
|
48235
|
+
"packages/claude-plugin/bin",
|
|
48236
|
+
"packages/claude-plugin/cli"
|
|
47651
48237
|
],
|
|
47652
48238
|
artifactIdentity: {
|
|
47653
48239
|
kind: "npm-pack",
|
|
@@ -47682,7 +48268,8 @@ var surfaces_default = {
|
|
|
47682
48268
|
additionalPaths: [
|
|
47683
48269
|
"packages/codex-plugin/.codex-plugin",
|
|
47684
48270
|
"packages/codex-plugin/skills",
|
|
47685
|
-
"packages/codex-plugin/bin"
|
|
48271
|
+
"packages/codex-plugin/bin",
|
|
48272
|
+
"packages/codex-plugin/cli"
|
|
47686
48273
|
],
|
|
47687
48274
|
artifactIdentity: {
|
|
47688
48275
|
kind: "npm-pack",
|
|
@@ -47713,7 +48300,8 @@ var surfaces_default = {
|
|
|
47713
48300
|
additionalPaths: [
|
|
47714
48301
|
"packages/cursor-plugin/.cursor-plugin",
|
|
47715
48302
|
"packages/cursor-plugin/skills",
|
|
47716
|
-
"packages/cursor-plugin/bin"
|
|
48303
|
+
"packages/cursor-plugin/bin",
|
|
48304
|
+
"packages/cursor-plugin/cli"
|
|
47717
48305
|
],
|
|
47718
48306
|
artifactIdentity: {
|
|
47719
48307
|
kind: "npm-pack",
|
|
@@ -50826,14 +51414,14 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
|
|
|
50826
51414
|
}
|
|
50827
51415
|
|
|
50828
51416
|
// src/tenant-artifact.ts
|
|
50829
|
-
var
|
|
51417
|
+
var import_node_crypto16 = require("node:crypto");
|
|
50830
51418
|
var import_node_fs48 = require("node:fs");
|
|
50831
51419
|
var import_promises11 = require("node:fs/promises");
|
|
50832
51420
|
var import_node_path46 = require("node:path");
|
|
50833
51421
|
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}$/;
|
|
50834
51422
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
50835
51423
|
async function sha256File(path2) {
|
|
50836
|
-
const hash = (0,
|
|
51424
|
+
const hash = (0, import_node_crypto16.createHash)("sha256");
|
|
50837
51425
|
for await (const chunk of (0, import_node_fs48.createReadStream)(path2)) hash.update(chunk);
|
|
50838
51426
|
return hash.digest("hex");
|
|
50839
51427
|
}
|
|
@@ -50843,8 +51431,8 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
|
50843
51431
|
const info = await (0, import_promises11.stat)(path2);
|
|
50844
51432
|
if (!info.isFile()) throw new Error("tenant artifact put: input path must be a file");
|
|
50845
51433
|
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`);
|
|
50846
|
-
const
|
|
50847
|
-
const prepared = await tenantArtifactUpload({ repo, stage, size: info.size, sha256:
|
|
51434
|
+
const sha2563 = await sha256File(path2);
|
|
51435
|
+
const prepared = await tenantArtifactUpload({ repo, stage, size: info.size, sha256: sha2563 }, deps);
|
|
50848
51436
|
if (!prepared.ok) {
|
|
50849
51437
|
const detail = prepared.body?.error ?? prepared.error ?? `HTTP ${prepared.status}`;
|
|
50850
51438
|
throw new Error(`tenant artifact put: ${detail}`);
|
|
@@ -50872,8 +51460,8 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
|
|
|
50872
51460
|
throw error;
|
|
50873
51461
|
}
|
|
50874
51462
|
if (!uploaded.ok) throw new Error(`tenant artifact put: object upload failed (HTTP ${uploaded.status})`);
|
|
50875
|
-
if (body.size !== info.size || body.sha256 !==
|
|
50876
|
-
return { artifactId: body.artifactId, repo, stage, size: info.size, sha256:
|
|
51463
|
+
if (body.size !== info.size || body.sha256 !== sha2563 || typeof body.expiresAt !== "string" || !Number.isFinite(Date.parse(body.expiresAt))) throw new Error("tenant artifact put: Hub returned inconsistent artifact metadata");
|
|
51464
|
+
return { artifactId: body.artifactId, repo, stage, size: info.size, sha256: sha2563, expiresAt: body.expiresAt };
|
|
50877
51465
|
}
|
|
50878
51466
|
|
|
50879
51467
|
// src/tenant-sweep.ts
|
|
@@ -51063,13 +51651,13 @@ async function executeWaveLand(plan, deps) {
|
|
|
51063
51651
|
|
|
51064
51652
|
// src/dist-drift.ts
|
|
51065
51653
|
var import_node_child_process18 = require("node:child_process");
|
|
51066
|
-
var
|
|
51654
|
+
var import_node_crypto18 = require("node:crypto");
|
|
51067
51655
|
var import_node_fs50 = require("node:fs");
|
|
51068
51656
|
var import_node_os22 = require("node:os");
|
|
51069
51657
|
var import_node_path48 = require("node:path");
|
|
51070
51658
|
|
|
51071
51659
|
// ../scripts/distribution-digest.mjs
|
|
51072
|
-
var
|
|
51660
|
+
var import_node_crypto17 = require("node:crypto");
|
|
51073
51661
|
var import_node_fs49 = require("node:fs");
|
|
51074
51662
|
var import_node_path47 = require("node:path");
|
|
51075
51663
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
@@ -51082,7 +51670,7 @@ function repoPath(root, declaredPath, label) {
|
|
|
51082
51670
|
return target;
|
|
51083
51671
|
}
|
|
51084
51672
|
function digestFiles(files) {
|
|
51085
|
-
const hash = (0,
|
|
51673
|
+
const hash = (0, import_node_crypto17.createHash)("sha256");
|
|
51086
51674
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
51087
51675
|
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs49.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs49.readFileSync)(file.absolute);
|
|
51088
51676
|
hash.update(file.relative, "utf8");
|
|
@@ -51112,10 +51700,10 @@ var DIST_ARTIFACTS = [
|
|
|
51112
51700
|
];
|
|
51113
51701
|
var BOM_DIST_TREE_ID = "mmi-cli-dist";
|
|
51114
51702
|
var ABSENT = "absent";
|
|
51115
|
-
var
|
|
51703
|
+
var sha2562 = (bytes) => `sha256:${(0, import_node_crypto18.createHash)("sha256").update(bytes).digest("hex")}`;
|
|
51116
51704
|
function artifactDrift(path2, committedBytes, rebuiltBytes) {
|
|
51117
|
-
const committed = committedBytes ?
|
|
51118
|
-
const rebuiltExpected = rebuiltBytes ?
|
|
51705
|
+
const committed = committedBytes ? sha2562(committedBytes) : ABSENT;
|
|
51706
|
+
const rebuiltExpected = rebuiltBytes ? sha2562(rebuiltBytes) : ABSENT;
|
|
51119
51707
|
return { path: path2, status: committed === rebuiltExpected ? "current" : "stale", committed, rebuiltExpected };
|
|
51120
51708
|
}
|
|
51121
51709
|
function distTreeIdentity(labels, rebuilt, committed, digest) {
|
package/package.json
CHANGED