@mutmutco/codex-plugin 4.6.8 → 4.6.10
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/.codex-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +328 -94
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/board/SKILL.md +5 -2
- package/skills/release/train-common.md +1 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -1236,7 +1236,7 @@ var init_house_map = __esm({
|
|
|
1236
1236
|
api: "devops",
|
|
1237
1237
|
// allowlisted GitHub REST GET reads for automation (pull files, compare, contents)
|
|
1238
1238
|
run: "devops",
|
|
1239
|
-
// #9479: one named workflow run —
|
|
1239
|
+
// #9479: one named workflow run — the bounded `run cancel <id>` and `run rerun <id>` writes
|
|
1240
1240
|
ci: "devops",
|
|
1241
1241
|
// the CI/gate audit
|
|
1242
1242
|
jev: "devops",
|
|
@@ -15347,7 +15347,12 @@ var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
|
|
|
15347
15347
|
["ci cancel-orphans", { metadata: "read", actions: "write" }],
|
|
15348
15348
|
// #9479: one named run cancelled under the same actions:write profile as the orphan sweep.
|
|
15349
15349
|
["run cancel", { metadata: "read", actions: "write" }],
|
|
15350
|
+
// #9587: one named run rerun (all or failed jobs) — same actions:write profile, nothing broader.
|
|
15351
|
+
["run rerun", { metadata: "read", actions: "write" }],
|
|
15350
15352
|
["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
|
|
15353
|
+
// #9614: closing an issue is an issue-state write — it rides the org App (mmi-github-app), so the close
|
|
15354
|
+
// is attributed to the App instead of the personal account. Nothing broader than issues.
|
|
15355
|
+
["issue close", { metadata: "read", issues: "write" }],
|
|
15351
15356
|
// #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
|
|
15352
15357
|
["issue discover-related", { metadata: "read", issues: "write" }]
|
|
15353
15358
|
]);
|
|
@@ -15736,6 +15741,13 @@ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none")
|
|
|
15736
15741
|
].join(" ")
|
|
15737
15742
|
};
|
|
15738
15743
|
}
|
|
15744
|
+
function pendingEvidenceItems(evidence) {
|
|
15745
|
+
const items = evidence.checks.filter((check) => check.state === "pending").map((check) => `${check.name} (${check.status ?? "unknown"}${check.conclusion ? `/${check.conclusion}` : ""}, startedAt=${check.startedAt ?? "null"}, appId=${check.appId ?? "null"})`);
|
|
15746
|
+
for (const run of evidence.workflows) {
|
|
15747
|
+
if (run.status !== "completed") items.push(`${run.name}#${run.id} (${run.status})`);
|
|
15748
|
+
}
|
|
15749
|
+
return items;
|
|
15750
|
+
}
|
|
15739
15751
|
async function waitForPrChecks(deps) {
|
|
15740
15752
|
let { policy, reason } = await deps.resolvePolicy();
|
|
15741
15753
|
const baseBranch = deps.baseBranch ?? "development";
|
|
@@ -15766,10 +15778,10 @@ async function waitForPrChecks(deps) {
|
|
|
15766
15778
|
const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
|
|
15767
15779
|
const started = now();
|
|
15768
15780
|
const deadline = started + timeoutMs;
|
|
15769
|
-
const report = (state) => {
|
|
15781
|
+
const report = (state, detail) => {
|
|
15770
15782
|
try {
|
|
15771
15783
|
const elapsedMs = now() - started;
|
|
15772
|
-
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
|
|
15784
|
+
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()), ...detail === void 0 ? {} : { detail } });
|
|
15773
15785
|
} catch {
|
|
15774
15786
|
}
|
|
15775
15787
|
};
|
|
@@ -15779,6 +15791,10 @@ async function waitForPrChecks(deps) {
|
|
|
15779
15791
|
let zeroRunsStreak = 0;
|
|
15780
15792
|
let evidenceHeadSha;
|
|
15781
15793
|
let lastEvidence;
|
|
15794
|
+
const pendingWaitDetail = (evidence) => {
|
|
15795
|
+
const items = pendingEvidenceItems(evidence);
|
|
15796
|
+
return items.length ? `pending (${items.join(", ")})` : void 0;
|
|
15797
|
+
};
|
|
15782
15798
|
report("starting");
|
|
15783
15799
|
while (now() < deadline) {
|
|
15784
15800
|
if (deps.pollRateLimit) {
|
|
@@ -15843,7 +15859,7 @@ async function waitForPrChecks(deps) {
|
|
|
15843
15859
|
await deps.sleep(waitMs);
|
|
15844
15860
|
continue;
|
|
15845
15861
|
}
|
|
15846
|
-
report(state);
|
|
15862
|
+
report(state, state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0);
|
|
15847
15863
|
if (state !== "success") successStreak = 0;
|
|
15848
15864
|
if (state !== "failure") failureStreak = 0;
|
|
15849
15865
|
if (state !== "no-checks-reported") zeroRunsStreak = 0;
|
|
@@ -15919,10 +15935,18 @@ async function waitForPrChecks(deps) {
|
|
|
15919
15935
|
await deps.sleep(pollIntervalMs(now() - started));
|
|
15920
15936
|
continue;
|
|
15921
15937
|
}
|
|
15922
|
-
|
|
15938
|
+
const pendingDetail = state === "pending" && lastEvidence ? pendingWaitDetail(lastEvidence) : void 0;
|
|
15939
|
+
lastDetail = pendingDetail ?? state;
|
|
15923
15940
|
await deps.sleep(pollIntervalMs(now() - started));
|
|
15924
15941
|
}
|
|
15925
|
-
return {
|
|
15942
|
+
return {
|
|
15943
|
+
policy,
|
|
15944
|
+
status: "timeout",
|
|
15945
|
+
reason,
|
|
15946
|
+
detail: lastDetail,
|
|
15947
|
+
waitedMs: now() - started,
|
|
15948
|
+
...lastEvidence ? { headSha: lastEvidence.headSha, checks: lastEvidence.checks, workflowRuns: lastEvidence.workflows } : {}
|
|
15949
|
+
};
|
|
15926
15950
|
}
|
|
15927
15951
|
|
|
15928
15952
|
// src/release-auxiliary-workflows.ts
|
|
@@ -16392,16 +16416,23 @@ async function fetchHeadCheckRuns(headSha, repo, gh2) {
|
|
|
16392
16416
|
const runsOut = await gh2(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id, details_url, started_at, completed_at}"]);
|
|
16393
16417
|
return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
|
|
16394
16418
|
}
|
|
16395
|
-
async function
|
|
16419
|
+
async function fetchHeadCheckEntriesWithRuns(headSha, repo, gh2) {
|
|
16396
16420
|
const [runs2, statusesOut] = await Promise.all([
|
|
16397
16421
|
fetchHeadCheckRuns(headSha, repo, gh2),
|
|
16398
16422
|
gh2(["--paginate", `repos/${repo}/commits/${headSha}/status?per_page=100`, "--jq", ".statuses[] | {context, state, target_url, description}"])
|
|
16399
16423
|
]);
|
|
16400
16424
|
const statuses = parseNdjsonLines(statusesOut);
|
|
16401
|
-
return
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16425
|
+
return {
|
|
16426
|
+
runs: runs2,
|
|
16427
|
+
statuses,
|
|
16428
|
+
entries: [
|
|
16429
|
+
...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).map((run) => ({ name: run.name ?? `check-run ${run.id ?? "unknown"}`, bucket: classifyCheckRun(run) })),
|
|
16430
|
+
...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", bucket: classifyCommitStatus(s.state) }))
|
|
16431
|
+
]
|
|
16432
|
+
};
|
|
16433
|
+
}
|
|
16434
|
+
async function fetchHeadCheckEntries(headSha, repo, gh2) {
|
|
16435
|
+
return (await fetchHeadCheckEntriesWithRuns(headSha, repo, gh2)).entries;
|
|
16405
16436
|
}
|
|
16406
16437
|
async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredContexts) {
|
|
16407
16438
|
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
|
|
@@ -16409,38 +16440,68 @@ async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredCont
|
|
|
16409
16440
|
const { relevant } = partitionByRequired(entries, requiredContexts);
|
|
16410
16441
|
return derivePollState(relevant);
|
|
16411
16442
|
}
|
|
16412
|
-
async function
|
|
16413
|
-
const
|
|
16414
|
-
|
|
16415
|
-
|
|
16416
|
-
gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(snapshot.headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
|
|
16443
|
+
async function pollRestHeadChecksWithWorkflowEvidence(headSha, repo, gh2) {
|
|
16444
|
+
const [{ runs: runs2, statuses }, rawRuns] = await Promise.all([
|
|
16445
|
+
fetchHeadCheckEntriesWithRuns(headSha, repo, gh2),
|
|
16446
|
+
gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
|
|
16417
16447
|
]);
|
|
16418
|
-
const checks = entries.filter((entry) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(entry.name)).map((entry) => ({ name: entry.name, state: entry.bucket }));
|
|
16419
16448
|
const workflows = parseNdjsonLines(rawRuns).flatMap((run) => {
|
|
16420
|
-
if (run.head_sha !==
|
|
16449
|
+
if (run.head_sha !== headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
|
|
16421
16450
|
if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name)) return [];
|
|
16422
16451
|
return [{
|
|
16423
16452
|
id: run.id,
|
|
16424
16453
|
name: run.name,
|
|
16425
|
-
headSha
|
|
16454
|
+
headSha,
|
|
16426
16455
|
status: run.status,
|
|
16427
16456
|
conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
|
|
16428
16457
|
...typeof run.html_url === "string" ? { url: run.html_url } : {}
|
|
16429
16458
|
}];
|
|
16430
16459
|
});
|
|
16431
|
-
const
|
|
16460
|
+
const workflowById = new Map(workflows.map((run) => [run.id, run]));
|
|
16461
|
+
const isStalePendingCheckRun = (run) => {
|
|
16462
|
+
if (classifyCheckRun(run) !== "pending") return false;
|
|
16463
|
+
const match = /\/actions\/runs\/(\d+)(?:\/|$)/.exec(run.details_url ?? "");
|
|
16464
|
+
const owning = match ? workflowById.get(Number(match[1])) : void 0;
|
|
16465
|
+
if (!owning) return false;
|
|
16466
|
+
if (owning.status === "completed") return true;
|
|
16467
|
+
return workflows.some((newer) => newer.id > owning.id && newer.name === owning.name && newer.status === "completed");
|
|
16468
|
+
};
|
|
16469
|
+
const checks = [
|
|
16470
|
+
...runs2.filter((run) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name ?? "")).filter((run) => !isStalePendingCheckRun(run)).map((run) => ({
|
|
16471
|
+
name: run.name ?? `check-run ${run.id ?? "unknown"}`,
|
|
16472
|
+
state: classifyCheckRun(run),
|
|
16473
|
+
...run.status !== void 0 ? { status: run.status } : {},
|
|
16474
|
+
conclusion: run.conclusion ?? null,
|
|
16475
|
+
startedAt: run.started_at ?? null,
|
|
16476
|
+
appId: run.app_id ?? null
|
|
16477
|
+
})),
|
|
16478
|
+
...statuses.filter((s) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(s.context ?? "")).map((s) => ({ name: s.context ?? "unknown-status", state: classifyCommitStatus(s.state) }))
|
|
16479
|
+
];
|
|
16480
|
+
const newestRunByName = /* @__PURE__ */ new Map();
|
|
16481
|
+
for (const run of workflows) {
|
|
16482
|
+
const current = newestRunByName.get(run.name);
|
|
16483
|
+
if (!current || run.id > current.id) newestRunByName.set(run.name, run);
|
|
16484
|
+
}
|
|
16485
|
+
const workflowBuckets = [...newestRunByName.values()].map((run) => {
|
|
16432
16486
|
if (run.status !== "completed") return "pending";
|
|
16433
16487
|
return run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped" ? "pass" : "fail";
|
|
16434
16488
|
});
|
|
16435
16489
|
return {
|
|
16436
16490
|
state: derivePollState([...checks.map((check) => check.state), ...workflowBuckets]),
|
|
16437
|
-
headSha
|
|
16491
|
+
headSha,
|
|
16438
16492
|
checks,
|
|
16439
16493
|
workflows
|
|
16440
16494
|
};
|
|
16441
16495
|
}
|
|
16496
|
+
async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
|
|
16497
|
+
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
|
|
16498
|
+
return pollRestHeadChecksWithWorkflowEvidence(snapshot.headSha, repo, gh2);
|
|
16499
|
+
}
|
|
16500
|
+
async function pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2 = defaultGhApi) {
|
|
16501
|
+
return pollRestHeadChecksWithWorkflowEvidence(sha, repo, gh2);
|
|
16502
|
+
}
|
|
16442
16503
|
async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
|
|
16443
|
-
return
|
|
16504
|
+
return (await pollRestCommitChecksWithWorkflowEvidence(sha, repo, gh2)).state;
|
|
16444
16505
|
}
|
|
16445
16506
|
var GH_ZERO_TIME = "0001-01-01T00:00:00Z";
|
|
16446
16507
|
var GH_PR_CHECKS_JSON_FIELDS = ["bucket", "completedAt", "description", "link", "name", "startedAt", "state"];
|
|
@@ -21016,8 +21077,9 @@ function resolveSpawnTarget(command, args, platform2 = process.platform) {
|
|
|
21016
21077
|
if (platform2 !== "win32") return { cmd: command, args };
|
|
21017
21078
|
for (const t of [command, ...args]) {
|
|
21018
21079
|
if (/[\r\n]/.test(t)) {
|
|
21080
|
+
const preview = JSON.stringify(t.length > 80 ? `${t.slice(0, 77)}...` : t);
|
|
21019
21081
|
throw new Error(
|
|
21020
|
-
`cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${
|
|
21082
|
+
`cannot spawn safely on Windows: cmd.exe truncates at a newline, even quoted: ${preview} \u2014 write the multiline script to a git-ignored file and run \`mmi-cli vault secrets use <KEY> -- node .jerv/tmp/<script>.mjs\` (#6048)`
|
|
21021
21083
|
);
|
|
21022
21084
|
}
|
|
21023
21085
|
}
|
|
@@ -22317,8 +22379,22 @@ function renderBoardItem(item) {
|
|
|
22317
22379
|
}
|
|
22318
22380
|
return lines2.join("\n");
|
|
22319
22381
|
}
|
|
22382
|
+
function renderCachedBoardSource(report) {
|
|
22383
|
+
if (typeof report.stale?.ageMs === "number") {
|
|
22384
|
+
const ageSeconds = Math.floor(report.stale.ageMs / 1e3);
|
|
22385
|
+
return `source: cache \xB7 built ${report.builtAt ? `${report.builtAt} (${ageSeconds}s old)` : `${ageSeconds}s ago`} \xB7 stale`;
|
|
22386
|
+
}
|
|
22387
|
+
if (report.builtAt) {
|
|
22388
|
+
const builtMs = Date.parse(report.builtAt);
|
|
22389
|
+
if (Number.isFinite(builtMs)) {
|
|
22390
|
+
return `source: cache \xB7 built ${report.builtAt} (${Math.max(0, Math.floor((Date.now() - builtMs) / 1e3))}s old) \xB7 fresh`;
|
|
22391
|
+
}
|
|
22392
|
+
return `source: cache \xB7 built ${report.builtAt} \xB7 fresh`;
|
|
22393
|
+
}
|
|
22394
|
+
return "source: cache";
|
|
22395
|
+
}
|
|
22320
22396
|
function renderBoardReport(report) {
|
|
22321
|
-
const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ?
|
|
22397
|
+
const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? renderCachedBoardSource(report) : renderBoardSource()];
|
|
22322
22398
|
if (report.github?.rateLimit) {
|
|
22323
22399
|
const { limit, remaining, cost, resetAt } = report.github.rateLimit;
|
|
22324
22400
|
lines2.push(`github: app installation \xB7 GraphQL ${remaining}/${limit} remaining \xB7 cost ${cost} \xB7 resets ${resetAt}`);
|
|
@@ -23574,7 +23650,7 @@ var contestedBoardReadCommand = (repo) => `mmi-cli oracle board read --json${rep
|
|
|
23574
23650
|
function contestedClaimRecovery(ref) {
|
|
23575
23651
|
const repo = /^([^/\s#]+\/[^/\s#]+)#\d+$/.exec(ref)?.[1];
|
|
23576
23652
|
const boardRead = contestedBoardReadCommand(repo);
|
|
23577
|
-
return `${CONTESTED_CLAIM_RECOVERY}; recovery order: refresh the board read (${boardRead}), then choose an item from its claimable list, or coordinate a handoff with the holder before using --force
|
|
23653
|
+
return `${CONTESTED_CLAIM_RECOVERY}; recovery order: refresh the board read (${boardRead}), then choose an item from its claimable list, or coordinate a handoff with the holder before using --force`;
|
|
23578
23654
|
}
|
|
23579
23655
|
function laneContestMessage(ref, contest, verb) {
|
|
23580
23656
|
const { holder, why } = laneContestParts(contest);
|
|
@@ -23592,6 +23668,7 @@ function ghError(e) {
|
|
|
23592
23668
|
init_github_client();
|
|
23593
23669
|
init_github_quota();
|
|
23594
23670
|
var CLAIM_CONCURRENCY = 5;
|
|
23671
|
+
var VERIFIED_FREE_CANDIDATE_LIMIT = 5;
|
|
23595
23672
|
function withGatedWriteChainHint(message2) {
|
|
23596
23673
|
return `${message2}
|
|
23597
23674
|
note: run refused writes separately or join dependent commands with '&&'; a ';' chain continues after failure`;
|
|
@@ -23852,9 +23929,16 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23852
23929
|
const { holder: holder2, why } = laneContestParts(contest);
|
|
23853
23930
|
throw new Error(isUnknownHold(contest) ? `${item.ref} cannot be verified as free: ${holder2} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 ${CONTESTED_CLAIM_RECOVERY}` : `${item.ref} is already claimed by ${holder2} with live evidence of active work (${why}); ${CONTESTED_CLAIM_RECOVERY}`);
|
|
23854
23931
|
}
|
|
23855
|
-
const
|
|
23856
|
-
const
|
|
23857
|
-
const
|
|
23932
|
+
const candidates = [...report.primary.claimable, ...report.secondary.claimable].filter((candidate) => candidate.contentType === "Issue" && candidate.state === "OPEN" && !candidate.claim?.currentlyClaimed && !ctx.selectedRefs.has(candidate.ref.toLowerCase())).slice(0, VERIFIED_FREE_CANDIDATE_LIMIT);
|
|
23933
|
+
const verifiedFreeRefs = [];
|
|
23934
|
+
for (const candidate of candidates) {
|
|
23935
|
+
const candidateContest = await checkLaneContest(client, candidate, ctx.session, report.viewer, ctx.appClient, ctx.landing).catch(() => void 0);
|
|
23936
|
+
if (candidateContest && !candidateContest.contested && candidateContest.unverifiable.length === 0) {
|
|
23937
|
+
verifiedFreeRefs.push(candidate.ref);
|
|
23938
|
+
}
|
|
23939
|
+
}
|
|
23940
|
+
const alternatives = verifiedFreeRefs.length ? `Next verified free item: \`mmi-cli oracle board claim ${verifiedFreeRefs[0]}\`${verifiedFreeRefs.length > 1 ? ` (also free: ${verifiedFreeRefs.slice(1).join(", ")})` : ""}` : `Verified free items in this read: none \u2014 refresh \`mmi-cli oracle board read --json --repo ${report.repo}\` or coordinate a handoff`;
|
|
23941
|
+
const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}`;
|
|
23858
23942
|
throw new Error(withGatedWriteChainHint(refusal));
|
|
23859
23943
|
}
|
|
23860
23944
|
previousHolder = displaced();
|
|
@@ -25039,10 +25123,15 @@ function checkRunbookCoverage(docs, base, git3) {
|
|
|
25039
25123
|
const heading = headings[i];
|
|
25040
25124
|
const end = headings.slice(i + 1).find((h) => h.level <= heading.level)?.start ?? lines2.length + 1;
|
|
25041
25125
|
const markerEnd = headings[i + 1]?.start ?? end;
|
|
25042
|
-
const
|
|
25126
|
+
const markers = lines2.slice(heading.start, markerEnd - 1).flatMap((text, j) => {
|
|
25127
|
+
const found = /<!--\s*runbook-covers:\s*(.*?)\s*-->/.exec(text)?.[1].split(",").map((s) => s.trim());
|
|
25128
|
+
return found ? [{ line: heading.start + 1 + j, globs: found }] : [];
|
|
25129
|
+
});
|
|
25130
|
+
const globs = markers.flatMap((m) => m.globs);
|
|
25043
25131
|
const affected = changed.filter((path2) => globs.some((glob) => matches(glob, path2)) && !changesOnlyUses(path2));
|
|
25044
25132
|
if (affected.length && !ranges.some((r) => r.count > 0 && r.start < end && r.start + r.count > heading.start)) {
|
|
25045
|
-
|
|
25133
|
+
const markerLine = markers.find((m) => m.globs.some((glob) => affected.some((path2) => matches(glob, path2)))).line;
|
|
25134
|
+
findings.push({ kind: "runbook-section-stale", doc, line: markerLine, detail: `${heading.title} (lines ${heading.start}\u2013${end - 1}, covers marker at line ${markerLine}): changed ${affected.join(", ")}; update this section's text (lines ${heading.start}\u2013${end - 1}) or runbook-covers list, or add a \`Runbook-Unaffected: <reason>\` trailer to a commit message in this PR`, section: { heading: heading.title, level: heading.level, startLine: heading.start, endLine: end - 1, covers: globs }, sources: affected });
|
|
25046
25135
|
}
|
|
25047
25136
|
}
|
|
25048
25137
|
}
|
|
@@ -27506,10 +27595,10 @@ var rollout_plan_default = {
|
|
|
27506
27595
|
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)."
|
|
27507
27596
|
},
|
|
27508
27597
|
baseline: {
|
|
27509
|
-
version: "4.6.
|
|
27510
|
-
tag: "v4.6.
|
|
27511
|
-
commit: "
|
|
27512
|
-
npm: "@mutmutco/cli@4.6.
|
|
27598
|
+
version: "4.6.10",
|
|
27599
|
+
tag: "v4.6.10",
|
|
27600
|
+
commit: "a4bbe43232e9",
|
|
27601
|
+
npm: "@mutmutco/cli@4.6.10"
|
|
27513
27602
|
},
|
|
27514
27603
|
exitCriterion: "fleet-n-of-n",
|
|
27515
27604
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27526,14 +27615,14 @@ var rollout_plan_default = {
|
|
|
27526
27615
|
repo: "mutmutco/mmi-hub",
|
|
27527
27616
|
role: "canary",
|
|
27528
27617
|
schedule: "train",
|
|
27529
|
-
v3Target: "v4.6.
|
|
27618
|
+
v3Target: "v4.6.10"
|
|
27530
27619
|
}
|
|
27531
27620
|
],
|
|
27532
27621
|
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.",
|
|
27533
27622
|
rollback: {
|
|
27534
27623
|
independent: true,
|
|
27535
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27536
|
-
v3Target: "v4.6.
|
|
27624
|
+
mechanism: "npm dist-tag latest -> 4.6.10 and redeploy the Hub Lambda from tag v4.6.10 (a4bbe43232e9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
27625
|
+
v3Target: "v4.6.10 (@mutmutco/cli@4.6.10, tag commit a4bbe43232e9 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
27537
27626
|
}
|
|
27538
27627
|
},
|
|
27539
27628
|
{
|
|
@@ -32106,8 +32195,11 @@ function classifyRunnerCost(selector) {
|
|
|
32106
32195
|
}
|
|
32107
32196
|
var STANDARD_HOSTED_ALLOWLIST = [
|
|
32108
32197
|
["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"],
|
|
32109
|
-
["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"]
|
|
32198
|
+
["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"],
|
|
32199
|
+
// #9571 (MM-Work#8894 chooser): owned mmi-heavy first, included ubuntu-latest only on a deep backlog.
|
|
32200
|
+
["mutmutco/MMI-Hub", ".github/workflows/gate.yml", "cli-part", "hosted-spill"]
|
|
32110
32201
|
];
|
|
32202
|
+
var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*'([^']+)'\s*\|\|\s*)?fromJSON\(needs\.[\w-]+\.outputs\.heavy_runs_on\s*\|\|\s*('[^']+')\)\s*\}\}$/;
|
|
32111
32203
|
function standardHostedLabelFor(repo, path2, jobId) {
|
|
32112
32204
|
if (repo === "mutmutco/Jerv-Hub") return ["ubuntu-latest", "windows-latest", "macos-14"];
|
|
32113
32205
|
return STANDARD_HOSTED_ALLOWLIST.find(([r, p, j]) => r === repo && p === path2 && j === jobId)?.[3];
|
|
@@ -32122,6 +32214,8 @@ function classifyJobRunnerCost(job, allowance2) {
|
|
|
32122
32214
|
if (approved(raw) || allowance2 === "windows-11-arm" && raw === allowance2) return "standard-hosted";
|
|
32123
32215
|
const fallback = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'([^']+)'\s*\|\|\s*(fromJSON\('[^']+'\))\s*\}\}$/.exec(raw);
|
|
32124
32216
|
if (fallback && approved(fallback[1]) && classifyRunnerCost("${{ " + fallback[2] + " }}") === "self-hosted") return "standard-hosted";
|
|
32217
|
+
const spill = allowance2 === "hosted-spill" ? HOSTED_SPILL_RE.exec(raw) : null;
|
|
32218
|
+
if (spill) return (!spill[1] || spill[1] === "ubuntu-latest") && classifyRunnerCost(`\${{ fromJSON(${spill[2]}) }}`) === "self-hosted" ? "standard-hosted" : "unknown";
|
|
32125
32219
|
const direct = classifyRunnerCost(raw);
|
|
32126
32220
|
const matrix = /^\$\{\{\s*matrix\.([\w-]+)(?:\.([\w-]+))?\s*\}\}$/.exec(raw);
|
|
32127
32221
|
if (!matrix || direct === "hosted") return direct;
|
|
@@ -38643,7 +38737,7 @@ function runsOnLabelSets(job) {
|
|
|
38643
38737
|
if (plainRecord(selector)) selector = selector.labels;
|
|
38644
38738
|
if (typeof selector !== "string" || !selector.includes("${{")) return [toSet(selector)].filter((s) => Boolean(s));
|
|
38645
38739
|
const sets = [];
|
|
38646
|
-
for (const m of selector.matchAll(/fromJSON\('([^']+)'\)/g)) {
|
|
38740
|
+
for (const m of selector.matchAll(/fromJSON\((?:needs\.[\w-]+\.outputs\.[\w-]+\s*\|\|\s*)?'([^']+)'\)/g)) {
|
|
38647
38741
|
try {
|
|
38648
38742
|
const parsed = toSet(JSON.parse(m[1]));
|
|
38649
38743
|
if (parsed) sets.push(parsed);
|
|
@@ -41250,7 +41344,18 @@ async function readGhPrStateWithRetry(fetchState, options) {
|
|
|
41250
41344
|
}
|
|
41251
41345
|
return { ok: false, error: lastError };
|
|
41252
41346
|
}
|
|
41347
|
+
function isNoFailedCheckQueueEjection(run) {
|
|
41348
|
+
return !run || run.conclusion === null || run.conclusion === "cancelled" || run.conclusion === "neutral" || run.conclusion === "skipped";
|
|
41349
|
+
}
|
|
41253
41350
|
async function runPrLand(prNumber, options, deps) {
|
|
41351
|
+
const first = await runPrLandOnce(prNumber, options, deps);
|
|
41352
|
+
if (options.enqueueOnly || options.expectHead || first.status !== "failed") return first;
|
|
41353
|
+
const queue = first.mergeQueue;
|
|
41354
|
+
const nothingFailed = queue?.state === "timeout" || queue?.state === "ejected" && isNoFailedCheckQueueEjection(queue.ejectedRun);
|
|
41355
|
+
if (!nothingFailed) return first;
|
|
41356
|
+
return { ...await runPrLandOnce(prNumber, options, deps), retriedAfterEjection: true };
|
|
41357
|
+
}
|
|
41358
|
+
async function runPrLandOnce(prNumber, options, deps) {
|
|
41254
41359
|
const resolved = await deps.resolveRepo(prNumber, options.repo);
|
|
41255
41360
|
const { repo } = resolved;
|
|
41256
41361
|
const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
|
|
@@ -41296,6 +41401,18 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
41296
41401
|
base.mergeStatus = "auto-merge-enqueued";
|
|
41297
41402
|
return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
|
|
41298
41403
|
}
|
|
41404
|
+
if (deps.preflightQueue) {
|
|
41405
|
+
const preflight2 = await deps.preflightQueue(prNumber, repo, pinned);
|
|
41406
|
+
if (preflight2 && preflight2.mergeStatus === "failed") {
|
|
41407
|
+
const rawError = preflight2.error ?? "queue preparation needed";
|
|
41408
|
+
return {
|
|
41409
|
+
...base,
|
|
41410
|
+
mergeStatus: "failed",
|
|
41411
|
+
...preflight2.preparation ? { preparation: preflight2.preparation } : {},
|
|
41412
|
+
error: !rawError.includes("read pr queue") ? `${rawError} \u2014 ${prQueueReadHint(prNumber, repo)}` : rawError
|
|
41413
|
+
};
|
|
41414
|
+
}
|
|
41415
|
+
}
|
|
41299
41416
|
}
|
|
41300
41417
|
const checksWaitError = (checksWait) => {
|
|
41301
41418
|
if (checksWait.status === "conflicting") {
|
|
@@ -41456,7 +41573,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
41456
41573
|
...base,
|
|
41457
41574
|
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
41458
41575
|
...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
|
|
41459
|
-
error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}; ${prQueueReadHint(pr, base.repo)}`
|
|
41576
|
+
error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}; ${prQueueReadHint(pr, base.repo)}${isNoFailedCheckQueueEjection(run) ? `; no check failed \u2014 re-run \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` once to re-enqueue, and if a land is refused again report the exact refusal verbatim (never fall back to a direct/admin merge or an \`MMI_ACTOR=personal\` landing workaround)` : ""}`
|
|
41460
41577
|
};
|
|
41461
41578
|
deps.queueProgress?.(result2);
|
|
41462
41579
|
return result2;
|
|
@@ -41466,7 +41583,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
41466
41583
|
const result = {
|
|
41467
41584
|
...base,
|
|
41468
41585
|
mergeQueue: { ...base.mergeQueue, state: "timeout" },
|
|
41469
|
-
error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 ${prQueueReadHint(pr, base.repo)} and rerun pr land to resume
|
|
41586
|
+
error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 ${prQueueReadHint(pr, base.repo)} and rerun \`mmi-cli devops pr land ${pr} --repo ${base.repo}\` once to resume/re-enqueue; if it still does not land, report the exact refusal verbatim (never bypass the ruleset or fall back to a direct/admin or \`MMI_ACTOR=personal\` merge)`
|
|
41470
41587
|
};
|
|
41471
41588
|
deps.queueProgress?.(result);
|
|
41472
41589
|
return result;
|
|
@@ -41486,7 +41603,7 @@ async function landMany(prs, landOne, cleanup, hooks) {
|
|
|
41486
41603
|
const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
|
|
41487
41604
|
return Promise.all(prs.map(async (pr) => {
|
|
41488
41605
|
let result = await safe(pr);
|
|
41489
|
-
if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
|
|
41606
|
+
if (prLandOutcome(result) === "ejected" && !result.pinnedHead && !result.retriedAfterEjection) {
|
|
41490
41607
|
const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
|
|
41491
41608
|
if (baseRed) {
|
|
41492
41609
|
result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
|
|
@@ -41658,10 +41775,16 @@ function trailerBlockBreak(body, line) {
|
|
|
41658
41775
|
const lines2 = String(body ?? "").split(/\r?\n/);
|
|
41659
41776
|
const at = lines2.findIndex((candidate) => candidate.trim() === line);
|
|
41660
41777
|
if (at === -1) return { after: "", before: "" };
|
|
41778
|
+
let sawBlank = false;
|
|
41661
41779
|
for (let i = at + 1; i < lines2.length; i += 1) {
|
|
41662
41780
|
const next = lines2[i];
|
|
41663
|
-
if (!next.trim())
|
|
41664
|
-
|
|
41781
|
+
if (!next.trim()) {
|
|
41782
|
+
sawBlank = true;
|
|
41783
|
+
continue;
|
|
41784
|
+
}
|
|
41785
|
+
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) {
|
|
41786
|
+
return sawBlank ? { after: "", before: "", splitByBlankBefore: next.trim() } : { after: "", before: "" };
|
|
41787
|
+
}
|
|
41665
41788
|
return { after: next.trim(), before: "" };
|
|
41666
41789
|
}
|
|
41667
41790
|
let paraStart = at;
|
|
@@ -41681,7 +41804,10 @@ function clipParagraph(line) {
|
|
|
41681
41804
|
}
|
|
41682
41805
|
function invisibleTrailer(sha, line, body = "") {
|
|
41683
41806
|
const broken = trailerBlockBreak(body, line);
|
|
41684
|
-
const brokenDiagnostic = broken.
|
|
41807
|
+
const brokenDiagnostic = broken.splitByBlankBefore ? ` Broken by a blank line before the final trailer paragraph, so git only read the last paragraph as the trailer block:
|
|
41808
|
+
${clipParagraph(broken.splitByBlankBefore)}
|
|
41809
|
+
Keep every Key: value trailer (e.g. ${TRAILER_KEY} and Runbook-Unaffected) in one contiguous final paragraph with no blank line between them.
|
|
41810
|
+
` : broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
|
|
41685
41811
|
${clipParagraph(broken.after)}
|
|
41686
41812
|
` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
|
|
41687
41813
|
${clipParagraph(broken.before)}
|
|
@@ -41701,9 +41827,11 @@ function invisibleTrailer(sha, line, body = "") {
|
|
|
41701
41827
|
The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
|
|
41702
41828
|
Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
|
|
41703
41829
|
git log -1 --format='%(trailers)' ${sha.slice(0, 8)}
|
|
41704
|
-
` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message
|
|
41830
|
+
` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message. Run this
|
|
41831
|
+
yourself, right now, in this worktree; it is not a blocker to report and it needs no human:
|
|
41705
41832
|
git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
|
|
41706
|
-
That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver
|
|
41833
|
+
That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.
|
|
41834
|
+
Re-run the policy receipt and the focused test command immediately after it.`
|
|
41707
41835
|
};
|
|
41708
41836
|
}
|
|
41709
41837
|
function unknownScope(sha, unknown) {
|
|
@@ -42834,9 +42962,7 @@ ${list}
|
|
|
42834
42962
|
Co-authored-by: GitHub <noreply@github.com>
|
|
42835
42963
|
`;
|
|
42836
42964
|
}
|
|
42837
|
-
async function
|
|
42838
|
-
if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
|
|
42839
|
-
pr = pr.replace(/^0+(?=\d)/, "");
|
|
42965
|
+
async function inspectQueuePreparation(pr, repo, options) {
|
|
42840
42966
|
const read = options.read ?? readApi;
|
|
42841
42967
|
const queueRead = options.read ?? actorReadApi;
|
|
42842
42968
|
const write = options.write ?? writeApi;
|
|
@@ -42848,14 +42974,14 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42848
42974
|
const pull = metadata.pullRequest;
|
|
42849
42975
|
if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
|
|
42850
42976
|
const head = options.expectedHeadOid ?? pull.headRefOid;
|
|
42851
|
-
if (head !== pull.headRefOid) return { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` };
|
|
42852
|
-
const stamp = (
|
|
42977
|
+
if (head !== pull.headRefOid) return { kind: "settled", result: { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` } };
|
|
42978
|
+
const stamp = (result, selected = false, intentId) => {
|
|
42853
42979
|
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
42854
|
-
return credential === "unknown" ?
|
|
42980
|
+
return credential === "unknown" ? result : { ...result, credential };
|
|
42855
42981
|
};
|
|
42856
|
-
if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
|
|
42857
|
-
if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
|
|
42858
|
-
if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
|
|
42982
|
+
if (pull.state === "MERGED") return { kind: "settled", result: stamp({ mergeStatus: "merged" }) };
|
|
42983
|
+
if (pull.isInMergeQueue) return { kind: "settled", result: stamp({ mergeStatus: "auto-merge-enqueued" }) };
|
|
42984
|
+
if (pull.state !== "OPEN") return { kind: "settled", result: { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` } };
|
|
42859
42985
|
const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
|
|
42860
42986
|
let page = pull.closingIssuesReferences.pageInfo;
|
|
42861
42987
|
while (page.hasNextPage) {
|
|
@@ -42888,7 +43014,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42888
43014
|
status: "preparation-needed",
|
|
42889
43015
|
reason,
|
|
42890
43016
|
issueTargets: targets.map((number) => `${repo}#${number}`),
|
|
42891
|
-
nextAction: reason === "queue-message" ?
|
|
43017
|
+
nextAction: reason === "queue-message" ? `PR author: if PR #${pr} is meant to close ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"}, add ${targets.length ? targets.map((number) => `\`Closes #${number}\``).join(", ") : "`Closes #N`"} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head; if ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"} must stay open, remove the closing phrases from the source commit or PR text using ${targets.length ? targets.map((number) => `\`Part of #${number}\``).join(", ") : "`Part of #N`"} / ${targets.length ? targets.map((number) => `\`Refs #${number}\``).join(", ") : "`Refs #N`"}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.` : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged."
|
|
42892
43018
|
},
|
|
42893
43019
|
error: `queue preparation needed: ${detail}`
|
|
42894
43020
|
});
|
|
@@ -42906,9 +43032,9 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42906
43032
|
);
|
|
42907
43033
|
method = legacy.mergeQueue?.configuration?.mergeMethod;
|
|
42908
43034
|
}
|
|
42909
|
-
if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred");
|
|
43035
|
+
if (!["SQUASH", "MERGE", "REBASE"].includes(method ?? "")) return { kind: "refused", result: preparation("queue-message-policy", [], "the effective queue merge method is unknown; no submission occurred") };
|
|
42910
43036
|
if (method === "SQUASH" && (!["PR_TITLE", "COMMIT_OR_PR_TITLE"].includes(policy.title ?? "") || !["PR_BODY", "COMMIT_MESSAGES", "BLANK"].includes(policy.body ?? ""))) {
|
|
42911
|
-
return preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred");
|
|
43037
|
+
return { kind: "refused", result: preparation("queue-message-policy", [], "the repository squash-message policy is unreadable; no submission occurred") };
|
|
42912
43038
|
}
|
|
42913
43039
|
const messages = parsed.commits;
|
|
42914
43040
|
const headline = policy.title === "COMMIT_OR_PR_TITLE" && messages.length === 1 ? messages[0].headline : pull.title;
|
|
@@ -42925,7 +43051,7 @@ ${body}`;
|
|
|
42925
43051
|
${message2.body}`).join("\n\n");
|
|
42926
43052
|
if (method === "MERGE") {
|
|
42927
43053
|
if (!["PR_TITLE", "MERGE_MESSAGE"].includes(repository.merge_commit_title ?? "") || !["PR_TITLE", "PR_BODY", "BLANK"].includes(repository.merge_commit_message ?? "")) {
|
|
42928
|
-
return preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred");
|
|
43054
|
+
return { kind: "refused", result: preparation("queue-message-policy", [], "the repository merge-message policy is unreadable; no submission occurred") };
|
|
42929
43055
|
}
|
|
42930
43056
|
if (repository.merge_commit_title === "PR_TITLE" || repository.merge_commit_message === "PR_TITLE") actualMessage += `
|
|
42931
43057
|
|
|
@@ -42947,19 +43073,22 @@ ${pull.body}`;
|
|
|
42947
43073
|
...findNegatedClosings(input.text, closing),
|
|
42948
43074
|
...findCrossRepoAmbiguousClosings(input.text, closing, repo)
|
|
42949
43075
|
])];
|
|
42950
|
-
return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}
|
|
43076
|
+
return { kind: "refused", result: preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${closingGuardDiagnostic(verdict.message)} See preparation.nextAction for the exact \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\` remedy.`) };
|
|
42951
43077
|
}
|
|
42952
43078
|
if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" && messages.length > 1 && options.bodyText == null && messages.some((message2) => /^\s*Test-Policy-Override\s*:/im.test(`${message2.headline}
|
|
42953
43079
|
${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title, pr, messages), process.cwd())) {
|
|
42954
43080
|
return {
|
|
42955
|
-
|
|
42956
|
-
|
|
42957
|
-
|
|
42958
|
-
|
|
42959
|
-
|
|
42960
|
-
|
|
42961
|
-
|
|
42962
|
-
|
|
43081
|
+
kind: "refused",
|
|
43082
|
+
result: {
|
|
43083
|
+
mergeStatus: "failed",
|
|
43084
|
+
preparation: {
|
|
43085
|
+
status: "preparation-needed",
|
|
43086
|
+
reason: "queue-message-override",
|
|
43087
|
+
issueTargets: [],
|
|
43088
|
+
nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. Put the change in ONE commit whose message ends with a trailer block carrying the waiver (no force-push: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one), or make the waiver the final trailer block of the LAST commit, then rerun pr land. Keep repository settings unchanged."
|
|
43089
|
+
},
|
|
43090
|
+
error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
|
|
43091
|
+
}
|
|
42963
43092
|
};
|
|
42964
43093
|
}
|
|
42965
43094
|
const requiredBody = options.bodyText ?? composedBody;
|
|
@@ -42967,18 +43096,38 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
|
|
|
42967
43096
|
if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
|
|
42968
43097
|
if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
|
|
42969
43098
|
return {
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
|
|
42975
|
-
|
|
42976
|
-
|
|
42977
|
-
|
|
43099
|
+
kind: "refused",
|
|
43100
|
+
result: {
|
|
43101
|
+
mergeStatus: "failed",
|
|
43102
|
+
preparation: {
|
|
43103
|
+
status: "preparation-needed",
|
|
43104
|
+
reason: "queue-message-override",
|
|
43105
|
+
issueTargets: [],
|
|
43106
|
+
nextAction: `PR author: this repository squashes with PR_BODY, so place the required Test-Policy-Override trailer(s) in the PR body final trailer block (e.g. \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) without a body override, then rerun \`pr land\`. Keep repository settings unchanged.`
|
|
43107
|
+
},
|
|
43108
|
+
error: "queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block"
|
|
43109
|
+
}
|
|
42978
43110
|
};
|
|
42979
43111
|
}
|
|
42980
|
-
return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
|
|
43112
|
+
return { kind: "refused", result: preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission") };
|
|
42981
43113
|
}
|
|
43114
|
+
return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
|
|
43115
|
+
}
|
|
43116
|
+
async function preflightPrMergeQueue(prNumber, repo, options = {}) {
|
|
43117
|
+
if (!/^\d+$/.test(prNumber)) return void 0;
|
|
43118
|
+
const inspected = await inspectQueuePreparation(prNumber.replace(/^0+(?=\d)/, ""), repo, options);
|
|
43119
|
+
return inspected.kind === "refused" ? inspected.result : void 0;
|
|
43120
|
+
}
|
|
43121
|
+
async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
43122
|
+
if (!/^\d+$/.test(pr)) throw new Error("merge queue enqueue requires a PR number");
|
|
43123
|
+
pr = pr.replace(/^0+(?=\d)/, "");
|
|
43124
|
+
const inspected = await inspectQueuePreparation(pr, repo, options);
|
|
43125
|
+
if (inspected.kind !== "ready") return inspected.result;
|
|
43126
|
+
const { head, pull, read, queueRead, write } = inspected;
|
|
43127
|
+
const stamp = (result2, selected = false, intentId) => {
|
|
43128
|
+
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
43129
|
+
return credential === "unknown" ? result2 : { ...result2, credential };
|
|
43130
|
+
};
|
|
42982
43131
|
const current = await queueGraphql(
|
|
42983
43132
|
repo,
|
|
42984
43133
|
`pullRequest(number:${pr}) { headRefOid title body }`,
|
|
@@ -42989,7 +43138,12 @@ ${message2.body}`)) && !readMessageOverride2(githubQueueSquashMessage(pull.title
|
|
|
42989
43138
|
}
|
|
42990
43139
|
const selection = { selected: false };
|
|
42991
43140
|
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
42992
|
-
return { ...stamp(result, selection.selected, selection.intentId), ...
|
|
43141
|
+
return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
|
|
43142
|
+
}
|
|
43143
|
+
function closingGuardDiagnostic(message2) {
|
|
43144
|
+
if (!message2) return "";
|
|
43145
|
+
const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
|
|
43146
|
+
return (remedy === -1 ? message2 : message2.slice(0, remedy)).trim();
|
|
42993
43147
|
}
|
|
42994
43148
|
function decideQueueMergeRoute(hasQueue, method, headRef) {
|
|
42995
43149
|
if (!hasQueue) return "normal";
|
|
@@ -43166,8 +43320,10 @@ function prMergeQueueLine(result) {
|
|
|
43166
43320
|
return `pr land: PR #${result.pr} queued on ${queue.base}${queue.position === void 0 ? "" : ` (position ${queue.position})`}`;
|
|
43167
43321
|
case "merged":
|
|
43168
43322
|
return `pr land: PR #${result.pr} merged via merge queue`;
|
|
43169
|
-
case "ejected":
|
|
43170
|
-
|
|
43323
|
+
case "ejected": {
|
|
43324
|
+
const line = `pr land: PR #${result.pr} ejected from merge queue \u2014 ${queue.ejectedRun ? `merge_group run ${queue.ejectedRun.id} ${queue.ejectedRun.conclusion} ${queue.ejectedRun.url}` : "merge_group run not found"}`;
|
|
43325
|
+
return isNoFailedCheckQueueEjection(queue.ejectedRun) ? `${line} \u2014 no check failed; re-run \`mmi-cli devops pr land ${result.pr}${result.repo ? ` --repo ${result.repo}` : ""}\` once to re-enqueue, and report any further refusal verbatim (never fall back to a direct/admin merge or \`MMI_ACTOR=personal\` workaround)` : line;
|
|
43326
|
+
}
|
|
43171
43327
|
case "timeout":
|
|
43172
43328
|
return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
|
|
43173
43329
|
}
|
|
@@ -43366,6 +43522,57 @@ function registerRunCancel(run, writeToken) {
|
|
|
43366
43522
|
else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 ${receipt.force ? "force-cancel" : "cancel"} requested (status: ${receipt.status}${receipt.runStatus ? `, runStatus: ${receipt.runStatus}` : ""})`);
|
|
43367
43523
|
});
|
|
43368
43524
|
}
|
|
43525
|
+
var RUN_RERUN_STATUS = "rerun-requested";
|
|
43526
|
+
async function rerunWorkflowRun(repo, id, deps, failed2 = false) {
|
|
43527
|
+
const raw = String(id).trim();
|
|
43528
|
+
if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run rerun: <id> must be a positive workflow run id (received '${String(id)}')`);
|
|
43529
|
+
const runId = Number(raw);
|
|
43530
|
+
if (!Number.isSafeInteger(runId)) throw new Error(`run rerun: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
|
|
43531
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run rerun: --repo must be owner/repo (received '${repo}')`);
|
|
43532
|
+
const projects = await deps.projects();
|
|
43533
|
+
if (!projects) throw new Error("run rerun: the Hub project registry is unreadable; refusing to rerun on a repo this command cannot prove registered");
|
|
43534
|
+
const key = registryRepoKey(repo);
|
|
43535
|
+
if (!projects.some((p) => (p.repos ?? []).some((r) => typeof r === "string" && registryRepoKey(qualifyRegistryRepo(r)) === key))) {
|
|
43536
|
+
throw new Error(`run rerun: ${repo} is not a registered org repo; App writes are limited to repos in the Hub registry`);
|
|
43537
|
+
}
|
|
43538
|
+
const mode = failed2 ? "failed" : "all";
|
|
43539
|
+
let httpStatus;
|
|
43540
|
+
try {
|
|
43541
|
+
httpStatus = await deps.post(`repos/${repo}/actions/runs/${runId}/${failed2 ? "rerun-failed-jobs" : "rerun"}`);
|
|
43542
|
+
} catch (error) {
|
|
43543
|
+
const e = error;
|
|
43544
|
+
const detail = String(e.stderr ?? error);
|
|
43545
|
+
const code = /\bHTTP(?:\/[\d.]+)? (\d{3})\b/.exec(`${detail} ${e.stdout ?? ""}`)?.[1];
|
|
43546
|
+
if (code === "403") throw new Error(`run rerun: GitHub App denied (HTTP 403) on ${repo}; actions:write permission is required; no personal-token fallback`);
|
|
43547
|
+
if (code === "404") throw new Error(`run rerun: run ${runId} not found on ${repo} (HTTP 404), or the App cannot see that repo`);
|
|
43548
|
+
if (code === "409" || code === "422") throw new Error(`run rerun: run ${runId} is not rerunnable (HTTP ${code}); it may still be in progress, or be past GitHub's rerun window`);
|
|
43549
|
+
throw new Error(`run rerun: ${detail}`);
|
|
43550
|
+
}
|
|
43551
|
+
return { ok: true, repo, runId, mode, httpStatus, credential: "app_installation", status: RUN_RERUN_STATUS };
|
|
43552
|
+
}
|
|
43553
|
+
function appRunPost(writeToken, exec3 = execFileP) {
|
|
43554
|
+
let token;
|
|
43555
|
+
return async (path2) => {
|
|
43556
|
+
token ??= await writeToken();
|
|
43557
|
+
const { stdout } = await exec3("gh", ["api", "--include", "--method", "POST", path2], {
|
|
43558
|
+
timeout: 2e4,
|
|
43559
|
+
env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
|
|
43560
|
+
});
|
|
43561
|
+
return Number(/^HTTP\/[\d.]+ (\d{3})/.exec(stdout)?.[1] ?? 0);
|
|
43562
|
+
};
|
|
43563
|
+
}
|
|
43564
|
+
function registerRunRerun(run, writeToken, projects) {
|
|
43565
|
+
run.command("rerun <id>").description("rerun one workflow run on the org App (actions:write), registered repos only; --failed reruns only the failed jobs; prints a receipt").requiredOption("--repo <owner/repo>", "registered repository that owns the run").option("--failed", "rerun only the failed jobs (POST .../rerun-failed-jobs)").option("--json", "machine-readable receipt").action(async (id, options) => {
|
|
43566
|
+
let receipt;
|
|
43567
|
+
try {
|
|
43568
|
+
receipt = await rerunWorkflowRun(options.repo, id, { projects, post: appRunPost(writeToken) }, options.failed === true);
|
|
43569
|
+
} catch (error) {
|
|
43570
|
+
return fail(error.message);
|
|
43571
|
+
}
|
|
43572
|
+
if (options.json) console.log(JSON.stringify(receipt));
|
|
43573
|
+
else console.log(`run rerun: ${receipt.repo} run ${receipt.runId} \u2014 ${receipt.mode === "failed" ? "failed jobs" : "all jobs"} rerun requested (HTTP ${receipt.httpStatus}, credential: ${receipt.credential})`);
|
|
43574
|
+
});
|
|
43575
|
+
}
|
|
43369
43576
|
|
|
43370
43577
|
// src/board-commands.ts
|
|
43371
43578
|
init_cli_shared();
|
|
@@ -43634,9 +43841,27 @@ function refuseNotClaimable(e, json) {
|
|
|
43634
43841
|
process.exitCode = 1;
|
|
43635
43842
|
return true;
|
|
43636
43843
|
}
|
|
43637
|
-
var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,title,status,assignees,labels,bucket}.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n";
|
|
43844
|
+
var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n";
|
|
43638
43845
|
function flattenBoardReport(report) {
|
|
43639
|
-
|
|
43846
|
+
const closingByRef = /* @__PURE__ */ new Map();
|
|
43847
|
+
for (const pull of report.pullRequests ?? []) {
|
|
43848
|
+
for (const closes of pull.closesIssues) {
|
|
43849
|
+
const rows = closingByRef.get(closes) ?? [];
|
|
43850
|
+
rows.push({ number: pull.number, url: pull.url });
|
|
43851
|
+
closingByRef.set(closes, rows);
|
|
43852
|
+
}
|
|
43853
|
+
}
|
|
43854
|
+
return [report.primary, report.secondary].flatMap((scope) => ["userOwned", "claimable", "taken", "unownedInFlight"].flatMap((bucket) => scope[bucket].map(({ ref, number, title, status, assignees, labels, claim }) => ({
|
|
43855
|
+
ref,
|
|
43856
|
+
number,
|
|
43857
|
+
title,
|
|
43858
|
+
status,
|
|
43859
|
+
assignees,
|
|
43860
|
+
labels,
|
|
43861
|
+
bucket,
|
|
43862
|
+
claim: claim ?? null,
|
|
43863
|
+
openPullRequests: closingByRef.get(ref) ?? []
|
|
43864
|
+
}))));
|
|
43640
43865
|
}
|
|
43641
43866
|
function registerBoardCommands(program3) {
|
|
43642
43867
|
function withDiscoverMissDetail(message2) {
|
|
@@ -54891,23 +55116,24 @@ async function closeIssue(client, options, deps = {}) {
|
|
|
54891
55116
|
if (options.evidence !== void 0) {
|
|
54892
55117
|
await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
|
|
54893
55118
|
}
|
|
55119
|
+
const appWriter = await selectClaimWriteClient(client, deps.appClient);
|
|
54894
55120
|
if (reason === "completed" && options.evidence !== void 0) {
|
|
54895
55121
|
const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
|
|
54896
55122
|
await ensureLabels([CLOSED_MERGED_LOOP_STATE_LABEL], repo);
|
|
54897
|
-
await
|
|
55123
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, {
|
|
54898
55124
|
body: { labels: [CLOSED_MERGED_LOOP_STATE_LABEL] }
|
|
54899
55125
|
});
|
|
54900
55126
|
}
|
|
54901
|
-
await
|
|
55127
|
+
await appWriter.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
|
|
54902
55128
|
body: { state: "closed", state_reason: stateReason }
|
|
54903
55129
|
});
|
|
54904
55130
|
if (reason === "duplicate-of" && options.duplicateOf) {
|
|
54905
|
-
await
|
|
55131
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
|
|
54906
55132
|
body: { body: `Duplicate of #${options.duplicateOf}` }
|
|
54907
55133
|
});
|
|
54908
55134
|
}
|
|
54909
55135
|
if (options.comment) {
|
|
54910
|
-
await
|
|
55136
|
+
await appWriter.rest("POST", `repos/${repo}/issues/${parsed.number}/comments`, {
|
|
54911
55137
|
body: { body: options.comment }
|
|
54912
55138
|
});
|
|
54913
55139
|
}
|
|
@@ -59546,7 +59772,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59546
59772
|
timeoutMs: Math.max(1, budgetMs - (Date.now() - waitStarted)),
|
|
59547
59773
|
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
59548
59774
|
// an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
|
|
59549
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
59775
|
+
progress: ({ state, elapsedMs, remainingMs, detail }) => console.warn(`pr checks-wait: ${detail && detail.startsWith("pending (") ? detail : state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
59550
59776
|
});
|
|
59551
59777
|
printChecksWaitResult("pr checks-wait", result, o.json);
|
|
59552
59778
|
});
|
|
@@ -59653,6 +59879,8 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59653
59879
|
readHeadSha: (prNumber, repo) => readPrLandPinnedHeadSha(prNumber, repo),
|
|
59654
59880
|
pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
|
|
59655
59881
|
hasMergeQueue: hasPrMergeQueue,
|
|
59882
|
+
// #9611: read-only preparation verdict BEFORE the check wait; the enqueue then re-reads and submits.
|
|
59883
|
+
preflightQueue: (prNumber, repo, expectedHeadOid) => preflightPrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly }),
|
|
59656
59884
|
enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
|
|
59657
59885
|
const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
|
|
59658
59886
|
if (receipt.warning) console.warn(receipt.warning);
|
|
@@ -60318,6 +60546,9 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
60318
60546
|
const result = await waitForPrChecks({
|
|
60319
60547
|
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
60320
60548
|
pollChecks: () => pollRestCommitChecks(sha, repo),
|
|
60549
|
+
// #9589: the evidence-bearing read lets the wait name the rows holding it open and carry the
|
|
60550
|
+
// full check/workflow evidence on the timeout receipt.
|
|
60551
|
+
pollChecksWithEvidence: () => pollRestCommitChecksWithWorkflowEvidence(sha, repo),
|
|
60321
60552
|
pollRateLimit: () => waitLoopCorePool("ci checks-wait"),
|
|
60322
60553
|
checksUrl: `https://github.com/${repo}/commit/${sha}/checks`,
|
|
60323
60554
|
checksDiagnosticCommand: `gh api repos/${repo}/commits/${sha}/check-runs`,
|
|
@@ -60330,7 +60561,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
60330
60561
|
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
60331
60562
|
log: (message2) => console.warn(message2),
|
|
60332
60563
|
timeoutMs: timeoutMs ?? PR_CHECKS_TIMEOUT_MS,
|
|
60333
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`ci checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
60564
|
+
progress: ({ state, elapsedMs, remainingMs, detail }) => console.warn(`ci checks-wait: ${detail && detail.startsWith("pending (") ? detail : state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
60334
60565
|
});
|
|
60335
60566
|
printChecksWaitResult("ci checks-wait", result, o.json);
|
|
60336
60567
|
});
|
|
@@ -66182,7 +66413,7 @@ function typedReadHint(path2) {
|
|
|
66182
66413
|
if (!match) return "";
|
|
66183
66414
|
const [, owner, repo, kind, item, sub] = match;
|
|
66184
66415
|
const slug = `${owner}/${repo}`;
|
|
66185
|
-
const say = (command, note = "") => ` Typed read for this route:
|
|
66416
|
+
const say = (command, note = "") => ` Typed read for this route: \`MMI_ACTOR=app ${command}\`${note}.`;
|
|
66186
66417
|
if (kind === "issues") {
|
|
66187
66418
|
if (!item) return say(`mmi-cli devops read issue list --repo ${slug} --json number,title,state,labels`);
|
|
66188
66419
|
if (item.startsWith("comments/")) {
|
|
@@ -69965,7 +70196,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
|
|
|
69965
70196
|
appActorCommand = commandPath4;
|
|
69966
70197
|
setAuditCommand(commandPath4);
|
|
69967
70198
|
if (commandPath4 !== "whoami diagnose-hub") emitCliCallTelemetry(commandPath4);
|
|
69968
|
-
if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel") return;
|
|
70199
|
+
if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel" || commandPath4 === "run rerun") return;
|
|
69969
70200
|
try {
|
|
69970
70201
|
await activateAppActor(commandPath4, process.env, () => APP_WRITE_COMMANDS.has(commandPath4) ? mintCommandWriteToken(appActorDeps(), commandPath4) : mintInstallationToken(appActorDeps()));
|
|
69971
70202
|
} catch (e) {
|
|
@@ -70800,9 +71031,12 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
|
|
|
70800
71031
|
});
|
|
70801
71032
|
registerCiChecksWait(ci);
|
|
70802
71033
|
registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
|
|
70803
|
-
|
|
70804
|
-
|
|
70805
|
-
|
|
71034
|
+
var runRoot = program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` and `run rerun <id> [--failed]`, both on the org App actions:write profile");
|
|
71035
|
+
registerRunCancel(runRoot, async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token);
|
|
71036
|
+
registerRunRerun(
|
|
71037
|
+
runRoot,
|
|
71038
|
+
async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run rerun")).token,
|
|
71039
|
+
async () => fetchProjectsList(registryClientDeps(await loadConfig()))
|
|
70806
71040
|
);
|
|
70807
71041
|
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it \u2014 keeps its required contexts (master role required)").action(async (o) => {
|
|
70808
71042
|
if (o.apply && o.parkRuleset) {
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
package/skills/board/SKILL.md
CHANGED
|
@@ -20,7 +20,10 @@ One call; the JSON carries `viewer`, `repo` and the project title. Use `primary`
|
|
|
20
20
|
`secondary` for other repos on the Project. Without `--flat`, `board read --json` returns an object
|
|
21
21
|
`{project, viewer, repo, primary, secondary, warnings, partial}` whose `primary` and `secondary` are bucket
|
|
22
22
|
objects `{userOwned, claimable, taken, unownedInFlight}` of item arrays (no top-level `items` array), so filter
|
|
23
|
-
a bucket (e.g. `report.primary.claimable`) or pass `--json --flat`, never `(j.items||j).filter`.
|
|
23
|
+
a bucket (e.g. `report.primary.claimable`) or pass `--json --flat`, never `(j.items||j).filter`. `--flat` rows
|
|
24
|
+
are `{ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}` (`claim` is the parsed mmi-claim
|
|
25
|
+
evidence object, null when unclaimed; `openPullRequests` lists the OPEN PRs whose closing-issue body links name
|
|
26
|
+
the item — on the full JSON object that list lives at `report.pullRequests`, never `openPullRequests`). Partial reads
|
|
24
27
|
exit nonzero; `--allow-partial` only when the dev accepts an incomplete board. The CLI partitions rows into
|
|
25
28
|
**Yours**, **Free to claim** and **Taken**; stale stamps (4 hours without work) hold nothing. Partition rules,
|
|
26
29
|
stamp details and `--out` rationale: [reference.md](reference.md).
|
|
@@ -62,7 +65,7 @@ Present the plan — batch count, issue count, one line per batch naming its sha
|
|
|
62
65
|
mmi-cli oracle board claim <ref> <ref> <ref> --json
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
Announce issue/PR counts; refresh `mmi-cli oracle board read` immediately before each claim (a list read earlier in the session may already be stale) and select ordinary claims from **Free to claim**. Read every result: failures exit nonzero. On conflict, do not blindly retry: stop that batch, never retry the same ref, refresh and reselect from the fresh **Free to claim** list, or coordinate a handoff; `--force` only for an owner-authorized intentional takeover. Run writes separately, never `;`; dependent follow-ups use `&&`.
|
|
68
|
+
Announce issue/PR counts; refresh `mmi-cli oracle board read` immediately before each claim (a list read earlier in the session may already be stale) and select ordinary claims from **Free to claim**. A cached read states its provenance — the render prints `source: cache · built <timestamp> · fresh|stale` and `--json` carries `builtAt` — so if a read in the same turn/window already returned a fresh snapshot and no board write has intervened since, re-reading the same `builtAt` snapshot buys no new information: reuse it instead of re-reading. Read every result: failures exit nonzero. On conflict, do not blindly retry: stop that batch, never retry the same ref, refresh and reselect from the fresh **Free to claim** list, or coordinate a handoff; `--force` only for an owner-authorized intentional takeover. Run writes separately, never `;`; dependent follow-ups use `&&`.
|
|
66
69
|
|
|
67
70
|
Run each `mmi-cli oracle issue create` as its own simple command. Any command whose text must carry quote
|
|
68
71
|
characters — an issue body, an inline `node -e` / `bash -c` one-liner — is written to a file, never spliced: a
|
|
@@ -12,7 +12,7 @@ Every refusal and every doctor blocker names its `code` and `anchor`: open the f
|
|
|
12
12
|
|
|
13
13
|
## Merge floor
|
|
14
14
|
|
|
15
|
-
- **Ordinary development PRs land with `mmi-cli devops pr land <number>`.** The rules below are for the train's own merges.
|
|
15
|
+
- **Ordinary development PRs land with `mmi-cli devops pr land <number>`.** If a merge-queue group leaves the queue with no failed check, `pr land` re-queues once (or rerun `mmi-cli devops pr land <number>` once); a refused land is a stop to report verbatim — never fall back to a hand/admin merge, actor override (`MMI_ACTOR=personal`), or interim workaround. The rules below are for the train's own merges.
|
|
16
16
|
- **Never squash-merge a tagged commit (#3167).** A squash re-mints the SHA and orphans the tag, so tag-anchored checks mis-read. Any merge that carries an already-tagged commit — the `main → development` roll-forward, an alignment PR, a hotfix fold PR — lands as a true merge: `mmi-cli devops pr merge <number> --auto --merge` (or `--wait --merge`); `--wait` alone still squashes. The hotfix main-base PR itself may squash: its tag lands after, on the merged `main` HEAD, and the `-x` trailer must survive the squash message.
|
|
17
17
|
- **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt — doctor blocker, authority probe, coverage guard, untolerated conflict, required checks, stray tag, ledger — ends the run. Fix the cause on `development` through a normal PR and rerun the train; never hand-resolve on `main`, hand-cherry-pick, bare-push a train branch, ack to save time, or bypass a check.
|
|
18
18
|
- **Probe the latest Release tag, not branches or stale local tags (#2734).** Queue: `mmi-cli devops read pr queue <number> --repo <owner/repo> --json`; gh rejects `mergeQueueEntry`.
|