@mutmutco/codex-plugin 4.6.5 → 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/cli/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") {
|
|
@@ -1448,7 +1467,7 @@ async function githubLogin() {
|
|
|
1448
1467
|
}
|
|
1449
1468
|
async function hubHeaders(extra = {}) {
|
|
1450
1469
|
const cfg = await loadConfig();
|
|
1451
|
-
const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken });
|
|
1470
|
+
const t = await hubAuthToken({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken });
|
|
1452
1471
|
const base = { ...clientVersionHeaders(), ...extra };
|
|
1453
1472
|
return t ? { ...base, Authorization: `Bearer ${t}` } : base;
|
|
1454
1473
|
}
|
|
@@ -16632,9 +16651,44 @@ var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
|
16632
16651
|
"startup_failure"
|
|
16633
16652
|
]);
|
|
16634
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
|
+
}
|
|
16635
16689
|
function firstTestFailureLine(log) {
|
|
16636
16690
|
for (const raw of log.split(/\r?\n/)) {
|
|
16637
|
-
const line = raw
|
|
16691
|
+
const line = logLine(raw);
|
|
16638
16692
|
if (TEST_FAILURE_LINE.test(line)) {
|
|
16639
16693
|
return line.replace(/\b(?:gh[pousr]_|github_pat_|AKIA|sk-)[\w-]+/g, "[redacted]").slice(0, 200);
|
|
16640
16694
|
}
|
|
@@ -17216,7 +17270,9 @@ function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
|
17216
17270
|
function commitClosingRefusalMessage(closing, context = "pr merge") {
|
|
17217
17271
|
const named = closing.map((n) => `#${n}`).join(", ");
|
|
17218
17272
|
const first = `#${closing[0] ?? "N"}`;
|
|
17219
|
-
|
|
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}`;
|
|
17220
17276
|
}
|
|
17221
17277
|
function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
|
|
17222
17278
|
const named = closed.map((n) => `#${n}`).join(", ");
|
|
@@ -21260,7 +21316,7 @@ async function repoSlug() {
|
|
|
21260
21316
|
return "-";
|
|
21261
21317
|
}
|
|
21262
21318
|
function registryClientDeps(cfg) {
|
|
21263
|
-
return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
21319
|
+
return { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
|
|
21264
21320
|
}
|
|
21265
21321
|
async function isOrgRegisteredRepo(cfg, deps = {}) {
|
|
21266
21322
|
const slug = await (deps.slug ?? repoSlug)();
|
|
@@ -27400,10 +27456,10 @@ var rollout_plan_default = {
|
|
|
27400
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)."
|
|
27401
27457
|
},
|
|
27402
27458
|
baseline: {
|
|
27403
|
-
version: "4.6.
|
|
27404
|
-
tag: "v4.6.
|
|
27405
|
-
commit: "
|
|
27406
|
-
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"
|
|
27407
27463
|
},
|
|
27408
27464
|
exitCriterion: "fleet-n-of-n",
|
|
27409
27465
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27420,14 +27476,14 @@ var rollout_plan_default = {
|
|
|
27420
27476
|
repo: "mutmutco/mmi-hub",
|
|
27421
27477
|
role: "canary",
|
|
27422
27478
|
schedule: "train",
|
|
27423
|
-
v3Target: "v4.6.
|
|
27479
|
+
v3Target: "v4.6.6"
|
|
27424
27480
|
}
|
|
27425
27481
|
],
|
|
27426
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.",
|
|
27427
27483
|
rollback: {
|
|
27428
27484
|
independent: true,
|
|
27429
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27430
|
-
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)"
|
|
27431
27487
|
}
|
|
27432
27488
|
},
|
|
27433
27489
|
{
|
|
@@ -41001,6 +41057,403 @@ var import_node_os17 = require("node:os");
|
|
|
41001
41057
|
var import_node_path30 = require("node:path");
|
|
41002
41058
|
init_cli_shared();
|
|
41003
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
|
+
|
|
41004
41457
|
// src/test-policy-core.ts
|
|
41005
41458
|
var import_node_child_process11 = require("node:child_process");
|
|
41006
41459
|
var import_node_fs32 = require("node:fs");
|
|
@@ -41539,6 +41992,9 @@ function loadPolicy(root, readFile10 = readFileOrNull2) {
|
|
|
41539
41992
|
if (raw == null) return { mandatory: [], declared: false };
|
|
41540
41993
|
return parsePolicy(raw, POLICY_FILE);
|
|
41541
41994
|
}
|
|
41995
|
+
function parseTestPolicy(raw) {
|
|
41996
|
+
return parsePolicy(raw, POLICY_FILE);
|
|
41997
|
+
}
|
|
41542
41998
|
function parsePolicy(raw, source) {
|
|
41543
41999
|
try {
|
|
41544
42000
|
const parsed = JSON.parse(raw);
|
|
@@ -41558,6 +42014,10 @@ function parsePolicy(raw, source) {
|
|
|
41558
42014
|
))) {
|
|
41559
42015
|
throw new Error("protected must be an array of { path, why? } entries");
|
|
41560
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
|
+
}
|
|
41561
42021
|
return { ...record2, declared: true };
|
|
41562
42022
|
} catch (error) {
|
|
41563
42023
|
throw new Error(`${source} is not valid JSON: ${error.message}`);
|
|
@@ -41975,7 +42435,7 @@ function admissionJournal(repo, pr, root) {
|
|
|
41975
42435
|
const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
|
|
41976
42436
|
const validate = (value, previous) => {
|
|
41977
42437
|
const row = value;
|
|
41978
|
-
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)))) {
|
|
41979
42439
|
throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
|
|
41980
42440
|
}
|
|
41981
42441
|
if (!previous) {
|
|
@@ -42013,7 +42473,17 @@ function admissionJournal(repo, pr, root) {
|
|
|
42013
42473
|
},
|
|
42014
42474
|
save(state, uuid, queueEntry, credential) {
|
|
42015
42475
|
const previous = load();
|
|
42016
|
-
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);
|
|
42017
42487
|
(0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
|
|
42018
42488
|
load();
|
|
42019
42489
|
}
|
|
@@ -42036,19 +42506,20 @@ function admissionResponse(output) {
|
|
|
42036
42506
|
}
|
|
42037
42507
|
return { code: Number(response[1]), body: result };
|
|
42038
42508
|
}
|
|
42039
|
-
function admissionRejected(reason) {
|
|
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).";
|
|
42040
42511
|
if (/enqueuer is not authorized to merge|not authorized to merge/i.test(reason)) {
|
|
42041
42512
|
return {
|
|
42042
42513
|
mergeStatus: "failed",
|
|
42043
42514
|
retryable: false,
|
|
42044
42515
|
admissionCause: "enqueuer-not-authorized",
|
|
42045
|
-
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
|
|
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}`
|
|
42046
42517
|
};
|
|
42047
42518
|
}
|
|
42048
42519
|
return {
|
|
42049
42520
|
mergeStatus: "failed",
|
|
42050
42521
|
retryable: false,
|
|
42051
|
-
error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request
|
|
42522
|
+
error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.${queueHint}`
|
|
42052
42523
|
};
|
|
42053
42524
|
}
|
|
42054
42525
|
async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
|
|
@@ -42056,10 +42527,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42056
42527
|
mergeStatus: "failed",
|
|
42057
42528
|
retryable: false,
|
|
42058
42529
|
outcomeUnknown: true,
|
|
42059
|
-
error: `queue submission outcome unknown: ${reason}.
|
|
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.`
|
|
42060
42531
|
});
|
|
42061
|
-
const rejected = admissionRejected;
|
|
42062
|
-
const reconcile = async (reason, recordEntry = false) => {
|
|
42532
|
+
const rejected = (reason) => admissionRejected(reason, repo, pr);
|
|
42533
|
+
const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
|
|
42063
42534
|
try {
|
|
42064
42535
|
const observed = await readPrMergeQueue(pr, repo, read);
|
|
42065
42536
|
if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
|
|
@@ -42071,15 +42542,15 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42071
42542
|
return { mergeStatus: "auto-merge-enqueued" };
|
|
42072
42543
|
}
|
|
42073
42544
|
const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
|
|
42074
|
-
|
|
42075
|
-
|
|
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) {
|
|
42076
42547
|
const events = await readArrayPages(
|
|
42077
42548
|
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
42078
42549
|
options.timelineRead ?? read,
|
|
42079
42550
|
"queue ejection timeline unreadable"
|
|
42080
42551
|
);
|
|
42081
42552
|
const times = (name) => events.filter((event) => event.event === name).map((event) => Date.parse(event.created_at));
|
|
42082
|
-
const adds = times("added_to_merge_queue").filter((at) => at >=
|
|
42553
|
+
const adds = times("added_to_merge_queue").filter((at) => at >= boundMs);
|
|
42083
42554
|
const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
|
|
42084
42555
|
if (removals.length === 1) {
|
|
42085
42556
|
journal.save("ejected");
|
|
@@ -42118,7 +42589,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42118
42589
|
options.selection.selected = true;
|
|
42119
42590
|
options.selection.intentId = receipt.intentId;
|
|
42120
42591
|
}
|
|
42121
|
-
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);
|
|
42122
42596
|
if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
|
|
42123
42597
|
} else {
|
|
42124
42598
|
if (resume) return rejected("the previous operation failed; rerun after reviewing its cause");
|
|
@@ -42171,31 +42645,19 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42171
42645
|
const tryAutoMergeFallback = async (reason, uuid) => {
|
|
42172
42646
|
const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
|
|
42173
42647
|
if (!/enqueuer is not authorized to merge/i.test(reason) || !autoMerge) return void 0;
|
|
42648
|
+
let fallback;
|
|
42174
42649
|
try {
|
|
42175
|
-
|
|
42176
|
-
const fallbackCredential = fallback?.credential ?? credential ?? receipt?.credential;
|
|
42177
|
-
const observed = await readPrMergeQueue(pr, repo, read);
|
|
42178
|
-
if (observed.state === "MERGED" && observed.headRefOid === head) {
|
|
42179
|
-
try {
|
|
42180
|
-
journal.save("merged", uuid, void 0, fallbackCredential);
|
|
42181
|
-
} catch {
|
|
42182
|
-
return reconcile("the completed auto-merge fallback could not be checkpointed");
|
|
42183
|
-
}
|
|
42184
|
-
return { mergeStatus: "merged" };
|
|
42185
|
-
}
|
|
42186
|
-
if (observed.isInMergeQueue && observed.headRefOid === head) {
|
|
42187
|
-
const entry = observed.mergeQueueEntry;
|
|
42188
|
-
const queueEntry = entry?.headCommit && entry.enqueuedAt ? { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt } : void 0;
|
|
42189
|
-
try {
|
|
42190
|
-
journal.save("enqueued", uuid, queueEntry, fallbackCredential);
|
|
42191
|
-
} catch {
|
|
42192
|
-
return reconcile("the enqueued auto-merge fallback could not be checkpointed");
|
|
42193
|
-
}
|
|
42194
|
-
return { mergeStatus: "auto-merge-enqueued" };
|
|
42195
|
-
}
|
|
42650
|
+
fallback = await autoMerge(pr, repo, head);
|
|
42196
42651
|
} catch {
|
|
42652
|
+
return void 0;
|
|
42197
42653
|
}
|
|
42198
|
-
|
|
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);
|
|
42199
42661
|
};
|
|
42200
42662
|
if (code === 400 || code === 403 || code === 404 || code === 422) {
|
|
42201
42663
|
const reason = body.details?.message ?? body.message ?? `HTTP ${code}`;
|
|
@@ -42241,29 +42703,21 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42241
42703
|
const reason = details?.message ?? "GitHub reported a failed operation";
|
|
42242
42704
|
const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
|
|
42243
42705
|
if (/enqueuer is not authorized to merge/i.test(reason) && autoMerge && receipt.head === head) {
|
|
42706
|
+
let fallback = void 0;
|
|
42707
|
+
let fallbackSucceeded = false;
|
|
42244
42708
|
try {
|
|
42245
|
-
|
|
42709
|
+
fallback = await autoMerge(pr, repo, head);
|
|
42710
|
+
fallbackSucceeded = true;
|
|
42711
|
+
} catch {
|
|
42712
|
+
}
|
|
42713
|
+
if (fallbackSucceeded) {
|
|
42246
42714
|
const fallbackCredential = fallback?.credential ?? receipt?.credential;
|
|
42247
|
-
|
|
42248
|
-
|
|
42249
|
-
|
|
42250
|
-
|
|
42251
|
-
} catch {
|
|
42252
|
-
return reconcile("the completed auto-merge fallback could not be checkpointed");
|
|
42253
|
-
}
|
|
42254
|
-
return { mergeStatus: "merged" };
|
|
42255
|
-
}
|
|
42256
|
-
if (observed.isInMergeQueue && observed.headRefOid === head) {
|
|
42257
|
-
const entry = observed.mergeQueueEntry;
|
|
42258
|
-
const queueEntry = entry?.headCommit && entry.enqueuedAt ? { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt } : void 0;
|
|
42259
|
-
try {
|
|
42260
|
-
journal.save("enqueued", receipt.uuid, queueEntry, fallbackCredential);
|
|
42261
|
-
} catch {
|
|
42262
|
-
return reconcile("the enqueued auto-merge fallback could not be checkpointed");
|
|
42263
|
-
}
|
|
42264
|
-
return { mergeStatus: "auto-merge-enqueued" };
|
|
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");
|
|
42265
42719
|
}
|
|
42266
|
-
|
|
42720
|
+
return reconcile("the auto-merge fallback completed, but matching-head membership or merged state is unconfirmed", true);
|
|
42267
42721
|
}
|
|
42268
42722
|
}
|
|
42269
42723
|
}
|
|
@@ -42338,7 +42792,7 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42338
42792
|
status: "preparation-needed",
|
|
42339
42793
|
reason,
|
|
42340
42794
|
issueTargets: targets.map((number) => `${repo}#${number}`),
|
|
42341
|
-
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."
|
|
42342
42796
|
},
|
|
42343
42797
|
error: `queue preparation needed: ${detail}`
|
|
42344
42798
|
});
|
|
@@ -42450,6 +42904,100 @@ async function findPrMergeGroupRun(pr, repo, base, gh2 = readApi) {
|
|
|
42450
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];
|
|
42451
42905
|
return run ? { id: run.id, url: run.html_url, conclusion: run.conclusion } : void 0;
|
|
42452
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
|
+
}
|
|
42453
43001
|
function prMergeQueueLine(result) {
|
|
42454
43002
|
const queue = result.mergeQueue;
|
|
42455
43003
|
if (!queue) return void 0;
|
|
@@ -42613,31 +43161,41 @@ async function cancelWorkflowRun(repo, id, deps, force = false) {
|
|
|
42613
43161
|
if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
|
|
42614
43162
|
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
|
|
42615
43163
|
try {
|
|
42616
|
-
if (force
|
|
42617
|
-
|
|
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);
|
|
42618
43168
|
} catch (error) {
|
|
42619
43169
|
const detail = String(error.stderr ?? error);
|
|
42620
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}`);
|
|
42621
43171
|
}
|
|
42622
|
-
|
|
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 };
|
|
42623
43174
|
}
|
|
42624
43175
|
function registerRunCancel(run, writeToken) {
|
|
42625
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) => {
|
|
42626
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;
|
|
42627
43184
|
try {
|
|
42628
43185
|
receipt = await cancelWorkflowRun(options.repo, id, {
|
|
43186
|
+
read,
|
|
42629
43187
|
cancel: async (repo, runId) => {
|
|
42630
|
-
|
|
43188
|
+
token ??= await writeToken();
|
|
42631
43189
|
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
|
|
42632
43190
|
timeout: 2e4,
|
|
42633
|
-
env: { ...process.env, GH_TOKEN: token }
|
|
43191
|
+
env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
|
|
42634
43192
|
});
|
|
42635
43193
|
},
|
|
42636
43194
|
forceCancel: async (repo, runId) => {
|
|
42637
|
-
|
|
43195
|
+
token ??= await writeToken();
|
|
42638
43196
|
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/force-cancel`, "--method", "POST"], {
|
|
42639
43197
|
timeout: 2e4,
|
|
42640
|
-
env: { ...process.env, GH_TOKEN: token }
|
|
43198
|
+
env: { ...process.env, GH_TOKEN: token, MMI_GH_POOL: "app" }
|
|
42641
43199
|
});
|
|
42642
43200
|
}
|
|
42643
43201
|
}, options.force === true);
|
|
@@ -42645,7 +43203,7 @@ function registerRunCancel(run, writeToken) {
|
|
|
42645
43203
|
return fail(error.message);
|
|
42646
43204
|
}
|
|
42647
43205
|
if (options.json) console.log(JSON.stringify(receipt));
|
|
42648
|
-
else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 ${receipt.force ? "force-cancel" : "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}` : ""})`);
|
|
42649
43207
|
});
|
|
42650
43208
|
}
|
|
42651
43209
|
|
|
@@ -45081,7 +45639,7 @@ function registerSecretsCommands(program3) {
|
|
|
45081
45639
|
return;
|
|
45082
45640
|
}
|
|
45083
45641
|
const d = makeSecretsDeps(cfg);
|
|
45084
|
-
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
45642
|
+
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
|
|
45085
45643
|
const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
|
|
45086
45644
|
const repo = o.repo ?? `mutmutco/${slug}`;
|
|
45087
45645
|
const meta = await fetchProjectBySlug(slug, regDeps);
|
|
@@ -45136,7 +45694,7 @@ function registerSecretsCommands(program3) {
|
|
|
45136
45694
|
return;
|
|
45137
45695
|
}
|
|
45138
45696
|
const d = makeSecretsDeps(cfg);
|
|
45139
|
-
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken }) };
|
|
45697
|
+
const regDeps = { baseUrl: cfg.sagaApiUrl, token: () => hubAuthToken({ baseUrl: cfg.sagaApiUrl, githubToken: githubPersonalToken }) };
|
|
45140
45698
|
const slug = (o.repo ? o.repo.split("/").pop() : await d.slug()).toLowerCase();
|
|
45141
45699
|
const repo = o.repo ?? `mutmutco/${slug}`;
|
|
45142
45700
|
const meta = await fetchProjectBySlug(slug, regDeps);
|
|
@@ -52080,386 +52638,6 @@ function boardAdvanceFailureMessage(result) {
|
|
|
52080
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.`;
|
|
52081
52639
|
}
|
|
52082
52640
|
|
|
52083
|
-
// src/pr-land.ts
|
|
52084
|
-
var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
|
|
52085
|
-
var PR_LAND_QUEUE_BATCH_SIZE = 5;
|
|
52086
|
-
function prHeadBehindBase(input) {
|
|
52087
|
-
if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
|
|
52088
|
-
return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
|
|
52089
|
-
}
|
|
52090
|
-
function prHeadUpdateRemedy(head, base) {
|
|
52091
|
-
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`;
|
|
52092
|
-
}
|
|
52093
|
-
function prLandedBranchFinality(prNumber, base) {
|
|
52094
|
-
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`;
|
|
52095
|
-
}
|
|
52096
|
-
var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
|
|
52097
|
-
function isProtectedPrHead(head) {
|
|
52098
|
-
return PROTECTED_PR_HEAD_BRANCHES.includes(head);
|
|
52099
|
-
}
|
|
52100
|
-
function protectedPrHeadRemedy(head, base) {
|
|
52101
|
-
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`;
|
|
52102
|
-
}
|
|
52103
|
-
var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
|
|
52104
|
-
var PR_HEAD_UPDATE_API_READ_DELAY_MS = 15e3;
|
|
52105
|
-
async function updatePrHeadFromBase(input) {
|
|
52106
|
-
const remedy = prHeadUpdateRemedy(input.head, input.base);
|
|
52107
|
-
if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
|
|
52108
|
-
if (input.localCheckedOut) {
|
|
52109
|
-
const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
52110
|
-
await input.git(["fetch", "origin", input.base]);
|
|
52111
|
-
try {
|
|
52112
|
-
await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
|
|
52113
|
-
} catch (e) {
|
|
52114
|
-
await input.git(["merge", "--abort"]).catch(() => {
|
|
52115
|
-
});
|
|
52116
|
-
throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
|
|
52117
|
-
}
|
|
52118
|
-
await input.git(["push", "origin", input.head]);
|
|
52119
|
-
const to = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
52120
|
-
return { from: from2, to, base: input.base, via: "local-merge" };
|
|
52121
|
-
}
|
|
52122
|
-
const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
|
|
52123
|
-
const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
|
|
52124
|
-
const from = await readHead2();
|
|
52125
|
-
try {
|
|
52126
|
-
await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
|
|
52127
|
-
} catch (e) {
|
|
52128
|
-
throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
|
|
52129
|
-
}
|
|
52130
|
-
const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52131
|
-
for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
|
|
52132
|
-
const to = await readHead2();
|
|
52133
|
-
if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
|
|
52134
|
-
await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
|
|
52135
|
-
}
|
|
52136
|
-
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`);
|
|
52137
|
-
}
|
|
52138
|
-
var PR_LAND_POLL_MS = 6e4;
|
|
52139
|
-
var PR_LAND_POLL_MAX_MS = 5 * 6e4;
|
|
52140
|
-
function prLandPollDelayMs(attempt, waiting) {
|
|
52141
|
-
const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
|
|
52142
|
-
const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
|
|
52143
|
-
return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
|
|
52144
|
-
}
|
|
52145
|
-
var PR_LAND_EJECT_CONFIRM_MS = 2e4;
|
|
52146
|
-
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
52147
|
-
var PR_LAND_STATE_READ_RETRIES = 3;
|
|
52148
|
-
var PR_LAND_STATE_READ_DELAY_MS = 15e3;
|
|
52149
|
-
var PR_LAND_MERGE_RETRY_DELAY_MS = 3e4;
|
|
52150
|
-
async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
52151
|
-
const now = deps.now ?? (() => Date.now());
|
|
52152
|
-
const deadline = now() + PR_LAND_ENQUEUE_TIMEOUT_MS;
|
|
52153
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52154
|
-
const first = await deps.mergeAuto(prNumber, repo);
|
|
52155
|
-
let result = first;
|
|
52156
|
-
let transientRetried = false;
|
|
52157
|
-
while (result.mergeStatus === "failed") {
|
|
52158
|
-
if (result.retryable === false || result.outcomeUnknown || result.preparation) return result;
|
|
52159
|
-
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false, checksPending: false }));
|
|
52160
|
-
if (ready.open && ready.checksPending) {
|
|
52161
|
-
if (now() >= deadline) break;
|
|
52162
|
-
await sleep2(Math.min(PR_LAND_POLL_MS, deadline - now()));
|
|
52163
|
-
if (now() >= deadline) break;
|
|
52164
|
-
const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
|
|
52165
|
-
if (checks.status !== "success" && checks.status !== "skipped") {
|
|
52166
|
-
return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
|
|
52167
|
-
registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
|
|
52168
|
-
}
|
|
52169
|
-
if (now() >= deadline) break;
|
|
52170
|
-
} else {
|
|
52171
|
-
if (!ready.open || !ready.mergeable || !ready.checksPassing || transientRetried) return result;
|
|
52172
|
-
transientRetried = true;
|
|
52173
|
-
if (now() >= deadline) break;
|
|
52174
|
-
await sleep2(Math.min(PR_LAND_MERGE_RETRY_DELAY_MS, deadline - now()));
|
|
52175
|
-
if (now() >= deadline) break;
|
|
52176
|
-
}
|
|
52177
|
-
const retried = await deps.mergeAuto(prNumber, repo);
|
|
52178
|
-
result = retried.mergeStatus !== "failed" ? retried : {
|
|
52179
|
-
...retried,
|
|
52180
|
-
error: `merge retry after transient failure also failed: first attempt: ${first.error ?? "unknown error"}
|
|
52181
|
-
retry: ${retried.error ?? "unknown error"}`
|
|
52182
|
-
};
|
|
52183
|
-
}
|
|
52184
|
-
return result.mergeStatus === "failed" ? { ...result, error: `${result.error ?? "merge failed"}
|
|
52185
|
-
required-check registration did not settle within the enqueue deadline` } : result;
|
|
52186
|
-
}
|
|
52187
|
-
var AUTO_MERGE_CONFIRM_RETRIES = 3;
|
|
52188
|
-
var AUTO_MERGE_CONFIRM_DELAY_MS = 15e3;
|
|
52189
|
-
async function confirmAutoMergeEnqueued(deps, options) {
|
|
52190
|
-
const retries = options?.retries ?? AUTO_MERGE_CONFIRM_RETRIES;
|
|
52191
|
-
const delayMs = options?.delayMs ?? AUTO_MERGE_CONFIRM_DELAY_MS;
|
|
52192
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52193
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
52194
|
-
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
52195
|
-
const stuck = await deps.readAutoMergeRequest().then((s) => s.trim()).catch(() => "");
|
|
52196
|
-
if (stuck) return "enqueued";
|
|
52197
|
-
if (attempt < retries - 1) {
|
|
52198
|
-
await sleep2(delayMs);
|
|
52199
|
-
await deps.reEnqueue().catch(() => {
|
|
52200
|
-
});
|
|
52201
|
-
}
|
|
52202
|
-
}
|
|
52203
|
-
if (await deps.readMerged().catch(() => false)) return "merged";
|
|
52204
|
-
return "not-stuck";
|
|
52205
|
-
}
|
|
52206
|
-
async function readGhPrStateWithRetry(fetchState, options) {
|
|
52207
|
-
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
52208
|
-
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
52209
|
-
const sleep2 = options?.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52210
|
-
let lastError = "empty state";
|
|
52211
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
52212
|
-
try {
|
|
52213
|
-
const state = (await fetchState()).trim();
|
|
52214
|
-
if (state) return { ok: true, state };
|
|
52215
|
-
lastError = "empty state";
|
|
52216
|
-
} catch (e) {
|
|
52217
|
-
lastError = String(e.message || "gh pr view failed");
|
|
52218
|
-
}
|
|
52219
|
-
if (attempt < retries - 1) await sleep2(delayMs);
|
|
52220
|
-
}
|
|
52221
|
-
return { ok: false, error: lastError };
|
|
52222
|
-
}
|
|
52223
|
-
async function runPrLand(prNumber, options, deps) {
|
|
52224
|
-
const resolved = await deps.resolveRepo(prNumber, options.repo);
|
|
52225
|
-
const { repo } = resolved;
|
|
52226
|
-
const base = { status: "failed", repo, pr: prNumber, base: resolved.base };
|
|
52227
|
-
if (options.requireTrain !== false) {
|
|
52228
|
-
const verdict = await deps.fetchTrainAuthority(repo);
|
|
52229
|
-
if (!verdict.ok) {
|
|
52230
|
-
return { ...base, error: `train authority unavailable: ${verdict.error}` };
|
|
52231
|
-
}
|
|
52232
|
-
base.train = { role: verdict.authority.role, train: verdict.authority.train };
|
|
52233
|
-
if (!verdict.authority.train) {
|
|
52234
|
-
return {
|
|
52235
|
-
...base,
|
|
52236
|
-
error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
|
|
52237
|
-
};
|
|
52238
|
-
}
|
|
52239
|
-
}
|
|
52240
|
-
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
52241
|
-
base.ciPolicy = ciPolicy;
|
|
52242
|
-
const pinned = options.expectHead;
|
|
52243
|
-
if (pinned) base.pinnedHead = { expected: pinned };
|
|
52244
|
-
const headDrift = async (stage) => {
|
|
52245
|
-
if (!pinned) return void 0;
|
|
52246
|
-
if (!deps.readHeadSha) throw new Error("pinned head reader unavailable");
|
|
52247
|
-
const observed = await deps.readHeadSha(prNumber, repo);
|
|
52248
|
-
base.pinnedHead = { expected: pinned, observed };
|
|
52249
|
-
if (observed === pinned) return void 0;
|
|
52250
|
-
return {
|
|
52251
|
-
...base,
|
|
52252
|
-
pinnedHead: { ...base.pinnedHead, refused: "drift" },
|
|
52253
|
-
error: `pr land #${prNumber}: head ${observed} is not the pinned ${pinned} (${stage}) \u2014 refusing to land a head that was not checked (--expect-head)`
|
|
52254
|
-
};
|
|
52255
|
-
};
|
|
52256
|
-
const drift = await headDrift("before landing");
|
|
52257
|
-
if (drift) return drift;
|
|
52258
|
-
const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
|
|
52259
|
-
let queueSnapshot;
|
|
52260
|
-
const queued = { base: resolved.base, state: "queued" };
|
|
52261
|
-
if (hasQueue) {
|
|
52262
|
-
if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
|
|
52263
|
-
queueSnapshot = await deps.readMergeQueue(prNumber, repo);
|
|
52264
|
-
if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
|
|
52265
|
-
base.mergeQueue = queued;
|
|
52266
|
-
base.mergeStatus = "auto-merge-enqueued";
|
|
52267
|
-
return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
|
|
52268
|
-
}
|
|
52269
|
-
}
|
|
52270
|
-
const checksWaitError = (checksWait) => {
|
|
52271
|
-
if (checksWait.status === "conflicting") {
|
|
52272
|
-
return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
|
|
52273
|
-
}
|
|
52274
|
-
if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
|
|
52275
|
-
return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
|
|
52276
|
-
}
|
|
52277
|
-
return void 0;
|
|
52278
|
-
};
|
|
52279
|
-
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
52280
|
-
const waitError = checksWaitError(base.checksWait);
|
|
52281
|
-
if (waitError) return { ...base, error: waitError };
|
|
52282
|
-
const driftAfterChecks = await headDrift("after checks");
|
|
52283
|
-
if (driftAfterChecks) return driftAfterChecks;
|
|
52284
|
-
let prState = await deps.readPrState(prNumber, repo);
|
|
52285
|
-
if (!hasQueue && pinned && prState.mergeStateStatus === "BEHIND") {
|
|
52286
|
-
return {
|
|
52287
|
-
...base,
|
|
52288
|
-
pinnedHead: { ...base.pinnedHead, refused: "behind" },
|
|
52289
|
-
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`
|
|
52290
|
-
};
|
|
52291
|
-
}
|
|
52292
|
-
if (!hasQueue && prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
|
|
52293
|
-
try {
|
|
52294
|
-
base.branchUpdated = await deps.updateBranch(prNumber, repo);
|
|
52295
|
-
} catch (e) {
|
|
52296
|
-
return { ...base, error: `pr land #${prNumber}: ${e.message}` };
|
|
52297
|
-
}
|
|
52298
|
-
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
52299
|
-
const reWaitError = checksWaitError(base.checksWait);
|
|
52300
|
-
if (reWaitError) return { ...base, error: reWaitError };
|
|
52301
|
-
prState = await deps.readPrState(prNumber, repo);
|
|
52302
|
-
}
|
|
52303
|
-
if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
|
|
52304
|
-
return {
|
|
52305
|
-
...base,
|
|
52306
|
-
error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing`
|
|
52307
|
-
};
|
|
52308
|
-
}
|
|
52309
|
-
if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
|
|
52310
|
-
const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
|
|
52311
|
-
base.mergeStatus = merge.mergeStatus;
|
|
52312
|
-
if (merge.credential) base.credential = merge.credential;
|
|
52313
|
-
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
52314
|
-
if (merge.mergeStatus === "queue-admission-pending") {
|
|
52315
|
-
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
52316
|
-
}
|
|
52317
|
-
if (merge.mergeStatus === "failed") {
|
|
52318
|
-
return { ...base, ...merge.preparation ? { preparation: merge.preparation } : {}, ...merge.outcomeUnknown ? { outcomeUnknown: true } : {}, error: merge.error ?? "merge failed" };
|
|
52319
|
-
}
|
|
52320
|
-
if (hasQueue) {
|
|
52321
|
-
base.mergeQueue = queued;
|
|
52322
|
-
if (merge.mergeStatus === "merged") {
|
|
52323
|
-
return queueMerged(base, deps);
|
|
52324
|
-
}
|
|
52325
|
-
const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
|
|
52326
|
-
let confirmed;
|
|
52327
|
-
if (options.enqueueOnly) {
|
|
52328
|
-
const read = await deps.readMergeQueue(prNumber, repo);
|
|
52329
|
-
if (read.isInMergeQueue || read.state === "MERGED") confirmed = read;
|
|
52330
|
-
}
|
|
52331
|
-
if (!confirmed) {
|
|
52332
|
-
await (deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
|
|
52333
|
-
}
|
|
52334
|
-
return pollPrMergeQueue(prNumber, options, deps, base, confirmed, deadline);
|
|
52335
|
-
}
|
|
52336
|
-
if (merge.mergeStatus === "merged") {
|
|
52337
|
-
return {
|
|
52338
|
-
...base,
|
|
52339
|
-
status: "merged",
|
|
52340
|
-
branchFinality: prLandedBranchFinality(prNumber, resolved.base)
|
|
52341
|
-
};
|
|
52342
|
-
}
|
|
52343
|
-
const now = deps.now ?? (() => Date.now());
|
|
52344
|
-
const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
|
|
52345
|
-
if (merged) {
|
|
52346
|
-
return {
|
|
52347
|
-
...base,
|
|
52348
|
-
status: "auto-merge-enqueued-then-merged",
|
|
52349
|
-
mergeStatus: "merged",
|
|
52350
|
-
branchFinality: prLandedBranchFinality(prNumber, resolved.base)
|
|
52351
|
-
};
|
|
52352
|
-
}
|
|
52353
|
-
return {
|
|
52354
|
-
...base,
|
|
52355
|
-
error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
|
|
52356
|
-
};
|
|
52357
|
-
}
|
|
52358
|
-
function queueMerged(base, deps) {
|
|
52359
|
-
const result = {
|
|
52360
|
-
...base,
|
|
52361
|
-
status: "merged-via-queue",
|
|
52362
|
-
mergeStatus: "merged",
|
|
52363
|
-
mergeQueue: { ...base.mergeQueue, state: "merged" },
|
|
52364
|
-
branchFinality: prLandedBranchFinality(base.pr, base.base)
|
|
52365
|
-
};
|
|
52366
|
-
deps.queueProgress?.(result);
|
|
52367
|
-
return result;
|
|
52368
|
-
}
|
|
52369
|
-
async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
52370
|
-
const now = deps.now ?? (() => Date.now());
|
|
52371
|
-
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
52372
|
-
const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
|
|
52373
|
-
const deadline = deadlineMs ?? now() + timeoutMs;
|
|
52374
|
-
let attempt = 0;
|
|
52375
|
-
let credentialMismatched = false;
|
|
52376
|
-
const observe = (read) => {
|
|
52377
|
-
const seen = deps.observeQueued?.(pr, base.repo, read);
|
|
52378
|
-
if (seen?.mismatched) {
|
|
52379
|
-
credentialMismatched = true;
|
|
52380
|
-
delete base.credential;
|
|
52381
|
-
}
|
|
52382
|
-
if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
|
|
52383
|
-
};
|
|
52384
|
-
while (now() < deadline) {
|
|
52385
|
-
const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
|
|
52386
|
-
snapshot = void 0;
|
|
52387
|
-
if (read.isInMergeQueue || read.state === "MERGED") observe(read);
|
|
52388
|
-
if (read.state === "MERGED") return queueMerged(base, deps);
|
|
52389
|
-
if (read.isInMergeQueue) {
|
|
52390
|
-
base.mergeQueue = {
|
|
52391
|
-
base: base.base,
|
|
52392
|
-
state: "queued",
|
|
52393
|
-
...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
|
|
52394
|
-
};
|
|
52395
|
-
deps.queueProgress?.(base);
|
|
52396
|
-
if (options.enqueueOnly) return { ...base, status: "queued" };
|
|
52397
|
-
} else if (read.state === "OPEN" && !read.autoMergeRequest) {
|
|
52398
|
-
await sleep2(PR_LAND_EJECT_CONFIRM_MS);
|
|
52399
|
-
const again = await deps.readMergeQueue(pr, base.repo);
|
|
52400
|
-
if (again.state === "MERGED") {
|
|
52401
|
-
observe(again);
|
|
52402
|
-
return queueMerged(base, deps);
|
|
52403
|
-
}
|
|
52404
|
-
if (again.isInMergeQueue || again.autoMergeRequest) {
|
|
52405
|
-
snapshot = again;
|
|
52406
|
-
continue;
|
|
52407
|
-
}
|
|
52408
|
-
const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
|
|
52409
|
-
const result2 = {
|
|
52410
|
-
...base,
|
|
52411
|
-
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
52412
|
-
...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
|
|
52413
|
-
error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}`
|
|
52414
|
-
};
|
|
52415
|
-
deps.queueProgress?.(result2);
|
|
52416
|
-
return result2;
|
|
52417
|
-
}
|
|
52418
|
-
await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
|
|
52419
|
-
}
|
|
52420
|
-
const result = {
|
|
52421
|
-
...base,
|
|
52422
|
-
mergeQueue: { ...base.mergeQueue, state: "timeout" },
|
|
52423
|
-
error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 rerun pr land to resume polling`
|
|
52424
|
-
};
|
|
52425
|
-
deps.queueProgress?.(result);
|
|
52426
|
-
return result;
|
|
52427
|
-
}
|
|
52428
|
-
function prLandOutcome(result) {
|
|
52429
|
-
if (result.status === "queue-admission-pending") return "queue-admission-pending";
|
|
52430
|
-
if (result.status !== "failed" && result.status !== "queued") return "merged";
|
|
52431
|
-
if (result.status === "queued") return "queued";
|
|
52432
|
-
const state = result.mergeQueue?.state;
|
|
52433
|
-
return state === "ejected" || state === "timeout" ? state : "failed";
|
|
52434
|
-
}
|
|
52435
|
-
async function landMany(prs, landOne, cleanup, hooks) {
|
|
52436
|
-
const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
|
|
52437
|
-
return Promise.all(prs.map(async (pr) => {
|
|
52438
|
-
let result = await safe(pr);
|
|
52439
|
-
if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
|
|
52440
|
-
hooks?.onRetry?.(pr, result);
|
|
52441
|
-
result = { ...await safe(pr), retriedAfterEjection: true };
|
|
52442
|
-
}
|
|
52443
|
-
const outcome = prLandOutcome(result);
|
|
52444
|
-
const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
|
|
52445
|
-
hooks?.onDone?.();
|
|
52446
|
-
return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
|
|
52447
|
-
}));
|
|
52448
|
-
}
|
|
52449
|
-
function renderPrLandTable(entries) {
|
|
52450
|
-
const rows = entries.map((e) => [
|
|
52451
|
-
`#${e.pr}`,
|
|
52452
|
-
e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
|
|
52453
|
-
[e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
|
|
52454
|
-
]);
|
|
52455
|
-
const width = (i) => Math.max(...rows.map((r) => r[i].length));
|
|
52456
|
-
return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
|
|
52457
|
-
}
|
|
52458
|
-
function landCredentials(results) {
|
|
52459
|
-
const each = results.map((r) => r.credential ?? "unknown");
|
|
52460
|
-
return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
|
|
52461
|
-
}
|
|
52462
|
-
|
|
52463
52641
|
// src/git-worktree-observation.ts
|
|
52464
52642
|
function parseGitWorktreePorcelain(text) {
|
|
52465
52643
|
const rows = [];
|
|
@@ -59270,7 +59448,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59270
59448
|
}
|
|
59271
59449
|
class PrHeadBehindBaseError extends Error {
|
|
59272
59450
|
}
|
|
59273
|
-
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) => {
|
|
59274
59452
|
const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
|
|
59275
59453
|
if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
|
|
59276
59454
|
if (opts.expectHead !== void 0) {
|
|
@@ -59316,7 +59494,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59316
59494
|
pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
|
|
59317
59495
|
hasMergeQueue: hasPrMergeQueue,
|
|
59318
59496
|
enqueueQueue: async (prNumber, repo, expectedHeadOid) => {
|
|
59319
|
-
const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force });
|
|
59497
|
+
const receipt = await enqueuePrMergeQueue(prNumber, repo, { expectedHeadOid, force: o.force, enqueueOnly: o.enqueueOnly });
|
|
59320
59498
|
if (receipt.warning) console.warn(receipt.warning);
|
|
59321
59499
|
return receipt;
|
|
59322
59500
|
},
|
|
@@ -59505,6 +59683,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59505
59683
|
return `cleanup ${c.mergeStatus ?? "unknown"}: worktree ${part(c.worktree)}, branch ${part(c.localBranch)}`;
|
|
59506
59684
|
};
|
|
59507
59685
|
const entries = await landMany(numbers, landOne, cleanupMerged, {
|
|
59686
|
+
baseRedCheck: (_pr, r) => readBaseRedVerdict(r),
|
|
59508
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)`),
|
|
59509
59688
|
onDone: () => {
|
|
59510
59689
|
inFlight = Math.max(1, inFlight - 1);
|
|
@@ -62545,8 +62724,47 @@ function adoptExportProdDeps() {
|
|
|
62545
62724
|
}
|
|
62546
62725
|
};
|
|
62547
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
|
+
}
|
|
62548
62753
|
function registerPackagesCommands(program3) {
|
|
62549
|
-
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
|
+
});
|
|
62550
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) => {
|
|
62551
62769
|
const id = (value, flag) => {
|
|
62552
62770
|
if (!/^[1-9][0-9]{0,15}$/.test(value)) fail(`${flag} must be a numeric id`);
|
|
@@ -65792,7 +66010,7 @@ var RELEASE_LIST_QUERY_VALUE = { per_page: RUNS_QUERY_VALUE.per_page, page: RUNS
|
|
|
65792
66010
|
var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
|
|
65793
66011
|
var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
|
|
65794
66012
|
var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
|
|
65795
|
-
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>)";
|
|
65796
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.";
|
|
65797
66015
|
var TYPED_READ_ROUTE_RE = new RegExp(`^repos/(${NAME})/(${NAME})/(issues|pulls)(?:/(comments/\\d+|\\d+)(?:/([a-z_]+))?)?$`);
|
|
65798
66016
|
function typedReadHint(path2) {
|
|
@@ -65829,7 +66047,7 @@ function orgScopedHint(path2) {
|
|
|
65829
66047
|
if (!match) return "";
|
|
65830
66048
|
const org = match[1];
|
|
65831
66049
|
const runners = /^orgs\/[^/?#]+\/actions\/runners(?:\/|$)/.test(path2.split("?")[0]);
|
|
65832
|
-
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.";
|
|
65833
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).`;
|
|
65834
66052
|
}
|
|
65835
66053
|
function tagsReadHint(path2) {
|
|
@@ -65861,8 +66079,8 @@ function parseApiGetPath(rawPath) {
|
|
|
65861
66079
|
if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
|
|
65862
66080
|
if (ACTIONS_RUNS_RE.test(base) && (q === -1 || validRunsQuery(query))) return { kind: "actions-runs", path: path2, paginate: false };
|
|
65863
66081
|
if (ACTIONS_RUN_RE.test(path2)) return { kind: "actions-run", path: path2, paginate: false };
|
|
65864
|
-
if (ACTIONS_JOBS_RE.test(
|
|
65865
|
-
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 };
|
|
65866
66084
|
if (ACTIONS_ARTIFACT_ZIP_RE.test(path2)) return { kind: "actions-artifact-zip", path: path2, paginate: false };
|
|
65867
66085
|
if (ACTIONS_JOB_LOGS_RE.test(path2)) return { kind: "actions-job-logs", path: path2, paginate: false };
|
|
65868
66086
|
if (RELEASE_LIST_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "release-list", path: path2, paginate: false };
|
|
@@ -68716,7 +68934,7 @@ function registerDeveloperCommands(program3) {
|
|
|
68716
68934
|
async function runWhoami(io = consoleIo) {
|
|
68717
68935
|
const cfg = await loadConfig();
|
|
68718
68936
|
const report = await resolveWhoami({
|
|
68719
|
-
hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
68937
|
+
hubSession: () => hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken: githubPersonalToken }),
|
|
68720
68938
|
ghLogin: githubLogin
|
|
68721
68939
|
});
|
|
68722
68940
|
io.log(JSON.stringify(report));
|
|
@@ -68727,7 +68945,7 @@ function registerDeveloperCommands(program3) {
|
|
|
68727
68945
|
});
|
|
68728
68946
|
whoami.command("diagnose-hub").description("bounded cache-free Hub issuance diagnostic").action(async () => {
|
|
68729
68947
|
const cfg = await loadConfig();
|
|
68730
|
-
const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve:
|
|
68948
|
+
const report = await diagnoseHub({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), resolve: githubPersonalToken, fetch });
|
|
68731
68949
|
consoleIo.log(JSON.stringify(report));
|
|
68732
68950
|
if (report.category !== "issued") process.exitCode = 1;
|
|
68733
68951
|
});
|