@mutmutco/cli 4.6.4 → 4.6.6
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 +760 -433
- package/dist/runbook-sweep.cjs +4 -4
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -526,6 +526,8 @@ __export(github_client_exports, {
|
|
|
526
526
|
createGitHubClient: () => createGitHubClient,
|
|
527
527
|
defaultGitHubClient: () => defaultGitHubClient,
|
|
528
528
|
githubKeyringTokenRead: () => githubKeyringTokenRead,
|
|
529
|
+
githubPersonalToken: () => githubPersonalToken,
|
|
530
|
+
githubPersonalTokenRead: () => githubPersonalTokenRead,
|
|
529
531
|
githubToken: () => githubToken,
|
|
530
532
|
githubTokenRead: () => githubTokenRead,
|
|
531
533
|
rateLimitResetNote: () => rateLimitResetNote,
|
|
@@ -563,6 +565,23 @@ async function githubTokenRead() {
|
|
|
563
565
|
if (process.env.GITHUB_TOKEN) return { state: "ok", token: process.env.GITHUB_TOKEN };
|
|
564
566
|
return githubKeyringTokenRead();
|
|
565
567
|
}
|
|
568
|
+
async function githubPersonalTokenRead() {
|
|
569
|
+
const mmiPat = process.env.MMI_GH_PAT?.trim();
|
|
570
|
+
if (mmiPat) return { state: "ok", token: mmiPat };
|
|
571
|
+
if (process.env.MMI_GH_POOL !== "app" && process.env.GH_TOKEN) return { state: "ok", token: process.env.GH_TOKEN };
|
|
572
|
+
if (process.env.GITHUB_TOKEN) return { state: "ok", token: process.env.GITHUB_TOKEN };
|
|
573
|
+
return githubKeyringTokenRead();
|
|
574
|
+
}
|
|
575
|
+
async function githubPersonalToken() {
|
|
576
|
+
const read = await githubPersonalTokenRead();
|
|
577
|
+
if (read.state === "failed") {
|
|
578
|
+
throw new GitHubApiError(
|
|
579
|
+
`GitHub identity read FAILED: \`gh auth token\` ${read.detail} \u2014 this is NOT "no token", so the call is refused rather than sent anonymously (an anonymous 401/404 would read as a permission or missing-object answer). Retry, or set MMI_GH_PAT/GH_TOKEN/GITHUB_TOKEN for this process.`,
|
|
580
|
+
{ status: 0 }
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
return read.state === "ok" ? read.token : void 0;
|
|
584
|
+
}
|
|
566
585
|
async function githubToken() {
|
|
567
586
|
const read = await githubTokenRead();
|
|
568
587
|
if (read.state === "failed") {
|
|
@@ -1355,6 +1374,7 @@ __export(cli_shared_exports, {
|
|
|
1355
1374
|
fail: () => fail,
|
|
1356
1375
|
failGraceful: () => failGraceful,
|
|
1357
1376
|
failGracefulEnvelope: () => failGracefulEnvelope,
|
|
1377
|
+
ghFilingAppEnv: () => ghFilingAppEnv,
|
|
1358
1378
|
gitOut: () => gitOut,
|
|
1359
1379
|
githubLogin: () => githubLogin,
|
|
1360
1380
|
hubHeaders: () => hubHeaders,
|
|
@@ -1385,10 +1405,10 @@ function setGhReadTokenProvider(provider) {
|
|
|
1385
1405
|
function setGhWriteTokenProvider(provider) {
|
|
1386
1406
|
ghWriteTokenProvider = provider;
|
|
1387
1407
|
}
|
|
1388
|
-
async function ghFilingAppEnv(file, args) {
|
|
1389
|
-
if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || process.env.MMI_ACTOR?.trim() === "personal") return void 0;
|
|
1408
|
+
async function ghFilingAppEnv(file, args, requireApp = false) {
|
|
1409
|
+
if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || !requireApp && process.env.MMI_ACTOR?.trim() === "personal") return void 0;
|
|
1390
1410
|
if (!isAppFilingWrite(args)) return void 0;
|
|
1391
|
-
const explicitApp = process.env.MMI_ACTOR?.trim() === "app";
|
|
1411
|
+
const explicitApp = requireApp || process.env.MMI_ACTOR?.trim() === "app";
|
|
1392
1412
|
let token;
|
|
1393
1413
|
try {
|
|
1394
1414
|
token = await ghWriteTokenProvider();
|
|
@@ -1447,7 +1467,7 @@ async function githubLogin() {
|
|
|
1447
1467
|
}
|
|
1448
1468
|
async function hubHeaders(extra = {}) {
|
|
1449
1469
|
const cfg = await loadConfig();
|
|
1450
|
-
const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
|
|
1470
|
+
const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken });
|
|
1451
1471
|
const base = { ...clientVersionHeaders(), ...extra };
|
|
1452
1472
|
return t ? { ...base, Authorization: `Bearer ${t}` } : base;
|
|
1453
1473
|
}
|
|
@@ -1616,7 +1636,7 @@ var init_cli_shared = __esm({
|
|
|
1616
1636
|
const pool = appEnv ? "app" : !callerEnv ? ambientPool() : !callerEnv.GH_TOKEN ? "personal" : callerEnv.MMI_GH_POOL === "app" ? "app" : callerEnv.GH_TOKEN === process.env.GH_TOKEN ? ambientPool() : "caller";
|
|
1617
1637
|
recordGhUsage("gh", pool, args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" "));
|
|
1618
1638
|
}
|
|
1619
|
-
if (!appEnv) return tag(run(), "user");
|
|
1639
|
+
if (!appEnv) return tag(run(), options.env?.MMI_GH_POOL === "app" ? "app_installation" : "user");
|
|
1620
1640
|
try {
|
|
1621
1641
|
return await tag(run(appEnv), "app_installation");
|
|
1622
1642
|
} catch (e) {
|
|
@@ -16631,9 +16651,44 @@ var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
|
16631
16651
|
"startup_failure"
|
|
16632
16652
|
]);
|
|
16633
16653
|
var TEST_FAILURE_LINE = /^(?:✖|not ok\b|FAIL\b|×)|\bAssertionError\b/;
|
|
16654
|
+
var logLine = (raw) => raw.replace(/^\uFEFF?\d{4}-\d\d-\d\dT[\d:.]+Z\s*/, "").replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
16655
|
+
function failingTestIds(log) {
|
|
16656
|
+
const ids = /* @__PURE__ */ new Set();
|
|
16657
|
+
let node = false, vitest = false, summary = false;
|
|
16658
|
+
let at;
|
|
16659
|
+
for (const raw of log.split(/\r?\n/)) {
|
|
16660
|
+
const line = logLine(raw);
|
|
16661
|
+
if (/^not ok\b/.test(line)) return void 0;
|
|
16662
|
+
if (/^\u00D7/.test(line)) vitest = true;
|
|
16663
|
+
const fail2 = /^FAIL\s+(\S+\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>\s+.+)?)$/.exec(line);
|
|
16664
|
+
if (fail2) {
|
|
16665
|
+
ids.add(fail2[1].replace(/\s+\d+(?:\.\d+)?m?s$/, "").trim());
|
|
16666
|
+
continue;
|
|
16667
|
+
}
|
|
16668
|
+
if (/^FAIL\b/.test(line)) return void 0;
|
|
16669
|
+
if (/^\u2716 failing tests:?$/.test(line)) {
|
|
16670
|
+
summary = true;
|
|
16671
|
+
at = void 0;
|
|
16672
|
+
continue;
|
|
16673
|
+
}
|
|
16674
|
+
if (!/^\u2716/.test(line)) {
|
|
16675
|
+
const loc = summary ? /^test at (\S+?):(\d+):\d+$/.exec(line) : null;
|
|
16676
|
+
if (loc) at = `${loc[1]}:${loc[2]}`;
|
|
16677
|
+
continue;
|
|
16678
|
+
}
|
|
16679
|
+
node = true;
|
|
16680
|
+
if (!summary) continue;
|
|
16681
|
+
const title = /^\u2716 (.+?)(?:\s+\(\d+(?:\.\d+)?m?s\))?$/.exec(line)?.[1];
|
|
16682
|
+
if (!title || !at) return void 0;
|
|
16683
|
+
ids.add(`${at} > ${title}`);
|
|
16684
|
+
at = void 0;
|
|
16685
|
+
}
|
|
16686
|
+
if ((node || vitest || summary) && (!ids.size || node && !summary)) return void 0;
|
|
16687
|
+
return [...ids];
|
|
16688
|
+
}
|
|
16634
16689
|
function firstTestFailureLine(log) {
|
|
16635
16690
|
for (const raw of log.split(/\r?\n/)) {
|
|
16636
|
-
const line = raw
|
|
16691
|
+
const line = logLine(raw);
|
|
16637
16692
|
if (TEST_FAILURE_LINE.test(line)) {
|
|
16638
16693
|
return line.replace(/\b(?:gh[pousr]_|github_pat_|AKIA|sk-)[\w-]+/g, "[redacted]").slice(0, 200);
|
|
16639
16694
|
}
|
|
@@ -17215,7 +17270,9 @@ function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
|
17215
17270
|
function commitClosingRefusalMessage(closing, context = "pr merge") {
|
|
17216
17271
|
const named = closing.map((n) => `#${n}`).join(", ");
|
|
17217
17272
|
const first = `#${closing[0] ?? "N"}`;
|
|
17218
|
-
|
|
17273
|
+
const isQueue = /queue/i.test(context);
|
|
17274
|
+
const remedy = isQueue ? `GitHub's merge queue composes the squash message directly from the commits and accepts no squash-body override (--squash-body-file cannot be sent to the queue). If this PR is meant to close ${named}, register ${named} in the PR body (\`mmi-cli devops pr edit <pr> --body-file <path>\` with \`Closes ${first}\`) and rerun \`pr land\`; if ${named} must stay open, reword the commit message to use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" (never "does not close ${first}"), or re-run with --force to enqueue anyway and let ${named} close.` : `Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), edit the squash body at merge time (--squash-body-file), or re-run with --force to merge anyway and let ${named} close.`;
|
|
17275
|
+
return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. ${remedy}`;
|
|
17219
17276
|
}
|
|
17220
17277
|
function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
|
|
17221
17278
|
const named = closed.map((n) => `#${n}`).join(", ");
|
|
@@ -21259,7 +21316,7 @@ async function repoSlug() {
|
|
|
21259
21316
|
return "-";
|
|
21260
21317
|
}
|
|
21261
21318
|
function registryClientDeps(cfg) {
|
|
21262
|
-
return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
21319
|
+
return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
|
|
21263
21320
|
}
|
|
21264
21321
|
async function isOrgRegisteredRepo(cfg, deps = {}) {
|
|
21265
21322
|
const slug = await (deps.slug ?? repoSlug)();
|
|
@@ -23741,10 +23798,14 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23741
23798
|
return;
|
|
23742
23799
|
}
|
|
23743
23800
|
if (!options.force) {
|
|
23801
|
+
if (options.bulk) {
|
|
23802
|
+
const { holder: holder2, why } = laneContestParts(contest);
|
|
23803
|
+
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}`);
|
|
23804
|
+
}
|
|
23744
23805
|
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);
|
|
23745
23806
|
const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
|
|
23746
23807
|
const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
|
|
23747
|
-
throw new Error(
|
|
23808
|
+
throw new Error(withGatedWriteChainHint(refusal));
|
|
23748
23809
|
}
|
|
23749
23810
|
previousHolder = displaced();
|
|
23750
23811
|
};
|
|
@@ -23799,6 +23860,13 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
23799
23860
|
const result = await claimOneBoardItem(ctx, selector, options);
|
|
23800
23861
|
return ctx.report.warnings.length ? { ...result, warnings: [...ctx.report.warnings] } : result;
|
|
23801
23862
|
}
|
|
23863
|
+
function formatBulkClaimSummary(results) {
|
|
23864
|
+
const skipped = results.filter((r) => !r.claimed);
|
|
23865
|
+
if (skipped.length === 0) return void 0;
|
|
23866
|
+
const landed = results.filter((r) => r.claimed).map((r) => r.ref);
|
|
23867
|
+
const notClaimed = skipped.map((r) => `${r.ref}${/already claimed by /i.test(r.reason) ? " (held by another lane)" : ""}`);
|
|
23868
|
+
return `claimed ${landed.length} of ${results.length}: ${landed.length ? `${landed.join(", ")} landed` : "none landed"}; not claimed: ${notClaimed.join(", ")}`;
|
|
23869
|
+
}
|
|
23802
23870
|
async function claimBoardIssues(options, deps = {}) {
|
|
23803
23871
|
const cfg = resolveBoardConfig(options.config);
|
|
23804
23872
|
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
@@ -27388,10 +27456,10 @@ var rollout_plan_default = {
|
|
|
27388
27456
|
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)."
|
|
27389
27457
|
},
|
|
27390
27458
|
baseline: {
|
|
27391
|
-
version: "4.6.
|
|
27392
|
-
tag: "v4.6.
|
|
27393
|
-
commit: "
|
|
27394
|
-
npm: "@mutmutco/cli@4.6.
|
|
27459
|
+
version: "4.6.6",
|
|
27460
|
+
tag: "v4.6.6",
|
|
27461
|
+
commit: "8431efc33b8c",
|
|
27462
|
+
npm: "@mutmutco/cli@4.6.6"
|
|
27395
27463
|
},
|
|
27396
27464
|
exitCriterion: "fleet-n-of-n",
|
|
27397
27465
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27408,14 +27476,14 @@ var rollout_plan_default = {
|
|
|
27408
27476
|
repo: "mutmutco/mmi-hub",
|
|
27409
27477
|
role: "canary",
|
|
27410
27478
|
schedule: "train",
|
|
27411
|
-
v3Target: "v4.6.
|
|
27479
|
+
v3Target: "v4.6.6"
|
|
27412
27480
|
}
|
|
27413
27481
|
],
|
|
27414
27482
|
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.",
|
|
27415
27483
|
rollback: {
|
|
27416
27484
|
independent: true,
|
|
27417
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27418
|
-
v3Target: "v4.6.
|
|
27485
|
+
mechanism: "npm dist-tag latest -> 4.6.6 and redeploy the Hub Lambda from tag v4.6.6 (8431efc33b8c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
27486
|
+
v3Target: "v4.6.6 (@mutmutco/cli@4.6.6, tag commit 8431efc33b8c \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
27419
27487
|
}
|
|
27420
27488
|
},
|
|
27421
27489
|
{
|
|
@@ -40989,6 +41057,403 @@ var import_node_os17 = require("node:os");
|
|
|
40989
41057
|
var import_node_path30 = require("node:path");
|
|
40990
41058
|
init_cli_shared();
|
|
40991
41059
|
|
|
41060
|
+
// src/pr-land.ts
|
|
41061
|
+
var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
|
|
41062
|
+
var PR_LAND_QUEUE_BATCH_SIZE = 5;
|
|
41063
|
+
function prQueueReadHint(prNumber, repo) {
|
|
41064
|
+
return `read the queue entry with \`mmi-cli devops read pr queue ${prNumber} --repo ${repo} --json\` (no queue-admission state file is meant to be inspected directly under %LOCALAPPDATA%/MMI Future/mmi-cli)`;
|
|
41065
|
+
}
|
|
41066
|
+
function prHeadBehindBase(input) {
|
|
41067
|
+
if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
|
|
41068
|
+
return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
|
|
41069
|
+
}
|
|
41070
|
+
function prHeadUpdateRemedy(head, base) {
|
|
41071
|
+
return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
41072
|
+
}
|
|
41073
|
+
function prLandedBranchFinality(prNumber, base) {
|
|
41074
|
+
return `PR #${prNumber} is merged, and that is final \u2014 stopping a local waiter or monitor does not unmerge it. The head branch is now landed; further commits pushed to it reach nothing. If more work is needed, run \`git fetch origin ${base} && git switch -c <new-branch> origin/${base}\`, then cherry-pick anything already committed \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merged-pr-head-reuse`;
|
|
41075
|
+
}
|
|
41076
|
+
var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
|
|
41077
|
+
function isProtectedPrHead(head) {
|
|
41078
|
+
return PROTECTED_PR_HEAD_BRANCHES.includes(head);
|
|
41079
|
+
}
|
|
41080
|
+
function protectedPrHeadRemedy(head, base) {
|
|
41081
|
+
return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
41082
|
+
}
|
|
41083
|
+
var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
|
|
41084
|
+
var PR_HEAD_UPDATE_API_READ_DELAY_MS = 15e3;
|
|
41085
|
+
async function updatePrHeadFromBase(input) {
|
|
41086
|
+
const remedy = prHeadUpdateRemedy(input.head, input.base);
|
|
41087
|
+
if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
|
|
41088
|
+
if (input.localCheckedOut) {
|
|
41089
|
+
const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
41090
|
+
await input.git(["fetch", "origin", input.base]);
|
|
41091
|
+
try {
|
|
41092
|
+
await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
|
|
41093
|
+
} catch (e) {
|
|
41094
|
+
await input.git(["merge", "--abort"]).catch(() => {
|
|
41095
|
+
});
|
|
41096
|
+
throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
|
|
41097
|
+
}
|
|
41098
|
+
await input.git(["push", "origin", input.head]);
|
|
41099
|
+
const to = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
41100
|
+
return { from: from2, to, base: input.base, via: "local-merge" };
|
|
41101
|
+
}
|
|
41102
|
+
const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
|
|
41103
|
+
const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
|
|
41104
|
+
const from = await readHead2();
|
|
41105
|
+
try {
|
|
41106
|
+
await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
|
|
41107
|
+
} catch (e) {
|
|
41108
|
+
throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
|
|
41109
|
+
}
|
|
41110
|
+
const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
41111
|
+
for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
|
|
41112
|
+
const to = await readHead2();
|
|
41113
|
+
if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
|
|
41114
|
+
await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
|
|
41115
|
+
}
|
|
41116
|
+
throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
|
|
41117
|
+
}
|
|
41118
|
+
var PR_LAND_POLL_MS = 6e4;
|
|
41119
|
+
var PR_LAND_POLL_MAX_MS = 5 * 6e4;
|
|
41120
|
+
function prLandPollDelayMs(attempt, waiting) {
|
|
41121
|
+
const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
|
|
41122
|
+
const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
|
|
41123
|
+
return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
|
|
41124
|
+
}
|
|
41125
|
+
var PR_LAND_EJECT_CONFIRM_MS = 2e4;
|
|
41126
|
+
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
41127
|
+
var PR_LAND_STATE_READ_RETRIES = 3;
|
|
41128
|
+
var PR_LAND_STATE_READ_DELAY_MS = 15e3;
|
|
41129
|
+
var PR_LAND_MERGE_RETRY_DELAY_MS = 3e4;
|
|
41130
|
+
async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
41131
|
+
const now = deps.now ?? (() => Date.now());
|
|
41132
|
+
const deadline = now() + PR_LAND_ENQUEUE_TIMEOUT_MS;
|
|
41133
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
41134
|
+
const first = await deps.mergeAuto(prNumber, repo);
|
|
41135
|
+
let result = first;
|
|
41136
|
+
let transientRetried = false;
|
|
41137
|
+
while (result.mergeStatus === "failed") {
|
|
41138
|
+
if (result.retryable === false || result.outcomeUnknown || result.preparation) return result;
|
|
41139
|
+
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false, checksPending: false }));
|
|
41140
|
+
if (ready.open && ready.checksPending) {
|
|
41141
|
+
if (now() >= deadline) break;
|
|
41142
|
+
await sleep2(Math.min(PR_LAND_POLL_MS, deadline - now()));
|
|
41143
|
+
if (now() >= deadline) break;
|
|
41144
|
+
const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
|
|
41145
|
+
if (checks.status !== "success" && checks.status !== "skipped") {
|
|
41146
|
+
return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
|
|
41147
|
+
registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
|
|
41148
|
+
}
|
|
41149
|
+
if (now() >= deadline) break;
|
|
41150
|
+
} else {
|
|
41151
|
+
if (!ready.open || !ready.mergeable || !ready.checksPassing || transientRetried) return result;
|
|
41152
|
+
transientRetried = true;
|
|
41153
|
+
if (now() >= deadline) break;
|
|
41154
|
+
await sleep2(Math.min(PR_LAND_MERGE_RETRY_DELAY_MS, deadline - now()));
|
|
41155
|
+
if (now() >= deadline) break;
|
|
41156
|
+
}
|
|
41157
|
+
const retried = await deps.mergeAuto(prNumber, repo);
|
|
41158
|
+
result = retried.mergeStatus !== "failed" ? retried : {
|
|
41159
|
+
...retried,
|
|
41160
|
+
error: `merge retry after transient failure also failed: first attempt: ${first.error ?? "unknown error"}
|
|
41161
|
+
retry: ${retried.error ?? "unknown error"}`
|
|
41162
|
+
};
|
|
41163
|
+
}
|
|
41164
|
+
return result.mergeStatus === "failed" ? { ...result, error: `${result.error ?? "merge failed"}
|
|
41165
|
+
required-check registration did not settle within the enqueue deadline` } : result;
|
|
41166
|
+
}
|
|
41167
|
+
var AUTO_MERGE_CONFIRM_RETRIES = 3;
|
|
41168
|
+
var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
|
|
41169
|
+
async function confirmAutoMergeEnqueued(deps, options) {
|
|
41170
|
+
const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
|
|
41171
|
+
const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
|
|
41172
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
41173
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
41174
|
+
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
41175
|
+
const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
|
|
41176
|
+
if (stuck) return "enqueued";
|
|
41177
|
+
if (attempt < retries - 1) {
|
|
41178
|
+
await sleep2(delayMs);
|
|
41179
|
+
await deps.reEnqueue().catch(() => {
|
|
41180
|
+
});
|
|
41181
|
+
}
|
|
41182
|
+
}
|
|
41183
|
+
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
41184
|
+
return "not-stuck";
|
|
41185
|
+
}
|
|
41186
|
+
async function readGhPrStateWithRetry(fetchState, options) {
|
|
41187
|
+
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
41188
|
+
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
41189
|
+
const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
41190
|
+
let lastError = "empty state";
|
|
41191
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
41192
|
+
try {
|
|
41193
|
+
const state = (await fetchState()).trim();
|
|
41194
|
+
if (state) return { ok: true, state };
|
|
41195
|
+
lastError = "empty state";
|
|
41196
|
+
} catch (e) {
|
|
41197
|
+
lastError = String(e.message || "gh pr view failed");
|
|
41198
|
+
}
|
|
41199
|
+
if (attempt < retries - 1) await sleep2(delayMs);
|
|
41200
|
+
}
|
|
41201
|
+
return { ok: false, error: lastError };
|
|
41202
|
+
}
|
|
41203
|
+
async function runPrLand(prNumber, options, deps) {
|
|
41204
|
+
const resolved = await deps.resolveRepo(prNumber, options.repo);
|
|
41205
|
+
const { repo } = resolved;
|
|
41206
|
+
const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
|
|
41207
|
+
if (options.requireTrain !== false) {
|
|
41208
|
+
const verdict = await deps.fetchTrainAuthority(repo);
|
|
41209
|
+
if (!verdict.ok) {
|
|
41210
|
+
return { ...base, error: `train authority unavailable: ${verdict.error}` };
|
|
41211
|
+
}
|
|
41212
|
+
base.train = { role: verdict.authority.role, train: verdict.authority.train };
|
|
41213
|
+
if (!verdict.authority.train) {
|
|
41214
|
+
return {
|
|
41215
|
+
...base,
|
|
41216
|
+
error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
|
|
41217
|
+
};
|
|
41218
|
+
}
|
|
41219
|
+
}
|
|
41220
|
+
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
41221
|
+
base.ciPolicy = ciPolicy;
|
|
41222
|
+
const pinned = options.expectHead;
|
|
41223
|
+
if (pinned) base.pinnedHead = { expected: pinned };
|
|
41224
|
+
const headDrift = async (stage) => {
|
|
41225
|
+
if (!pinned) return void 0;
|
|
41226
|
+
if (!deps.readHeadSha) throw new Error("pinned head reader unavailable");
|
|
41227
|
+
const observed = await deps.readHeadSha(prNumber, repo);
|
|
41228
|
+
base.pinnedHead = { expected: pinned, observed };
|
|
41229
|
+
if (observed === pinned) return void 0;
|
|
41230
|
+
return {
|
|
41231
|
+
...base,
|
|
41232
|
+
pinnedHead: { ...base.pinnedHead, refused: "drift" },
|
|
41233
|
+
error: `pr land #${prNumber}: head ${observed} is not the pinned ${pinned} (${stage}) \u2014 refusing to land a head that was not checked (--expect-head)`
|
|
41234
|
+
};
|
|
41235
|
+
};
|
|
41236
|
+
const drift = await headDrift("before landing");
|
|
41237
|
+
if (drift) return drift;
|
|
41238
|
+
const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
|
|
41239
|
+
let queueSnapshot;
|
|
41240
|
+
const queued = { base: resolved.base, state: "queued" };
|
|
41241
|
+
if (hasQueue) {
|
|
41242
|
+
if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
|
|
41243
|
+
queueSnapshot = await deps.readMergeQueue(prNumber, repo);
|
|
41244
|
+
if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
|
|
41245
|
+
base.mergeQueue = queued;
|
|
41246
|
+
base.mergeStatus = "auto-merge-enqueued";
|
|
41247
|
+
return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
|
|
41248
|
+
}
|
|
41249
|
+
}
|
|
41250
|
+
const checksWaitError = (checksWait) => {
|
|
41251
|
+
if (checksWait.status === "conflicting") {
|
|
41252
|
+
return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
|
|
41253
|
+
}
|
|
41254
|
+
if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
|
|
41255
|
+
return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
|
|
41256
|
+
}
|
|
41257
|
+
return void 0;
|
|
41258
|
+
};
|
|
41259
|
+
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
41260
|
+
const waitError = checksWaitError(base.checksWait);
|
|
41261
|
+
if (waitError) return { ...base, error: hasQueue ? `${waitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : waitError };
|
|
41262
|
+
const driftAfterChecks = await headDrift("after checks");
|
|
41263
|
+
if (driftAfterChecks) return driftAfterChecks;
|
|
41264
|
+
let prState = await deps.readPrState(prNumber, repo);
|
|
41265
|
+
if (!hasQueue && pinned && prState.mergeStateStatus === "BEHIND") {
|
|
41266
|
+
return {
|
|
41267
|
+
...base,
|
|
41268
|
+
pinnedHead: { ...base.pinnedHead, refused: "behind" },
|
|
41269
|
+
error: `pr land #${prNumber}: head is BEHIND ${resolved.base} and pinned by --expect-head \u2014 refusing to update it. Update and re-check the head yourself, then land with the new SHA`
|
|
41270
|
+
};
|
|
41271
|
+
}
|
|
41272
|
+
if (!hasQueue && prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
|
|
41273
|
+
try {
|
|
41274
|
+
base.branchUpdated = await deps.updateBranch(prNumber, repo);
|
|
41275
|
+
} catch (e) {
|
|
41276
|
+
return { ...base, error: `pr land #${prNumber}: ${e.message}` };
|
|
41277
|
+
}
|
|
41278
|
+
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
41279
|
+
const reWaitError = checksWaitError(base.checksWait);
|
|
41280
|
+
if (reWaitError) return { ...base, error: hasQueue ? `${reWaitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : reWaitError };
|
|
41281
|
+
prState = await deps.readPrState(prNumber, repo);
|
|
41282
|
+
}
|
|
41283
|
+
if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
|
|
41284
|
+
return {
|
|
41285
|
+
...base,
|
|
41286
|
+
error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`
|
|
41287
|
+
};
|
|
41288
|
+
}
|
|
41289
|
+
if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
|
|
41290
|
+
const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
|
|
41291
|
+
base.mergeStatus = merge.mergeStatus;
|
|
41292
|
+
if (merge.credential) base.credential = merge.credential;
|
|
41293
|
+
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
41294
|
+
if (merge.mergeStatus === "queue-admission-pending") {
|
|
41295
|
+
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
41296
|
+
}
|
|
41297
|
+
if (merge.mergeStatus === "failed") {
|
|
41298
|
+
const rawError = merge.error ?? "merge failed";
|
|
41299
|
+
const error = hasQueue && !rawError.includes("read pr queue") ? `${rawError} \u2014 ${prQueueReadHint(prNumber, repo)}` : rawError;
|
|
41300
|
+
return { ...base, ...merge.preparation ? { preparation: merge.preparation } : {}, ...merge.outcomeUnknown ? { outcomeUnknown: true } : {}, error };
|
|
41301
|
+
}
|
|
41302
|
+
if (hasQueue) {
|
|
41303
|
+
base.mergeQueue = queued;
|
|
41304
|
+
if (merge.mergeStatus === "merged") {
|
|
41305
|
+
return queueMerged(base, deps);
|
|
41306
|
+
}
|
|
41307
|
+
const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
|
|
41308
|
+
let confirmed;
|
|
41309
|
+
if (options.enqueueOnly) {
|
|
41310
|
+
const read = await deps.readMergeQueue(prNumber, repo);
|
|
41311
|
+
if (read.isInMergeQueue || read.state === "MERGED") confirmed = read;
|
|
41312
|
+
}
|
|
41313
|
+
if (!confirmed) {
|
|
41314
|
+
await (deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
|
|
41315
|
+
}
|
|
41316
|
+
return pollPrMergeQueue(prNumber, options, deps, base, confirmed, deadline);
|
|
41317
|
+
}
|
|
41318
|
+
if (merge.mergeStatus === "merged") {
|
|
41319
|
+
return {
|
|
41320
|
+
...base,
|
|
41321
|
+
status: "merged",
|
|
41322
|
+
branchFinality: prLandedBranchFinality(prNumber, resolved.base)
|
|
41323
|
+
};
|
|
41324
|
+
}
|
|
41325
|
+
const now = deps.now ?? (() => Date.now());
|
|
41326
|
+
const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
|
|
41327
|
+
if (merged) {
|
|
41328
|
+
return {
|
|
41329
|
+
...base,
|
|
41330
|
+
status: "auto-merge-enqueued-then-merged",
|
|
41331
|
+
mergeStatus: "merged",
|
|
41332
|
+
branchFinality: prLandedBranchFinality(prNumber, resolved.base)
|
|
41333
|
+
};
|
|
41334
|
+
}
|
|
41335
|
+
return {
|
|
41336
|
+
...base,
|
|
41337
|
+
error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
|
|
41338
|
+
};
|
|
41339
|
+
}
|
|
41340
|
+
function queueMerged(base, deps) {
|
|
41341
|
+
const result = {
|
|
41342
|
+
...base,
|
|
41343
|
+
status: "merged-via-queue",
|
|
41344
|
+
mergeStatus: "merged",
|
|
41345
|
+
mergeQueue: { ...base.mergeQueue, state: "merged" },
|
|
41346
|
+
branchFinality: prLandedBranchFinality(base.pr, base.base)
|
|
41347
|
+
};
|
|
41348
|
+
deps.queueProgress?.(result);
|
|
41349
|
+
return result;
|
|
41350
|
+
}
|
|
41351
|
+
async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
41352
|
+
const now = deps.now ?? (() => Date.now());
|
|
41353
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
41354
|
+
const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
|
|
41355
|
+
const deadline = deadlineMs ?? now() + timeoutMs;
|
|
41356
|
+
let attempt = 0;
|
|
41357
|
+
let credentialMismatched = false;
|
|
41358
|
+
const observe = (read) => {
|
|
41359
|
+
const seen = deps.observeQueued?.(pr, base.repo, read);
|
|
41360
|
+
if (seen?.mismatched) {
|
|
41361
|
+
credentialMismatched = true;
|
|
41362
|
+
delete base.credential;
|
|
41363
|
+
}
|
|
41364
|
+
if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
|
|
41365
|
+
};
|
|
41366
|
+
while (now() < deadline) {
|
|
41367
|
+
const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
|
|
41368
|
+
snapshot = void 0;
|
|
41369
|
+
if (read.isInMergeQueue || read.state === "MERGED") observe(read);
|
|
41370
|
+
if (read.state === "MERGED") return queueMerged(base, deps);
|
|
41371
|
+
if (read.isInMergeQueue) {
|
|
41372
|
+
base.mergeQueue = {
|
|
41373
|
+
base: base.base,
|
|
41374
|
+
state: "queued",
|
|
41375
|
+
...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
|
|
41376
|
+
};
|
|
41377
|
+
deps.queueProgress?.(base);
|
|
41378
|
+
if (options.enqueueOnly) return { ...base, status: "queued" };
|
|
41379
|
+
} else if (read.state === "OPEN" && !read.autoMergeRequest) {
|
|
41380
|
+
await sleep2(PR_LAND_EJECT_CONFIRM_MS);
|
|
41381
|
+
const again = await deps.readMergeQueue(pr, base.repo);
|
|
41382
|
+
if (again.state === "MERGED") {
|
|
41383
|
+
observe(again);
|
|
41384
|
+
return queueMerged(base, deps);
|
|
41385
|
+
}
|
|
41386
|
+
if (again.isInMergeQueue || again.autoMergeRequest) {
|
|
41387
|
+
snapshot = again;
|
|
41388
|
+
continue;
|
|
41389
|
+
}
|
|
41390
|
+
const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
|
|
41391
|
+
const result2 = {
|
|
41392
|
+
...base,
|
|
41393
|
+
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
41394
|
+
...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
|
|
41395
|
+
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)}`
|
|
41396
|
+
};
|
|
41397
|
+
deps.queueProgress?.(result2);
|
|
41398
|
+
return result2;
|
|
41399
|
+
}
|
|
41400
|
+
await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
|
|
41401
|
+
}
|
|
41402
|
+
const result = {
|
|
41403
|
+
...base,
|
|
41404
|
+
mergeQueue: { ...base.mergeQueue, state: "timeout" },
|
|
41405
|
+
error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 ${prQueueReadHint(pr, base.repo)} and rerun pr land to resume polling`
|
|
41406
|
+
};
|
|
41407
|
+
deps.queueProgress?.(result);
|
|
41408
|
+
return result;
|
|
41409
|
+
}
|
|
41410
|
+
function prLandOutcome(result) {
|
|
41411
|
+
if (result.status === "queue-admission-pending") return "queue-admission-pending";
|
|
41412
|
+
if (result.status !== "failed" && result.status !== "queued") return "merged";
|
|
41413
|
+
if (result.status === "queued") return "queued";
|
|
41414
|
+
const state = result.mergeQueue?.state;
|
|
41415
|
+
return state === "ejected" || state === "timeout" ? state : "failed";
|
|
41416
|
+
}
|
|
41417
|
+
function failuresAllOnBase(prFailures, baseFailures) {
|
|
41418
|
+
const base = new Set(baseFailures);
|
|
41419
|
+
return prFailures.length > 0 && prFailures.every((f) => base.has(f));
|
|
41420
|
+
}
|
|
41421
|
+
async function landMany(prs, landOne, cleanup, hooks) {
|
|
41422
|
+
const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
|
|
41423
|
+
return Promise.all(prs.map(async (pr) => {
|
|
41424
|
+
let result = await safe(pr);
|
|
41425
|
+
if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
|
|
41426
|
+
const baseRed = await hooks?.baseRedCheck?.(pr, result).catch(() => void 0);
|
|
41427
|
+
if (baseRed) {
|
|
41428
|
+
result = { ...result, baseRed: { base: result.base, baseRunId: baseRed.baseRunId }, error: `base-red: failures already on ${result.base} (${baseRed.baseRunId})` };
|
|
41429
|
+
} else {
|
|
41430
|
+
hooks?.onRetry?.(pr, result);
|
|
41431
|
+
result = { ...await safe(pr), retriedAfterEjection: true };
|
|
41432
|
+
}
|
|
41433
|
+
}
|
|
41434
|
+
const outcome = prLandOutcome(result);
|
|
41435
|
+
const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
|
|
41436
|
+
hooks?.onDone?.();
|
|
41437
|
+
return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
|
|
41438
|
+
}));
|
|
41439
|
+
}
|
|
41440
|
+
function renderPrLandTable(entries) {
|
|
41441
|
+
const rows = entries.map((e) => [
|
|
41442
|
+
`#${e.pr}`,
|
|
41443
|
+
e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
|
|
41444
|
+
[e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
|
|
41445
|
+
]);
|
|
41446
|
+
const width = (i) => Math.max(...rows.map((r) => r[i].length));
|
|
41447
|
+
return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
|
|
41448
|
+
}
|
|
41449
|
+
function landCredentials(results) {
|
|
41450
|
+
const each = results.map((r) => r.credential ?? "unknown");
|
|
41451
|
+
return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
|
|
41452
|
+
}
|
|
41453
|
+
|
|
41454
|
+
// src/pr-merge-queue.ts
|
|
41455
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
41456
|
+
|
|
40992
41457
|
// src/test-policy-core.ts
|
|
40993
41458
|
var import_node_child_process11 = require("node:child_process");
|
|
40994
41459
|
var import_node_fs32 = require("node:fs");
|
|
@@ -41527,6 +41992,9 @@ function loadPolicy(root, readFile10 = readFileOrNull2) {
|
|
|
41527
41992
|
if (raw == null) return { mandatory: [], declared: false };
|
|
41528
41993
|
return parsePolicy(raw, POLICY_FILE);
|
|
41529
41994
|
}
|
|
41995
|
+
function parseTestPolicy(raw) {
|
|
41996
|
+
return parsePolicy(raw, POLICY_FILE);
|
|
41997
|
+
}
|
|
41530
41998
|
function parsePolicy(raw, source) {
|
|
41531
41999
|
try {
|
|
41532
42000
|
const parsed = JSON.parse(raw);
|
|
@@ -41546,6 +42014,10 @@ function parsePolicy(raw, source) {
|
|
|
41546
42014
|
))) {
|
|
41547
42015
|
throw new Error("protected must be an array of { path, why? } entries");
|
|
41548
42016
|
}
|
|
42017
|
+
const aggregateJobs = record2.aggregateJobs;
|
|
42018
|
+
if (aggregateJobs !== void 0 && (!Array.isArray(aggregateJobs) || aggregateJobs.some((job) => typeof job !== "string" || !job.trim()))) {
|
|
42019
|
+
throw new Error("aggregateJobs must be an array of workflow job keys");
|
|
42020
|
+
}
|
|
41549
42021
|
return { ...record2, declared: true };
|
|
41550
42022
|
} catch (error) {
|
|
41551
42023
|
throw new Error(`${source} is not valid JSON: ${error.message}`);
|
|
@@ -41941,6 +42413,13 @@ var writeApi = async (args) => {
|
|
|
41941
42413
|
const { stdout, credential } = await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
|
|
41942
42414
|
return { stdout, credential };
|
|
41943
42415
|
};
|
|
42416
|
+
var autoMergeApi = async (pr, repo, head) => {
|
|
42417
|
+
const args = ["pr", "merge", pr, "-R", repo, "--squash", "--auto", "--match-head-commit", head];
|
|
42418
|
+
const env = await ghFilingAppEnv("gh", args, true);
|
|
42419
|
+
if (!env) throw new Error("app actor: filing write token unavailable for merge-queue auto-merge fallback");
|
|
42420
|
+
await execFileP("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024, env });
|
|
42421
|
+
return { credential: "app_installation" };
|
|
42422
|
+
};
|
|
41944
42423
|
var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
|
|
41945
42424
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
41946
42425
|
function admissionJournal(repo, pr, root) {
|
|
@@ -41956,7 +42435,7 @@ function admissionJournal(repo, pr, root) {
|
|
|
41956
42435
|
const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
|
|
41957
42436
|
const validate = (value, previous) => {
|
|
41958
42437
|
const row = value;
|
|
41959
|
-
if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
|
|
42438
|
+
if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
|
|
41960
42439
|
throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
|
|
41961
42440
|
}
|
|
41962
42441
|
if (!previous) {
|
|
@@ -41994,7 +42473,17 @@ function admissionJournal(repo, pr, root) {
|
|
|
41994
42473
|
},
|
|
41995
42474
|
save(state, uuid, queueEntry, credential) {
|
|
41996
42475
|
const previous = load();
|
|
41997
|
-
const
|
|
42476
|
+
const nextEntry = queueEntry ?? previous.queueEntry;
|
|
42477
|
+
const queueWitnessed = nextEntry ? true : state === "enqueued" ? previous.queueWitnessed ?? false : previous.queueWitnessed;
|
|
42478
|
+
const receipt = validate({
|
|
42479
|
+
...previous,
|
|
42480
|
+
state,
|
|
42481
|
+
observedAt: Date.now(),
|
|
42482
|
+
...uuid ? { uuid } : {},
|
|
42483
|
+
...nextEntry ? { queueEntry: nextEntry } : {},
|
|
42484
|
+
...queueWitnessed !== void 0 ? { queueWitnessed } : {},
|
|
42485
|
+
...credential && previous.credential === void 0 ? { credential } : {}
|
|
42486
|
+
}, previous);
|
|
41998
42487
|
(0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
|
|
41999
42488
|
load();
|
|
42000
42489
|
}
|
|
@@ -42017,19 +42506,31 @@ function admissionResponse(output) {
|
|
|
42017
42506
|
}
|
|
42018
42507
|
return { code: Number(response[1]), body: result };
|
|
42019
42508
|
}
|
|
42509
|
+
function admissionRejected(reason, repo, pr) {
|
|
42510
|
+
const queueHint = repo && pr ? ` Read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\` (no queue-admission state file is meant to be inspected or edited directly under %LOCALAPPDATA%/MMI Future/mmi-cli).` : " Read the queue entry with `mmi-cli devops read pr queue <pr> --repo <owner/repo> --json` (no queue-admission state file is meant to be inspected or edited directly under %LOCALAPPDATA%/MMI Future/mmi-cli).";
|
|
42511
|
+
if (/enqueuer is not authorized to merge|not authorized to merge/i.test(reason)) {
|
|
42512
|
+
return {
|
|
42513
|
+
mergeStatus: "failed",
|
|
42514
|
+
retryable: false,
|
|
42515
|
+
admissionCause: "enqueuer-not-authorized",
|
|
42516
|
+
error: `queue admission rejected (missing merge permission): ${reason}. The landing identity reported in \`credential\` may not merge into this base through the merge queue. Granting it (the GitHub App's merge permission, or a ruleset bypass) is a repository or organization setting only an admin can change; do not rerun pr land until that grant exists, and never fall back to a personal token.${queueHint}`
|
|
42517
|
+
};
|
|
42518
|
+
}
|
|
42519
|
+
return {
|
|
42520
|
+
mergeStatus: "failed",
|
|
42521
|
+
retryable: false,
|
|
42522
|
+
error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.${queueHint}`
|
|
42523
|
+
};
|
|
42524
|
+
}
|
|
42020
42525
|
async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
|
|
42021
42526
|
const unknown = (reason) => ({
|
|
42022
42527
|
mergeStatus: "failed",
|
|
42023
42528
|
retryable: false,
|
|
42024
42529
|
outcomeUnknown: true,
|
|
42025
|
-
error: `queue submission outcome unknown: ${reason}.
|
|
42026
|
-
});
|
|
42027
|
-
const rejected = (reason) => ({
|
|
42028
|
-
mergeStatus: "failed",
|
|
42029
|
-
retryable: false,
|
|
42030
|
-
error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.`
|
|
42530
|
+
error: `queue submission outcome unknown: ${reason}. Never hand-edit files under the mmi-cli queue-admissions receipt store (%LOCALAPPDATA%/MMI Future/mmi-cli/queue-admissions); read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\` and rerun \`mmi-cli devops pr land ${pr} --repo ${repo} --json\` to reconcile read-only \u2014 no new request was sent after this uncertainty.`
|
|
42031
42531
|
});
|
|
42032
|
-
const
|
|
42532
|
+
const rejected = (reason) => admissionRejected(reason, repo, pr);
|
|
42533
|
+
const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
|
|
42033
42534
|
try {
|
|
42034
42535
|
const observed = await readPrMergeQueue(pr, repo, read);
|
|
42035
42536
|
if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
|
|
@@ -42041,15 +42542,15 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42041
42542
|
return { mergeStatus: "auto-merge-enqueued" };
|
|
42042
42543
|
}
|
|
42043
42544
|
const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
|
|
42044
|
-
|
|
42045
|
-
|
|
42545
|
+
const boundMs = saved?.queueEntry ? Date.parse(saved.queueEntry.enqueuedAt) : resumingSaved && saved?.state === "enqueued" && saved.queueWitnessed === void 0 ? saved.observedAt : void 0;
|
|
42546
|
+
if (saved && boundMs !== void 0 && Number.isFinite(boundMs) && observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === saved.head) {
|
|
42046
42547
|
const events = await readArrayPages(
|
|
42047
42548
|
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
42048
42549
|
options.timelineRead ?? read,
|
|
42049
42550
|
"queue ejection timeline unreadable"
|
|
42050
42551
|
);
|
|
42051
42552
|
const times = (name) => events.filter((event) => event.event === name).map((event) => Date.parse(event.created_at));
|
|
42052
|
-
const adds = times("added_to_merge_queue").filter((at) => at >=
|
|
42553
|
+
const adds = times("added_to_merge_queue").filter((at) => at >= boundMs);
|
|
42053
42554
|
const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
|
|
42054
42555
|
if (removals.length === 1) {
|
|
42055
42556
|
journal.save("ejected");
|
|
@@ -42088,7 +42589,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42088
42589
|
options.selection.selected = true;
|
|
42089
42590
|
options.selection.intentId = receipt.intentId;
|
|
42090
42591
|
}
|
|
42091
|
-
if (
|
|
42592
|
+
if (options.enqueueOnly && options.force) {
|
|
42593
|
+
return unknown(`refusing --enqueue-only --force for ${repo}#${pr} while receipt for head ${receipt.head} is in state '${receipt.state}' (operation ${receipt.uuid ?? "unconfirmed"}): drop --enqueue-only --force and run the read-only reconcile first`);
|
|
42594
|
+
}
|
|
42595
|
+
if (receipt.state === "enqueued" || receipt.state === "merged") return reconcile(`reconcile-needed for ${repo}#${pr}, submitted head ${receipt.head}, operation ${receipt.uuid ?? "completed immediately"}: admission completed but current membership is absent; inspect the exact queue candidate/ejection before a new intent`, receipt.state === "enqueued", true);
|
|
42092
42596
|
if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
|
|
42093
42597
|
} else {
|
|
42094
42598
|
if (resume) return rejected("the previous operation failed; rerun after reviewing its cause");
|
|
@@ -42138,13 +42642,33 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42138
42642
|
}
|
|
42139
42643
|
return reconcile(`GitHub reported ${body.status}, but matching-head membership or merged state is unconfirmed`, body.status === "enqueued");
|
|
42140
42644
|
}
|
|
42645
|
+
const tryAutoMergeFallback = async (reason, uuid) => {
|
|
42646
|
+
const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
|
|
42647
|
+
if (!/enqueuer is not authorized to merge/i.test(reason) || !autoMerge) return void 0;
|
|
42648
|
+
let fallback;
|
|
42649
|
+
try {
|
|
42650
|
+
fallback = await autoMerge(pr, repo, head);
|
|
42651
|
+
} catch {
|
|
42652
|
+
return void 0;
|
|
42653
|
+
}
|
|
42654
|
+
const fallbackCredential = fallback?.credential ?? credential ?? receipt?.credential;
|
|
42655
|
+
try {
|
|
42656
|
+
journal.save("enqueued", uuid, void 0, fallbackCredential);
|
|
42657
|
+
} catch {
|
|
42658
|
+
return reconcile("the accepted auto-merge fallback could not be checkpointed");
|
|
42659
|
+
}
|
|
42660
|
+
return reconcile("the auto-merge fallback completed, but matching-head membership or merged state is unconfirmed", true);
|
|
42661
|
+
};
|
|
42141
42662
|
if (code === 400 || code === 403 || code === 404 || code === 422) {
|
|
42663
|
+
const reason = body.details?.message ?? body.message ?? `HTTP ${code}`;
|
|
42664
|
+
const fallbackResult = await tryAutoMergeFallback(reason);
|
|
42665
|
+
if (fallbackResult) return fallbackResult;
|
|
42142
42666
|
try {
|
|
42143
42667
|
journal.save("failed", void 0, void 0, credential);
|
|
42144
42668
|
} catch {
|
|
42145
42669
|
return unknown("the explicit rejection could not be checkpointed");
|
|
42146
42670
|
}
|
|
42147
|
-
return rejected(
|
|
42671
|
+
return rejected(reason);
|
|
42148
42672
|
}
|
|
42149
42673
|
const details = body.details;
|
|
42150
42674
|
if (code !== 202 && code !== 409 || body.status !== "pending" || !UUID.test(details?.uuid ?? "")) return reconcile("the send returned no recognized admission result");
|
|
@@ -42175,6 +42699,28 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42175
42699
|
continue;
|
|
42176
42700
|
}
|
|
42177
42701
|
if (!["failed", "enqueued", "merged"].includes(status ?? "")) return reconcile("the operation returned an unrecognized state");
|
|
42702
|
+
if (status === "failed") {
|
|
42703
|
+
const reason = details?.message ?? "GitHub reported a failed operation";
|
|
42704
|
+
const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
|
|
42705
|
+
if (/enqueuer is not authorized to merge/i.test(reason) && autoMerge && receipt.head === head) {
|
|
42706
|
+
let fallback = void 0;
|
|
42707
|
+
let fallbackSucceeded = false;
|
|
42708
|
+
try {
|
|
42709
|
+
fallback = await autoMerge(pr, repo, head);
|
|
42710
|
+
fallbackSucceeded = true;
|
|
42711
|
+
} catch {
|
|
42712
|
+
}
|
|
42713
|
+
if (fallbackSucceeded) {
|
|
42714
|
+
const fallbackCredential = fallback?.credential ?? receipt?.credential;
|
|
42715
|
+
try {
|
|
42716
|
+
journal.save("enqueued", receipt.uuid, void 0, fallbackCredential);
|
|
42717
|
+
} catch {
|
|
42718
|
+
return reconcile("the accepted auto-merge fallback could not be checkpointed");
|
|
42719
|
+
}
|
|
42720
|
+
return reconcile("the auto-merge fallback completed, but matching-head membership or merged state is unconfirmed", true);
|
|
42721
|
+
}
|
|
42722
|
+
}
|
|
42723
|
+
}
|
|
42178
42724
|
try {
|
|
42179
42725
|
journal.save(status, receipt.uuid);
|
|
42180
42726
|
} catch {
|
|
@@ -42246,7 +42792,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42246
42792
|
status: "preparation-needed",
|
|
42247
42793
|
reason,
|
|
42248
42794
|
issueTargets: targets.map((number) => `${repo}#${number}`),
|
|
42249
|
-
nextAction: reason === "queue-message" ? "PR author: remove the closing phrases from the source commit or PR text using Part of #N / 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."
|
|
42795
|
+
nextAction: reason === "queue-message" ? "PR author: if the PR is meant to close these issues, add `Closes #N` to the PR body (`mmi-cli devops pr edit <pr> --body-file <path>`) and rerun pr land without pushing a new head; if they must stay open, remove the closing phrases from the source commit or PR text using Part of #N / 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."
|
|
42250
42796
|
},
|
|
42251
42797
|
error: `queue preparation needed: ${detail}`
|
|
42252
42798
|
});
|
|
@@ -42358,6 +42904,100 @@ async function findPrMergeGroupRun(pr, repo, base, gh2 = readApi) {
|
|
|
42358
42904
|
const run = pages.flatMap((page) => page.workflow_runs).filter((run2) => run2.head_branch?.startsWith(prefix)).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id - a.id)[0];
|
|
42359
42905
|
return run ? { id: run.id, url: run.html_url, conclusion: run.conclusion } : void 0;
|
|
42360
42906
|
}
|
|
42907
|
+
function workflowNeeds(text) {
|
|
42908
|
+
const doc = (0, import_yaml3.parse)(text);
|
|
42909
|
+
const out = /* @__PURE__ */ new Map();
|
|
42910
|
+
for (const [key, job] of Object.entries(doc?.jobs ?? {})) {
|
|
42911
|
+
const raw = job?.needs;
|
|
42912
|
+
const needs = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw.filter((n) => typeof n === "string") : [];
|
|
42913
|
+
const name = typeof job?.name === "string" ? job.name : key;
|
|
42914
|
+
out.set(key, { name, needs, matrix: job?.strategy?.matrix != null, dynamicName: name.includes("${{") });
|
|
42915
|
+
}
|
|
42916
|
+
return out;
|
|
42917
|
+
}
|
|
42918
|
+
function jobKey(name, jobs) {
|
|
42919
|
+
const leg = name.replace(/ \([^()]*\)$/, "");
|
|
42920
|
+
const matches = [...jobs].filter(([, job]) => job.name === name || job.matrix && leg !== name && job.name === leg).map(([key]) => key);
|
|
42921
|
+
if (matches.length !== 1) return void 0;
|
|
42922
|
+
const collides = [...jobs.keys()].some((key) => key !== matches[0] && (key === name || key === leg));
|
|
42923
|
+
return collides ? void 0 : matches[0];
|
|
42924
|
+
}
|
|
42925
|
+
function aggregateKey(name, jobs, declared) {
|
|
42926
|
+
const matches = [...jobs].filter(([, job]) => job.name === name).map(([key2]) => key2);
|
|
42927
|
+
if (matches.length !== 1) return void 0;
|
|
42928
|
+
const key = matches[0];
|
|
42929
|
+
if ([...jobs.keys()].some((other) => other !== key && other === name)) return void 0;
|
|
42930
|
+
return declared.has(key) && !jobs.get(key).matrix ? key : void 0;
|
|
42931
|
+
}
|
|
42932
|
+
var JOB_PAGES = 10;
|
|
42933
|
+
var JOBS_PER_PAGE = 100;
|
|
42934
|
+
async function attemptJobs(repo, run, gh2) {
|
|
42935
|
+
if (!Number.isSafeInteger(run.run_attempt) || run.run_attempt < 1) return void 0;
|
|
42936
|
+
const jobs = [];
|
|
42937
|
+
let total;
|
|
42938
|
+
for (let page = 1; page <= JOB_PAGES; page++) {
|
|
42939
|
+
const body = JSON.parse(await gh2([`repos/${repo}/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=${JOBS_PER_PAGE}&page=${page}`]));
|
|
42940
|
+
if (!Number.isSafeInteger(body.total_count) || !Array.isArray(body.jobs)) return void 0;
|
|
42941
|
+
total ??= body.total_count;
|
|
42942
|
+
if (body.total_count !== total) return void 0;
|
|
42943
|
+
jobs.push(...body.jobs);
|
|
42944
|
+
if (jobs.length >= total) return jobs.length === total && new Set(jobs.map((j) => j.id)).size === jobs.length ? jobs : void 0;
|
|
42945
|
+
if (body.jobs.length !== JOBS_PER_PAGE) return void 0;
|
|
42946
|
+
}
|
|
42947
|
+
return void 0;
|
|
42948
|
+
}
|
|
42949
|
+
async function runFailingTests(repo, run, gh2, declared) {
|
|
42950
|
+
const jobs = await attemptJobs(repo, run, gh2);
|
|
42951
|
+
if (!jobs) return void 0;
|
|
42952
|
+
const bad = jobs.filter((j) => !["success", "skipped", "neutral"].includes(j.conclusion ?? ""));
|
|
42953
|
+
if (!bad.length || bad.some((j) => j.conclusion !== "failure")) return void 0;
|
|
42954
|
+
const names = [];
|
|
42955
|
+
const markerless = [];
|
|
42956
|
+
for (const job of bad) {
|
|
42957
|
+
const found = failingTestIds(await gh2([`repos/${repo}/actions/jobs/${job.id}/logs`]));
|
|
42958
|
+
if (!found) return void 0;
|
|
42959
|
+
if (found.length) names.push(...found);
|
|
42960
|
+
else markerless.push(job);
|
|
42961
|
+
}
|
|
42962
|
+
if (!names.length) return void 0;
|
|
42963
|
+
if (!markerless.length) return names;
|
|
42964
|
+
const aggregates = await declared();
|
|
42965
|
+
if (!aggregates.size || !run.path || !/^[a-f0-9]{40}$/i.test(run.head_sha ?? "")) return void 0;
|
|
42966
|
+
const workflow = workflowNeeds(await gh2([`repos/${repo}/contents/${run.path}?ref=${run.head_sha}`, "-H", "Accept: application/vnd.github.raw"]));
|
|
42967
|
+
if ([...workflow.values()].some((job) => job.dynamicName)) return void 0;
|
|
42968
|
+
const failedKeys = new Set(bad.map((j) => jobKey(j.name ?? "", workflow)).filter((k) => !!k));
|
|
42969
|
+
for (const job of markerless) {
|
|
42970
|
+
const key = aggregateKey(job.name ?? "", workflow, aggregates);
|
|
42971
|
+
if (!key || !workflow.get(key).needs.some((n) => failedKeys.has(n))) return void 0;
|
|
42972
|
+
}
|
|
42973
|
+
return names;
|
|
42974
|
+
}
|
|
42975
|
+
async function declaredAggregates(repo, sha, gh2) {
|
|
42976
|
+
if (!/^[a-f0-9]{40}$/i.test(sha ?? "")) return /* @__PURE__ */ new Set();
|
|
42977
|
+
try {
|
|
42978
|
+
return new Set(parseTestPolicy(await gh2([`repos/${repo}/contents/test-policy.json?ref=${sha}`, "-H", "Accept: application/vnd.github.raw"])).aggregateJobs ?? []);
|
|
42979
|
+
} catch {
|
|
42980
|
+
return /* @__PURE__ */ new Set();
|
|
42981
|
+
}
|
|
42982
|
+
}
|
|
42983
|
+
var appOnlyReadApi = async (args) => {
|
|
42984
|
+
const env = await ghReadEnv();
|
|
42985
|
+
if (env?.MMI_GH_POOL !== "app") throw new Error("base-red: no App read environment; evidence unread");
|
|
42986
|
+
return (await execFileP("gh", ["api", ...args], { timeout: 2e4, maxBuffer: 16 * 1024 * 1024, env })).stdout;
|
|
42987
|
+
};
|
|
42988
|
+
async function readBaseRedVerdict(result, gh2 = appOnlyReadApi) {
|
|
42989
|
+
const run = result.mergeQueue?.ejectedRun;
|
|
42990
|
+
if (run?.conclusion !== "failure") return void 0;
|
|
42991
|
+
const ejected = JSON.parse(await gh2([`repos/${result.repo}/actions/runs/${run.id}`]));
|
|
42992
|
+
const { workflow_runs } = JSON.parse(await gh2([`repos/${result.repo}/actions/workflows/${ejected.workflow_id}/runs?branch=${encodeURIComponent(result.base)}&event=push&per_page=1`]));
|
|
42993
|
+
const baseRun = workflow_runs?.[0];
|
|
42994
|
+
if (baseRun?.status !== "completed" || baseRun.conclusion !== "failure") return void 0;
|
|
42995
|
+
let declared;
|
|
42996
|
+
const aggregates = () => declared ??= declaredAggregates(result.repo, baseRun.head_sha, gh2);
|
|
42997
|
+
const mine = await runFailingTests(result.repo, { ...ejected, id: run.id }, gh2, aggregates);
|
|
42998
|
+
const base = mine && await runFailingTests(result.repo, baseRun, gh2, aggregates);
|
|
42999
|
+
return mine && base && failuresAllOnBase(mine, base) ? { baseRunId: String(baseRun.id) } : void 0;
|
|
43000
|
+
}
|
|
42361
43001
|
function prMergeQueueLine(result) {
|
|
42362
43002
|
const queue = result.mergeQueue;
|
|
42363
43003
|
if (!queue) return void 0;
|
|
@@ -42513,38 +43153,57 @@ function registerCancelOrphans(ci2, writeToken) {
|
|
|
42513
43153
|
});
|
|
42514
43154
|
}
|
|
42515
43155
|
var RUN_CANCEL_STATUS = "cancel-requested";
|
|
42516
|
-
|
|
43156
|
+
var RUN_FORCE_CANCEL_STATUS = "force-cancel-requested";
|
|
43157
|
+
async function cancelWorkflowRun(repo, id, deps, force = false) {
|
|
42517
43158
|
const raw = String(id).trim();
|
|
42518
43159
|
if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run cancel: <id> must be a positive workflow run id (received '${String(id)}')`);
|
|
42519
43160
|
const runId = Number(raw);
|
|
42520
43161
|
if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
|
|
42521
43162
|
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
|
|
42522
43163
|
try {
|
|
42523
|
-
|
|
43164
|
+
if (force) {
|
|
43165
|
+
if (!deps.forceCancel) throw new Error("force-cancel adapter unavailable");
|
|
43166
|
+
await deps.forceCancel(repo, runId);
|
|
43167
|
+
} else await deps.cancel(repo, runId);
|
|
42524
43168
|
} catch (error) {
|
|
42525
43169
|
const detail = String(error.stderr ?? error);
|
|
42526
43170
|
throw new Error(/\b403\b/.test(detail) ? "run cancel: GitHub App denied (HTTP 403); actions:write permission is required; no personal-token fallback" : `run cancel: ${detail}`);
|
|
42527
43171
|
}
|
|
42528
|
-
|
|
43172
|
+
const settledInfo = force && deps.read ? await settled(repo, runId, { read: deps.read, sleep: deps.sleep }).then((runStatus) => ({ settled: runStatus === "completed", runStatus })) : void 0;
|
|
43173
|
+
return { ok: true, repo, runId, ...force ? { force: true } : {}, status: force ? RUN_FORCE_CANCEL_STATUS : RUN_CANCEL_STATUS, ...settledInfo };
|
|
42529
43174
|
}
|
|
42530
43175
|
function registerRunCancel(run, writeToken) {
|
|
42531
|
-
run.command("cancel <id>").description("cancel one queued or in-progress workflow run on the org App (actions:write); requests the cancel and prints a receipt").requiredOption("--repo <owner/repo>", "repository that owns the run").option("--json", "machine-readable receipt").action(async (id, options) => {
|
|
43176
|
+
run.command("cancel <id>").description("cancel one queued or in-progress workflow run on the org App (actions:write); requests the cancel and prints a receipt").requiredOption("--repo <owner/repo>", "repository that owns the run").option("--force", "force-cancel the run so always() jobs stop too (#9512)").option("--json", "machine-readable receipt").action(async (id, options) => {
|
|
42532
43177
|
let receipt;
|
|
43178
|
+
let token;
|
|
43179
|
+
const read = async (args) => (await execFileP("gh", ["api", ...args], {
|
|
43180
|
+
timeout: 2e4,
|
|
43181
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
43182
|
+
env: await ghReadEnv()
|
|
43183
|
+
})).stdout;
|
|
42533
43184
|
try {
|
|
42534
43185
|
receipt = await cancelWorkflowRun(options.repo, id, {
|
|
43186
|
+
read,
|
|
42535
43187
|
cancel: async (repo, runId) => {
|
|
42536
|
-
|
|
43188
|
+
token ??= await writeToken();
|
|
42537
43189
|
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
|
|
42538
43190
|
timeout: 2e4,
|
|
42539
|
-
env: { ...process.env, GH_TOKEN: token }
|
|
43191
|
+
env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
|
|
43192
|
+
});
|
|
43193
|
+
},
|
|
43194
|
+
forceCancel: async (repo, runId) => {
|
|
43195
|
+
token ??= await writeToken();
|
|
43196
|
+
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/force-cancel`, "--method", "POST"], {
|
|
43197
|
+
timeout: 2e4,
|
|
43198
|
+
env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
|
|
42540
43199
|
});
|
|
42541
43200
|
}
|
|
42542
|
-
});
|
|
43201
|
+
}, options.force === true);
|
|
42543
43202
|
} catch (error) {
|
|
42544
43203
|
return fail(error.message);
|
|
42545
43204
|
}
|
|
42546
43205
|
if (options.json) console.log(JSON.stringify(receipt));
|
|
42547
|
-
else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 cancel requested (status: ${receipt.status})`);
|
|
43206
|
+
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}` : ""})`);
|
|
42548
43207
|
});
|
|
42549
43208
|
}
|
|
42550
43209
|
|
|
@@ -42963,6 +43622,8 @@ function registerBoardCommands(program3) {
|
|
|
42963
43622
|
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
42964
43623
|
if (result.claimed) for (const line of formatDecisionLines(result.ref, ruled[i].decisions ?? [])) console.log(line);
|
|
42965
43624
|
});
|
|
43625
|
+
const summary = formatBulkClaimSummary(bulk.results);
|
|
43626
|
+
if (summary) console.log(summary);
|
|
42966
43627
|
printClaimWarnings([...bulk.warnings ?? [], ...rulingWarnings]);
|
|
42967
43628
|
}
|
|
42968
43629
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
@@ -44978,7 +45639,7 @@ function registerSecretsCommands(program3) {
|
|
|
44978
45639
|
return;
|
|
44979
45640
|
}
|
|
44980
45641
|
const d = makeSecretsDeps(cfg);
|
|
44981
|
-
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
45642
|
+
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
|
|
44982
45643
|
const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
|
|
44983
45644
|
const repo = o.repo ?? `mutmutco/${slug}`;
|
|
44984
45645
|
const meta = await fetchProjectBySlug(slug, regDeps);
|
|
@@ -45033,7 +45694,7 @@ function registerSecretsCommands(program3) {
|
|
|
45033
45694
|
return;
|
|
45034
45695
|
}
|
|
45035
45696
|
const d = makeSecretsDeps(cfg);
|
|
45036
|
-
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
45697
|
+
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
|
|
45037
45698
|
const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
|
|
45038
45699
|
const repo = o.repo ?? `mutmutco/${slug}`;
|
|
45039
45700
|
const meta = await fetchProjectBySlug(slug, regDeps);
|
|
@@ -51977,386 +52638,6 @@ function boardAdvanceFailureMessage(result) {
|
|
|
51977
52638
|
return `pr merge: board advance incomplete for ${named} \u2014 the PR MERGED, but ${failures.length === 1 ? "this issue" : "these issues"} did not reach Done on the board.`;
|
|
51978
52639
|
}
|
|
51979
52640
|
|
|
51980
|
-
// src/pr-land.ts
|
|
51981
|
-
var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
|
|
51982
|
-
var PR_LAND_QUEUE_BATCH_SIZE = 5;
|
|
51983
|
-
function prHeadBehindBase(input) {
|
|
51984
|
-
if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
|
|
51985
|
-
return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
|
|
51986
|
-
}
|
|
51987
|
-
function prHeadUpdateRemedy(head, base) {
|
|
51988
|
-
return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
51989
|
-
}
|
|
51990
|
-
function prLandedBranchFinality(prNumber, base) {
|
|
51991
|
-
return `PR #${prNumber} is merged, and that is final \u2014 stopping a local waiter or monitor does not unmerge it. The head branch is now landed; further commits pushed to it reach nothing. If more work is needed, run \`git fetch origin ${base} && git switch -c <new-branch> origin/${base}\`, then cherry-pick anything already committed \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#merged-pr-head-reuse`;
|
|
51992
|
-
}
|
|
51993
|
-
var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
|
|
51994
|
-
function isProtectedPrHead(head) {
|
|
51995
|
-
return PROTECTED_PR_HEAD_BRANCHES.includes(head);
|
|
51996
|
-
}
|
|
51997
|
-
function protectedPrHeadRemedy(head, base) {
|
|
51998
|
-
return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
51999
|
-
}
|
|
52000
|
-
var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
|
|
52001
|
-
var PR_HEAD_UPDATE_API_READ_DELAY_MS = 15e3;
|
|
52002
|
-
async function updatePrHeadFromBase(input) {
|
|
52003
|
-
const remedy = prHeadUpdateRemedy(input.head, input.base);
|
|
52004
|
-
if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
|
|
52005
|
-
if (input.localCheckedOut) {
|
|
52006
|
-
const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
52007
|
-
await input.git(["fetch", "origin", input.base]);
|
|
52008
|
-
try {
|
|
52009
|
-
await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
|
|
52010
|
-
} catch (e) {
|
|
52011
|
-
await input.git(["merge", "--abort"]).catch(() => {
|
|
52012
|
-
});
|
|
52013
|
-
throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
|
|
52014
|
-
}
|
|
52015
|
-
await input.git(["push", "origin", input.head]);
|
|
52016
|
-
const to = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
52017
|
-
return { from: from2, to, base: input.base, via: "local-merge" };
|
|
52018
|
-
}
|
|
52019
|
-
const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
|
|
52020
|
-
const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
|
|
52021
|
-
const from = await readHead2();
|
|
52022
|
-
try {
|
|
52023
|
-
await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
|
|
52024
|
-
} catch (e) {
|
|
52025
|
-
throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
|
|
52026
|
-
}
|
|
52027
|
-
const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52028
|
-
for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
|
|
52029
|
-
const to = await readHead2();
|
|
52030
|
-
if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
|
|
52031
|
-
await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
|
|
52032
|
-
}
|
|
52033
|
-
throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
|
|
52034
|
-
}
|
|
52035
|
-
var PR_LAND_POLL_MS = 6e4;
|
|
52036
|
-
var PR_LAND_POLL_MAX_MS = 5 * 6e4;
|
|
52037
|
-
function prLandPollDelayMs(attempt, waiting) {
|
|
52038
|
-
const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
|
|
52039
|
-
const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
|
|
52040
|
-
return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
|
|
52041
|
-
}
|
|
52042
|
-
var PR_LAND_EJECT_CONFIRM_MS = 2e4;
|
|
52043
|
-
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
52044
|
-
var PR_LAND_STATE_READ_RETRIES = 3;
|
|
52045
|
-
var PR_LAND_STATE_READ_DELAY_MS = 15e3;
|
|
52046
|
-
var PR_LAND_MERGE_RETRY_DELAY_MS = 3e4;
|
|
52047
|
-
async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
52048
|
-
const now = deps.now ?? (() => Date.now());
|
|
52049
|
-
const deadline = now() + PR_LAND_ENQUEUE_TIMEOUT_MS;
|
|
52050
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52051
|
-
const first = await deps.mergeAuto(prNumber, repo);
|
|
52052
|
-
let result = first;
|
|
52053
|
-
let transientRetried = false;
|
|
52054
|
-
while (result.mergeStatus === "failed") {
|
|
52055
|
-
if (result.retryable === false || result.outcomeUnknown || result.preparation) return result;
|
|
52056
|
-
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false, checksPending: false }));
|
|
52057
|
-
if (ready.open && ready.checksPending) {
|
|
52058
|
-
if (now() >= deadline) break;
|
|
52059
|
-
await sleep2(Math.min(PR_LAND_POLL_MS, deadline - now()));
|
|
52060
|
-
if (now() >= deadline) break;
|
|
52061
|
-
const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
|
|
52062
|
-
if (checks.status !== "success" && checks.status !== "skipped") {
|
|
52063
|
-
return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
|
|
52064
|
-
registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
|
|
52065
|
-
}
|
|
52066
|
-
if (now() >= deadline) break;
|
|
52067
|
-
} else {
|
|
52068
|
-
if (!ready.open || !ready.mergeable || !ready.checksPassing || transientRetried) return result;
|
|
52069
|
-
transientRetried = true;
|
|
52070
|
-
if (now() >= deadline) break;
|
|
52071
|
-
await sleep2(Math.min(PR_LAND_MERGE_RETRY_DELAY_MS, deadline - now()));
|
|
52072
|
-
if (now() >= deadline) break;
|
|
52073
|
-
}
|
|
52074
|
-
const retried = await deps.mergeAuto(prNumber, repo);
|
|
52075
|
-
result = retried.mergeStatus !== "failed" ? retried : {
|
|
52076
|
-
...retried,
|
|
52077
|
-
error: `merge retry after transient failure also failed: first attempt: ${first.error ?? "unknown error"}
|
|
52078
|
-
retry: ${retried.error ?? "unknown error"}`
|
|
52079
|
-
};
|
|
52080
|
-
}
|
|
52081
|
-
return result.mergeStatus === "failed" ? { ...result, error: `${result.error ?? "merge failed"}
|
|
52082
|
-
required-check registration did not settle within the enqueue deadline` } : result;
|
|
52083
|
-
}
|
|
52084
|
-
var AUTO_MERGE_CONFIRM_RETRIES = 3;
|
|
52085
|
-
var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
|
|
52086
|
-
async function confirmAutoMergeEnqueued(deps, options) {
|
|
52087
|
-
const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
|
|
52088
|
-
const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
|
|
52089
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52090
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
52091
|
-
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
52092
|
-
const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
|
|
52093
|
-
if (stuck) return "enqueued";
|
|
52094
|
-
if (attempt < retries - 1) {
|
|
52095
|
-
await sleep2(delayMs);
|
|
52096
|
-
await deps.reEnqueue().catch(() => {
|
|
52097
|
-
});
|
|
52098
|
-
}
|
|
52099
|
-
}
|
|
52100
|
-
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
52101
|
-
return "not-stuck";
|
|
52102
|
-
}
|
|
52103
|
-
async function readGhPrStateWithRetry(fetchState, options) {
|
|
52104
|
-
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
52105
|
-
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
52106
|
-
const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52107
|
-
let lastError = "empty state";
|
|
52108
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
52109
|
-
try {
|
|
52110
|
-
const state = (await fetchState()).trim();
|
|
52111
|
-
if (state) return { ok: true, state };
|
|
52112
|
-
lastError = "empty state";
|
|
52113
|
-
} catch (e) {
|
|
52114
|
-
lastError = String(e.message || "gh pr view failed");
|
|
52115
|
-
}
|
|
52116
|
-
if (attempt < retries - 1) await sleep2(delayMs);
|
|
52117
|
-
}
|
|
52118
|
-
return { ok: false, error: lastError };
|
|
52119
|
-
}
|
|
52120
|
-
async function runPrLand(prNumber, options, deps) {
|
|
52121
|
-
const resolved = await deps.resolveRepo(prNumber, options.repo);
|
|
52122
|
-
const { repo } = resolved;
|
|
52123
|
-
const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
|
|
52124
|
-
if (options.requireTrain !== false) {
|
|
52125
|
-
const verdict = await deps.fetchTrainAuthority(repo);
|
|
52126
|
-
if (!verdict.ok) {
|
|
52127
|
-
return { ...base, error: `train authority unavailable: ${verdict.error}` };
|
|
52128
|
-
}
|
|
52129
|
-
base.train = { role: verdict.authority.role, train: verdict.authority.train };
|
|
52130
|
-
if (!verdict.authority.train) {
|
|
52131
|
-
return {
|
|
52132
|
-
...base,
|
|
52133
|
-
error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
|
|
52134
|
-
};
|
|
52135
|
-
}
|
|
52136
|
-
}
|
|
52137
|
-
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
52138
|
-
base.ciPolicy = ciPolicy;
|
|
52139
|
-
const pinned = options.expectHead;
|
|
52140
|
-
if (pinned) base.pinnedHead = { expected: pinned };
|
|
52141
|
-
const headDrift = async (stage) => {
|
|
52142
|
-
if (!pinned) return void 0;
|
|
52143
|
-
if (!deps.readHeadSha) throw new Error("pinned head reader unavailable");
|
|
52144
|
-
const observed = await deps.readHeadSha(prNumber, repo);
|
|
52145
|
-
base.pinnedHead = { expected: pinned, observed };
|
|
52146
|
-
if (observed === pinned) return void 0;
|
|
52147
|
-
return {
|
|
52148
|
-
...base,
|
|
52149
|
-
pinnedHead: { ...base.pinnedHead, refused: "drift" },
|
|
52150
|
-
error: `pr land #${prNumber}: head ${observed} is not the pinned ${pinned} (${stage}) \u2014 refusing to land a head that was not checked (--expect-head)`
|
|
52151
|
-
};
|
|
52152
|
-
};
|
|
52153
|
-
const drift = await headDrift("before landing");
|
|
52154
|
-
if (drift) return drift;
|
|
52155
|
-
const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
|
|
52156
|
-
let queueSnapshot;
|
|
52157
|
-
const queued = { base: resolved.base, state: "queued" };
|
|
52158
|
-
if (hasQueue) {
|
|
52159
|
-
if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
|
|
52160
|
-
queueSnapshot = await deps.readMergeQueue(prNumber, repo);
|
|
52161
|
-
if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
|
|
52162
|
-
base.mergeQueue = queued;
|
|
52163
|
-
base.mergeStatus = "auto-merge-enqueued";
|
|
52164
|
-
return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
|
|
52165
|
-
}
|
|
52166
|
-
}
|
|
52167
|
-
const checksWaitError = (checksWait) => {
|
|
52168
|
-
if (checksWait.status === "conflicting") {
|
|
52169
|
-
return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
|
|
52170
|
-
}
|
|
52171
|
-
if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
|
|
52172
|
-
return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
|
|
52173
|
-
}
|
|
52174
|
-
return void 0;
|
|
52175
|
-
};
|
|
52176
|
-
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
52177
|
-
const waitError = checksWaitError(base.checksWait);
|
|
52178
|
-
if (waitError) return { ...base, error: waitError };
|
|
52179
|
-
const driftAfterChecks = await headDrift("after checks");
|
|
52180
|
-
if (driftAfterChecks) return driftAfterChecks;
|
|
52181
|
-
let prState = await deps.readPrState(prNumber, repo);
|
|
52182
|
-
if (!hasQueue && pinned && prState.mergeStateStatus === "BEHIND") {
|
|
52183
|
-
return {
|
|
52184
|
-
...base,
|
|
52185
|
-
pinnedHead: { ...base.pinnedHead, refused: "behind" },
|
|
52186
|
-
error: `pr land #${prNumber}: head is BEHIND ${resolved.base} and pinned by --expect-head \u2014 refusing to update it. Update and re-check the head yourself, then land with the new SHA`
|
|
52187
|
-
};
|
|
52188
|
-
}
|
|
52189
|
-
if (!hasQueue && prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
|
|
52190
|
-
try {
|
|
52191
|
-
base.branchUpdated = await deps.updateBranch(prNumber, repo);
|
|
52192
|
-
} catch (e) {
|
|
52193
|
-
return { ...base, error: `pr land #${prNumber}: ${e.message}` };
|
|
52194
|
-
}
|
|
52195
|
-
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
52196
|
-
const reWaitError = checksWaitError(base.checksWait);
|
|
52197
|
-
if (reWaitError) return { ...base, error: reWaitError };
|
|
52198
|
-
prState = await deps.readPrState(prNumber, repo);
|
|
52199
|
-
}
|
|
52200
|
-
if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
|
|
52201
|
-
return {
|
|
52202
|
-
...base,
|
|
52203
|
-
error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
|
|
52204
|
-
};
|
|
52205
|
-
}
|
|
52206
|
-
if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
|
|
52207
|
-
const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
|
|
52208
|
-
base.mergeStatus = merge.mergeStatus;
|
|
52209
|
-
if (merge.credential) base.credential = merge.credential;
|
|
52210
|
-
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
52211
|
-
if (merge.mergeStatus === "queue-admission-pending") {
|
|
52212
|
-
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
52213
|
-
}
|
|
52214
|
-
if (merge.mergeStatus === "failed") {
|
|
52215
|
-
return { ...base, ...merge.preparation ? { preparation: merge.preparation } : {}, ...merge.outcomeUnknown ? { outcomeUnknown: true } : {}, error: merge.error ?? "merge failed" };
|
|
52216
|
-
}
|
|
52217
|
-
if (hasQueue) {
|
|
52218
|
-
base.mergeQueue = queued;
|
|
52219
|
-
if (merge.mergeStatus === "merged") {
|
|
52220
|
-
return queueMerged(base, deps);
|
|
52221
|
-
}
|
|
52222
|
-
const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
|
|
52223
|
-
let confirmed;
|
|
52224
|
-
if (options.enqueueOnly) {
|
|
52225
|
-
const read = await deps.readMergeQueue(prNumber, repo);
|
|
52226
|
-
if (read.isInMergeQueue || read.state === "MERGED") confirmed = read;
|
|
52227
|
-
}
|
|
52228
|
-
if (!confirmed) {
|
|
52229
|
-
await (deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
|
|
52230
|
-
}
|
|
52231
|
-
return pollPrMergeQueue(prNumber, options, deps, base, confirmed, deadline);
|
|
52232
|
-
}
|
|
52233
|
-
if (merge.mergeStatus === "merged") {
|
|
52234
|
-
return {
|
|
52235
|
-
...base,
|
|
52236
|
-
status: "merged",
|
|
52237
|
-
branchFinality: prLandedBranchFinality(prNumber, resolved.base)
|
|
52238
|
-
};
|
|
52239
|
-
}
|
|
52240
|
-
const now = deps.now ?? (() => Date.now());
|
|
52241
|
-
const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
|
|
52242
|
-
if (merged) {
|
|
52243
|
-
return {
|
|
52244
|
-
...base,
|
|
52245
|
-
status: "auto-merge-enqueued-then-merged",
|
|
52246
|
-
mergeStatus: "merged",
|
|
52247
|
-
branchFinality: prLandedBranchFinality(prNumber, resolved.base)
|
|
52248
|
-
};
|
|
52249
|
-
}
|
|
52250
|
-
return {
|
|
52251
|
-
...base,
|
|
52252
|
-
error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
|
|
52253
|
-
};
|
|
52254
|
-
}
|
|
52255
|
-
function queueMerged(base, deps) {
|
|
52256
|
-
const result = {
|
|
52257
|
-
...base,
|
|
52258
|
-
status: "merged-via-queue",
|
|
52259
|
-
mergeStatus: "merged",
|
|
52260
|
-
mergeQueue: { ...base.mergeQueue, state: "merged" },
|
|
52261
|
-
branchFinality: prLandedBranchFinality(base.pr, base.base)
|
|
52262
|
-
};
|
|
52263
|
-
deps.queueProgress?.(result);
|
|
52264
|
-
return result;
|
|
52265
|
-
}
|
|
52266
|
-
async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
52267
|
-
const now = deps.now ?? (() => Date.now());
|
|
52268
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52269
|
-
const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
|
|
52270
|
-
const deadline = deadlineMs ?? now() + timeoutMs;
|
|
52271
|
-
let attempt = 0;
|
|
52272
|
-
let credentialMismatched = false;
|
|
52273
|
-
const observe = (read) => {
|
|
52274
|
-
const seen = deps.observeQueued?.(pr, base.repo, read);
|
|
52275
|
-
if (seen?.mismatched) {
|
|
52276
|
-
credentialMismatched = true;
|
|
52277
|
-
delete base.credential;
|
|
52278
|
-
}
|
|
52279
|
-
if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
|
|
52280
|
-
};
|
|
52281
|
-
while (now() < deadline) {
|
|
52282
|
-
const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
|
|
52283
|
-
snapshot = void 0;
|
|
52284
|
-
if (read.isInMergeQueue || read.state === "MERGED") observe(read);
|
|
52285
|
-
if (read.state === "MERGED") return queueMerged(base, deps);
|
|
52286
|
-
if (read.isInMergeQueue) {
|
|
52287
|
-
base.mergeQueue = {
|
|
52288
|
-
base: base.base,
|
|
52289
|
-
state: "queued",
|
|
52290
|
-
...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
|
|
52291
|
-
};
|
|
52292
|
-
deps.queueProgress?.(base);
|
|
52293
|
-
if (options.enqueueOnly) return { ...base, status: "queued" };
|
|
52294
|
-
} else if (read.state === "OPEN" && !read.autoMergeRequest) {
|
|
52295
|
-
await sleep2(PR_LAND_EJECT_CONFIRM_MS);
|
|
52296
|
-
const again = await deps.readMergeQueue(pr, base.repo);
|
|
52297
|
-
if (again.state === "MERGED") {
|
|
52298
|
-
observe(again);
|
|
52299
|
-
return queueMerged(base, deps);
|
|
52300
|
-
}
|
|
52301
|
-
if (again.isInMergeQueue || again.autoMergeRequest) {
|
|
52302
|
-
snapshot = again;
|
|
52303
|
-
continue;
|
|
52304
|
-
}
|
|
52305
|
-
const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
|
|
52306
|
-
const result2 = {
|
|
52307
|
-
...base,
|
|
52308
|
-
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
52309
|
-
...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
|
|
52310
|
-
error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}`
|
|
52311
|
-
};
|
|
52312
|
-
deps.queueProgress?.(result2);
|
|
52313
|
-
return result2;
|
|
52314
|
-
}
|
|
52315
|
-
await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
|
|
52316
|
-
}
|
|
52317
|
-
const result = {
|
|
52318
|
-
...base,
|
|
52319
|
-
mergeQueue: { ...base.mergeQueue, state: "timeout" },
|
|
52320
|
-
error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 rerun pr land to resume polling`
|
|
52321
|
-
};
|
|
52322
|
-
deps.queueProgress?.(result);
|
|
52323
|
-
return result;
|
|
52324
|
-
}
|
|
52325
|
-
function prLandOutcome(result) {
|
|
52326
|
-
if (result.status === "queue-admission-pending") return "queue-admission-pending";
|
|
52327
|
-
if (result.status !== "failed" && result.status !== "queued") return "merged";
|
|
52328
|
-
if (result.status === "queued") return "queued";
|
|
52329
|
-
const state = result.mergeQueue?.state;
|
|
52330
|
-
return state === "ejected" || state === "timeout" ? state : "failed";
|
|
52331
|
-
}
|
|
52332
|
-
async function landMany(prs, landOne, cleanup, hooks) {
|
|
52333
|
-
const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
|
|
52334
|
-
return Promise.all(prs.map(async (pr) => {
|
|
52335
|
-
let result = await safe(pr);
|
|
52336
|
-
if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
|
|
52337
|
-
hooks?.onRetry?.(pr, result);
|
|
52338
|
-
result = { ...await safe(pr), retriedAfterEjection: true };
|
|
52339
|
-
}
|
|
52340
|
-
const outcome = prLandOutcome(result);
|
|
52341
|
-
const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
|
|
52342
|
-
hooks?.onDone?.();
|
|
52343
|
-
return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
|
|
52344
|
-
}));
|
|
52345
|
-
}
|
|
52346
|
-
function renderPrLandTable(entries) {
|
|
52347
|
-
const rows = entries.map((e) => [
|
|
52348
|
-
`#${e.pr}`,
|
|
52349
|
-
e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
|
|
52350
|
-
[e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
|
|
52351
|
-
]);
|
|
52352
|
-
const width = (i) => Math.max(...rows.map((r) => r[i].length));
|
|
52353
|
-
return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
|
|
52354
|
-
}
|
|
52355
|
-
function landCredentials(results) {
|
|
52356
|
-
const each = results.map((r) => r.credential ?? "unknown");
|
|
52357
|
-
return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
|
|
52358
|
-
}
|
|
52359
|
-
|
|
52360
52641
|
// src/git-worktree-observation.ts
|
|
52361
52642
|
function parseGitWorktreePorcelain(text) {
|
|
52362
52643
|
const rows = [];
|
|
@@ -59167,7 +59448,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59167
59448
|
}
|
|
59168
59449
|
class PrHeadBehindBaseError extends Error {
|
|
59169
59450
|
}
|
|
59170
|
-
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014
|
|
59451
|
+
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
|
|
59171
59452
|
const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
|
|
59172
59453
|
if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
|
|
59173
59454
|
if (opts.expectHead !== void 0) {
|
|
@@ -59213,7 +59494,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59213
59494
|
pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
|
|
59214
59495
|
hasMergeQueue: hasPrMergeQueue,
|
|
59215
59496
|
enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
|
|
59216
|
-
const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force });
|
|
59497
|
+
const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
|
|
59217
59498
|
if (receipt.warning) console.warn(receipt.warning);
|
|
59218
59499
|
return receipt;
|
|
59219
59500
|
},
|
|
@@ -59402,6 +59683,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59402
59683
|
return `cleanup ${c.mergeStatus ?? "unknown"}: worktree ${part(c.worktree)}, branch ${part(c.localBranch)}`;
|
|
59403
59684
|
};
|
|
59404
59685
|
const entries = await landMany(numbers, landOne, cleanupMerged, {
|
|
59686
|
+
baseRedCheck: (_pr, r) => readBaseRedVerdict(r),
|
|
59405
59687
|
onRetry: (pr2, r) => console.warn(`pr land: queue ejected PR #${pr2} (${r.error}) \u2014 landing it once more (head updated from base if behind)`),
|
|
59406
59688
|
onDone: () => {
|
|
59407
59689
|
inFlight = Math.max(1, inFlight - 1);
|
|
@@ -62442,8 +62724,47 @@ function adoptExportProdDeps() {
|
|
|
62442
62724
|
}
|
|
62443
62725
|
};
|
|
62444
62726
|
}
|
|
62727
|
+
var PACKAGE_ORG = "mutmutco";
|
|
62728
|
+
var PACKAGE_REGISTRY = "https://npm.pkg.github.com";
|
|
62729
|
+
function normalizeOrgPackageName(raw) {
|
|
62730
|
+
const trimmed = raw.trim().replace(/^@mutmutco\//, "");
|
|
62731
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(trimmed)) {
|
|
62732
|
+
throw new Error(`--package must be a @mutmutco/<name> or <name> package slug (received '${raw}')`);
|
|
62733
|
+
}
|
|
62734
|
+
return { short: trimmed, full: `@${PACKAGE_ORG}/${trimmed}` };
|
|
62735
|
+
}
|
|
62736
|
+
async function listPackageVersions(opts, deps) {
|
|
62737
|
+
const { short: short3, full } = normalizeOrgPackageName(opts.package);
|
|
62738
|
+
const limit = opts.limit ?? 5;
|
|
62739
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
62740
|
+
throw new Error(`--limit must be an integer from 1 to 100 (received '${String(opts.limit)}')`);
|
|
62741
|
+
}
|
|
62742
|
+
const raw = (await deps.ghApi(`orgs/${PACKAGE_ORG}/packages/npm/${encodeURIComponent(short3)}/versions?per_page=${limit}`)).toString("utf8");
|
|
62743
|
+
const parsed = JSON.parse(raw);
|
|
62744
|
+
if (!Array.isArray(parsed)) {
|
|
62745
|
+
throw new Error(`unexpected GitHub Packages response for ${full}`);
|
|
62746
|
+
}
|
|
62747
|
+
const versions = parsed.slice(0, limit).map((entry) => ({
|
|
62748
|
+
version: String(entry?.name ?? ""),
|
|
62749
|
+
publishedAt: typeof entry?.created_at === "string" ? entry.created_at : typeof entry?.updated_at === "string" ? entry.updated_at : null
|
|
62750
|
+
}));
|
|
62751
|
+
return { package: full, registry: PACKAGE_REGISTRY, versions };
|
|
62752
|
+
}
|
|
62445
62753
|
function registerPackagesCommands(program3) {
|
|
62446
|
-
const packages = program3.command("packages").description("consumer adoption of verified package exports (#7833)");
|
|
62754
|
+
const packages = program3.command("packages").description("GitHub Packages version lookup and consumer adoption of verified package exports (#7833, #9542)");
|
|
62755
|
+
packages.command("versions").description("list newest-first published versions of a @mutmutco/* npm package from GitHub Packages (orgs/mutmutco/packages/npm/<pkg>/versions; never bare npm view)").option("--package <pkg>", "@mutmutco/<pkg> or <pkg> name on GitHub Packages", "mmwork-ds").option("--limit <n>", "maximum number of versions to return (1-100)", "5").option("--json", "machine-readable output").action(async (opts) => {
|
|
62756
|
+
const limit = /^[1-9]\d*$/.test(opts.limit.trim()) ? Number(opts.limit.trim()) : NaN;
|
|
62757
|
+
try {
|
|
62758
|
+
const res = await listPackageVersions({ package: opts.package, limit }, adoptExportProdDeps());
|
|
62759
|
+
if (opts.json) {
|
|
62760
|
+
console.log(JSON.stringify(res, null, 2));
|
|
62761
|
+
} else {
|
|
62762
|
+
for (const v of res.versions) console.log(`${v.version} ${v.publishedAt ?? ""}`);
|
|
62763
|
+
}
|
|
62764
|
+
} catch (error) {
|
|
62765
|
+
fail(`packages versions refused: ${error.message}`);
|
|
62766
|
+
}
|
|
62767
|
+
});
|
|
62447
62768
|
packages.command("adopt-export").description("verify a package-export artifact and install its exact kit pair offline, keeping registry URLs and SRI in the lock").requiredOption("--run <id>", "package-export workflow run id").requiredOption("--artifact <id>", "artifact id from the run summary").requiredOption("--digest <sha256:hex>", "artifact digest from the run summary").requiredOption("--ds <version>", "exact @mutmutco/mmwork-ds version").requiredOption("--screens <version>", "exact @mutmutco/mmwork-screens version").option("--dir <path>", "npm project holding package.json and package-lock.json", ".").action(async (opts) => {
|
|
62448
62769
|
const id = (value, flag) => {
|
|
62449
62770
|
if (!/^[1-9][0-9]{0,15}$/.test(value)) fail(`${flag} must be a numeric id`);
|
|
@@ -65689,7 +66010,7 @@ var RELEASE_LIST_QUERY_VALUE = { per_page: RUNS_QUERY_VALUE.per_page, page: RUNS
|
|
|
65689
66010
|
var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
|
|
65690
66011
|
var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
|
|
65691
66012
|
var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
|
|
65692
|
-
var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/issues/comments/{id}, repos/{owner}/{repo}/issues/{n}/comments[?per_page=1-100&page=<n>, each key at most once], 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>)";
|
|
66013
|
+
var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/issues/comments/{id}, repos/{owner}/{repo}/issues/{n}/comments[?per_page=1-100&page=<n>, each key at most once], 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[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/artifacts[?per_page=1-100&page=<n>, each key at most once], 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>)";
|
|
65693
66014
|
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.";
|
|
65694
66015
|
var TYPED_READ_ROUTE_RE = new RegExp(`^repos/(${NAME})/(${NAME})/(issues|pulls)(?:/(comments/\\d+|\\d+)(?:/([a-z_]+))?)?$`);
|
|
65695
66016
|
function typedReadHint(path2) {
|
|
@@ -65726,7 +66047,7 @@ function orgScopedHint(path2) {
|
|
|
65726
66047
|
if (!match) return "";
|
|
65727
66048
|
const org = match[1];
|
|
65728
66049
|
const runners = /^orgs\/[^/?#]+\/actions\/runners(?:\/|$)/.test(path2.split("?")[0]);
|
|
65729
|
-
const surface = runners ? `
|
|
66050
|
+
const surface = runners ? `For which lane is busy in a repo, use the App-token reads \`mmi-cli devops read run list --repo <owner>/<repo> --status in_progress --json databaseId,workflowName\` followed by \`mmi-cli devops read run view <id> --repo <owner>/<repo> --json jobs\` (runner-name/status per job); fleet-wide runner online/offline state has no App-token read and a human token holder answers it with \`gh api orgs/${org}/actions/runners\` (docs/Guides/gh-runner-runbook.md).` : "It needs a human token holder with an org-scoped token, or a dedicated mmi-cli verb when one exists for this resource.";
|
|
65730
66051
|
return ` This route is ORG-SCOPED (\`orgs/${org}/...\`) and is deliberately outside the allowlist \u2014 no App-token read exists for org-level resources, so retrying path variants cannot work and no \`repos/{owner}/{repo}/...\` respelling of it will be accepted. ${surface} A refusal writes nothing to stdout: check the exit code and never merge stderr into the JSON pipe (2>&1).`;
|
|
65731
66052
|
}
|
|
65732
66053
|
function tagsReadHint(path2) {
|
|
@@ -65735,11 +66056,17 @@ function tagsReadHint(path2) {
|
|
|
65735
66056
|
const slug = `${match[1]}/${match[2]}`;
|
|
65736
66057
|
return ` No App-token REST read exists for git tags or refs \u2014 read git tags directly with \`git ls-remote --tags origin 'refs/tags/*'\` (no token needed; \`git ls-remote origin refs/tags/<tag>\` for one tag), or read tag-backed GitHub Releases with \`MMI_ACTOR=app mmi-cli devops api get "repos/${slug}/releases?per_page=20"\`.`;
|
|
65737
66058
|
}
|
|
66059
|
+
function branchRulesReadHint(path2) {
|
|
66060
|
+
const match = new RegExp(`^repos/(${NAME})/(${NAME})/(?:rules/branches/[^/?#]+|branches/[^/?#]+/protection(?:/|$)|rulesets(?:/|$))`).exec(path2.split("?")[0]);
|
|
66061
|
+
if (!match) return "";
|
|
66062
|
+
const slug = `${match[1]}/${match[2]}`;
|
|
66063
|
+
return ` No raw rules, rulesets or branch-protection read is on the App allowlist, so retrying path or query variants cannot work \u2014 inspect a PR's required status contexts with \`MMI_ACTOR=app mmi-cli devops pr checks-required <pr> --repo ${slug} --json\`, or verify repository ruleset and branch-protection health with \`mmi-cli devops bootstrap verify --repo ${slug} --json\` and \`mmi-cli devops ci audit --repo ${slug} --json\`.`;
|
|
66064
|
+
}
|
|
65738
66065
|
function parseApiGetPath(rawPath) {
|
|
65739
66066
|
const path2 = rawPath.trim();
|
|
65740
66067
|
const refuse = () => {
|
|
65741
66068
|
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." : "";
|
|
65742
|
-
const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${hint}`;
|
|
66069
|
+
const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${branchRulesReadHint(path2)}${hint}`;
|
|
65743
66070
|
return new Error(`api get: refusing '${rawPath}' \u2014${routeHints}${API_GET_RECOVERY} Only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.`);
|
|
65744
66071
|
};
|
|
65745
66072
|
if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
|
|
@@ -65752,8 +66079,8 @@ function parseApiGetPath(rawPath) {
|
|
|
65752
66079
|
if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
|
|
65753
66080
|
if (ACTIONS_RUNS_RE.test(base) && (q === -1 || validRunsQuery(query))) return { kind: "actions-runs", path: path2, paginate: false };
|
|
65754
66081
|
if (ACTIONS_RUN_RE.test(path2)) return { kind: "actions-run", path: path2, paginate: false };
|
|
65755
|
-
if (ACTIONS_JOBS_RE.test(
|
|
65756
|
-
if (ACTIONS_ARTIFACTS_RE.test(
|
|
66082
|
+
if (ACTIONS_JOBS_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "actions-jobs", path: path2, paginate: q === -1 };
|
|
66083
|
+
if (ACTIONS_ARTIFACTS_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "actions-artifacts", path: path2, paginate: q === -1 };
|
|
65757
66084
|
if (ACTIONS_ARTIFACT_ZIP_RE.test(path2)) return { kind: "actions-artifact-zip", path: path2, paginate: false };
|
|
65758
66085
|
if (ACTIONS_JOB_LOGS_RE.test(path2)) return { kind: "actions-job-logs", path: path2, paginate: false };
|
|
65759
66086
|
if (RELEASE_LIST_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "release-list", path: path2, paginate: false };
|
|
@@ -68607,7 +68934,7 @@ function registerDeveloperCommands(program3) {
|
|
|
68607
68934
|
async function runWhoami(io = consoleIo) {
|
|
68608
68935
|
const cfg = await loadConfig();
|
|
68609
68936
|
const report = await resolveWhoami({
|
|
68610
|
-
hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
68937
|
+
hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken }),
|
|
68611
68938
|
ghLogin: githubLogin
|
|
68612
68939
|
});
|
|
68613
68940
|
io.log(JSON.stringify(report));
|
|
@@ -68618,7 +68945,7 @@ function registerDeveloperCommands(program3) {
|
|
|
68618
68945
|
});
|
|
68619
68946
|
whoami.command("diagnose-hub").description("bounded cache-free Hub issuance diagnostic").action(async () => {
|
|
68620
68947
|
const cfg = await loadConfig();
|
|
68621
|
-
const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve:
|
|
68948
|
+
const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve: githubPersonalToken, fetch });
|
|
68622
68949
|
consoleIo.log(JSON.stringify(report));
|
|
68623
68950
|
if (report.category !== "issued") process.exitCode = 1;
|
|
68624
68951
|
});
|
|
@@ -69457,7 +69784,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
|
|
|
69457
69784
|
appActorCommand = commandPath4;
|
|
69458
69785
|
setAuditCommand(commandPath4);
|
|
69459
69786
|
if (commandPath4 !== "whoami diagnose-hub") emitCliCallTelemetry(commandPath4);
|
|
69460
|
-
if (commandPath4 === "ci cancel-orphans") return;
|
|
69787
|
+
if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel") return;
|
|
69461
69788
|
try {
|
|
69462
69789
|
await activateAppActor(commandPath4, process.env, () => APP_WRITE_COMMANDS.has(commandPath4) ? mintCommandWriteToken(appActorDeps(), commandPath4) : mintInstallationToken(appActorDeps()));
|
|
69463
69790
|
} catch (e) {
|
|
@@ -70294,7 +70621,7 @@ registerCiChecksWait(ci);
|
|
|
70294
70621
|
registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
|
|
70295
70622
|
registerRunCancel(
|
|
70296
70623
|
program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` is the only one, and it rides the org App actions:write profile"),
|
|
70297
|
-
async () => (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
|
|
70624
|
+
async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
|
|
70298
70625
|
);
|
|
70299
70626
|
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) => {
|
|
70300
70627
|
if (o.apply && o.parkRuleset) {
|