@mutmutco/cli 4.5.208 → 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 +500 -299
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -1598,13 +1598,15 @@ __export(index_exports, {
|
|
|
1598
1598
|
commandOwnLongFlags: () => commandOwnLongFlags,
|
|
1599
1599
|
envHealLockPath: () => envHealLockPath,
|
|
1600
1600
|
isOrgRegisteredRepo: () => isOrgRegisteredRepo,
|
|
1601
|
+
normalizeBoardClaimIssueArgv: () => normalizeBoardClaimIssueArgv,
|
|
1601
1602
|
parseInvalidChoiceError: () => parseInvalidChoiceError,
|
|
1602
1603
|
positionalTargetForm: () => positionalTargetForm,
|
|
1603
1604
|
registryClientDeps: () => registryClientDeps,
|
|
1604
1605
|
repoSlug: () => repoSlug,
|
|
1605
1606
|
suggestCommandPath: () => suggestCommandPath,
|
|
1606
1607
|
unknownCommandCandidates: () => unknownCommandCandidates,
|
|
1607
|
-
unknownCommandTail: () => unknownCommandTail
|
|
1608
|
+
unknownCommandTail: () => unknownCommandTail,
|
|
1609
|
+
workboardClaimRecovery: () => workboardClaimRecovery
|
|
1608
1610
|
});
|
|
1609
1611
|
module.exports = __toCommonJS(index_exports);
|
|
1610
1612
|
|
|
@@ -6350,6 +6352,276 @@ function boardClaimStatusFlagRecovery(flag, canonicalPath, argv) {
|
|
|
6350
6352
|
corrected: `mmi-cli ${kept.join(" ")}`
|
|
6351
6353
|
};
|
|
6352
6354
|
}
|
|
6355
|
+
var BOARD_CLAIM_OWN_FLAGS = ["--repo", "--for", "--force", "--check", "--allow-partial", "--json"];
|
|
6356
|
+
function splitInlineSelectors(argv, flags) {
|
|
6357
|
+
const out = [];
|
|
6358
|
+
for (const tok of argv) {
|
|
6359
|
+
const eq = tok.indexOf("=");
|
|
6360
|
+
if (tok.startsWith("--") && eq > 0 && flags.includes(tok.slice(0, eq))) {
|
|
6361
|
+
out.push(tok.slice(0, eq), tok.slice(eq + 1));
|
|
6362
|
+
} else {
|
|
6363
|
+
out.push(tok);
|
|
6364
|
+
}
|
|
6365
|
+
}
|
|
6366
|
+
return out;
|
|
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;
|
|
6459
|
+
function workboardClaimRecovery(unknownToken, argv, paths, ownFlags = BOARD_CLAIM_OWN_FLAGS) {
|
|
6460
|
+
if (unknownToken !== "workboard") return void 0;
|
|
6461
|
+
const catalog = [...paths];
|
|
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}`;
|
|
6493
|
+
let corrected;
|
|
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(" ");
|
|
6530
|
+
} else {
|
|
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(" ");
|
|
6539
|
+
}
|
|
6540
|
+
return { canonicalPath, corrected, hint: `did you mean \`${corrected}\`?` };
|
|
6541
|
+
}
|
|
6542
|
+
var BOARD_CLAIM_VALUE_FLAGS = /* @__PURE__ */ new Set(["--repo", "--for", "--status"]);
|
|
6543
|
+
var BOARD_CLAIM_BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--json", "--force", "--check", "--allow-partial", "--help", "-h"]);
|
|
6544
|
+
function normalizeBoardClaimIssueArgv(argv) {
|
|
6545
|
+
let claimAt;
|
|
6546
|
+
if (argv[2] === "board" && argv[3] === "claim") claimAt = 3;
|
|
6547
|
+
else if (argv[2] === "oracle" && argv[3] === "board" && argv[4] === "claim") claimAt = 4;
|
|
6548
|
+
else return false;
|
|
6549
|
+
let issueAt = -1;
|
|
6550
|
+
let issueSpan = 0;
|
|
6551
|
+
let value;
|
|
6552
|
+
for (let i = claimAt + 1; i < argv.length; i += 1) {
|
|
6553
|
+
const tok = argv[i];
|
|
6554
|
+
if (tok === "--issue" || tok.startsWith("--issue=")) {
|
|
6555
|
+
if (issueAt >= 0) return false;
|
|
6556
|
+
issueAt = i;
|
|
6557
|
+
if (tok === "--issue") {
|
|
6558
|
+
const next = argv[i + 1];
|
|
6559
|
+
if (next === void 0 || next === "" || next.startsWith("-")) return false;
|
|
6560
|
+
value = next;
|
|
6561
|
+
issueSpan = 2;
|
|
6562
|
+
i += 1;
|
|
6563
|
+
} else {
|
|
6564
|
+
value = tok.slice("--issue=".length);
|
|
6565
|
+
if (value === "" || value.startsWith("-")) return false;
|
|
6566
|
+
issueSpan = 1;
|
|
6567
|
+
}
|
|
6568
|
+
continue;
|
|
6569
|
+
}
|
|
6570
|
+
if (tok.startsWith("-")) {
|
|
6571
|
+
const name = tok.includes("=") ? tok.slice(0, tok.indexOf("=")) : tok;
|
|
6572
|
+
if (BOARD_CLAIM_VALUE_FLAGS.has(name)) {
|
|
6573
|
+
if (!tok.includes("=") && argv[i + 1] !== void 0 && !argv[i + 1].startsWith("-")) i += 1;
|
|
6574
|
+
continue;
|
|
6575
|
+
}
|
|
6576
|
+
if (BOARD_CLAIM_BOOLEAN_FLAGS.has(tok)) continue;
|
|
6577
|
+
return false;
|
|
6578
|
+
}
|
|
6579
|
+
return false;
|
|
6580
|
+
}
|
|
6581
|
+
if (issueAt < 0 || value === void 0) return false;
|
|
6582
|
+
argv.splice(issueAt, issueSpan);
|
|
6583
|
+
argv.splice(claimAt + 1, 0, value);
|
|
6584
|
+
return true;
|
|
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
|
+
}
|
|
6353
6625
|
function classifyParseError(plain) {
|
|
6354
6626
|
if (/unknown command/i.test(plain)) return "unknown-command";
|
|
6355
6627
|
if (/unknown option|missing required argument|too many arguments|argument missing|is invalid\. Allowed choices are|cannot be used with/i.test(plain)) {
|
|
@@ -15065,6 +15337,12 @@ function renderClaimReleaseBody(release) {
|
|
|
15065
15337
|
|
|
15066
15338
|
<!-- mmi-claim-release: ${JSON.stringify(release)} -->`;
|
|
15067
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
|
+
}
|
|
15068
15346
|
function latestClaimMarker(comments) {
|
|
15069
15347
|
for (let i = comments.length - 1; i >= 0; i -= 1) {
|
|
15070
15348
|
const body = comments[i].body;
|
|
@@ -15389,6 +15667,7 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
|
|
|
15389
15667
|
`repos/${repo}/branches/${encodeURIComponent(out.branch)}`
|
|
15390
15668
|
);
|
|
15391
15669
|
out.branchAt = head?.commit?.commit?.committer?.date ?? void 0;
|
|
15670
|
+
out.branchSha = head?.commit?.sha ?? void 0;
|
|
15392
15671
|
} catch {
|
|
15393
15672
|
}
|
|
15394
15673
|
}
|
|
@@ -15455,13 +15734,27 @@ function isSeatClaimMarker(marker, current, context = {}) {
|
|
|
15455
15734
|
const state = context.sessionState ?? probeLocalClaimSession(marker, context.now);
|
|
15456
15735
|
return state === "dead";
|
|
15457
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
|
+
}
|
|
15458
15751
|
function laneResumeEvidence(evidence) {
|
|
15459
15752
|
if (!evidence.marker || evidence.sessionState !== "dead") return void 0;
|
|
15460
15753
|
if (!isHostSessionId(evidence.marker.session)) return void 0;
|
|
15461
15754
|
const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
|
|
15462
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("; ")}` : ""}`;
|
|
15463
15756
|
}
|
|
15464
|
-
async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient) {
|
|
15757
|
+
async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin, appClient, landing) {
|
|
15465
15758
|
const livenessClient = await selectLivenessClient(client, appClient);
|
|
15466
15759
|
let evidence = await gatherClaimLiveness(livenessClient, item.repository, item.number, openPullsFetcher(livenessClient));
|
|
15467
15760
|
if (evidence.failed.some((channel) => channel !== "session")) {
|
|
@@ -15473,10 +15766,21 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
15473
15766
|
markerBy: evidence.markerBy,
|
|
15474
15767
|
sessionState: evidence.sessionState
|
|
15475
15768
|
});
|
|
15476
|
-
|
|
15769
|
+
let resume = ownership !== "mine" && ownSeat ? laneResumeEvidence(evidence) : void 0;
|
|
15477
15770
|
const live = ownSeat ? [] : liveEvidenceLines(evidence, item.repository);
|
|
15478
15771
|
const unverifiable = ownSeat ? [] : evidence.failed;
|
|
15479
|
-
|
|
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
|
+
}
|
|
15480
15784
|
const carry = staleWorkLines(evidence);
|
|
15481
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;
|
|
15482
15786
|
return {
|
|
@@ -15486,7 +15790,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
15486
15790
|
ownership,
|
|
15487
15791
|
marker: evidence.marker,
|
|
15488
15792
|
resume,
|
|
15489
|
-
...staleStamp ? { staleStamp } : {}
|
|
15793
|
+
...staleStamp ? { staleStamp } : {},
|
|
15794
|
+
...evidence.branch ? { branch: evidence.branch } : {},
|
|
15795
|
+
...evidence.branchSha ? { branchSha: evidence.branchSha } : {}
|
|
15490
15796
|
};
|
|
15491
15797
|
}
|
|
15492
15798
|
function laneContestParts(contest) {
|
|
@@ -15637,6 +15943,7 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
|
|
|
15637
15943
|
cfg,
|
|
15638
15944
|
client,
|
|
15639
15945
|
items: collected.items,
|
|
15946
|
+
selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
|
|
15640
15947
|
writable: writableOrUnknown(writable),
|
|
15641
15948
|
report,
|
|
15642
15949
|
session: deps.session ?? describeSessionIdentity(),
|
|
@@ -15748,7 +16055,9 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
15748
16055
|
return;
|
|
15749
16056
|
}
|
|
15750
16057
|
if (!options.force) {
|
|
15751
|
-
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.`;
|
|
15752
16061
|
throw new Error(options.bulk ? refusal : withGatedWriteChainHint(refusal));
|
|
15753
16062
|
}
|
|
15754
16063
|
previousHolder = displaced();
|
|
@@ -19344,10 +19653,10 @@ var rollout_plan_default = {
|
|
|
19344
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)."
|
|
19345
19654
|
},
|
|
19346
19655
|
baseline: {
|
|
19347
|
-
version: "4.5.
|
|
19348
|
-
tag: "v4.5.
|
|
19349
|
-
commit: "
|
|
19350
|
-
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"
|
|
19351
19660
|
},
|
|
19352
19661
|
exitCriterion: "fleet-n-of-n",
|
|
19353
19662
|
hubOnlyShortcut: "forbidden",
|
|
@@ -19364,14 +19673,14 @@ var rollout_plan_default = {
|
|
|
19364
19673
|
repo: "mutmutco/mmi-hub",
|
|
19365
19674
|
role: "canary",
|
|
19366
19675
|
schedule: "train",
|
|
19367
|
-
v3Target: "v4.5.
|
|
19676
|
+
v3Target: "v4.5.210"
|
|
19368
19677
|
}
|
|
19369
19678
|
],
|
|
19370
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.",
|
|
19371
19680
|
rollback: {
|
|
19372
19681
|
independent: true,
|
|
19373
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
19374
|
-
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)"
|
|
19375
19684
|
}
|
|
19376
19685
|
},
|
|
19377
19686
|
{
|
|
@@ -23875,46 +24184,45 @@ var import_node_fs30 = require("node:fs");
|
|
|
23875
24184
|
var import_promises4 = require("node:fs/promises");
|
|
23876
24185
|
var import_node_path28 = require("node:path");
|
|
23877
24186
|
|
|
23878
|
-
//
|
|
23879
|
-
var
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
23883
|
-
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
|
|
23888
|
-
|
|
23889
|
-
|
|
23890
|
-
|
|
23891
|
-
if (jobs.some((job) => isActionsBillingBlockText(JSON.stringify(job)))) return "billing-blocked";
|
|
23892
|
-
if (jobs.some((job) => Array.isArray(job.steps) && job.steps.length > 0 || Boolean(job.startedAt))) {
|
|
23893
|
-
return "started";
|
|
23894
|
-
}
|
|
23895
|
-
if (jobs.length > 0 && jobs.every((job) => {
|
|
23896
|
-
const finished = job.conclusion === "failure" || job.status === "completed";
|
|
23897
|
-
const empty = !Array.isArray(job.steps) || job.steps.length === 0;
|
|
23898
|
-
return finished && empty && !job.startedAt;
|
|
23899
|
-
})) {
|
|
23900
|
-
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
|
+
}
|
|
23901
24200
|
}
|
|
23902
|
-
return "
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
function isCanaryAccessBlockText(text) {
|
|
23906
|
-
return !isActionsBillingBlockText(text) && CANARY_ACCESS_BLOCK_RE.test(text);
|
|
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";
|
|
23907
24204
|
}
|
|
23908
|
-
function
|
|
23909
|
-
|
|
23910
|
-
|
|
23911
|
-
);
|
|
23912
|
-
}
|
|
23913
|
-
|
|
23914
|
-
|
|
23915
|
-
|
|
23916
|
-
);
|
|
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";
|
|
23917
24223
|
}
|
|
24224
|
+
|
|
24225
|
+
// src/actions-billing-preflight.ts
|
|
23918
24226
|
function parseJson(raw, label) {
|
|
23919
24227
|
try {
|
|
23920
24228
|
return JSON.parse(raw);
|
|
@@ -23922,153 +24230,30 @@ function parseJson(raw, label) {
|
|
|
23922
24230
|
throw new Error(`${label} was not valid JSON`);
|
|
23923
24231
|
}
|
|
23924
24232
|
}
|
|
23925
|
-
|
|
23926
|
-
|
|
23927
|
-
|
|
23928
|
-
|
|
23929
|
-
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
"8",
|
|
23935
|
-
"--json",
|
|
23936
|
-
"databaseId,conclusion,status,displayTitle,url"
|
|
23937
|
-
]);
|
|
23938
|
-
} catch {
|
|
23939
|
-
return void 0;
|
|
23940
|
-
}
|
|
23941
|
-
const rows = parseJson(raw, `gh run list --repo ${repo}`);
|
|
23942
|
-
if (!Array.isArray(rows)) return void 0;
|
|
23943
|
-
for (const row of rows) {
|
|
23944
|
-
const blob = `${row.displayTitle ?? ""} ${row.conclusion ?? ""} ${row.url ?? ""}`;
|
|
23945
|
-
if (isActionsBillingBlockText(blob)) {
|
|
23946
|
-
return `${repo} run ${row.databaseId ?? row.url ?? "(unknown)"} already names a billing/spending block`;
|
|
23947
|
-
}
|
|
23948
|
-
if (row.conclusion !== "failure" && row.status !== "completed") continue;
|
|
23949
|
-
if (typeof row.databaseId !== "number") continue;
|
|
23950
|
-
try {
|
|
23951
|
-
const view = await deps.run("gh", [
|
|
23952
|
-
"run",
|
|
23953
|
-
"view",
|
|
23954
|
-
String(row.databaseId),
|
|
23955
|
-
"--repo",
|
|
23956
|
-
repo,
|
|
23957
|
-
"--json",
|
|
23958
|
-
"jobs,conclusion,displayTitle,url"
|
|
23959
|
-
]);
|
|
23960
|
-
const parsed = parseJson(view, `gh run view ${row.databaseId}`);
|
|
23961
|
-
const verdict = interpretActionsJobStart({
|
|
23962
|
-
jobs: parsed.jobs,
|
|
23963
|
-
text: `${parsed.displayTitle ?? ""} ${parsed.conclusion ?? ""} ${view}`
|
|
23964
|
-
});
|
|
23965
|
-
if (verdict === "billing-blocked" || verdict === "never-started") {
|
|
23966
|
-
return `${repo} run ${row.databaseId} (${parsed.url ?? row.url ?? "no url"}): hosted job never started` + (verdict === "billing-blocked" ? " (billing/spending refusal)" : " (empty steps)");
|
|
23967
|
-
}
|
|
23968
|
-
} catch {
|
|
23969
|
-
}
|
|
23970
|
-
}
|
|
23971
|
-
return void 0;
|
|
23972
|
-
}
|
|
23973
|
-
async function correlateCanaryRun(deps, nonce) {
|
|
23974
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
23975
|
-
let lastError = "no matching canary run";
|
|
23976
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
23977
|
-
if (attempt > 0) await sleep2(CANARY_POLL_MS);
|
|
23978
|
-
let raw;
|
|
23979
|
-
try {
|
|
23980
|
-
raw = await deps.run("gh", [
|
|
23981
|
-
"run",
|
|
23982
|
-
"list",
|
|
23983
|
-
"--repo",
|
|
23984
|
-
CANARY_REPO,
|
|
23985
|
-
"--workflow",
|
|
23986
|
-
CANARY_WORKFLOW,
|
|
23987
|
-
"--limit",
|
|
23988
|
-
"10",
|
|
23989
|
-
"--json",
|
|
23990
|
-
"databaseId,displayTitle,status,createdAt"
|
|
23991
|
-
]);
|
|
23992
|
-
} catch (e) {
|
|
23993
|
-
lastError = e instanceof Error ? e.message : String(e);
|
|
23994
|
-
continue;
|
|
23995
|
-
}
|
|
23996
|
-
const rows = parseJson(
|
|
23997
|
-
raw,
|
|
23998
|
-
"canary gh run list"
|
|
23999
|
-
);
|
|
24000
|
-
const match = rows.find((row) => (row.displayTitle ?? "").includes(nonce) && typeof row.databaseId === "number");
|
|
24001
|
-
if (match?.databaseId) return match.databaseId;
|
|
24002
|
-
}
|
|
24003
|
-
throw new Error(
|
|
24004
|
-
`could not correlate ${CANARY_WORKFLOW} on ${CANARY_REPO} (nonce ${nonce}): ${lastError}`
|
|
24005
|
-
);
|
|
24006
|
-
}
|
|
24007
|
-
async function dispatchCanary(deps, targetRepo2, nonce) {
|
|
24008
|
-
const attempts = [];
|
|
24009
|
-
if (deps.dispatchActionsCanary) {
|
|
24010
|
-
try {
|
|
24011
|
-
await deps.dispatchActionsCanary({ repo: targetRepo2, nonce });
|
|
24012
|
-
return;
|
|
24013
|
-
} catch (e) {
|
|
24014
|
-
attempts.push(`Hub App dispatch: ${e instanceof Error ? e.message : String(e)}`);
|
|
24015
|
-
}
|
|
24016
|
-
}
|
|
24017
|
-
try {
|
|
24018
|
-
await deps.run("gh", [
|
|
24019
|
-
"workflow",
|
|
24020
|
-
"run",
|
|
24021
|
-
CANARY_WORKFLOW,
|
|
24022
|
-
"--repo",
|
|
24023
|
-
CANARY_REPO,
|
|
24024
|
-
"-f",
|
|
24025
|
-
`nonce=${nonce}`
|
|
24026
|
-
]);
|
|
24027
|
-
return;
|
|
24028
|
-
} catch (e) {
|
|
24029
|
-
attempts.push(`gh workflow run --repo ${CANARY_REPO}: ${e instanceof Error ? e.message : String(e)}`);
|
|
24030
|
-
}
|
|
24031
|
-
const detail = `could not dispatch ${CANARY_WORKFLOW} on ${CANARY_REPO} \u2014 ${attempts.join(" | ")}.`;
|
|
24032
|
-
if (attempts.some((a) => isActionsBillingBlockText(a))) {
|
|
24033
|
-
throw actionsBillingRefusal(`${detail} A hosted publish job may not start (the v1.54.2 class).`);
|
|
24034
|
-
}
|
|
24035
|
-
throw actionsCanaryUndispatchedRefusal(
|
|
24036
|
-
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
|
|
24037
|
-
);
|
|
24038
|
-
}
|
|
24039
|
-
var HOSTED_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/;
|
|
24040
|
-
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;
|
|
24041
|
-
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"]);
|
|
24042
|
-
function workflowTriggers(yaml) {
|
|
24043
|
-
const lines2 = yaml.split(/\r?\n/);
|
|
24044
|
-
const i = lines2.findIndex((l) => /^(?:on|"on"|'on'):(?:\s|$)/.test(l));
|
|
24045
|
-
if (i < 0) return [];
|
|
24046
|
-
const inline = lines2[i].replace(/^[^:]+:\s*/, "").replace(/\s+#.*$/, "").trim();
|
|
24047
|
-
if (inline) return inline.replace(/^\[|\]$/g, "").split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
24048
|
-
const out = [];
|
|
24049
|
-
for (const line of lines2.slice(i + 1)) {
|
|
24050
|
-
if (/^\S/.test(line)) break;
|
|
24051
|
-
const m = /^ {2}([A-Za-z_]+):/.exec(line) ?? /^ {2}- ([A-Za-z_]+)\s*$/.exec(line);
|
|
24052
|
-
if (m) out.push(m[1]);
|
|
24053
|
-
}
|
|
24054
|
-
return out;
|
|
24055
|
-
}
|
|
24056
|
-
function workflowNeedsHostedRunner(yaml) {
|
|
24057
|
-
const body = yaml.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join("\n").replace(FALLBACK_EXPR_GLOBAL_RE, "");
|
|
24058
|
-
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
|
+
});
|
|
24059
24242
|
}
|
|
24060
24243
|
async function hostedWorkflowsOf(deps, repo) {
|
|
24061
24244
|
const list = parseJson(
|
|
24062
24245
|
await deps.run("gh", ["api", `repos/${repo}/contents/.github/workflows`]),
|
|
24063
24246
|
`${repo} workflow listing`
|
|
24064
24247
|
);
|
|
24065
|
-
|
|
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 = [];
|
|
24066
24250
|
for (const f of list) {
|
|
24067
|
-
if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path)
|
|
24251
|
+
if (f.type !== "file" || !f.path || !/\.ya?ml$/.test(f.path)) continue;
|
|
24068
24252
|
const raw = parseJson(await deps.run("gh", ["api", `repos/${repo}/contents/${f.path}`]), `${repo} ${f.path}`);
|
|
24069
|
-
|
|
24253
|
+
workflows.push({ path: f.path, body: Buffer.from(raw.content ?? "", "base64").toString("utf8") });
|
|
24070
24254
|
}
|
|
24071
|
-
|
|
24255
|
+
const local = new Set(workflows.map((file) => file.path));
|
|
24256
|
+
return workflows.filter((file) => workflowNeedsHostedRunner(file.body, local)).map((file) => file.path);
|
|
24072
24257
|
}
|
|
24073
24258
|
async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
24074
24259
|
let hosted;
|
|
@@ -24078,50 +24263,11 @@ async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
|
24078
24263
|
hosted = void 0;
|
|
24079
24264
|
}
|
|
24080
24265
|
if (hosted && hosted.length === 0) {
|
|
24081
|
-
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)
|
|
24082
24267
|
`);
|
|
24083
24268
|
return;
|
|
24084
24269
|
}
|
|
24085
|
-
|
|
24086
|
-
if (scanned) throw actionsBillingRefusal(scanned);
|
|
24087
|
-
if (targetRepo2.toLowerCase() !== CANARY_REPO.toLowerCase()) {
|
|
24088
|
-
const hubScan = await scanRepoForBillingBlock(deps, CANARY_REPO);
|
|
24089
|
-
if (hubScan) throw actionsBillingRefusal(hubScan);
|
|
24090
|
-
}
|
|
24091
|
-
const nonce = `5604-${(deps.now ?? Date.now)().toString(36)}`;
|
|
24092
|
-
await dispatchCanary(deps, targetRepo2, nonce);
|
|
24093
|
-
const runId = await correlateCanaryRun(deps, nonce);
|
|
24094
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
24095
|
-
let last = "pending";
|
|
24096
|
-
let url = `https://github.com/${CANARY_REPO}/actions/runs/${runId}`;
|
|
24097
|
-
for (let attempt = 0; attempt < CANARY_POLLS; attempt++) {
|
|
24098
|
-
if (attempt > 0) await sleep2(CANARY_POLL_MS);
|
|
24099
|
-
const view = await deps.run("gh", [
|
|
24100
|
-
"run",
|
|
24101
|
-
"view",
|
|
24102
|
-
String(runId),
|
|
24103
|
-
"--repo",
|
|
24104
|
-
CANARY_REPO,
|
|
24105
|
-
"--json",
|
|
24106
|
-
"jobs,status,conclusion,url,displayTitle"
|
|
24107
|
-
]);
|
|
24108
|
-
const parsed = parseJson(view, `canary gh run view ${runId}`);
|
|
24109
|
-
if (parsed.url) url = parsed.url;
|
|
24110
|
-
last = interpretActionsJobStart({
|
|
24111
|
-
jobs: parsed.jobs,
|
|
24112
|
-
text: `${parsed.displayTitle ?? ""} ${parsed.conclusion ?? ""} ${view}`
|
|
24113
|
-
});
|
|
24114
|
-
if (last === "started") {
|
|
24115
|
-
await deps.run("gh", ["run", "cancel", String(runId), "--repo", CANARY_REPO]).catch(() => "");
|
|
24116
|
-
return;
|
|
24117
|
-
}
|
|
24118
|
-
if (last === "billing-blocked" || last === "never-started") {
|
|
24119
|
-
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`);
|
|
24120
|
-
}
|
|
24121
|
-
}
|
|
24122
|
-
throw actionsBillingRefusal(
|
|
24123
|
-
`${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`
|
|
24124
|
-
);
|
|
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.`);
|
|
24125
24271
|
}
|
|
24126
24272
|
|
|
24127
24273
|
// src/train-apply-recovery.ts
|
|
@@ -30224,33 +30370,6 @@ function registryRepoKey(repo) {
|
|
|
30224
30370
|
return repo.toLowerCase();
|
|
30225
30371
|
}
|
|
30226
30372
|
|
|
30227
|
-
// ../shared/runner-fallback.ts
|
|
30228
|
-
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*\}\}$/;
|
|
30229
|
-
var FLEET_ONLY_RUNNER_JOBS = {
|
|
30230
|
-
"backup-health.yml:judge": "SSH backup inventory; hosted source-IP access unverified",
|
|
30231
|
-
"browser-pass.yml:browser-pass": "owner-bounded two-lane browser capacity",
|
|
30232
|
-
"app-publish.yml:publish": "SSH tenant publish; hosted source-IP access unverified",
|
|
30233
|
-
"get-publish.yml:publish": "SSH installer publish; hosted source-IP access unverified",
|
|
30234
|
-
"get-publish-desktop.yml:publish": "SSH installer publish; hosted source-IP access unverified",
|
|
30235
|
-
"hetzner-knownhosts-pin.yml:pin": "SSH host-key probe; hosted source-IP access unverified",
|
|
30236
|
-
"module-rc-publish.yml:publish": "SSH live-container snapshot and tenant persistent state",
|
|
30237
|
-
"tenant-control.yml:control": "SSH tenant control; hosted source-IP access unverified",
|
|
30238
|
-
"tenant-deploy.yml:deploy": "SSH tenant deploy; hosted source-IP access unverified",
|
|
30239
|
-
"tenant-reconcile.yml:reconcile": "SSH tenant reconciliation; hosted source-IP access unverified"
|
|
30240
|
-
};
|
|
30241
|
-
function parseRunnerFallbackExpression(runsOn) {
|
|
30242
|
-
const match = RUNNER_FALLBACK_EXPR_RE.exec(runsOn.trim());
|
|
30243
|
-
if (!match) return void 0;
|
|
30244
|
-
try {
|
|
30245
|
-
const parsed = JSON.parse(match[2]);
|
|
30246
|
-
if (!Array.isArray(parsed) || !parsed.length || !parsed.every((item) => typeof item === "string")) return void 0;
|
|
30247
|
-
if (!parsed.some((item) => item.toLowerCase() === "self-hosted")) return void 0;
|
|
30248
|
-
return { hosted: match[1], labels: parsed };
|
|
30249
|
-
} catch {
|
|
30250
|
-
return void 0;
|
|
30251
|
-
}
|
|
30252
|
-
}
|
|
30253
|
-
|
|
30254
30373
|
// src/ci-audit.ts
|
|
30255
30374
|
var HUB_REPO4 = "mutmutco/MMI-Hub";
|
|
30256
30375
|
var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
|
|
@@ -30310,25 +30429,28 @@ var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
|
|
|
30310
30429
|
var SHARED_ACTION_PINS = [
|
|
30311
30430
|
{ path: "npm-publish", floor: NPM_PUBLISH_SAFE_FLOOR_SHA }
|
|
30312
30431
|
];
|
|
30432
|
+
var STANDARD_HOSTED_ALLOWLIST = [
|
|
30433
|
+
["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"]
|
|
30434
|
+
];
|
|
30313
30435
|
function auditRunnerFallbacks(repo, files) {
|
|
30314
30436
|
const checks = [];
|
|
30315
30437
|
for (const { path: path2, body } of files) {
|
|
30316
30438
|
const jobs = parseWorkflowJobs(body.split(/\r?\n/).filter((line) => !/^\s*#/.test(line)).join("\n"));
|
|
30317
30439
|
for (const job of jobs) {
|
|
30318
|
-
const runsOn = /^ runs-on:\s*(.+)$/m.exec(job.body)?.[1] ?? "";
|
|
30319
30440
|
const runnerSection = /^ runs-on:([^\n]*(?:\n {6,}[^\n]*)*)/m.exec(job.body)?.[1] ?? "";
|
|
30320
|
-
const
|
|
30321
|
-
|
|
30322
|
-
if (
|
|
30323
|
-
const
|
|
30324
|
-
|
|
30325
|
-
|
|
30326
|
-
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");
|
|
30327
30449
|
checks.push({
|
|
30328
|
-
ok
|
|
30329
|
-
label: `runner
|
|
30330
|
-
detail:
|
|
30331
|
-
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}).`
|
|
30332
30454
|
});
|
|
30333
30455
|
}
|
|
30334
30456
|
}
|
|
@@ -30553,7 +30675,7 @@ function rulesetCoversReleaseBranches(payload, track) {
|
|
|
30553
30675
|
function releaseBranchRefsCovered(conditions, track) {
|
|
30554
30676
|
const include = conditions?.ref_name?.include;
|
|
30555
30677
|
if (!Array.isArray(include)) return [];
|
|
30556
|
-
const releaseRefs = track
|
|
30678
|
+
const releaseRefs = branchesForTrack(track).filter((branch) => branch !== "development").map((branch) => `refs/heads/${branch}`);
|
|
30557
30679
|
return releaseRefs.filter((ref) => include.some((entry) => typeof entry === "string" && refPatternCovers(entry, ref)));
|
|
30558
30680
|
}
|
|
30559
30681
|
function unreachableReleaseContexts(input) {
|
|
@@ -30782,6 +30904,18 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
30782
30904
|
const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
|
|
30783
30905
|
const deployableGated = repoClass === "deployable" && !explicitNoCi;
|
|
30784
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;
|
|
30785
30919
|
const baseBranch = repoClass === "content" ? "main" : "development";
|
|
30786
30920
|
checks.push({
|
|
30787
30921
|
ok: info.allow_auto_merge === true,
|
|
@@ -30984,14 +31118,29 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
30984
31118
|
const releaseBranches = releaseBranchRefsCovered(productRuleset.conditions, resolveReleaseTrack(meta, void 0, repo));
|
|
30985
31119
|
if (releaseBranches.length > 0) {
|
|
30986
31120
|
const emittedByBranch = {};
|
|
31121
|
+
const absent = [];
|
|
31122
|
+
const unreadable = [];
|
|
30987
31123
|
for (const ref of releaseBranches) {
|
|
30988
|
-
|
|
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
|
+
}
|
|
30989
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("; ");
|
|
30990
31138
|
const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
|
|
30991
31139
|
checks.push({
|
|
30992
31140
|
ok: unreachable.length === 0,
|
|
30993
31141
|
label: RELEASE_BRANCH_REACHABLE_LABEL,
|
|
30994
|
-
|
|
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}` : ""),
|
|
30995
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`
|
|
30996
31145
|
});
|
|
30997
31146
|
}
|
|
@@ -31047,7 +31196,7 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
31047
31196
|
const body = await fetchFileContent(deps, repo, baseBranch, path2);
|
|
31048
31197
|
if (body == null) continue;
|
|
31049
31198
|
if (isGateWorkflowPath(path2)) gateBodies.push({ path: path2, body });
|
|
31050
|
-
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)) {
|
|
31051
31200
|
if (NO_CANCEL_WAIVER.test(body)) waived.push(path2);
|
|
31052
31201
|
else missing.push(path2);
|
|
31053
31202
|
}
|
|
@@ -31106,34 +31255,45 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
31106
31255
|
if (failure) {
|
|
31107
31256
|
return { repo, class: repoClass, mergePolicy: policy, ok: false, checks: [], explicitNoCi, unauditedReason: failure };
|
|
31108
31257
|
}
|
|
31109
|
-
const ok = checks.every((c) => c.ok);
|
|
31110
|
-
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
|
+
};
|
|
31111
31268
|
}
|
|
31112
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).`;
|
|
31113
31270
|
async function auditOrgCi(deps, repoFilter) {
|
|
31114
31271
|
if (repoFilter) {
|
|
31115
31272
|
const report = await auditRepoCi(repoFilter, deps);
|
|
31116
|
-
return { ok: report.ok, scope: "single-repo", repos: [report] };
|
|
31273
|
+
return { ok: report.ok, scope: "single-repo", repos: [report], excludedCount: report.excludedReason ? 1 : 0 };
|
|
31117
31274
|
}
|
|
31118
31275
|
const projects = await deps.listProjects();
|
|
31119
31276
|
if (!projects || projects.length === 0) {
|
|
31120
|
-
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 };
|
|
31121
31278
|
}
|
|
31122
31279
|
const targets = collectRegistryRepos(projects);
|
|
31123
31280
|
const repos = [];
|
|
31124
31281
|
for (const repo of targets) {
|
|
31125
31282
|
repos.push(await auditRepoCi(repo, deps));
|
|
31126
31283
|
}
|
|
31127
|
-
|
|
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 };
|
|
31128
31286
|
}
|
|
31129
31287
|
function renderCiAuditScopeLine(report) {
|
|
31130
31288
|
if (report.scope === "registry-unreadable") {
|
|
31131
31289
|
return `scope: REGISTRY UNREADABLE \u2014 repos audited: 0 \u2014 ${report.scopeDetail ?? REGISTRY_UNREADABLE_DETAIL}`;
|
|
31132
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}` : ""}`;
|
|
31133
31293
|
if (report.scope === "single-repo") {
|
|
31134
|
-
return `scope: single-repo (explicit --repo) \u2014
|
|
31294
|
+
return `scope: single-repo (explicit --repo) \u2014 ${count} \u2014 this is NOT a fleet verdict`;
|
|
31135
31295
|
}
|
|
31136
|
-
return `scope: fleet (registry roster) \u2014
|
|
31296
|
+
return `scope: fleet (registry roster) \u2014 ${count}`;
|
|
31137
31297
|
}
|
|
31138
31298
|
function renderCiAuditMarkdown(report) {
|
|
31139
31299
|
const lines2 = [
|
|
@@ -31141,16 +31301,16 @@ function renderCiAuditMarkdown(report) {
|
|
|
31141
31301
|
"",
|
|
31142
31302
|
renderCiAuditScopeLine(report),
|
|
31143
31303
|
"",
|
|
31144
|
-
`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)`,
|
|
31145
31305
|
"",
|
|
31146
31306
|
"| Repo | Class | Policy | OK | Top gap |",
|
|
31147
31307
|
"|------|-------|--------|----|---------|"
|
|
31148
31308
|
];
|
|
31149
31309
|
for (const r of report.repos) {
|
|
31150
|
-
const gap = r.unauditedReason ? `CANNOT AUDIT \u2014 ${r.unauditedReason}` : r.checks.find((c) => !c.ok)?.label ?? "\u2014";
|
|
31151
|
-
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} |`);
|
|
31152
31312
|
}
|
|
31153
|
-
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 ?? ""}`));
|
|
31154
31314
|
if (advisories.length) lines2.push("", ...advisories);
|
|
31155
31315
|
return lines2.join("\n");
|
|
31156
31316
|
}
|
|
@@ -31160,12 +31320,15 @@ function renderCiAuditText(report) {
|
|
|
31160
31320
|
renderCiAuditScopeLine(report)
|
|
31161
31321
|
];
|
|
31162
31322
|
for (const r of report.repos) {
|
|
31163
|
-
const verdict = r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
|
|
31323
|
+
const verdict = r.excludedReason ? "EXCLUDED" : r.unauditedReason ? "CANNOT AUDIT" : r.ok ? "OK" : "GAP";
|
|
31164
31324
|
lines2.push(`
|
|
31165
|
-
${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
|
+
}
|
|
31166
31330
|
if (r.unauditedReason) {
|
|
31167
31331
|
lines2.push(` UNAUDITED \u2014 ${r.unauditedReason}`);
|
|
31168
|
-
continue;
|
|
31169
31332
|
}
|
|
31170
31333
|
for (const c of r.checks) {
|
|
31171
31334
|
lines2.push(` ${!c.ok ? "FAIL" : c.severity === "warn" ? "WARN" : c.severity === "info" ? "INFO" : "OK"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
@@ -31184,6 +31347,10 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
|
|
|
31184
31347
|
errors.push(unauditedReconcileError(report.unauditedReason));
|
|
31185
31348
|
return { repo, applied, skipped, errors };
|
|
31186
31349
|
}
|
|
31350
|
+
if (report.excludedReason) {
|
|
31351
|
+
skipped.push(`excluded \u2014 ${report.excludedReason}; no reconcile change made`);
|
|
31352
|
+
return { repo, applied, skipped, errors };
|
|
31353
|
+
}
|
|
31187
31354
|
const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
|
|
31188
31355
|
const needsPatch = mergeChecks.some((c) => !c.ok);
|
|
31189
31356
|
if (!needsPatch) {
|
|
@@ -31487,6 +31654,9 @@ async function applyCiReconcileRepo(repo, deps, options = {}) {
|
|
|
31487
31654
|
return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
|
|
31488
31655
|
}
|
|
31489
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
|
+
}
|
|
31490
31660
|
if (report.unauditedReason) {
|
|
31491
31661
|
return { repo, applied: [], skipped: [], errors: [unauditedReconcileError(report.unauditedReason)] };
|
|
31492
31662
|
}
|
|
@@ -45899,7 +46069,7 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
45899
46069
|
return {
|
|
45900
46070
|
ok: false,
|
|
45901
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"})`,
|
|
45902
|
-
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"
|
|
45903
46073
|
};
|
|
45904
46074
|
}
|
|
45905
46075
|
|
|
@@ -45989,6 +46159,10 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
45989
46159
|
if (state?.toLowerCase() === "closed") continue;
|
|
45990
46160
|
const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin, deps.appClient);
|
|
45991
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
|
+
}
|
|
45992
46166
|
const ref = `${repo}#${number}`;
|
|
45993
46167
|
const commentsPath = `repos/${repo}/issues/${number}/comments`;
|
|
45994
46168
|
const now = deps.now ?? Date.now;
|
|
@@ -48511,9 +48685,14 @@ ${list}`);
|
|
|
48511
48685
|
if (docsCheck && !docsCheck.ok) {
|
|
48512
48686
|
return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
|
|
48513
48687
|
}
|
|
48514
|
-
const claimRefusal = await prCreateClaimRefusal(body, o.repo);
|
|
48515
|
-
if (claimRefusal) return fail(claimRefusal);
|
|
48516
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);
|
|
48517
48696
|
const mergedHeadWarning = await prCreateMergedHeadWarning({ head: createHead || void 0, repo: o.repo });
|
|
48518
48697
|
if (mergedHeadWarning) process.stderr.write(`${mergedHeadWarning}
|
|
48519
48698
|
`);
|
|
@@ -54660,9 +54839,13 @@ function parseBrokerAssignee(raw) {
|
|
|
54660
54839
|
var SINGLE_SHOT_TIMEOUT_MS = 6e4;
|
|
54661
54840
|
var BROKER_MAX_BUFFER = 64 * 1024 * 1024;
|
|
54662
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
|
+
}
|
|
54663
54846
|
function buildPrQueueSpec(number, o) {
|
|
54664
54847
|
const repo = parseBrokerRepo(o.repo, "--repo");
|
|
54665
|
-
if (!repo) throw new Error("
|
|
54848
|
+
if (!repo) throw new Error(missingBrokerPrRepoMessage("queue", number));
|
|
54666
54849
|
const [owner, name] = repo.split("/");
|
|
54667
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 };
|
|
54668
54851
|
}
|
|
@@ -54818,7 +55001,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
|
|
|
54818
55001
|
return 0;
|
|
54819
55002
|
} catch (e) {
|
|
54820
55003
|
const err = e;
|
|
54821
|
-
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));
|
|
54822
55005
|
const code = ghExitCodeOf(e);
|
|
54823
55006
|
if (code !== null) {
|
|
54824
55007
|
if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
|
|
@@ -54944,13 +55127,16 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
|
|
|
54944
55127
|
await dispatchGh(spec, "read run watch");
|
|
54945
55128
|
});
|
|
54946
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)");
|
|
54947
|
-
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"));
|
|
54948
55132
|
await dispatchGh(buildPrListSpec(o), "read pr list");
|
|
54949
55133
|
});
|
|
54950
|
-
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));
|
|
54951
55136
|
await dispatchGh(buildPrDiffSpec(number, o), "read pr diff");
|
|
54952
55137
|
});
|
|
54953
|
-
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));
|
|
54954
55140
|
const spec = buildPrQueueSpec(number, o);
|
|
54955
55141
|
requireBrokerApp("read pr queue");
|
|
54956
55142
|
try {
|
|
@@ -55083,9 +55269,13 @@ var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
|
|
|
55083
55269
|
var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
|
|
55084
55270
|
var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
|
|
55085
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.";
|
|
55086
55273
|
function parseApiGetPath(rawPath) {
|
|
55087
55274
|
const path2 = rawPath.trim();
|
|
55088
|
-
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
|
+
};
|
|
55089
55279
|
if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
|
|
55090
55280
|
const q = path2.indexOf("?");
|
|
55091
55281
|
const base = q === -1 ? path2 : path2.slice(0, q);
|
|
@@ -55106,7 +55296,7 @@ function parseApiGetPath(rawPath) {
|
|
|
55106
55296
|
if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
|
|
55107
55297
|
return { kind: "contents", path: path2, paginate: false };
|
|
55108
55298
|
}
|
|
55109
|
-
throw
|
|
55299
|
+
throw refuse();
|
|
55110
55300
|
}
|
|
55111
55301
|
var ZIP_TIMEOUT_MS = 12e4;
|
|
55112
55302
|
var ZIP_MAX_BUFFER = 512 * 1024 * 1024;
|
|
@@ -58009,6 +58199,7 @@ var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, yo
|
|
|
58009
58199
|
var lastParseErrorKind = "other";
|
|
58010
58200
|
var lastUnknownCommand;
|
|
58011
58201
|
var lastParseErrorConflict = false;
|
|
58202
|
+
var lastParseHintOwnsGuess = false;
|
|
58012
58203
|
function parseInvalidChoiceError(plain) {
|
|
58013
58204
|
const m = /option '([^']+)' argument '([^']*)' is invalid\. Allowed choices are ([^.]+)\./.exec(plain);
|
|
58014
58205
|
if (!m) return void 0;
|
|
@@ -58077,6 +58268,12 @@ function resolveParseHint() {
|
|
|
58077
58268
|
const parentPath = parent ? canonicalPathFor(commandPath3(parent)) ?? commandPath3(parent) : void 0;
|
|
58078
58269
|
const guide = unknownCommandDomainGuide(parentPath, lastUnknownCommand);
|
|
58079
58270
|
if (guide) return `(${guide.hint}. ${DISCOVERY_HINT})`;
|
|
58271
|
+
const claimCmd = resolveCommandFromArgv(program2, ["board", "claim"]);
|
|
58272
|
+
const workboard = claimCmd ? workboardClaimRecovery(lastUnknownCommand, process.argv.slice(2), allCommandPaths(), commandOwnLongFlags(claimCmd)) : void 0;
|
|
58273
|
+
if (workboard) {
|
|
58274
|
+
lastParseHintOwnsGuess = true;
|
|
58275
|
+
return `(${workboard.hint} ${DISCOVERY_HINT})`;
|
|
58276
|
+
}
|
|
58080
58277
|
const candidates = unknownCommandCandidates(parent, allCommandPaths());
|
|
58081
58278
|
const tail = unknownCommandTail(process.argv.slice(2), lastUnknownCommand);
|
|
58082
58279
|
const path3 = lastUnknownCommand ? suggestCommandPath(lastUnknownCommand, candidates, tail) : void 0;
|
|
@@ -58135,7 +58332,7 @@ function envelopeAwareWriteErr(str2) {
|
|
|
58135
58332
|
return;
|
|
58136
58333
|
}
|
|
58137
58334
|
const resolvedHint = resolveParseHint();
|
|
58138
|
-
if (lastParseErrorKind === "unknown-command" && (resolvedHint === STALE_HINT || resolvedHint === `(${DISCOVERY_HINT})`)) {
|
|
58335
|
+
if (lastParseErrorKind === "unknown-command" && (lastParseHintOwnsGuess || resolvedHint === STALE_HINT || resolvedHint === `(${DISCOVERY_HINT})`)) {
|
|
58139
58336
|
pendingHumanParseError = pendingHumanParseError?.replace(/\n\(Did you mean [^)]+\?\)/g, "");
|
|
58140
58337
|
}
|
|
58141
58338
|
const hint = str2.replace(PARSE_HINT_SENTINEL, resolvedHint);
|
|
@@ -59096,7 +59293,7 @@ ci.command("reconcile").description("audit + optionally apply merge settings and
|
|
|
59096
59293
|
}
|
|
59097
59294
|
const deps = ciAuditDeps2();
|
|
59098
59295
|
const audit = await auditOrgCi(deps, o.repo);
|
|
59099
|
-
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))) : [];
|
|
59100
59297
|
const payload = { audit, apply: applyResults };
|
|
59101
59298
|
if (o.json) console.log(JSON.stringify(payload, null, 2));
|
|
59102
59299
|
else {
|
|
@@ -59427,7 +59624,9 @@ function printHouseRootHelp(house) {
|
|
|
59427
59624
|
consoleIo.log(lines2.join("\n"));
|
|
59428
59625
|
}
|
|
59429
59626
|
resolveVerbAliasShim(process.argv);
|
|
59627
|
+
normalizeWorkboardClaimArgv(process.argv);
|
|
59430
59628
|
resolveHouseShim(process.argv);
|
|
59629
|
+
normalizeBoardClaimIssueArgv(process.argv);
|
|
59431
59630
|
program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
|
|
59432
59631
|
// Annotate the CommonJS export names for ESM import in node:
|
|
59433
59632
|
0 && (module.exports = {
|
|
@@ -59438,11 +59637,13 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
|
|
|
59438
59637
|
commandOwnLongFlags,
|
|
59439
59638
|
envHealLockPath,
|
|
59440
59639
|
isOrgRegisteredRepo,
|
|
59640
|
+
normalizeBoardClaimIssueArgv,
|
|
59441
59641
|
parseInvalidChoiceError,
|
|
59442
59642
|
positionalTargetForm,
|
|
59443
59643
|
registryClientDeps,
|
|
59444
59644
|
repoSlug,
|
|
59445
59645
|
suggestCommandPath,
|
|
59446
59646
|
unknownCommandCandidates,
|
|
59447
|
-
unknownCommandTail
|
|
59647
|
+
unknownCommandTail,
|
|
59648
|
+
workboardClaimRecovery
|
|
59448
59649
|
});
|
package/package.json
CHANGED