@mutmutco/cli 4.5.209 → 4.5.210
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 +420 -306
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -6365,21 +6365,177 @@ function splitInlineSelectors(argv, flags) {
|
|
|
6365
6365
|
}
|
|
6366
6366
|
return out;
|
|
6367
6367
|
}
|
|
6368
|
+
var BOARD_MOVE_OWN_FLAGS = ["--repo", "--allow-partial", "--json"];
|
|
6369
|
+
var BOARD_SHOW_OWN_FLAGS = ["--json", "--repo", "--allow-partial", "--out"];
|
|
6370
|
+
var BOARD_READ_OWN_FLAGS = [
|
|
6371
|
+
"--json",
|
|
6372
|
+
"--repo",
|
|
6373
|
+
"--direct",
|
|
6374
|
+
"--snapshot-only",
|
|
6375
|
+
"--min-cursor",
|
|
6376
|
+
"--bundle-details",
|
|
6377
|
+
"--bodies",
|
|
6378
|
+
"--allow-partial",
|
|
6379
|
+
"--out"
|
|
6380
|
+
];
|
|
6381
|
+
var BOARD_OTHER_OWN_FLAGS = {
|
|
6382
|
+
unclaim: ["--json", "--repo", "--to-status", "--force", "--holder-dead", "--allow-partial"],
|
|
6383
|
+
attach: ["--repo", "--json"],
|
|
6384
|
+
"set-priority": ["--json", "--repo"],
|
|
6385
|
+
"archive-done": ["--json", "--repo", "--apply"],
|
|
6386
|
+
doctor: ["--json", "--repo", "--fix", "--concurrency"],
|
|
6387
|
+
events: ["--since", "--wait", "--repo", "--json"]
|
|
6388
|
+
};
|
|
6389
|
+
var BOARD_TARGET_PLACEHOLDER = {
|
|
6390
|
+
unclaim: "<issue>",
|
|
6391
|
+
attach: "<issues>",
|
|
6392
|
+
"set-priority": "<issue> <priority>"
|
|
6393
|
+
};
|
|
6394
|
+
var WORKBOARD_VERB_ALIASES = {
|
|
6395
|
+
claim: "claim",
|
|
6396
|
+
move: "move",
|
|
6397
|
+
status: "move",
|
|
6398
|
+
"set-status": "move",
|
|
6399
|
+
show: "show",
|
|
6400
|
+
view: "show",
|
|
6401
|
+
get: "show",
|
|
6402
|
+
read: "read",
|
|
6403
|
+
list: "read",
|
|
6404
|
+
ls: "read",
|
|
6405
|
+
items: "read",
|
|
6406
|
+
unclaim: "unclaim",
|
|
6407
|
+
release: "unclaim",
|
|
6408
|
+
attach: "attach",
|
|
6409
|
+
add: "attach",
|
|
6410
|
+
"set-priority": "set-priority",
|
|
6411
|
+
priority: "set-priority",
|
|
6412
|
+
"archive-done": "archive-done",
|
|
6413
|
+
archive: "archive-done",
|
|
6414
|
+
doctor: "doctor",
|
|
6415
|
+
events: "events"
|
|
6416
|
+
};
|
|
6417
|
+
var WORKBOARD_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
6418
|
+
"--status",
|
|
6419
|
+
"--to-status",
|
|
6420
|
+
"--issue",
|
|
6421
|
+
"--number",
|
|
6422
|
+
"--repo",
|
|
6423
|
+
"--for",
|
|
6424
|
+
"--out",
|
|
6425
|
+
"--min-cursor",
|
|
6426
|
+
"--since",
|
|
6427
|
+
"--wait",
|
|
6428
|
+
"--concurrency"
|
|
6429
|
+
]);
|
|
6430
|
+
var ISSUE_TOKEN = /^(?:\d+|[\w.-]+\/[\w.-]+#\d+|#\d+)$/;
|
|
6431
|
+
function scanWorkboardArgs(rest) {
|
|
6432
|
+
const positionals = [];
|
|
6433
|
+
const values = /* @__PURE__ */ new Map();
|
|
6434
|
+
const flags = [];
|
|
6435
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
6436
|
+
const tok = rest[i];
|
|
6437
|
+
if (!tok.startsWith("-")) {
|
|
6438
|
+
positionals.push(tok);
|
|
6439
|
+
continue;
|
|
6440
|
+
}
|
|
6441
|
+
const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
|
|
6442
|
+
const name = eq > 0 ? tok.slice(0, eq) : tok;
|
|
6443
|
+
let value;
|
|
6444
|
+
if (eq > 0) value = tok.slice(eq + 1);
|
|
6445
|
+
else if (WORKBOARD_VALUE_FLAGS.has(name) && rest[i + 1] !== void 0 && !rest[i + 1].startsWith("-")) {
|
|
6446
|
+
value = rest[i + 1];
|
|
6447
|
+
i += 1;
|
|
6448
|
+
}
|
|
6449
|
+
if (value !== void 0) values.set(name, [...values.get(name) ?? [], value]);
|
|
6450
|
+
flags.push({ name, tokens: eq > 0 ? [tok] : value !== void 0 ? [name, value] : [name] });
|
|
6451
|
+
}
|
|
6452
|
+
return {
|
|
6453
|
+
positionals,
|
|
6454
|
+
values,
|
|
6455
|
+
flagArgs: (ownFlags) => flags.filter((f) => ownFlags.includes(f.name)).flatMap((f) => f.tokens)
|
|
6456
|
+
};
|
|
6457
|
+
}
|
|
6458
|
+
var quoteArg = (v) => /\s/.test(v) ? `"${v}"` : v;
|
|
6368
6459
|
function workboardClaimRecovery(unknownToken, argv, paths, ownFlags = BOARD_CLAIM_OWN_FLAGS) {
|
|
6369
6460
|
if (unknownToken !== "workboard") return void 0;
|
|
6370
6461
|
const catalog = [...paths];
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6462
|
+
const inCatalog = (sub2) => catalog.includes(`oracle board ${sub2}`) || catalog.includes(`board ${sub2}`);
|
|
6463
|
+
let tail = unknownCommandTail(argv, "workboard");
|
|
6464
|
+
let skip = 0;
|
|
6465
|
+
if ((tail[0] === "item" || tail[0] === "items") && !(tail.length === 1 && tail[0] === "items")) {
|
|
6466
|
+
tail = tail.slice(1);
|
|
6467
|
+
skip = 1;
|
|
6468
|
+
}
|
|
6469
|
+
const verb = tail[0];
|
|
6470
|
+
let sub = verb === void 0 ? "read" : WORKBOARD_VERB_ALIASES[verb] ?? (inCatalog(verb) ? verb : void 0);
|
|
6471
|
+
if (sub === void 0) return void 0;
|
|
6472
|
+
if (sub === "claim") {
|
|
6473
|
+
if (!inCatalog("claim")) return void 0;
|
|
6474
|
+
const canonicalPath2 = "oracle board claim";
|
|
6475
|
+
const positionals = tail.slice(1);
|
|
6476
|
+
let corrected2;
|
|
6477
|
+
if (positionals.length > 0) {
|
|
6478
|
+
corrected2 = [`mmi-cli ${canonicalPath2}`, ...positionals, ...survivingFlagArgs(argv, "", ownFlags)].join(" ");
|
|
6479
|
+
} else {
|
|
6480
|
+
const normalized = splitInlineSelectors(argv, ["--issue", "--number"]);
|
|
6481
|
+
const usedFlag = normalized.includes("--issue") ? "--issue" : normalized.includes("--number") ? "--number" : "--issue";
|
|
6482
|
+
corrected2 = formatPositionalTarget(canonicalPath2, "issues", { flag: usedFlag, argv: normalized, ownFlags });
|
|
6483
|
+
}
|
|
6484
|
+
return { canonicalPath: canonicalPath2, corrected: corrected2, hint: `did you mean \`${corrected2}\`?` };
|
|
6485
|
+
}
|
|
6486
|
+
const verbAt = argv.indexOf("workboard") + 1 + skip;
|
|
6487
|
+
const scan = scanWorkboardArgs(argv.slice(verb === void 0 ? verbAt : verbAt + 1));
|
|
6488
|
+
const issueValues = [...scan.values.get("--issue") ?? [], ...scan.values.get("--number") ?? []];
|
|
6489
|
+
if (sub === "read" && verb !== void 0 && (scan.positionals.length > 0 || issueValues.length > 0)) sub = "show";
|
|
6490
|
+
if (!inCatalog(sub)) return void 0;
|
|
6491
|
+
const canonicalPath = `oracle board ${sub}`;
|
|
6492
|
+
const head = `mmi-cli ${canonicalPath}`;
|
|
6376
6493
|
let corrected;
|
|
6377
|
-
if (
|
|
6378
|
-
|
|
6494
|
+
if (sub === "move") {
|
|
6495
|
+
const flagStatus = scan.values.get("--status")?.[0] ?? scan.values.get("--to-status")?.[0];
|
|
6496
|
+
let status = flagStatus;
|
|
6497
|
+
let issues = [];
|
|
6498
|
+
const pos = scan.positionals;
|
|
6499
|
+
if (status !== void 0) {
|
|
6500
|
+
issues = pos;
|
|
6501
|
+
} else if (pos.length >= 2) {
|
|
6502
|
+
if (ISSUE_TOKEN.test(pos[0]) && !ISSUE_TOKEN.test(pos[pos.length - 1])) {
|
|
6503
|
+
status = pos[pos.length - 1];
|
|
6504
|
+
issues = pos.slice(0, -1);
|
|
6505
|
+
} else {
|
|
6506
|
+
status = pos[0];
|
|
6507
|
+
issues = pos.slice(1);
|
|
6508
|
+
}
|
|
6509
|
+
} else if (pos.length === 1) {
|
|
6510
|
+
if (ISSUE_TOKEN.test(pos[0])) issues = pos;
|
|
6511
|
+
else status = pos[0];
|
|
6512
|
+
}
|
|
6513
|
+
if (issues.length === 0) issues = issueValues;
|
|
6514
|
+
corrected = [
|
|
6515
|
+
head,
|
|
6516
|
+
status !== void 0 ? quoteArg(status) : "<status>",
|
|
6517
|
+
...issues.length > 0 ? issues : ["<issues>"],
|
|
6518
|
+
...scan.flagArgs(BOARD_MOVE_OWN_FLAGS)
|
|
6519
|
+
].join(" ");
|
|
6520
|
+
} else if (sub === "show") {
|
|
6521
|
+
if (scan.positionals.length > 0) {
|
|
6522
|
+
corrected = [head, ...scan.positionals, ...scan.flagArgs(BOARD_SHOW_OWN_FLAGS)].join(" ");
|
|
6523
|
+
} else {
|
|
6524
|
+
const normalized = splitInlineSelectors(argv, ["--issue", "--number"]);
|
|
6525
|
+
const usedFlag = normalized.includes("--issue") ? "--issue" : normalized.includes("--number") ? "--number" : "--issue";
|
|
6526
|
+
corrected = formatPositionalTarget(canonicalPath, "issue", { flag: usedFlag, argv: normalized, ownFlags: BOARD_SHOW_OWN_FLAGS });
|
|
6527
|
+
}
|
|
6528
|
+
} else if (sub === "read") {
|
|
6529
|
+
corrected = [head, ...survivingFlagArgs(argv, "", BOARD_READ_OWN_FLAGS)].join(" ");
|
|
6379
6530
|
} else {
|
|
6380
|
-
const
|
|
6381
|
-
const
|
|
6382
|
-
|
|
6531
|
+
const declared = BOARD_OTHER_OWN_FLAGS[sub] ?? ["--json", "--repo"];
|
|
6532
|
+
const targets = [...issueValues, ...scan.positionals];
|
|
6533
|
+
const placeholder = BOARD_TARGET_PLACEHOLDER[sub];
|
|
6534
|
+
corrected = [
|
|
6535
|
+
head,
|
|
6536
|
+
...targets.length > 0 ? targets.map(quoteArg) : placeholder ? [placeholder] : [],
|
|
6537
|
+
...scan.flagArgs(declared)
|
|
6538
|
+
].join(" ");
|
|
6383
6539
|
}
|
|
6384
6540
|
return { canonicalPath, corrected, hint: `did you mean \`${corrected}\`?` };
|
|
6385
6541
|
}
|
|
@@ -6427,6 +6583,45 @@ function normalizeBoardClaimIssueArgv(argv) {
|
|
|
6427
6583
|
argv.splice(claimAt + 1, 0, value);
|
|
6428
6584
|
return true;
|
|
6429
6585
|
}
|
|
6586
|
+
function normalizeWorkboardClaimArgv(argv) {
|
|
6587
|
+
const workboardAt = argv[2] === "oracle" ? 3 : 2;
|
|
6588
|
+
if (argv[workboardAt] !== "workboard" || argv[workboardAt + 1] !== "claim") return false;
|
|
6589
|
+
const tail = argv.slice(workboardAt + 2);
|
|
6590
|
+
let selectorAt = -1;
|
|
6591
|
+
let selectorSpan = 0;
|
|
6592
|
+
let selected;
|
|
6593
|
+
let targets = 0;
|
|
6594
|
+
for (let i = 0; i < tail.length; i += 1) {
|
|
6595
|
+
const tok = tail[i];
|
|
6596
|
+
const eq = tok.indexOf("=");
|
|
6597
|
+
const name = eq > 0 ? tok.slice(0, eq) : tok;
|
|
6598
|
+
if (name === "--issue" || name === "--number") {
|
|
6599
|
+
if (selectorAt >= 0) return false;
|
|
6600
|
+
selectorAt = i;
|
|
6601
|
+
selectorSpan = eq > 0 ? 1 : 2;
|
|
6602
|
+
selected = eq > 0 ? tok.slice(eq + 1) : tail[++i];
|
|
6603
|
+
if (!selected || selected.startsWith("-")) return false;
|
|
6604
|
+
} else if (BOARD_CLAIM_VALUE_FLAGS.has(name)) {
|
|
6605
|
+
const value = eq > 0 ? tok.slice(eq + 1) : tail[++i];
|
|
6606
|
+
if (!value || value.startsWith("-")) return false;
|
|
6607
|
+
} else if (BOARD_CLAIM_BOOLEAN_FLAGS.has(tok) || tok === "--dry-run" || tok === "--validate-only") {
|
|
6608
|
+
continue;
|
|
6609
|
+
} else if (tok.startsWith("-") || !tok) {
|
|
6610
|
+
return false;
|
|
6611
|
+
} else {
|
|
6612
|
+
targets += 1;
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
if (selected !== void 0) {
|
|
6616
|
+
if (targets > 0) return false;
|
|
6617
|
+
tail.splice(selectorAt, selectorSpan);
|
|
6618
|
+
tail.unshift(selected);
|
|
6619
|
+
} else if (targets === 0) {
|
|
6620
|
+
return false;
|
|
6621
|
+
}
|
|
6622
|
+
argv.splice(2, argv.length - 2, "oracle", "board", "claim", ...tail);
|
|
6623
|
+
return true;
|
|
6624
|
+
}
|
|
6430
6625
|
function classifyParseError(plain) {
|
|
6431
6626
|
if (/unknown command/i.test(plain)) return "unknown-command";
|
|
6432
6627
|
if (/unknown option|missing required argument|too many arguments|argument missing|is invalid\. Allowed choices are|cannot be used with/i.test(plain)) {
|
|
@@ -15142,6 +15337,12 @@ function renderClaimReleaseBody(release) {
|
|
|
15142
15337
|
|
|
15143
15338
|
<!-- mmi-claim-release: ${JSON.stringify(release)} -->`;
|
|
15144
15339
|
}
|
|
15340
|
+
function formatClaimMarker(actor, ts = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
15341
|
+
const marker = { v: 1, session: actor.session, surface: actor.surface, host: actor.host, ts };
|
|
15342
|
+
return `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.
|
|
15343
|
+
|
|
15344
|
+
<!-- mmi-claim: ${JSON.stringify(marker)} -->`;
|
|
15345
|
+
}
|
|
15145
15346
|
function latestClaimMarker(comments) {
|
|
15146
15347
|
for (let i = comments.length - 1; i >= 0; i -= 1) {
|
|
15147
15348
|
const body = comments[i].body;
|
|
@@ -15466,6 +15667,7 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
|
|
|
15466
15667
|
`repos/${repo}/branches/${encodeURIComponent(out.branch)}`
|
|
15467
15668
|
);
|
|
15468
15669
|
out.branchAt = head?.commit?.commit?.committer?.date ?? void 0;
|
|
15670
|
+
out.branchSha = head?.commit?.sha ?? void 0;
|
|
15469
15671
|
} catch {
|
|
15470
15672
|
}
|
|
15471
15673
|
}
|
|
@@ -15532,13 +15734,27 @@ function isSeatClaimMarker(marker, current, context = {}) {
|
|
|
15532
15734
|
const state = context.sessionState ?? probeLocalClaimSession(marker, context.now);
|
|
15533
15735
|
return state === "dead";
|
|
15534
15736
|
}
|
|
15737
|
+
function isSameBranchClaimResume(contest, repo, landing = {}) {
|
|
15738
|
+
const headBranch = landing.headBranch?.replace(/^refs\/heads\//, "").replace(/^[^/:]+:/, "").trim();
|
|
15739
|
+
const headSha = landing.headSha?.trim();
|
|
15740
|
+
if (!headBranch && !headSha) return false;
|
|
15741
|
+
if (!contest.marker || contest.unverifiable.length > 0 || contest.live.length === 0) return false;
|
|
15742
|
+
const branchLine = /^live branch ([^\s]+) on /;
|
|
15743
|
+
const fromContest = contest.branch?.replace(/ \(head of open PR .*\)$/, "").trim();
|
|
15744
|
+
const fromLive = contest.live.map((line) => branchLine.exec(line)?.[1]).find(Boolean);
|
|
15745
|
+
const liveBranch = fromContest || fromLive;
|
|
15746
|
+
const matches = Boolean(headBranch && liveBranch && liveBranch === headBranch) || Boolean(headSha && contest.branchSha && contest.branchSha === headSha);
|
|
15747
|
+
if (!matches) return false;
|
|
15748
|
+
const ownBranchLine = liveBranch ? `live branch ${liveBranch} ` : "live branch ";
|
|
15749
|
+
return contest.live.every((line) => /^claim marker from /.test(line) || line.startsWith(ownBranchLine) && line.endsWith(` on ${repo}`));
|
|
15750
|
+
}
|
|
15535
15751
|
function laneResumeEvidence(evidence) {
|
|
15536
15752
|
if (!evidence.marker || evidence.sessionState !== "dead") return void 0;
|
|
15537
15753
|
if (!isHostSessionId(evidence.marker.session)) return void 0;
|
|
15538
15754
|
const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
|
|
15539
15755
|
return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
|
|
15540
15756
|
}
|
|
15541
|
-
async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient) {
|
|
15757
|
+
async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient, landing) {
|
|
15542
15758
|
const livenessClient = await selectLivenessClient(client, appClient);
|
|
15543
15759
|
let evidence = await gatherClaimLiveness(livenessClient, item.repository, item.number, openPullsFetcher(livenessClient));
|
|
15544
15760
|
if (evidence.failed.some((channel) => channel !== "session")) {
|
|
@@ -15550,10 +15766,21 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
15550
15766
|
markerBy: evidence.markerBy,
|
|
15551
15767
|
sessionState: evidence.sessionState
|
|
15552
15768
|
});
|
|
15553
|
-
|
|
15769
|
+
let resume = ownership !== "mine" && ownSeat ? laneResumeEvidence(evidence) : void 0;
|
|
15554
15770
|
const live = ownSeat ? [] : liveEvidenceLines(evidence, item.repository);
|
|
15555
15771
|
const unverifiable = ownSeat ? [] : evidence.failed;
|
|
15556
|
-
|
|
15772
|
+
let contested = !ownSeat && !resume && (live.length > 0 || unverifiable.length > 0);
|
|
15773
|
+
if (contested && landing && isSameBranchClaimResume({
|
|
15774
|
+
contested: true,
|
|
15775
|
+
live,
|
|
15776
|
+
unverifiable,
|
|
15777
|
+
marker: evidence.marker,
|
|
15778
|
+
branch: evidence.branch,
|
|
15779
|
+
branchSha: evidence.branchSha
|
|
15780
|
+
}, item.repository, landing)) {
|
|
15781
|
+
contested = false;
|
|
15782
|
+
resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
|
|
15783
|
+
}
|
|
15557
15784
|
const carry = staleWorkLines(evidence);
|
|
15558
15785
|
const staleStamp = !contested && !resume && !ownSeat && evidence.marker ? `stale claim stamp ${describeClaimMarker(evidence.marker)} (idle ${formatClaimAge(claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] }).idleMs)})${carry.length ? `; continue its ${carry.join(" and ")}` : ""}` : void 0;
|
|
15559
15786
|
return {
|
|
@@ -15563,7 +15790,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
15563
15790
|
ownership,
|
|
15564
15791
|
marker: evidence.marker,
|
|
15565
15792
|
resume,
|
|
15566
|
-
...staleStamp ? { staleStamp } : {}
|
|
15793
|
+
...staleStamp ? { staleStamp } : {},
|
|
15794
|
+
...evidence.branch ? { branch: evidence.branch } : {},
|
|
15795
|
+
...evidence.branchSha ? { branchSha: evidence.branchSha } : {}
|
|
15567
15796
|
};
|
|
15568
15797
|
}
|
|
15569
15798
|
function laneContestParts(contest) {
|
|
@@ -15714,6 +15943,7 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
|
|
|
15714
15943
|
cfg,
|
|
15715
15944
|
client,
|
|
15716
15945
|
items: collected.items,
|
|
15946
|
+
selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
|
|
15717
15947
|
writable: writableOrUnknown(writable),
|
|
15718
15948
|
report,
|
|
15719
15949
|
session: deps.session ?? describeSessionIdentity(),
|
|
@@ -15825,7 +16055,9 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
15825
16055
|
return;
|
|
15826
16056
|
}
|
|
15827
16057
|
if (!options.force) {
|
|
15828
|
-
const
|
|
16058
|
+
const candidateRefs = [...report.primary.claimable, ...report.secondary.claimable].filter((candidate) => candidate.contentType === "Issue" && candidate.state === "OPEN" && !candidate.claim?.currentlyClaimed && !ctx.selectedRefs.has(candidate.ref.toLowerCase())).map((candidate) => candidate.ref);
|
|
16059
|
+
const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
|
|
16060
|
+
const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
|
|
15829
16061
|
throw new Error(options.bulk ? refusal : withGatedWriteChainHint(refusal));
|
|
15830
16062
|
}
|
|
15831
16063
|
previousHolder = displaced();
|
|
@@ -19421,10 +19653,10 @@ var rollout_plan_default = {
|
|
|
19421
19653
|
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)."
|
|
19422
19654
|
},
|
|
19423
19655
|
baseline: {
|
|
19424
|
-
version: "4.5.
|
|
19425
|
-
tag: "v4.5.
|
|
19426
|
-
commit: "
|
|
19427
|
-
npm: "@mutmutco/cli@4.5.
|
|
19656
|
+
version: "4.5.210",
|
|
19657
|
+
tag: "v4.5.210",
|
|
19658
|
+
commit: "a9e5d838bc19",
|
|
19659
|
+
npm: "@mutmutco/cli@4.5.210"
|
|
19428
19660
|
},
|
|
19429
19661
|
exitCriterion: "fleet-n-of-n",
|
|
19430
19662
|
hubOnlyShortcut: "forbidden",
|
|
@@ -19441,14 +19673,14 @@ var rollout_plan_default = {
|
|
|
19441
19673
|
repo: "mutmutco/mmi-hub",
|
|
19442
19674
|
role: "canary",
|
|
19443
19675
|
schedule: "train",
|
|
19444
|
-
v3Target: "v4.5.
|
|
19676
|
+
v3Target: "v4.5.210"
|
|
19445
19677
|
}
|
|
19446
19678
|
],
|
|
19447
19679
|
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.",
|
|
19448
19680
|
rollback: {
|
|
19449
19681
|
independent: true,
|
|
19450
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
19451
|
-
v3Target: "v4.5.
|
|
19682
|
+
mechanism: "npm dist-tag latest -> 4.5.210 and redeploy the Hub Lambda from tag v4.5.210 (a9e5d838bc19); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
19683
|
+
v3Target: "v4.5.210 (@mutmutco/cli@4.5.210, tag commit a9e5d838bc19 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
19452
19684
|
}
|
|
19453
19685
|
},
|
|
19454
19686
|
{
|
|
@@ -23952,46 +24184,45 @@ var import_node_fs30 = require("node:fs");
|
|
|
23952
24184
|
var import_promises4 = require("node:fs/promises");
|
|
23953
24185
|
var import_node_path28 = require("node:path");
|
|
23954
24186
|
|
|
23955
|
-
//
|
|
23956
|
-
var
|
|
23957
|
-
|
|
23958
|
-
|
|
23959
|
-
|
|
23960
|
-
|
|
23961
|
-
|
|
23962
|
-
|
|
23963
|
-
|
|
23964
|
-
|
|
23965
|
-
|
|
23966
|
-
|
|
23967
|
-
|
|
23968
|
-
if (jobs.some((job) => isActionsBillingBlockText(JSON.stringify(job)))) return "billing-blocked";
|
|
23969
|
-
if (jobs.some((job) => Array.isArray(job.steps) && job.steps.length > 0 || Boolean(job.startedAt))) {
|
|
23970
|
-
return "started";
|
|
23971
|
-
}
|
|
23972
|
-
if (jobs.length > 0 && jobs.every((job) => {
|
|
23973
|
-
const finished = job.conclusion === "failure" || job.status === "completed";
|
|
23974
|
-
const empty = !Array.isArray(job.steps) || job.steps.length === 0;
|
|
23975
|
-
return finished && empty && !job.startedAt;
|
|
23976
|
-
})) {
|
|
23977
|
-
return "never-started";
|
|
24187
|
+
// ../shared/runner-fallback.ts
|
|
24188
|
+
var HOSTED_RUNNER_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/i;
|
|
24189
|
+
function classifyRunnerCost(selector) {
|
|
24190
|
+
const raw = selector.replace(/\s+#.*$/gm, "").trim();
|
|
24191
|
+
if (HOSTED_RUNNER_LABEL_RE.test(raw)) return "hosted";
|
|
24192
|
+
const json = /^\$\{\{\s*fromJSON\('([^']+)'\)\s*\}\}$/.exec(raw);
|
|
24193
|
+
if (json) {
|
|
24194
|
+
try {
|
|
24195
|
+
const labels2 = JSON.parse(json[1]);
|
|
24196
|
+
return Array.isArray(labels2) && labels2.length > 0 && labels2.every((l) => typeof l === "string" && /^[\w-]+$/.test(l)) && labels2.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
|
|
24197
|
+
} catch {
|
|
24198
|
+
return "unknown";
|
|
24199
|
+
}
|
|
23978
24200
|
}
|
|
23979
|
-
return "
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
function isCanaryAccessBlockText(text) {
|
|
23983
|
-
return !isActionsBillingBlockText(text) && CANARY_ACCESS_BLOCK_RE.test(text);
|
|
23984
|
-
}
|
|
23985
|
-
function actionsCanaryUndispatchedRefusal(detail) {
|
|
23986
|
-
return new Error(
|
|
23987
|
-
`release refused: the hosted-job canary (#5604) was not dispatched, so a hosted job start is UNPROVEN \u2014 this is NOT a billing verdict. ${detail} The Hub dispatches ${CANARY_WORKFLOW} with its App token for a train-authorized caller (#6319): refresh the Hub session (\`gh auth login\`) and retry, and confirm \`mmi-cli oracle org access role <owner/repo>\` reports train authority. Nothing was tagged or pushed \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
23988
|
-
);
|
|
24201
|
+
if (raw.includes("${{")) return "unknown";
|
|
24202
|
+
const labels = /^\[[^\[\]]+\]$/.test(raw) ? raw.slice(1, -1).split(",").map((s) => s.trim().replace(/^(['"])(.*)\1$/, "$2")) : /^(?:-\s*[^\n]+\s*)+$/.test(raw) ? raw.split("\n").map((s) => s.trim().replace(/^-\s*/, "").replace(/^(['"])(.*)\1$/, "$2")) : [raw.replace(/^(['"])(.*)\1$/, "$2")];
|
|
24203
|
+
return labels.every((l) => /^[\w-]+$/.test(l)) && labels.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
|
|
23989
24204
|
}
|
|
23990
|
-
function
|
|
23991
|
-
|
|
23992
|
-
|
|
23993
|
-
);
|
|
24205
|
+
function classifyJobRunnerCost(selector, jobBody, standardHostedLabel) {
|
|
24206
|
+
const direct = classifyRunnerCost(selector);
|
|
24207
|
+
const matrix = /^\$\{\{\s*matrix\.([\w-]+)\s*\}\}$/.exec(selector.trim());
|
|
24208
|
+
if (!matrix || direct === "hosted") return direct;
|
|
24209
|
+
const strategy = /^ strategy:\s*\n((?: {6,}[^\n]*\n?|\s*\n)*)/m.exec(jobBody)?.[1];
|
|
24210
|
+
const matrixBody = strategy && /^ matrix:[^\S\n]*\n((?: {8,}[^\n]*\n?|\s*\n)*)/m.exec(strategy)?.[1];
|
|
24211
|
+
const include = matrixBody && /^ include:[^\S\n]*\n((?: {10,}[^\n]*\n?|\s*\n)*)$/.exec(matrixBody)?.[1];
|
|
24212
|
+
if (!include) return "unknown";
|
|
24213
|
+
const rows = include.split(/^ {10}- /m).slice(1);
|
|
24214
|
+
if (!rows.length) return "unknown";
|
|
24215
|
+
const costs = rows.map((row) => {
|
|
24216
|
+
const match = new RegExp(`^(?: {12})?${matrix[1]}:\\s*(.+)$`, "m").exec(row);
|
|
24217
|
+
if (standardHostedLabel && match?.[1].trim() === standardHostedLabel) return "standard-hosted";
|
|
24218
|
+
return match ? classifyRunnerCost(match[1]) : "unknown";
|
|
24219
|
+
});
|
|
24220
|
+
if (costs.includes("hosted")) return "hosted";
|
|
24221
|
+
if (costs.includes("unknown")) return "unknown";
|
|
24222
|
+
return costs.includes("standard-hosted") ? "standard-hosted" : "self-hosted";
|
|
23994
24223
|
}
|
|
24224
|
+
|
|
24225
|
+
// src/actions-billing-preflight.ts
|
|
23995
24226
|
function parseJson(raw, label) {
|
|
23996
24227
|
try {
|
|
23997
24228
|
return JSON.parse(raw);
|
|
@@ -23999,153 +24230,30 @@ function parseJson(raw, label) {
|
|
|
23999
24230
|
throw new Error(`${label} was not valid JSON`);
|
|
24000
24231
|
}
|
|
24001
24232
|
}
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
|
|
24007
|
-
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
24011
|
-
"8",
|
|
24012
|
-
"--json",
|
|
24013
|
-
"databaseId,conclusion,status,displayTitle,url"
|
|
24014
|
-
]);
|
|
24015
|
-
} catch {
|
|
24016
|
-
return void 0;
|
|
24017
|
-
}
|
|
24018
|
-
const rows = parseJson(raw, `gh run list --repo ${repo}`);
|
|
24019
|
-
if (!Array.isArray(rows)) return void 0;
|
|
24020
|
-
for (const row of rows) {
|
|
24021
|
-
const blob = `${row.displayTitle ?? ""} ${row.conclusion ?? ""} ${row.url ?? ""}`;
|
|
24022
|
-
if (isActionsBillingBlockText(blob)) {
|
|
24023
|
-
return `${repo} run ${row.databaseId ?? row.url ?? "(unknown)"} already names a billing/spending block`;
|
|
24024
|
-
}
|
|
24025
|
-
if (row.conclusion !== "failure" && row.status !== "completed") continue;
|
|
24026
|
-
if (typeof row.databaseId !== "number") continue;
|
|
24027
|
-
try {
|
|
24028
|
-
const view = await deps.run("gh", [
|
|
24029
|
-
"run",
|
|
24030
|
-
"view",
|
|
24031
|
-
String(row.databaseId),
|
|
24032
|
-
"--repo",
|
|
24033
|
-
repo,
|
|
24034
|
-
"--json",
|
|
24035
|
-
"jobs,conclusion,displayTitle,url"
|
|
24036
|
-
]);
|
|
24037
|
-
const parsed = parseJson(view, `gh run view ${row.databaseId}`);
|
|
24038
|
-
const verdict = interpretActionsJobStart({
|
|
24039
|
-
jobs: parsed.jobs,
|
|
24040
|
-
text: `${parsed.displayTitle ?? ""} ${parsed.conclusion ?? ""} ${view}`
|
|
24041
|
-
});
|
|
24042
|
-
if (verdict === "billing-blocked" || verdict === "never-started") {
|
|
24043
|
-
return `${repo} run ${row.databaseId} (${parsed.url ?? row.url ?? "no url"}): hosted job never started` + (verdict === "billing-blocked" ? " (billing/spending refusal)" : " (empty steps)");
|
|
24044
|
-
}
|
|
24045
|
-
} catch {
|
|
24046
|
-
}
|
|
24047
|
-
}
|
|
24048
|
-
return void 0;
|
|
24049
|
-
}
|
|
24050
|
-
async function correlateCanaryRun(deps, nonce) {
|
|
24051
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
24052
|
-
let lastError = "no matching canary run";
|
|
24053
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
24054
|
-
if (attempt > 0) await sleep2(CANARY_POLL_MS);
|
|
24055
|
-
let raw;
|
|
24056
|
-
try {
|
|
24057
|
-
raw = await deps.run("gh", [
|
|
24058
|
-
"run",
|
|
24059
|
-
"list",
|
|
24060
|
-
"--repo",
|
|
24061
|
-
CANARY_REPO,
|
|
24062
|
-
"--workflow",
|
|
24063
|
-
CANARY_WORKFLOW,
|
|
24064
|
-
"--limit",
|
|
24065
|
-
"10",
|
|
24066
|
-
"--json",
|
|
24067
|
-
"databaseId,displayTitle,status,createdAt"
|
|
24068
|
-
]);
|
|
24069
|
-
} catch (e) {
|
|
24070
|
-
lastError = e instanceof Error ? e.message : String(e);
|
|
24071
|
-
continue;
|
|
24072
|
-
}
|
|
24073
|
-
const rows = parseJson(
|
|
24074
|
-
raw,
|
|
24075
|
-
"canary gh run list"
|
|
24076
|
-
);
|
|
24077
|
-
const match = rows.find((row) => (row.displayTitle ?? "").includes(nonce) && typeof row.databaseId === "number");
|
|
24078
|
-
if (match?.databaseId) return match.databaseId;
|
|
24079
|
-
}
|
|
24080
|
-
throw new Error(
|
|
24081
|
-
`could not correlate ${CANARY_WORKFLOW} on ${CANARY_REPO} (nonce ${nonce}): ${lastError}`
|
|
24082
|
-
);
|
|
24083
|
-
}
|
|
24084
|
-
async function dispatchCanary(deps, targetRepo2, nonce) {
|
|
24085
|
-
const attempts = [];
|
|
24086
|
-
if (deps.dispatchActionsCanary) {
|
|
24087
|
-
try {
|
|
24088
|
-
await deps.dispatchActionsCanary({ repo: targetRepo2, nonce });
|
|
24089
|
-
return;
|
|
24090
|
-
} catch (e) {
|
|
24091
|
-
attempts.push(`Hub App dispatch: ${e instanceof Error ? e.message : String(e)}`);
|
|
24092
|
-
}
|
|
24093
|
-
}
|
|
24094
|
-
try {
|
|
24095
|
-
await deps.run("gh", [
|
|
24096
|
-
"workflow",
|
|
24097
|
-
"run",
|
|
24098
|
-
CANARY_WORKFLOW,
|
|
24099
|
-
"--repo",
|
|
24100
|
-
CANARY_REPO,
|
|
24101
|
-
"-f",
|
|
24102
|
-
`nonce=${nonce}`
|
|
24103
|
-
]);
|
|
24104
|
-
return;
|
|
24105
|
-
} catch (e) {
|
|
24106
|
-
attempts.push(`gh workflow run --repo ${CANARY_REPO}: ${e instanceof Error ? e.message : String(e)}`);
|
|
24107
|
-
}
|
|
24108
|
-
const detail = `could not dispatch ${CANARY_WORKFLOW} on ${CANARY_REPO} \u2014 ${attempts.join(" | ")}.`;
|
|
24109
|
-
if (attempts.some((a) => isActionsBillingBlockText(a))) {
|
|
24110
|
-
throw actionsBillingRefusal(`${detail} A hosted publish job may not start (the v1.54.2 class).`);
|
|
24111
|
-
}
|
|
24112
|
-
throw actionsCanaryUndispatchedRefusal(
|
|
24113
|
-
attempts.every((a) => isCanaryAccessBlockText(a)) ? `${detail} Every door answered with an access refusal \u2014 the caller may read MMI-Hub but not start its workflows.` : detail
|
|
24114
|
-
);
|
|
24115
|
-
}
|
|
24116
|
-
var HOSTED_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/;
|
|
24117
|
-
var FALLBACK_EXPR_GLOBAL_RE = /\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)'\s*\|\|\s*fromJSON\('\[[^\n']+\]'\)\s*\}\}/g;
|
|
24118
|
-
var AUTOMATIC_TRIGGERS = /* @__PURE__ */ new Set(["push", "pull_request", "pull_request_target", "release", "schedule", "workflow_run", "merge_group", "create", "deployment", "repository_dispatch", "check_run", "check_suite", "issues", "issue_comment"]);
|
|
24119
|
-
function workflowTriggers(yaml) {
|
|
24120
|
-
const lines2 = yaml.split(/\r?\n/);
|
|
24121
|
-
const i = lines2.findIndex((l) => /^(?:on|"on"|'on'):(?:\s|$)/.test(l));
|
|
24122
|
-
if (i < 0) return [];
|
|
24123
|
-
const inline = lines2[i].replace(/^[^:]+:\s*/, "").replace(/\s+#.*$/, "").trim();
|
|
24124
|
-
if (inline) return inline.replace(/^\[|\]$/g, "").split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
24125
|
-
const out = [];
|
|
24126
|
-
for (const line of lines2.slice(i + 1)) {
|
|
24127
|
-
if (/^\S/.test(line)) break;
|
|
24128
|
-
const m = /^ {2}([A-Za-z_]+):/.exec(line) ?? /^ {2}- ([A-Za-z_]+)\s*$/.exec(line);
|
|
24129
|
-
if (m) out.push(m[1]);
|
|
24130
|
-
}
|
|
24131
|
-
return out;
|
|
24132
|
-
}
|
|
24133
|
-
function workflowNeedsHostedRunner(yaml) {
|
|
24134
|
-
const body = yaml.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join("\n").replace(FALLBACK_EXPR_GLOBAL_RE, "");
|
|
24135
|
-
return HOSTED_LABEL_RE.test(body) && workflowTriggers(yaml).some((t) => AUTOMATIC_TRIGGERS.has(t));
|
|
24233
|
+
function workflowNeedsHostedRunner(yaml, localWorkflows = /* @__PURE__ */ new Set()) {
|
|
24234
|
+
const jobs = parseWorkflowJobs(yaml.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join("\n"));
|
|
24235
|
+
return !jobs.length || jobs.some((job) => {
|
|
24236
|
+
const callee = /^ uses:\s*\.\/(.+)$/m.exec(job.body)?.[1];
|
|
24237
|
+
if (callee && localWorkflows.has(callee)) return false;
|
|
24238
|
+
const section = /^ runs-on:([^\n]*(?:\n {6,}[^\n]*)*)/m.exec(job.body)?.[1] ?? "";
|
|
24239
|
+
const selector = section.split("\n").map((line) => line.trim()).join("\n");
|
|
24240
|
+
return classifyJobRunnerCost(selector, job.body) !== "self-hosted";
|
|
24241
|
+
});
|
|
24136
24242
|
}
|
|
24137
24243
|
async function hostedWorkflowsOf(deps, repo) {
|
|
24138
24244
|
const list = parseJson(
|
|
24139
24245
|
await deps.run("gh", ["api", `repos/${repo}/contents/.github/workflows`]),
|
|
24140
24246
|
`${repo} workflow listing`
|
|
24141
24247
|
);
|
|
24142
|
-
|
|
24248
|
+
if (!Array.isArray(list) || !list.some((f) => f.type === "file" && /\.ya?ml$/.test(f.path ?? ""))) throw new Error("no workflow inventory");
|
|
24249
|
+
const workflows = [];
|
|
24143
24250
|
for (const f of list) {
|
|
24144
|
-
if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path)
|
|
24251
|
+
if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path)) continue;
|
|
24145
24252
|
const raw = parseJson(await deps.run("gh", ["api", `repos/${repo}/contents/${f.path}`]), `${repo} ${f.path}`);
|
|
24146
|
-
|
|
24253
|
+
workflows.push({ path: f.path, body: Buffer.from(raw.content ?? "", "base64").toString("utf8") });
|
|
24147
24254
|
}
|
|
24148
|
-
|
|
24255
|
+
const local = new Set(workflows.map((file) => file.path));
|
|
24256
|
+
return workflows.filter((file) => workflowNeedsHostedRunner(file.body, local)).map((file) => file.path);
|
|
24149
24257
|
}
|
|
24150
24258
|
async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
24151
24259
|
let hosted;
|
|
@@ -24155,50 +24263,11 @@ async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
|
24155
24263
|
hosted = void 0;
|
|
24156
24264
|
}
|
|
24157
24265
|
if (hosted && hosted.length === 0) {
|
|
24158
|
-
process.stderr.write(`actions preflight: ${targetRepo2}
|
|
24266
|
+
process.stderr.write(`actions preflight: ${targetRepo2} source selectors are self-hosted only; no canary dispatch; access/isolation/execution unproven (#8911)
|
|
24159
24267
|
`);
|
|
24160
24268
|
return;
|
|
24161
24269
|
}
|
|
24162
|
-
|
|
24163
|
-
if (scanned) throw actionsBillingRefusal(scanned);
|
|
24164
|
-
if (targetRepo2.toLowerCase() !== CANARY_REPO.toLowerCase()) {
|
|
24165
|
-
const hubScan = await scanRepoForBillingBlock(deps, CANARY_REPO);
|
|
24166
|
-
if (hubScan) throw actionsBillingRefusal(hubScan);
|
|
24167
|
-
}
|
|
24168
|
-
const nonce = `5604-${(deps.now ?? Date.now)().toString(36)}`;
|
|
24169
|
-
await dispatchCanary(deps, targetRepo2, nonce);
|
|
24170
|
-
const runId = await correlateCanaryRun(deps, nonce);
|
|
24171
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
24172
|
-
let last = "pending";
|
|
24173
|
-
let url = `https://github.com/${CANARY_REPO}/actions/runs/${runId}`;
|
|
24174
|
-
for (let attempt = 0; attempt < CANARY_POLLS; attempt++) {
|
|
24175
|
-
if (attempt > 0) await sleep2(CANARY_POLL_MS);
|
|
24176
|
-
const view = await deps.run("gh", [
|
|
24177
|
-
"run",
|
|
24178
|
-
"view",
|
|
24179
|
-
String(runId),
|
|
24180
|
-
"--repo",
|
|
24181
|
-
CANARY_REPO,
|
|
24182
|
-
"--json",
|
|
24183
|
-
"jobs,status,conclusion,url,displayTitle"
|
|
24184
|
-
]);
|
|
24185
|
-
const parsed = parseJson(view, `canary gh run view ${runId}`);
|
|
24186
|
-
if (parsed.url) url = parsed.url;
|
|
24187
|
-
last = interpretActionsJobStart({
|
|
24188
|
-
jobs: parsed.jobs,
|
|
24189
|
-
text: `${parsed.displayTitle ?? ""} ${parsed.conclusion ?? ""} ${view}`
|
|
24190
|
-
});
|
|
24191
|
-
if (last === "started") {
|
|
24192
|
-
await deps.run("gh", ["run", "cancel", String(runId), "--repo", CANARY_REPO]).catch(() => "");
|
|
24193
|
-
return;
|
|
24194
|
-
}
|
|
24195
|
-
if (last === "billing-blocked" || last === "never-started") {
|
|
24196
|
-
throw actionsBillingRefusal(`${url}: hosted canary ${last} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`);
|
|
24197
|
-
}
|
|
24198
|
-
}
|
|
24199
|
-
throw actionsBillingRefusal(
|
|
24200
|
-
`${url}: hosted canary stayed ${last} after ${CANARY_POLLS} polls \u2014 cannot prove a hosted job can start \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
24201
|
-
);
|
|
24270
|
+
throw new Error(`release refused (#8911): ${hosted ? `paid or unresolved runner path(s): ${hosted.join(", ")}` : "runner inventory unreadable; self-hosted-only selection unproven"}. Remove paid selectors/fallbacks and verify existing self-hosted access and isolation before tagging.`);
|
|
24202
24271
|
}
|
|
24203
24272
|
|
|
24204
24273
|
// src/train-apply-recovery.ts
|
|
@@ -30301,33 +30370,6 @@ function registryRepoKey(repo) {
|
|
|
30301
30370
|
return repo.toLowerCase();
|
|
30302
30371
|
}
|
|
30303
30372
|
|
|
30304
|
-
// ../shared/runner-fallback.ts
|
|
30305
|
-
var RUNNER_FALLBACK_EXPR_RE = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'((?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*))'\s*\|\|\s*fromJSON\('(\[[^\n']+\])'\)\s*\}\}$/;
|
|
30306
|
-
var FLEET_ONLY_RUNNER_JOBS = {
|
|
30307
|
-
"backup-health.yml:judge": "SSH backup inventory; hosted source-IP access unverified",
|
|
30308
|
-
"browser-pass.yml:browser-pass": "owner-bounded two-lane browser capacity",
|
|
30309
|
-
"app-publish.yml:publish": "SSH tenant publish; hosted source-IP access unverified",
|
|
30310
|
-
"get-publish.yml:publish": "SSH installer publish; hosted source-IP access unverified",
|
|
30311
|
-
"get-publish-desktop.yml:publish": "SSH installer publish; hosted source-IP access unverified",
|
|
30312
|
-
"hetzner-knownhosts-pin.yml:pin": "SSH host-key probe; hosted source-IP access unverified",
|
|
30313
|
-
"module-rc-publish.yml:publish": "SSH live-container snapshot and tenant persistent state",
|
|
30314
|
-
"tenant-control.yml:control": "SSH tenant control; hosted source-IP access unverified",
|
|
30315
|
-
"tenant-deploy.yml:deploy": "SSH tenant deploy; hosted source-IP access unverified",
|
|
30316
|
-
"tenant-reconcile.yml:reconcile": "SSH tenant reconciliation; hosted source-IP access unverified"
|
|
30317
|
-
};
|
|
30318
|
-
function parseRunnerFallbackExpression(runsOn) {
|
|
30319
|
-
const match = RUNNER_FALLBACK_EXPR_RE.exec(runsOn.trim());
|
|
30320
|
-
if (!match) return void 0;
|
|
30321
|
-
try {
|
|
30322
|
-
const parsed = JSON.parse(match[2]);
|
|
30323
|
-
if (!Array.isArray(parsed) || !parsed.length || !parsed.every((item) => typeof item === "string")) return void 0;
|
|
30324
|
-
if (!parsed.some((item) => item.toLowerCase() === "self-hosted")) return void 0;
|
|
30325
|
-
return { hosted: match[1], labels: parsed };
|
|
30326
|
-
} catch {
|
|
30327
|
-
return void 0;
|
|
30328
|
-
}
|
|
30329
|
-
}
|
|
30330
|
-
|
|
30331
30373
|
// src/ci-audit.ts
|
|
30332
30374
|
var HUB_REPO4 = "mutmutco/MMI-Hub";
|
|
30333
30375
|
var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
|
|
@@ -30387,25 +30429,28 @@ var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
|
|
|
30387
30429
|
var SHARED_ACTION_PINS = [
|
|
30388
30430
|
{ path: "npm-publish", floor: NPM_PUBLISH_SAFE_FLOOR_SHA }
|
|
30389
30431
|
];
|
|
30432
|
+
var STANDARD_HOSTED_ALLOWLIST = [
|
|
30433
|
+
["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"]
|
|
30434
|
+
];
|
|
30390
30435
|
function auditRunnerFallbacks(repo, files) {
|
|
30391
30436
|
const checks = [];
|
|
30392
30437
|
for (const { path: path2, body } of files) {
|
|
30393
30438
|
const jobs = parseWorkflowJobs(body.split(/\r?\n/).filter((line) => !/^\s*#/.test(line)).join("\n"));
|
|
30394
30439
|
for (const job of jobs) {
|
|
30395
|
-
const runsOn = /^ runs-on:\s*(.+)$/m.exec(job.body)?.[1] ?? "";
|
|
30396
30440
|
const runnerSection = /^ runs-on:([^\n]*(?:\n {6,}[^\n]*)*)/m.exec(job.body)?.[1] ?? "";
|
|
30397
|
-
const
|
|
30398
|
-
|
|
30399
|
-
if (
|
|
30400
|
-
const
|
|
30401
|
-
|
|
30402
|
-
|
|
30403
|
-
const
|
|
30441
|
+
const callee = /^ uses:\s*(.+)$/m.exec(job.body)?.[1];
|
|
30442
|
+
if (callee?.startsWith("./") && files.some((file) => file.path === callee.slice(2))) continue;
|
|
30443
|
+
if (callee && /^mutmutco\/MMI-Hub\/\.github\/workflows\/[\w.-]+\.ya?ml@[\w./-]+$/.test(callee)) continue;
|
|
30444
|
+
const standardHostedLabel = STANDARD_HOSTED_ALLOWLIST.find(
|
|
30445
|
+
([allowedRepo, allowedPath, allowedJob]) => repo === allowedRepo && path2 === allowedPath && job.id === allowedJob
|
|
30446
|
+
)?.[3];
|
|
30447
|
+
const cost = classifyJobRunnerCost(runnerSection.split("\n").map((line) => line.trim()).join("\n"), job.body, standardHostedLabel);
|
|
30448
|
+
const ok = !callee && (cost === "self-hosted" || cost === "standard-hosted");
|
|
30404
30449
|
checks.push({
|
|
30405
|
-
ok
|
|
30406
|
-
label: `runner
|
|
30407
|
-
detail:
|
|
30408
|
-
remediation:
|
|
30450
|
+
ok,
|
|
30451
|
+
label: cost === "standard-hosted" ? `runner uses exact standard-hosted exception (#8912): ${path2}:${job.id}` : `runner is self-hosted only (#8911): ${path2}:${job.id}`,
|
|
30452
|
+
detail: callee ? `unknown callee runner policy: ${callee}` : cost === "hosted" ? "paid GitHub-hosted runner selection is forbidden" : cost === "unknown" ? "unknown runner selector; not proven self-hosted" : cost === "standard-hosted" ? "exact Hub launcher windows-latest matrix leg permitted within included Enterprise minutes; runner access and isolation not established" : "self-hosted selection only; runner access and isolation not established",
|
|
30453
|
+
remediation: ok ? void 0 : `Use only a verified accessible self-hosted tuple, with no hosted fallback; resolve unknown selectors/callees. Preserve checks and artifacts. See gh-runner-runbook.md (${repo}).`
|
|
30409
30454
|
});
|
|
30410
30455
|
}
|
|
30411
30456
|
}
|
|
@@ -30630,7 +30675,7 @@ function rulesetCoversReleaseBranches(payload, track) {
|
|
|
30630
30675
|
function releaseBranchRefsCovered(conditions, track) {
|
|
30631
30676
|
const include = conditions?.ref_name?.include;
|
|
30632
30677
|
if (!Array.isArray(include)) return [];
|
|
30633
|
-
const releaseRefs = track
|
|
30678
|
+
const releaseRefs = branchesForTrack(track).filter((branch) => branch !== "development").map((branch) => `refs/heads/${branch}`);
|
|
30634
30679
|
return releaseRefs.filter((ref) => include.some((entry) => typeof entry === "string" && refPatternCovers(entry, ref)));
|
|
30635
30680
|
}
|
|
30636
30681
|
function unreachableReleaseContexts(input) {
|
|
@@ -30859,6 +30904,18 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
30859
30904
|
const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
|
|
30860
30905
|
const deployableGated = repoClass === "deployable" && !explicitNoCi;
|
|
30861
30906
|
const info = await restJson2(deps, `repos/${repo}`, {});
|
|
30907
|
+
if (info.archived === true) {
|
|
30908
|
+
return {
|
|
30909
|
+
repo,
|
|
30910
|
+
class: repoClass,
|
|
30911
|
+
mergePolicy: "wait-for-checks",
|
|
30912
|
+
ok: false,
|
|
30913
|
+
checks: [],
|
|
30914
|
+
explicitNoCi,
|
|
30915
|
+
excludedReason: "GitHub archived repository (read-only)"
|
|
30916
|
+
};
|
|
30917
|
+
}
|
|
30918
|
+
let releaseReadFailure;
|
|
30862
30919
|
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
30863
30920
|
checks.push({
|
|
30864
30921
|
ok: info.allow_auto_merge === true,
|
|
@@ -31061,14 +31118,29 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
31061
31118
|
const releaseBranches = releaseBranchRefsCovered(productRuleset.conditions, resolveReleaseTrack(meta, void 0, repo));
|
|
31062
31119
|
if (releaseBranches.length > 0) {
|
|
31063
31120
|
const emittedByBranch = {};
|
|
31121
|
+
const absent = [];
|
|
31122
|
+
const unreadable = [];
|
|
31064
31123
|
for (const ref of releaseBranches) {
|
|
31065
|
-
|
|
31124
|
+
const branch = ref.replace("refs/heads/", "");
|
|
31125
|
+
const present = await branchPresence(deps, repo, branch);
|
|
31126
|
+
if (present === false) absent.push(branch);
|
|
31127
|
+
else if (present === void 0) unreadable.push(branch);
|
|
31128
|
+
else {
|
|
31129
|
+
emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, branch);
|
|
31130
|
+
if (emittedByBranch[ref] === void 0) unreadable.push(branch);
|
|
31131
|
+
}
|
|
31066
31132
|
}
|
|
31133
|
+
if (unreadable.length) releaseReadFailure = `release branch reachability unreadable: [${unreadable.join(", ")}] \u2014 no emitter conclusion`;
|
|
31134
|
+
const presenceNote = [
|
|
31135
|
+
absent.length ? `absent: [${absent.join(", ")}] \u2014 no emitter requirement audited` : "",
|
|
31136
|
+
unreadable.length ? `unreadable: [${unreadable.join(", ")}] \u2014 cannot audit emitters` : ""
|
|
31137
|
+
].filter(Boolean).join("; ");
|
|
31067
31138
|
const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
|
|
31068
31139
|
checks.push({
|
|
31069
31140
|
ok: unreachable.length === 0,
|
|
31070
31141
|
label: RELEASE_BRANCH_REACHABLE_LABEL,
|
|
31071
|
-
|
|
31142
|
+
severity: unreadable.length ? "warn" : absent.length ? "info" : void 0,
|
|
31143
|
+
detail: unreachable.length === 0 ? presenceNote || void 0 : unreachable.map((u) => `${u.branch.replace("refs/heads/", "")} requires [${u.unreachable.join(", ")}] but its own workflows emit none of them`).join("; ") + " \u2014 a base-" + unreachable.map((u) => u.branch.replace("refs/heads/", "")).join("/") + " PR (e.g. a hotfix) can never satisfy the gate and stays permanently BLOCKED" + (presenceNote ? `; ${presenceNote}` : ""),
|
|
31072
31144
|
remediation: unreachable.length === 0 ? void 0 : `Remove the unreachable context(s) from those branches in the ${PRODUCT_RULESET_NAME} ruleset conditions and ${PRODUCT_RULESET_REF}, or add the emitting workflow to the branch`
|
|
31073
31145
|
});
|
|
31074
31146
|
}
|
|
@@ -31124,7 +31196,7 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
31124
31196
|
const body = await fetchFileContent(deps, repo, baseBranch, path2);
|
|
31125
31197
|
if (body == null) continue;
|
|
31126
31198
|
if (isGateWorkflowPath(path2)) gateBodies.push({ path: path2, body });
|
|
31127
|
-
if (!/^\s*concurrency:/m.test(body) || !/^\s*cancel-in-progress:[ \t]*(?:true|\$\{\{\s*github\.event_name\s
|
|
31199
|
+
if (!/^\s*concurrency:/m.test(body) || !/^\s*cancel-in-progress:[ \t]*(?:true|\$\{\{\s*github\.event_name\s*(?:==\s*(['"])pull_request\1|!=\s*(['"])merge_group\2)\s*\}\})[ \t]*(?:#.*)?$/m.test(body)) {
|
|
31128
31200
|
if (NO_CANCEL_WAIVER.test(body)) waived.push(path2);
|
|
31129
31201
|
else missing.push(path2);
|
|
31130
31202
|
}
|
|
@@ -31183,34 +31255,45 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
31183
31255
|
if (failure) {
|
|
31184
31256
|
return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
|
|
31185
31257
|
}
|
|
31186
|
-
const ok = checks.every((c) => c.ok);
|
|
31187
|
-
return {
|
|
31258
|
+
const ok = !releaseReadFailure && checks.every((c) => c.ok);
|
|
31259
|
+
return {
|
|
31260
|
+
repo,
|
|
31261
|
+
class: repoClass,
|
|
31262
|
+
mergePolicy: policy,
|
|
31263
|
+
ok,
|
|
31264
|
+
checks,
|
|
31265
|
+
explicitNoCi,
|
|
31266
|
+
...releaseReadFailure ? { unauditedReason: releaseReadFailure } : {}
|
|
31267
|
+
};
|
|
31188
31268
|
}
|
|
31189
31269
|
var REGISTRY_UNREADABLE_DETAIL = `the registry roster read returned nothing \u2014 the fleet was NOT audited. 0 repos read from the registry; the number left unaudited is unknown because the roster is what names them. This is "cannot audit the fleet", never "the fleet is green". Under GitHub Actions the CLI needs a CI Hub session (MMI_HUB_TOKEN, minted from the job's OIDC claim via POST /auth/session-ci) \u2014 an App installation token has no /user identity and can never pass /auth/session (#3215).`;
|
|
31190
31270
|
async function auditOrgCi(deps, repoFilter) {
|
|
31191
31271
|
if (repoFilter) {
|
|
31192
31272
|
const report = await auditRepoCi(repoFilter, deps);
|
|
31193
|
-
return { ok: report.ok, scope: "single-repo", repos: [report] };
|
|
31273
|
+
return { ok: report.ok, scope: "single-repo", repos: [report], excludedCount: report.excludedReason ? 1 : 0 };
|
|
31194
31274
|
}
|
|
31195
31275
|
const projects = await deps.listProjects();
|
|
31196
31276
|
if (!projects || projects.length === 0) {
|
|
31197
|
-
return { ok: false, scope: "registry-unreadable", repos: [], scopeDetail: REGISTRY_UNREADABLE_DETAIL };
|
|
31277
|
+
return { ok: false, scope: "registry-unreadable", repos: [], excludedCount: 0, scopeDetail: REGISTRY_UNREADABLE_DETAIL };
|
|
31198
31278
|
}
|
|
31199
31279
|
const targets = collectRegistryRepos(projects);
|
|
31200
31280
|
const repos = [];
|
|
31201
31281
|
for (const repo of targets) {
|
|
31202
31282
|
repos.push(await auditRepoCi(repo, deps));
|
|
31203
31283
|
}
|
|
31204
|
-
|
|
31284
|
+
const audited = repos.filter((r) => !r.excludedReason);
|
|
31285
|
+
return { ok: audited.length > 0 && audited.every((r) => r.ok), scope: "fleet", repos, excludedCount: repos.length - audited.length };
|
|
31205
31286
|
}
|
|
31206
31287
|
function renderCiAuditScopeLine(report) {
|
|
31207
31288
|
if (report.scope === "registry-unreadable") {
|
|
31208
31289
|
return `scope: REGISTRY UNREADABLE \u2014 repos audited: 0 \u2014 ${report.scopeDetail ?? REGISTRY_UNREADABLE_DETAIL}`;
|
|
31209
31290
|
}
|
|
31291
|
+
const excluded = report.repos.filter((r) => r.excludedReason).length;
|
|
31292
|
+
const count = `repos audited: ${report.repos.length - excluded}${excluded ? ` \u2014 repos excluded: ${excluded}` : ""}`;
|
|
31210
31293
|
if (report.scope === "single-repo") {
|
|
31211
|
-
return `scope: single-repo (explicit --repo) \u2014
|
|
31294
|
+
return `scope: single-repo (explicit --repo) \u2014 ${count} \u2014 this is NOT a fleet verdict`;
|
|
31212
31295
|
}
|
|
31213
|
-
return `scope: fleet (registry roster) \u2014
|
|
31296
|
+
return `scope: fleet (registry roster) \u2014 ${count}`;
|
|
31214
31297
|
}
|
|
31215
31298
|
function renderCiAuditMarkdown(report) {
|
|
31216
31299
|
const lines2 = [
|
|
@@ -31218,16 +31301,16 @@ function renderCiAuditMarkdown(report) {
|
|
|
31218
31301
|
"",
|
|
31219
31302
|
renderCiAuditScopeLine(report),
|
|
31220
31303
|
"",
|
|
31221
|
-
`Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
|
|
31304
|
+
`Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok && !r.excludedReason).length}/${report.repos.filter((r) => !r.excludedReason).length} repos ready)`,
|
|
31222
31305
|
"",
|
|
31223
31306
|
"| Repo | Class | Policy | OK | Top gap |",
|
|
31224
31307
|
"|------|-------|--------|----|---------|"
|
|
31225
31308
|
];
|
|
31226
31309
|
for (const r of report.repos) {
|
|
31227
|
-
const gap = r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
|
|
31228
|
-
lines2.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap} |`);
|
|
31310
|
+
const gap = r.excludedReason ? `EXCLUDED \u2014 ${r.excludedReason}` : r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
|
|
31311
|
+
lines2.push(`| ${r.repo} | ${r.class} | ${r.excludedReason ? "\u2014" : r.mergePolicy} | ${r.excludedReason ? "excluded" : r.ok ? "yes" : "no"} | ${gap} |`);
|
|
31229
31312
|
}
|
|
31230
|
-
const advisories = report.repos.flatMap((r) => r.checks.filter((c) => c.severity
|
|
31313
|
+
const advisories = report.repos.flatMap((r) => r.checks.filter((c) => c.severity).map((c) => `- ${c.severity === "warn" ? "WARN" : "INFO"} ${r.repo}: ${c.label} \u2014 ${c.detail ?? ""}`));
|
|
31231
31314
|
if (advisories.length) lines2.push("", ...advisories);
|
|
31232
31315
|
return lines2.join("\n");
|
|
31233
31316
|
}
|
|
@@ -31237,12 +31320,15 @@ function renderCiAuditText(report) {
|
|
|
31237
31320
|
renderCiAuditScopeLine(report)
|
|
31238
31321
|
];
|
|
31239
31322
|
for (const r of report.repos) {
|
|
31240
|
-
const verdict = r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
|
|
31323
|
+
const verdict = r.excludedReason ? "EXCLUDED" : r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
|
|
31241
31324
|
lines2.push(`
|
|
31242
|
-
${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
|
|
31325
|
+
${r.repo} (${r.class}, policy=${r.excludedReason ? "not audited" : r.mergePolicy}) ${verdict}`);
|
|
31326
|
+
if (r.excludedReason) {
|
|
31327
|
+
lines2.push(` EXCLUDED \u2014 ${r.excludedReason}`);
|
|
31328
|
+
continue;
|
|
31329
|
+
}
|
|
31243
31330
|
if (r.unauditedReason) {
|
|
31244
31331
|
lines2.push(` UNAUDITED \u2014 ${r.unauditedReason}`);
|
|
31245
|
-
continue;
|
|
31246
31332
|
}
|
|
31247
31333
|
for (const c of r.checks) {
|
|
31248
31334
|
lines2.push(` ${!c.ok ? "FAIL" : c.severity === "warn" ? "WARN" : c.severity === "info" ? "INFO" : "OK"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
@@ -31261,6 +31347,10 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
|
|
|
31261
31347
|
errors.push(unauditedReconcileError(report.unauditedReason));
|
|
31262
31348
|
return { repo, applied, skipped, errors };
|
|
31263
31349
|
}
|
|
31350
|
+
if (report.excludedReason) {
|
|
31351
|
+
skipped.push(`excluded \u2014 ${report.excludedReason}; no reconcile change made`);
|
|
31352
|
+
return { repo, applied, skipped, errors };
|
|
31353
|
+
}
|
|
31264
31354
|
const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
|
|
31265
31355
|
const needsPatch = mergeChecks.some((c) => !c.ok);
|
|
31266
31356
|
if (!needsPatch) {
|
|
@@ -31564,6 +31654,9 @@ async function applyCiReconcileRepo(repo, deps, options = {}) {
|
|
|
31564
31654
|
return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
|
|
31565
31655
|
}
|
|
31566
31656
|
const report = await auditRepoCi(repo, deps);
|
|
31657
|
+
if (report.excludedReason) {
|
|
31658
|
+
return { repo, applied: [], skipped: [`excluded \u2014 ${report.excludedReason}; no reconcile change made`], errors: [] };
|
|
31659
|
+
}
|
|
31567
31660
|
if (report.unauditedReason) {
|
|
31568
31661
|
return { repo, applied: [], skipped: [], errors: [unauditedReconcileError(report.unauditedReason)] };
|
|
31569
31662
|
}
|
|
@@ -45976,7 +46069,7 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
45976
46069
|
return {
|
|
45977
46070
|
ok: false,
|
|
45978
46071
|
detail: `${DOCS_INDEX_PATH} is stale at the commit this PR would open from \u2014 it no longer matches the docs/ tree there (checked ${ref} over ${relDocs.length} indexed doc${relDocs.length === 1 ? "" : "s"})`,
|
|
45979
|
-
fix: "run `mmi-cli oracle docs index --write`, commit docs/index.md, and push again before retrying `pr create`"
|
|
46072
|
+
fix: "run `mmi-cli oracle docs index --write`, commit docs/index.md, and push again before retrying `pr create`; freshness is checked at the pushed commit regardless of merge-queue state, so regenerate and commit docs/index.md BEFORE `pr create`, never defer it to after merge"
|
|
45980
46073
|
};
|
|
45981
46074
|
}
|
|
45982
46075
|
|
|
@@ -46066,6 +46159,10 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
46066
46159
|
if (state?.toLowerCase() === "closed") continue;
|
|
46067
46160
|
const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin, deps.appClient);
|
|
46068
46161
|
if (!contest.contested) continue;
|
|
46162
|
+
if (isSameBranchClaimResume(contest, repo, { headBranch: deps.head, headSha: deps.headSha })) {
|
|
46163
|
+
await client.rest("POST", `repos/${repo}/issues/${number}/comments`, { body: { body: formatClaimMarker(actor) } }).catch(() => void 0);
|
|
46164
|
+
continue;
|
|
46165
|
+
}
|
|
46069
46166
|
const ref = `${repo}#${number}`;
|
|
46070
46167
|
const commentsPath = `repos/${repo}/issues/${number}/comments`;
|
|
46071
46168
|
const now = deps.now ?? Date.now;
|
|
@@ -48588,9 +48685,14 @@ ${list}`);
|
|
|
48588
48685
|
if (docsCheck && !docsCheck.ok) {
|
|
48589
48686
|
return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
|
|
48590
48687
|
}
|
|
48591
|
-
const claimRefusal = await prCreateClaimRefusal(body, o.repo);
|
|
48592
|
-
if (claimRefusal) return fail(claimRefusal);
|
|
48593
48688
|
const createHead = o.head ?? await gitOut(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "");
|
|
48689
|
+
const shaRef = o.head ? `refs/heads/${o.head.replace(/^[^/:]+:/, "")}` : "HEAD";
|
|
48690
|
+
const createHeadSha = await gitOut(["rev-parse", "--verify", "--quiet", shaRef]).catch(() => "");
|
|
48691
|
+
const claimRefusal = await prCreateClaimRefusal(body, o.repo, {
|
|
48692
|
+
head: createHead || void 0,
|
|
48693
|
+
headSha: createHeadSha || void 0
|
|
48694
|
+
});
|
|
48695
|
+
if (claimRefusal) return fail(claimRefusal);
|
|
48594
48696
|
const mergedHeadWarning = await prCreateMergedHeadWarning({ head: createHead || void 0, repo: o.repo });
|
|
48595
48697
|
if (mergedHeadWarning) process.stderr.write(`${mergedHeadWarning}
|
|
48596
48698
|
`);
|
|
@@ -54737,9 +54839,13 @@ function parseBrokerAssignee(raw) {
|
|
|
54737
54839
|
var SINGLE_SHOT_TIMEOUT_MS = 6e4;
|
|
54738
54840
|
var BROKER_MAX_BUFFER = 64 * 1024 * 1024;
|
|
54739
54841
|
var PR_QUEUE_QUERY = "query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){mergeQueueEntry{state position enqueuedAt estimatedTimeToMerge headCommit{oid}}}}}";
|
|
54842
|
+
function missingBrokerPrRepoMessage(subcommand, number) {
|
|
54843
|
+
const example = subcommand === "list" ? "mmi-cli devops read pr list --repo <owner/repo> --state open --json number,title,headRefName" : subcommand === "diff" ? `mmi-cli devops read pr diff ${number ?? "<number>"} --repo <owner/repo> --name-only` : `mmi-cli devops read pr queue ${number ?? "<number>"} --repo <owner/repo> --json`;
|
|
54844
|
+
return `read pr ${subcommand}: required option '--repo <owner/repo>' not specified \u2014 broker PR ${subcommand} reads do not infer the repo from a worktree's git origin; pass --repo <owner/repo> explicitly (e.g. \`${example}\`)`;
|
|
54845
|
+
}
|
|
54740
54846
|
function buildPrQueueSpec(number, o) {
|
|
54741
54847
|
const repo = parseBrokerRepo(o.repo, "--repo");
|
|
54742
|
-
if (!repo) throw new Error("
|
|
54848
|
+
if (!repo) throw new Error(missingBrokerPrRepoMessage("queue", number));
|
|
54743
54849
|
const [owner, name] = repo.split("/");
|
|
54744
54850
|
return { program: "gh", args: ["api", "graphql", "-f", `query=${PR_QUEUE_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-F", `number=${parseBrokerId(number, "PR number")}`], timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
|
|
54745
54851
|
}
|
|
@@ -54895,7 +55001,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
|
|
|
54895
55001
|
return 0;
|
|
54896
55002
|
} catch (e) {
|
|
54897
55003
|
const err = e;
|
|
54898
|
-
if (typeof err.stdout === "string" && err.stdout) write(redactPrivateCredential(err.stdout, env));
|
|
55004
|
+
if (typeof err.stdout === "string" && err.stdout) process.stderr.write(redactPrivateCredential(err.stdout, env));
|
|
54899
55005
|
const code = ghExitCodeOf(e);
|
|
54900
55006
|
if (code !== null) {
|
|
54901
55007
|
if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
|
|
@@ -55021,13 +55127,16 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
|
|
|
55021
55127
|
await dispatchGh(spec, "read run watch");
|
|
55022
55128
|
});
|
|
55023
55129
|
const pr = read.command("pr").description("pull-request reads \u2014 view/list/diff ride gh; queue uses fixed GraphQL; checks is REST-backed (deterministic closed-custody adapter; native was EMPTY only on the historical #3035 fixture)");
|
|
55024
|
-
pr.command("list").description("list viewer-free PRs (author/viewer filters stay personal)").
|
|
55130
|
+
pr.command("list").description("list viewer-free PRs (author/viewer filters stay personal)").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--json [fields]", "native gh JSON fields", jsonOpt).addOption(new Option("--state <state>", "open|closed|merged|all").choices(["open", "closed", "merged", "all"])).option("--base <branch>", "base branch", branchOpt).option("--head <branch>", "head branch", branchOpt).option("--label <label>", "filter by label", labelOpt).option("--search <query>", "viewer-free search", (v) => parseBrokerPrSearch(v)).option("--assignee <login>", "explicit assignee (viewer refused)", assigneeOpt).option("--limit <n>", "maximum PRs (1-100)", intOpt("--limit", 1, 100)).action(async (o) => {
|
|
55131
|
+
if (!o.repo) return fail(missingBrokerPrRepoMessage("list"));
|
|
55025
55132
|
await dispatchGh(buildPrListSpec(o), "read pr list");
|
|
55026
55133
|
});
|
|
55027
|
-
pr.command("diff").description("read a bounded PR diff on the App pool").
|
|
55134
|
+
pr.command("diff").description("read a bounded PR diff on the App pool").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--name-only", "only changed file names").argument("<number>", "PR number", idOpt("PR number")).action(async (number, o) => {
|
|
55135
|
+
if (!o.repo) return fail(missingBrokerPrRepoMessage("diff", number));
|
|
55028
55136
|
await dispatchGh(buildPrDiffSpec(number, o), "read pr diff");
|
|
55029
55137
|
});
|
|
55030
|
-
pr.command("queue").description("read one PR mergeQueueEntry (fixed GraphQL query, no permission widening)").
|
|
55138
|
+
pr.command("queue").description("read one PR mergeQueueEntry (fixed GraphQL query, no permission widening)").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--json", "machine-readable mergeQueueEntry (null when not queued)").argument("<number>", "PR number", idOpt("PR number")).action(async (number, o) => {
|
|
55139
|
+
if (!o.repo) return fail(missingBrokerPrRepoMessage("queue", number));
|
|
55031
55140
|
const spec = buildPrQueueSpec(number, o);
|
|
55032
55141
|
requireBrokerApp("read pr queue");
|
|
55033
55142
|
try {
|
|
@@ -55160,9 +55269,13 @@ var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
|
|
|
55160
55269
|
var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
|
|
55161
55270
|
var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
|
|
55162
55271
|
var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs, repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs, repos/{owner}/{repo}/actions/runs/{id}/artifacts, repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>), repos/{owner}/{repo}/releases[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/releases/latest, repos/{owner}/{repo}/releases/tags/{tag}, repos/{owner}/{repo}/releases/assets/{id} (needs --out <file>)";
|
|
55272
|
+
var API_GET_RECOVERY = " No other GET read exists \u2014 do not retry path variants; use a dedicated mmi-cli subcommand or ask a human token holder. This refusal exits 1 on stderr with nothing on stdout: check the exit code and never merge stderr into stdout (2>&1) before JSON-parsing.";
|
|
55163
55273
|
function parseApiGetPath(rawPath) {
|
|
55164
55274
|
const path2 = rawPath.trim();
|
|
55165
|
-
const refuse = () =>
|
|
55275
|
+
const refuse = () => {
|
|
55276
|
+
const hint = /^[A-Za-z]:[/\\]/.test(path2) ? " Git Bash/MSYS may have rewritten a leading-slash argument; retry with MSYS_NO_PATHCONV=1 and the slash-free repos/... path. The endpoint must still be allowlisted." : "";
|
|
55277
|
+
return new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.${API_GET_RECOVERY}${hint}`);
|
|
55278
|
+
};
|
|
55166
55279
|
if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
|
|
55167
55280
|
const q = path2.indexOf("?");
|
|
55168
55281
|
const base = q === -1 ? path2 : path2.slice(0, q);
|
|
@@ -55183,7 +55296,7 @@ function parseApiGetPath(rawPath) {
|
|
|
55183
55296
|
if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
|
|
55184
55297
|
return { kind: "contents", path: path2, paginate: false };
|
|
55185
55298
|
}
|
|
55186
|
-
throw
|
|
55299
|
+
throw refuse();
|
|
55187
55300
|
}
|
|
55188
55301
|
var ZIP_TIMEOUT_MS = 12e4;
|
|
55189
55302
|
var ZIP_MAX_BUFFER = 512 * 1024 * 1024;
|
|
@@ -59180,7 +59293,7 @@ ci.command("reconcile").description("audit + optionally apply merge settings and
|
|
|
59180
59293
|
}
|
|
59181
59294
|
const deps = ciAuditDeps2();
|
|
59182
59295
|
const audit = await auditOrgCi(deps, o.repo);
|
|
59183
|
-
const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : o.parkRuleset ? await Promise.all(audit.repos.map((r) => parkProductRuleset(r.repo, deps))) : [];
|
|
59296
|
+
const applyResults = o.apply ? await Promise.all(audit.repos.filter((r) => !r.excludedReason).map((r) => applyCiReconcileRepo(r.repo, deps))) : o.parkRuleset ? await Promise.all(audit.repos.filter((r) => !r.excludedReason).map((r) => parkProductRuleset(r.repo, deps))) : [];
|
|
59184
59297
|
const payload = { audit, apply: applyResults };
|
|
59185
59298
|
if (o.json) console.log(JSON.stringify(payload, null, 2));
|
|
59186
59299
|
else {
|
|
@@ -59511,6 +59624,7 @@ function printHouseRootHelp(house) {
|
|
|
59511
59624
|
consoleIo.log(lines2.join("\n"));
|
|
59512
59625
|
}
|
|
59513
59626
|
resolveVerbAliasShim(process.argv);
|
|
59627
|
+
normalizeWorkboardClaimArgv(process.argv);
|
|
59514
59628
|
resolveHouseShim(process.argv);
|
|
59515
59629
|
normalizeBoardClaimIssueArgv(process.argv);
|
|
59516
59630
|
program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
package/package.json
CHANGED