@mutmutco/claude-plugin 4.6.3 → 4.6.5
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/.claude-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +665 -131
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/board/SKILL.md +12 -11
- package/skills/board/reference.md +5 -1
- package/skills/bootstrap/SKILL.md +5 -5
- package/skills/bootstrap/reference.md +16 -77
- package/skills/epic/SKILL.md +3 -3
- package/skills/hotfix/SKILL.md +10 -12
- package/skills/hotfix/recovery.md +3 -7
- package/skills/intercom-guest/SKILL.md +6 -7
- package/skills/mmi-help/SKILL.md +5 -4
- package/skills/org-guide/SKILL.md +18 -27
- package/skills/org-guide/references/design.md +301 -0
- package/skills/org-guide/references/vault.md +2 -9
- package/skills/org-guide/references/work-and-releases.md +11 -298
- package/skills/rcand/SKILL.md +5 -9
- package/skills/rcand/recovery.md +1 -10
- package/skills/rcand/suite.md +1 -1
- package/skills/release/SKILL.md +7 -11
- package/skills/release/recovery.md +1 -2
- package/skills/release/suite.md +1 -1
- package/skills/release/train-common.md +28 -0
- package/skills/release/verification.md +11 -46
- package/skills/secrets/SKILL.md +6 -5
- package/skills/secrets/reference.md +5 -47
package/cli/dist/main.cjs
CHANGED
|
@@ -400,7 +400,7 @@ function isFilingWriteApi(rest) {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
if (method === "POST" && path2 && /^repos\/[^/]+\/[^/]+\/(issues|pulls)$/.test(path2)) return true;
|
|
403
|
-
if (method === "PUT" && path2 && /^repos\/[^/]+\/[^/]+\/pulls\/\d+\/merge
|
|
403
|
+
if (method === "PUT" && path2 && /^repos\/[^/]+\/[^/]+\/pulls\/\d+\/merge(-async)?$/.test(path2)) return true;
|
|
404
404
|
return false;
|
|
405
405
|
}
|
|
406
406
|
function isViewerFreeGhRead(args) {
|
|
@@ -1216,6 +1216,8 @@ var init_house_map = __esm({
|
|
|
1216
1216
|
// personal and App allowance visibility
|
|
1217
1217
|
api: "devops",
|
|
1218
1218
|
// allowlisted GitHub REST GET reads for automation (pull files, compare, contents)
|
|
1219
|
+
run: "devops",
|
|
1220
|
+
// #9479: one named workflow run — currently the bounded `run cancel <id>` write
|
|
1219
1221
|
ci: "devops",
|
|
1220
1222
|
// the CI/gate audit
|
|
1221
1223
|
jev: "devops",
|
|
@@ -1353,6 +1355,7 @@ __export(cli_shared_exports, {
|
|
|
1353
1355
|
fail: () => fail,
|
|
1354
1356
|
failGraceful: () => failGraceful,
|
|
1355
1357
|
failGracefulEnvelope: () => failGracefulEnvelope,
|
|
1358
|
+
ghFilingAppEnv: () => ghFilingAppEnv,
|
|
1356
1359
|
gitOut: () => gitOut,
|
|
1357
1360
|
githubLogin: () => githubLogin,
|
|
1358
1361
|
hubHeaders: () => hubHeaders,
|
|
@@ -1383,10 +1386,10 @@ function setGhReadTokenProvider(provider) {
|
|
|
1383
1386
|
function setGhWriteTokenProvider(provider) {
|
|
1384
1387
|
ghWriteTokenProvider = provider;
|
|
1385
1388
|
}
|
|
1386
|
-
async function ghFilingAppEnv(file, args) {
|
|
1387
|
-
if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || process.env.MMI_ACTOR?.trim() === "personal") return void 0;
|
|
1389
|
+
async function ghFilingAppEnv(file, args, requireApp = false) {
|
|
1390
|
+
if (!ghWriteTokenProvider || !/(^|[\\/])gh(\.exe)?$/i.test(file) || !requireApp && process.env.MMI_ACTOR?.trim() === "personal") return void 0;
|
|
1388
1391
|
if (!isAppFilingWrite(args)) return void 0;
|
|
1389
|
-
const explicitApp = process.env.MMI_ACTOR?.trim() === "app";
|
|
1392
|
+
const explicitApp = requireApp || process.env.MMI_ACTOR?.trim() === "app";
|
|
1390
1393
|
let token;
|
|
1391
1394
|
try {
|
|
1392
1395
|
token = await ghWriteTokenProvider();
|
|
@@ -1599,16 +1602,26 @@ var init_cli_shared = __esm({
|
|
|
1599
1602
|
rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options, ...env ? { env } : {} })
|
|
1600
1603
|
);
|
|
1601
1604
|
const appEnv = options.env ? void 0 : await ghReadAppEnv(file, args) ?? await ghFilingAppEnv(file, args);
|
|
1605
|
+
const reportsCredential = !options.env && /(^|[\\/])gh(\.exe)?$/i.test(file) && isAppFilingWrite(args);
|
|
1606
|
+
const tag = async (call, credential) => {
|
|
1607
|
+
if (!reportsCredential) return call;
|
|
1608
|
+
try {
|
|
1609
|
+
return { ...await call, credential };
|
|
1610
|
+
} catch (e) {
|
|
1611
|
+
if (e && typeof e === "object") e.credential = credential;
|
|
1612
|
+
throw e;
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1602
1615
|
if (/(^|[\\/])gh(\.exe)?$/i.test(file) && args[0] !== "auth") {
|
|
1603
1616
|
const callerEnv = options.env;
|
|
1604
1617
|
const pool = appEnv ? "app" : !callerEnv ? ambientPool() : !callerEnv.GH_TOKEN ? "personal" : callerEnv.MMI_GH_POOL === "app" ? "app" : callerEnv.GH_TOKEN === process.env.GH_TOKEN ? ambientPool() : "caller";
|
|
1605
1618
|
recordGhUsage("gh", pool, args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" "));
|
|
1606
1619
|
}
|
|
1607
|
-
if (!appEnv) return run();
|
|
1620
|
+
if (!appEnv) return tag(run(), options.env?.MMI_GH_POOL === "app" ? "app_installation" : "user");
|
|
1608
1621
|
try {
|
|
1609
|
-
return await run(appEnv);
|
|
1622
|
+
return await tag(run(appEnv), "app_installation");
|
|
1610
1623
|
} catch (e) {
|
|
1611
|
-
if (shouldRetryIntegrationRefusalOnAmbient(e)) return run();
|
|
1624
|
+
if (shouldRetryIntegrationRefusalOnAmbient(e)) return tag(run(), "user");
|
|
1612
1625
|
throw e;
|
|
1613
1626
|
}
|
|
1614
1627
|
};
|
|
@@ -15263,6 +15276,8 @@ async function mintInstallationToken(deps) {
|
|
|
15263
15276
|
}
|
|
15264
15277
|
var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
|
|
15265
15278
|
["ci cancel-orphans", { metadata: "read", actions: "write" }],
|
|
15279
|
+
// #9479: one named run cancelled under the same actions:write profile as the orphan sweep.
|
|
15280
|
+
["run cancel", { metadata: "read", actions: "write" }],
|
|
15266
15281
|
["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
|
|
15267
15282
|
// #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
|
|
15268
15283
|
["issue discover-related", { metadata: "read", issues: "write" }]
|
|
@@ -15953,9 +15968,6 @@ var REST_GH_TIMEOUT_MS = 2e4;
|
|
|
15953
15968
|
var pollTokenProvider;
|
|
15954
15969
|
var pollTokenOnce;
|
|
15955
15970
|
var lastPollSource;
|
|
15956
|
-
function lastRestPollSource() {
|
|
15957
|
-
return lastPollSource;
|
|
15958
|
-
}
|
|
15959
15971
|
function setPollTokenProvider(provider) {
|
|
15960
15972
|
pollTokenProvider = provider;
|
|
15961
15973
|
pollTokenOnce = void 0;
|
|
@@ -16798,6 +16810,30 @@ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
|
|
|
16798
16810
|
}
|
|
16799
16811
|
|
|
16800
16812
|
// src/claim-marker-client.ts
|
|
16813
|
+
var claimWriteTokenProvider;
|
|
16814
|
+
var claimWriteTokenOnce;
|
|
16815
|
+
function setClaimWriteTokenProvider(provider) {
|
|
16816
|
+
claimWriteTokenProvider = provider;
|
|
16817
|
+
claimWriteTokenOnce = void 0;
|
|
16818
|
+
}
|
|
16819
|
+
async function selectClaimWriteClient(client, appClient) {
|
|
16820
|
+
if (appClient) return appClient;
|
|
16821
|
+
if (!claimWriteTokenProvider) return client;
|
|
16822
|
+
let token;
|
|
16823
|
+
try {
|
|
16824
|
+
claimWriteTokenOnce ??= claimWriteTokenProvider();
|
|
16825
|
+
token = await claimWriteTokenOnce;
|
|
16826
|
+
} catch (e) {
|
|
16827
|
+
claimWriteTokenOnce = void 0;
|
|
16828
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
16829
|
+
throw new Error(`${detail} \u2014 refusing personal-token fallback for claim write (#9511)`);
|
|
16830
|
+
}
|
|
16831
|
+
if (!token) {
|
|
16832
|
+
claimWriteTokenOnce = void 0;
|
|
16833
|
+
throw new Error("app actor: claim write token unavailable \u2014 refusing personal-token fallback for claim write (#9511)");
|
|
16834
|
+
}
|
|
16835
|
+
return createGitHubClient({ token: async () => token });
|
|
16836
|
+
}
|
|
16801
16837
|
async function selectLivenessClient(client, appClient) {
|
|
16802
16838
|
if (appClient) return appClient;
|
|
16803
16839
|
let token;
|
|
@@ -21309,7 +21345,7 @@ async function loadConfigForBoardSelector(selector, repoOption) {
|
|
|
21309
21345
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
21310
21346
|
var SKILL_LESSON_FILE_LABELS = [SKILL_LESSON_LABEL, LEARNING_LABEL2];
|
|
21311
21347
|
var SKILL_LESSON_LOOP_KIND = "lesson";
|
|
21312
|
-
var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "rcand", "release", "secrets"];
|
|
21348
|
+
var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "intercom-guest", "mmi-help", "org-guide", "rcand", "release", "secrets"];
|
|
21313
21349
|
function assertSkillName(name) {
|
|
21314
21350
|
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
21315
21351
|
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
@@ -21342,6 +21378,9 @@ function findDuplicateLesson(source, openLessons) {
|
|
|
21342
21378
|
// src/board-doctor-config.ts
|
|
21343
21379
|
init_github_client();
|
|
21344
21380
|
|
|
21381
|
+
// src/issue-comment.ts
|
|
21382
|
+
init_github_client();
|
|
21383
|
+
|
|
21345
21384
|
// src/sub-issue.ts
|
|
21346
21385
|
init_error_codes();
|
|
21347
21386
|
init_github_quota();
|
|
@@ -21551,6 +21590,40 @@ function parentLinkFields(result, error, parentRef, childRef, reason = "failed")
|
|
|
21551
21590
|
}
|
|
21552
21591
|
|
|
21553
21592
|
// src/issue-comment.ts
|
|
21593
|
+
async function editIssueComment(client, input) {
|
|
21594
|
+
const parsed = parseIssueRef(input.ref);
|
|
21595
|
+
const repo = parsed.repo ?? input.defaultRepo;
|
|
21596
|
+
if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
|
|
21597
|
+
if (!input.replace.length) throw new Error("--replace must not be empty; use --edit <commentId> --replace <text> --with <text>");
|
|
21598
|
+
if (!/^\d+$/.test(input.commentId)) throw new Error(`--edit takes a numeric comment id, got: ${input.commentId}`);
|
|
21599
|
+
const path2 = `repos/${repo}/issues/comments/${input.commentId}`;
|
|
21600
|
+
let comment;
|
|
21601
|
+
try {
|
|
21602
|
+
comment = await client.rest("GET", path2);
|
|
21603
|
+
} catch (e) {
|
|
21604
|
+
if (e instanceof GitHubApiError && e.status === 404) {
|
|
21605
|
+
throw new Error(`comment ${input.commentId} does not exist on ${repo}`);
|
|
21606
|
+
}
|
|
21607
|
+
throw e;
|
|
21608
|
+
}
|
|
21609
|
+
const expectedAnchor = `/issues/${parsed.number}#issuecomment-${input.commentId}`;
|
|
21610
|
+
if (!comment.html_url?.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
|
|
21611
|
+
throw new Error(`comment ${input.commentId} is not a comment on ${repo}#${parsed.number} (it lives at ${comment.html_url ?? "unknown"})`);
|
|
21612
|
+
}
|
|
21613
|
+
const body = comment.body ?? "";
|
|
21614
|
+
const count = body.split(input.replace).length - 1;
|
|
21615
|
+
if (count !== 1) throw new Error(`--replace must match exactly once; found ${count} occurrences in comment ${input.commentId}`);
|
|
21616
|
+
const index = body.indexOf(input.replace);
|
|
21617
|
+
const newBody = body.slice(0, index) + input.with + body.slice(index + input.replace.length);
|
|
21618
|
+
await client.rest("PATCH", path2, { body: { body: newBody } });
|
|
21619
|
+
return {
|
|
21620
|
+
number: parsed.number,
|
|
21621
|
+
repo,
|
|
21622
|
+
url: `https://github.com/${repo}/issues/${parsed.number}`,
|
|
21623
|
+
commentUrl: comment.html_url,
|
|
21624
|
+
edited: true
|
|
21625
|
+
};
|
|
21626
|
+
}
|
|
21554
21627
|
async function postIssueComment(client, input) {
|
|
21555
21628
|
const parsed = parseIssueRef(input.ref);
|
|
21556
21629
|
const repo = parsed.repo ?? input.defaultRepo;
|
|
@@ -22059,9 +22132,12 @@ function detailCandidates(report, options = {}) {
|
|
|
22059
22132
|
const scopes = [report.primary, report.secondary];
|
|
22060
22133
|
return scopes.flatMap((scope) => options.all ? [...scope.userOwned, ...scope.claimable, ...scope.taken, ...scope.unownedInFlight] : [...scope.userOwned, ...scope.claimable]).filter((item) => item.contentType === "Issue");
|
|
22061
22134
|
}
|
|
22135
|
+
function boardNotFoundRemedy(board, repoExplicit) {
|
|
22136
|
+
return repoExplicit ? `--repo already names the repo, so inspect the repo's registered board coordinates with \`mmi-cli oracle org project get <owner/repo>\` (resolved board: ${board.owner} #${board.number})` : "if it lives on a different board, pass --repo <owner/repo> for the repo that owns it";
|
|
22137
|
+
}
|
|
22062
22138
|
function boardNotFoundError(ref, board, opts = {}) {
|
|
22063
22139
|
const verb = opts.verb ?? "is not on";
|
|
22064
|
-
const remedy = opts.remedy ??
|
|
22140
|
+
const remedy = opts.remedy ?? boardNotFoundRemedy(board, opts.repoExplicit === true);
|
|
22065
22141
|
return new Error(`${ref} ${verb} the ${board.owner} #${board.number} board; ${remedy}`);
|
|
22066
22142
|
}
|
|
22067
22143
|
var NotClaimableError = class extends Error {
|
|
@@ -22489,7 +22565,7 @@ async function showBoardItem(options, deps = {}) {
|
|
|
22489
22565
|
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
22490
22566
|
const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
|
|
22491
22567
|
const { item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
22492
|
-
if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
22568
|
+
if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
|
|
22493
22569
|
if (item.contentType === "Issue") {
|
|
22494
22570
|
try {
|
|
22495
22571
|
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
@@ -23319,7 +23395,7 @@ function isSameBranchClaimResume(contest, repo, landing = {}) {
|
|
|
23319
23395
|
const headBranch = landing.headBranch?.replace(/^refs\/heads\//, "").replace(/^[^/:]+:/, "").trim();
|
|
23320
23396
|
const headSha = landing.headSha?.trim();
|
|
23321
23397
|
if (!headBranch && !headSha) return false;
|
|
23322
|
-
if (
|
|
23398
|
+
if (contest.unverifiable.length > 0 || contest.live.length === 0) return false;
|
|
23323
23399
|
const branchLine = /^live branch ([^\s]+) on /;
|
|
23324
23400
|
const fromContest = contest.branch?.replace(/ \(head of open PR .*\)$/, "").trim();
|
|
23325
23401
|
const fromLive = contest.live.map((line) => branchLine.exec(line)?.[1]).find(Boolean);
|
|
@@ -23360,7 +23436,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
23360
23436
|
branchSha: evidence.branchSha
|
|
23361
23437
|
}, item.repository, landing)) {
|
|
23362
23438
|
contested = false;
|
|
23363
|
-
|
|
23439
|
+
if (evidence.marker) {
|
|
23440
|
+
resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
|
|
23441
|
+
}
|
|
23364
23442
|
}
|
|
23365
23443
|
const carry = staleWorkLines(evidence);
|
|
23366
23444
|
const staleStamp = !contested && !resume && !ownSeat && evidence.marker ? `stale claim stamp ${describeClaimMarker(evidence.marker)} (idle ${formatClaimAge(claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] }).idleMs)})${carry.length ? `; continue its ${carry.join(" and ")}` : ""}` : void 0;
|
|
@@ -23426,7 +23504,7 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
23426
23504
|
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
23427
23505
|
const item = lookup.item;
|
|
23428
23506
|
if (!item) {
|
|
23429
|
-
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
23507
|
+
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
|
|
23430
23508
|
}
|
|
23431
23509
|
const optionId = cfg.statusOptions[options.status];
|
|
23432
23510
|
let credential = "user";
|
|
@@ -23529,6 +23607,9 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
|
|
|
23529
23607
|
return {
|
|
23530
23608
|
cfg,
|
|
23531
23609
|
client,
|
|
23610
|
+
appClient: deps.appClient,
|
|
23611
|
+
git: deps.git ?? defaultGit,
|
|
23612
|
+
landing: deps.landing,
|
|
23532
23613
|
items: collected.items,
|
|
23533
23614
|
selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
|
|
23534
23615
|
writable: writableOrUnknown(writable),
|
|
@@ -23621,7 +23702,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23621
23702
|
if (!verdict.ok) throw new NotClaimableError(verdict.reason, { ref: fresh.ref, status: fresh.status });
|
|
23622
23703
|
item = fresh;
|
|
23623
23704
|
const refuseIfContested = async () => {
|
|
23624
|
-
const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
|
|
23705
|
+
const contest = await checkLaneContest(client, item, ctx.session, report.viewer, ctx.appClient, ctx.landing);
|
|
23625
23706
|
resumeEvidence = void 0;
|
|
23626
23707
|
staleStamp = void 0;
|
|
23627
23708
|
const displaced = () => ({
|
|
@@ -23632,6 +23713,24 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23632
23713
|
host: contest.marker.host
|
|
23633
23714
|
} : {}
|
|
23634
23715
|
});
|
|
23716
|
+
if (contest.contested && !ctx.landing && contest.branch) {
|
|
23717
|
+
const candidateBranch = contest.branch.replace(/ \(head of open PR .*\)$/, "").trim();
|
|
23718
|
+
const originRepo = repoFromRemoteUrl(await ctx.git(["remote", "get-url", "origin"]).catch(() => ""));
|
|
23719
|
+
if (originRepo && originRepo.toLowerCase() === item.repository.toLowerCase()) {
|
|
23720
|
+
const localSha = (await ctx.git(["rev-parse", "--verify", `refs/heads/${candidateBranch}^{commit}`]).catch(() => "")).trim();
|
|
23721
|
+
const currentBranch2 = (await ctx.git(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "")).trim();
|
|
23722
|
+
if (localSha && (!contest.branchSha || localSha === contest.branchSha) || currentBranch2 === candidateBranch) {
|
|
23723
|
+
const localLanding = { headBranch: candidateBranch, ...localSha ? { headSha: localSha } : {} };
|
|
23724
|
+
if (isSameBranchClaimResume(contest, item.repository, localLanding)) {
|
|
23725
|
+
if (contest.marker) {
|
|
23726
|
+
resumeEvidence = `same-branch resume over live branch ${contest.branch}`;
|
|
23727
|
+
previousHolder = displaced();
|
|
23728
|
+
}
|
|
23729
|
+
return;
|
|
23730
|
+
}
|
|
23731
|
+
}
|
|
23732
|
+
}
|
|
23733
|
+
}
|
|
23635
23734
|
if (!contest.contested) {
|
|
23636
23735
|
if (contest.resume && contest.marker) {
|
|
23637
23736
|
resumeEvidence = contest.resume;
|
|
@@ -23643,10 +23742,14 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23643
23742
|
return;
|
|
23644
23743
|
}
|
|
23645
23744
|
if (!options.force) {
|
|
23745
|
+
if (options.bulk) {
|
|
23746
|
+
const { holder: holder2, why } = laneContestParts(contest);
|
|
23747
|
+
throw new Error(isUnknownHold(contest) ? `${item.ref} cannot be verified as free: ${holder2} (${why}); the liveness reads failed so the claim is refused fail-closed \u2014 ${CONTESTED_CLAIM_RECOVERY}` : `${item.ref} is already claimed by ${holder2} with live evidence of active work (${why}); ${CONTESTED_CLAIM_RECOVERY}`);
|
|
23748
|
+
}
|
|
23646
23749
|
const candidateRefs = [...report.primary.claimable, ...report.secondary.claimable].filter((candidate) => candidate.contentType === "Issue" && candidate.state === "OPEN" && !candidate.claim?.currentlyClaimed && !ctx.selectedRefs.has(candidate.ref.toLowerCase())).map((candidate) => candidate.ref);
|
|
23647
23750
|
const alternatives = candidateRefs.length ? `Board snapshot candidates (not guaranteed free): ${candidateRefs.join(", ")}` : "Board snapshot candidates: none in this read";
|
|
23648
23751
|
const refusal = `${laneContestMessage(item.ref, contest, "claim")}. ${alternatives}; refresh mmi-cli oracle board read before claiming.`;
|
|
23649
|
-
throw new Error(
|
|
23752
|
+
throw new Error(withGatedWriteChainHint(refusal));
|
|
23650
23753
|
}
|
|
23651
23754
|
previousHolder = displaced();
|
|
23652
23755
|
};
|
|
@@ -23655,19 +23758,21 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23655
23758
|
if (options.check) {
|
|
23656
23759
|
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
|
|
23657
23760
|
}
|
|
23658
|
-
const
|
|
23761
|
+
const appWriter2 = await selectClaimWriteClient(client, ctx.appClient);
|
|
23762
|
+
const renewed = await postClaimMarkerComment(appWriter2, item, ctx.session, staleStamp);
|
|
23659
23763
|
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, renewed };
|
|
23660
23764
|
}
|
|
23661
23765
|
if (options.check) {
|
|
23662
23766
|
return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, ...claimedReceipt(), checked: true };
|
|
23663
23767
|
}
|
|
23664
23768
|
await refuseIfContested();
|
|
23769
|
+
const appWriter = await selectClaimWriteClient(client, ctx.appClient);
|
|
23665
23770
|
try {
|
|
23666
|
-
await
|
|
23771
|
+
await appWriter.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
|
|
23667
23772
|
} catch (e) {
|
|
23668
23773
|
throw new Error(`claim failed before board status changed: ${ghError(e)}`);
|
|
23669
23774
|
}
|
|
23670
|
-
await postClaimMarkerComment(
|
|
23775
|
+
await postClaimMarkerComment(appWriter, item, ctx.session, staleStamp);
|
|
23671
23776
|
let credential = "user";
|
|
23672
23777
|
try {
|
|
23673
23778
|
({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
|
|
@@ -23699,6 +23804,13 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
23699
23804
|
const result = await claimOneBoardItem(ctx, selector, options);
|
|
23700
23805
|
return ctx.report.warnings.length ? { ...result, warnings: [...ctx.report.warnings] } : result;
|
|
23701
23806
|
}
|
|
23807
|
+
function formatBulkClaimSummary(results) {
|
|
23808
|
+
const skipped = results.filter((r) => !r.claimed);
|
|
23809
|
+
if (skipped.length === 0) return void 0;
|
|
23810
|
+
const landed = results.filter((r) => r.claimed).map((r) => r.ref);
|
|
23811
|
+
const notClaimed = skipped.map((r) => `${r.ref}${/already claimed by /i.test(r.reason) ? " (held by another lane)" : ""}`);
|
|
23812
|
+
return `claimed ${landed.length} of ${results.length}: ${landed.length ? `${landed.join(", ")} landed` : "none landed"}; not claimed: ${notClaimed.join(", ")}`;
|
|
23813
|
+
}
|
|
23702
23814
|
async function claimBoardIssues(options, deps = {}) {
|
|
23703
23815
|
const cfg = resolveBoardConfig(options.config);
|
|
23704
23816
|
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
@@ -23828,7 +23940,7 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
23828
23940
|
const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
|
|
23829
23941
|
const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
23830
23942
|
if (!item) {
|
|
23831
|
-
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
23943
|
+
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
|
|
23832
23944
|
}
|
|
23833
23945
|
const toStatus = options.toStatus ?? "Todo";
|
|
23834
23946
|
if (item.contentType === "Issue") {
|
|
@@ -23846,8 +23958,9 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
23846
23958
|
}
|
|
23847
23959
|
}
|
|
23848
23960
|
}
|
|
23961
|
+
const appWriter = await selectClaimWriteClient(client, deps.appClient);
|
|
23849
23962
|
try {
|
|
23850
|
-
await
|
|
23963
|
+
await appWriter.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
|
|
23851
23964
|
body: { assignees: item.assignees }
|
|
23852
23965
|
});
|
|
23853
23966
|
} catch (e) {
|
|
@@ -23864,7 +23977,7 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
23864
23977
|
host: actor.host,
|
|
23865
23978
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
23866
23979
|
};
|
|
23867
|
-
await postIssueComment(
|
|
23980
|
+
await postIssueComment(appWriter, { ref: item.ref, body: renderClaimReleaseBody(release) });
|
|
23868
23981
|
} catch (e) {
|
|
23869
23982
|
process.stderr.write(`warning: ${item.ref} was unclaimed, but recording the release marker comment failed: ${ghError(e)}
|
|
23870
23983
|
`);
|
|
@@ -27287,10 +27400,10 @@ var rollout_plan_default = {
|
|
|
27287
27400
|
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)."
|
|
27288
27401
|
},
|
|
27289
27402
|
baseline: {
|
|
27290
|
-
version: "4.6.
|
|
27291
|
-
tag: "v4.6.
|
|
27292
|
-
commit: "
|
|
27293
|
-
npm: "@mutmutco/cli@4.6.
|
|
27403
|
+
version: "4.6.5",
|
|
27404
|
+
tag: "v4.6.5",
|
|
27405
|
+
commit: "cd6f8a959ed5",
|
|
27406
|
+
npm: "@mutmutco/cli@4.6.5"
|
|
27294
27407
|
},
|
|
27295
27408
|
exitCriterion: "fleet-n-of-n",
|
|
27296
27409
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27307,14 +27420,14 @@ var rollout_plan_default = {
|
|
|
27307
27420
|
repo: "mutmutco/mmi-hub",
|
|
27308
27421
|
role: "canary",
|
|
27309
27422
|
schedule: "train",
|
|
27310
|
-
v3Target: "v4.6.
|
|
27423
|
+
v3Target: "v4.6.5"
|
|
27311
27424
|
}
|
|
27312
27425
|
],
|
|
27313
27426
|
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.",
|
|
27314
27427
|
rollback: {
|
|
27315
27428
|
independent: true,
|
|
27316
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27317
|
-
v3Target: "v4.6.
|
|
27429
|
+
mechanism: "npm dist-tag latest -> 4.6.5 and redeploy the Hub Lambda from tag v4.6.5 (cd6f8a959ed5); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
27430
|
+
v3Target: "v4.6.5 (@mutmutco/cli@4.6.5, tag commit cd6f8a959ed5 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
27318
27431
|
}
|
|
27319
27432
|
},
|
|
27320
27433
|
{
|
|
@@ -28993,16 +29106,16 @@ async function writeFleetLockstepRuleset(client, body, liveId, target, org) {
|
|
|
28993
29106
|
await client.rest("PUT", `orgs/${org}/rulesets/${liveId}`, { body, timeoutMs: 2e4 });
|
|
28994
29107
|
}
|
|
28995
29108
|
const read = await fetchFleetLockstepRuleset(client, org);
|
|
28996
|
-
const
|
|
28997
|
-
const shapeDrift =
|
|
28998
|
-
const includeSettled =
|
|
28999
|
-
const enforcementSettled =
|
|
29109
|
+
const settled2 = read.kind === "ok" ? read.ruleset : void 0;
|
|
29110
|
+
const shapeDrift = settled2 ? diffFleetLockstepShape(settled2) : [`ruleset '${FLEET_LOCKSTEP_RULESET_NAME}' absent after apply`];
|
|
29111
|
+
const includeSettled = settled2 ? sameSetCi(settled2.conditions?.repository_name?.include, body.conditions.repository_name.include) : false;
|
|
29112
|
+
const enforcementSettled = settled2?.enforcement === body.enforcement;
|
|
29000
29113
|
if (shapeDrift.length || !includeSettled || !enforcementSettled) {
|
|
29001
29114
|
throw new Error(`fleet lockstep ruleset apply did not settle: ${[...shapeDrift, includeSettled ? "" : "include list differs", enforcementSettled ? "" : "enforcement differs"].filter(Boolean).join("; ")}`);
|
|
29002
29115
|
}
|
|
29003
29116
|
return {
|
|
29004
29117
|
action: liveId == null ? "created" : "updated",
|
|
29005
|
-
id:
|
|
29118
|
+
id: settled2?.id,
|
|
29006
29119
|
target,
|
|
29007
29120
|
include: body.conditions.repository_name.include,
|
|
29008
29121
|
verified: true
|
|
@@ -40015,7 +40128,7 @@ var PRIMARY_GROUPS = [
|
|
|
40015
40128
|
["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
|
|
40016
40129
|
];
|
|
40017
40130
|
var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["oauth", "org", "runtime", "plugin", "packages", "github", "archive-prune", "intercom", "jev"]);
|
|
40018
|
-
var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read"]);
|
|
40131
|
+
var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read", "run"]);
|
|
40019
40132
|
var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
|
|
40020
40133
|
var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
|
|
40021
40134
|
var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
|
|
@@ -40061,6 +40174,8 @@ var COMMAND_OWNERSHIP = {
|
|
|
40061
40174
|
api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
|
|
40062
40175
|
github: { module_owner: "cli/src/github-budget.ts", consumer: "release-operator" },
|
|
40063
40176
|
read: { module_owner: "cli/src/app-read-broker.ts", consumer: "agent-workflow" },
|
|
40177
|
+
run: { module_owner: "cli/src/merge-group-orphans.ts", consumer: "agent-workflow" },
|
|
40178
|
+
// #9479
|
|
40064
40179
|
ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
|
|
40065
40180
|
rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
40066
40181
|
release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
@@ -41025,14 +41140,23 @@ function parseScope(value) {
|
|
|
41025
41140
|
function trailerBlockBreak(body, line) {
|
|
41026
41141
|
const lines2 = String(body ?? "").split(/\r?\n/);
|
|
41027
41142
|
const at = lines2.findIndex((candidate) => candidate.trim() === line);
|
|
41028
|
-
if (at === -1) return "";
|
|
41143
|
+
if (at === -1) return { after: "", before: "" };
|
|
41029
41144
|
for (let i = at + 1; i < lines2.length; i += 1) {
|
|
41030
41145
|
const next = lines2[i];
|
|
41031
41146
|
if (!next.trim()) continue;
|
|
41032
|
-
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return "";
|
|
41033
|
-
return next.trim();
|
|
41147
|
+
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return { after: "", before: "" };
|
|
41148
|
+
return { after: next.trim(), before: "" };
|
|
41034
41149
|
}
|
|
41035
|
-
|
|
41150
|
+
let paraStart = at;
|
|
41151
|
+
while (paraStart > 0 && lines2[paraStart - 1].trim() !== "") {
|
|
41152
|
+
paraStart -= 1;
|
|
41153
|
+
}
|
|
41154
|
+
for (let i = paraStart; i < at; i += 1) {
|
|
41155
|
+
const prev = lines2[i];
|
|
41156
|
+
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(prev) || /^[ \t]+\S/.test(prev)) continue;
|
|
41157
|
+
return { after: "", before: prev.trim() };
|
|
41158
|
+
}
|
|
41159
|
+
return { after: "", before: "" };
|
|
41036
41160
|
}
|
|
41037
41161
|
function clipParagraph(line) {
|
|
41038
41162
|
const flat = String(line).trim().replace(/\s+/g, " ");
|
|
@@ -41040,6 +41164,11 @@ function clipParagraph(line) {
|
|
|
41040
41164
|
}
|
|
41041
41165
|
function invisibleTrailer(sha, line, body = "") {
|
|
41042
41166
|
const broken = trailerBlockBreak(body, line);
|
|
41167
|
+
const brokenDiagnostic = broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
|
|
41168
|
+
${clipParagraph(broken.after)}
|
|
41169
|
+
` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
|
|
41170
|
+
${clipParagraph(broken.before)}
|
|
41171
|
+
` : " The trailer-shaped line is not in the message's FINAL `Key: value` block \u2014 prose or an\n unindented wrap either before or after it ends that block.\n";
|
|
41043
41172
|
return {
|
|
41044
41173
|
kind: "malformed-override-trailer",
|
|
41045
41174
|
paths: [],
|
|
@@ -41055,9 +41184,7 @@ function invisibleTrailer(sha, line, body = "") {
|
|
|
41055
41184
|
The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
|
|
41056
41185
|
Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
|
|
41057
41186
|
git log -1 --format='%(trailers)' ${sha.slice(0, 8)}
|
|
41058
|
-
` +
|
|
41059
|
-
${clipParagraph(broken)}
|
|
41060
|
-
` : " The trailer-shaped line is not in the message's FINAL `Key: value` block \u2014 prose or an\n unindented wrap either before or after it ends that block.\n") + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
|
|
41187
|
+
` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
|
|
41061
41188
|
git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
|
|
41062
41189
|
That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.`
|
|
41063
41190
|
};
|
|
@@ -41595,6 +41722,31 @@ ${commit.body}`.trim();
|
|
|
41595
41722
|
if (!overrides.length) return null;
|
|
41596
41723
|
return [...bodies, [...overrides, ...coauthors].join("\n")].join("\n\n");
|
|
41597
41724
|
}
|
|
41725
|
+
function prBodyTrailerSource(prBody) {
|
|
41726
|
+
const lines2 = prBody.split("\n");
|
|
41727
|
+
const separator = lines2.findIndex((line) => GH_MESSAGE_SEPARATOR.test(line.trim()));
|
|
41728
|
+
return separator === -1 ? prBody : lines2.slice(0, separator).join("\n");
|
|
41729
|
+
}
|
|
41730
|
+
function prBodyPreservesCommitOverrides(commits, headline, prBody, cwd) {
|
|
41731
|
+
const key = `${TRAILER_KEY.toLowerCase()}:`;
|
|
41732
|
+
const required = [];
|
|
41733
|
+
for (const commit of commits) {
|
|
41734
|
+
for (const trailer of parseTrailers(`${commit.headline}
|
|
41735
|
+
|
|
41736
|
+
${commit.body}`.trim(), cwd)) {
|
|
41737
|
+
const normalized = trailer.toLowerCase();
|
|
41738
|
+
if (normalized.startsWith(key) && !required.includes(normalized)) required.push(normalized);
|
|
41739
|
+
}
|
|
41740
|
+
}
|
|
41741
|
+
if (!required.length) return true;
|
|
41742
|
+
const source = prBodyTrailerSource(prBody);
|
|
41743
|
+
const recognized = parseTrailers(`${headline}
|
|
41744
|
+
|
|
41745
|
+
${source}`.trim(), cwd).map((trailer) => trailer.toLowerCase()).filter((trailer) => trailer.startsWith(key));
|
|
41746
|
+
const textual = (source.match(/^Test-Policy-Override:\s*\S/gim) ?? []).length;
|
|
41747
|
+
if (textual !== recognized.length) return false;
|
|
41748
|
+
return required.every((trailer) => recognized.includes(trailer));
|
|
41749
|
+
}
|
|
41598
41750
|
function resolveBase(cwd, explicit) {
|
|
41599
41751
|
const candidates = [explicit, process.env.TEST_POLICY_BASE, "origin/development", "origin/main"].filter(Boolean);
|
|
41600
41752
|
for (const ref of candidates) {
|
|
@@ -41797,7 +41949,17 @@ async function hasPrMergeQueue(repo, base, gh2 = readApi) {
|
|
|
41797
41949
|
if (result.mergeQueue === void 0) throw new Error(`merge queue policy unreadable for ${repo}:${base}`);
|
|
41798
41950
|
return result.mergeQueue !== null;
|
|
41799
41951
|
}
|
|
41800
|
-
var writeApi = async (args) =>
|
|
41952
|
+
var writeApi = async (args) => {
|
|
41953
|
+
const { stdout, credential } = await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
|
|
41954
|
+
return { stdout, credential };
|
|
41955
|
+
};
|
|
41956
|
+
var autoMergeApi = async (pr, repo, head) => {
|
|
41957
|
+
const args = ["pr", "merge", pr, "-R", repo, "--squash", "--auto", "--match-head-commit", head];
|
|
41958
|
+
const env = await ghFilingAppEnv("gh", args, true);
|
|
41959
|
+
if (!env) throw new Error("app actor: filing write token unavailable for merge-queue auto-merge fallback");
|
|
41960
|
+
await execFileP("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024, env });
|
|
41961
|
+
return { credential: "app_installation" };
|
|
41962
|
+
};
|
|
41801
41963
|
var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
|
|
41802
41964
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
41803
41965
|
function admissionJournal(repo, pr, root) {
|
|
@@ -41813,23 +41975,31 @@ function admissionJournal(repo, pr, root) {
|
|
|
41813
41975
|
const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
|
|
41814
41976
|
const validate = (value, previous) => {
|
|
41815
41977
|
const row = value;
|
|
41816
|
-
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.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)))) {
|
|
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)))) {
|
|
41817
41979
|
throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
|
|
41818
41980
|
}
|
|
41819
41981
|
if (!previous) {
|
|
41820
41982
|
if (row.state !== "dispatching" || row.uuid || row.queueEntry || row.observedAt !== row.createdAt) throw new Error("queue admission intent record is invalid");
|
|
41821
|
-
} else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && row.state === "ejected" && row.uuid === previous.uuid)) {
|
|
41983
|
+
} else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && row.state === "ejected" && row.uuid === previous.uuid)) {
|
|
41822
41984
|
throw new Error("queue admission receipt has an illegal transition or changed operation identity");
|
|
41823
41985
|
}
|
|
41824
41986
|
return row;
|
|
41825
41987
|
};
|
|
41826
|
-
const load = () => {
|
|
41988
|
+
const load = (number = attempt) => {
|
|
41827
41989
|
let previous;
|
|
41828
|
-
for (const line of (0, import_node_fs33.readFileSync)(path2(
|
|
41990
|
+
for (const line of (0, import_node_fs33.readFileSync)(path2(number), "utf8").trimEnd().split("\n")) previous = validate(JSON.parse(line), previous);
|
|
41829
41991
|
return previous;
|
|
41830
41992
|
};
|
|
41831
41993
|
return {
|
|
41832
41994
|
receipt: attempt ? load() : void 0,
|
|
41995
|
+
/** #9504: the exact attempt this invocation selected, so a later attempt recorded at the same head can never supply its credential. */
|
|
41996
|
+
findByIntentId(intentId) {
|
|
41997
|
+
for (let number = 1; number <= attempt; number++) {
|
|
41998
|
+
const receipt = load(number);
|
|
41999
|
+
if (receipt.intentId === intentId) return receipt;
|
|
42000
|
+
}
|
|
42001
|
+
return void 0;
|
|
42002
|
+
},
|
|
41833
42003
|
create(head) {
|
|
41834
42004
|
const createdAt = Date.now();
|
|
41835
42005
|
const initial = { version: 1, repo: repo.toLowerCase(), pr, head, intentId: (0, import_node_crypto10.randomUUID)(), createdAt, observedAt: createdAt, state: "dispatching" };
|
|
@@ -41841,9 +42011,9 @@ function admissionJournal(repo, pr, root) {
|
|
|
41841
42011
|
}
|
|
41842
42012
|
return true;
|
|
41843
42013
|
},
|
|
41844
|
-
save(state, uuid, queueEntry) {
|
|
42014
|
+
save(state, uuid, queueEntry, credential) {
|
|
41845
42015
|
const previous = load();
|
|
41846
|
-
const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {} }, previous);
|
|
42016
|
+
const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {}, ...credential && previous.credential === void 0 ? { credential } : {} }, previous);
|
|
41847
42017
|
(0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
|
|
41848
42018
|
load();
|
|
41849
42019
|
}
|
|
@@ -41866,6 +42036,21 @@ function admissionResponse(output) {
|
|
|
41866
42036
|
}
|
|
41867
42037
|
return { code: Number(response[1]), body: result };
|
|
41868
42038
|
}
|
|
42039
|
+
function admissionRejected(reason) {
|
|
42040
|
+
if (/enqueuer is not authorized to merge|not authorized to merge/i.test(reason)) {
|
|
42041
|
+
return {
|
|
42042
|
+
mergeStatus: "failed",
|
|
42043
|
+
retryable: false,
|
|
42044
|
+
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.`
|
|
42046
|
+
};
|
|
42047
|
+
}
|
|
42048
|
+
return {
|
|
42049
|
+
mergeStatus: "failed",
|
|
42050
|
+
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.`
|
|
42052
|
+
};
|
|
42053
|
+
}
|
|
41869
42054
|
async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
|
|
41870
42055
|
const unknown = (reason) => ({
|
|
41871
42056
|
mergeStatus: "failed",
|
|
@@ -41873,11 +42058,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41873
42058
|
outcomeUnknown: true,
|
|
41874
42059
|
error: `queue submission outcome unknown: ${reason}. Preserve the receipt and rerun pr land to reconcile read-only; no new request was sent after this uncertainty.`
|
|
41875
42060
|
});
|
|
41876
|
-
const rejected =
|
|
41877
|
-
mergeStatus: "failed",
|
|
41878
|
-
retryable: false,
|
|
41879
|
-
error: `queue admission rejected: ${reason}. Resolve the reported cause, then explicitly rerun pr land; it will repeat preflight before a new request.`
|
|
41880
|
-
});
|
|
42061
|
+
const rejected = admissionRejected;
|
|
41881
42062
|
const reconcile = async (reason, recordEntry = false) => {
|
|
41882
42063
|
try {
|
|
41883
42064
|
const observed = await readPrMergeQueue(pr, repo, read);
|
|
@@ -41889,6 +42070,22 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41889
42070
|
}
|
|
41890
42071
|
return { mergeStatus: "auto-merge-enqueued" };
|
|
41891
42072
|
}
|
|
42073
|
+
const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
|
|
42074
|
+
if (saved?.queueEntry && observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === saved.head) {
|
|
42075
|
+
const enqueuedAt = Date.parse(saved.queueEntry.enqueuedAt);
|
|
42076
|
+
const events = await readArrayPages(
|
|
42077
|
+
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
42078
|
+
options.timelineRead ?? read,
|
|
42079
|
+
"queue ejection timeline unreadable"
|
|
42080
|
+
);
|
|
42081
|
+
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 >= enqueuedAt);
|
|
42083
|
+
const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
|
|
42084
|
+
if (removals.length === 1) {
|
|
42085
|
+
journal.save("ejected");
|
|
42086
|
+
return rejected("the previous admission was ejected by GitHub; rerun to submit a new intent");
|
|
42087
|
+
}
|
|
42088
|
+
}
|
|
41892
42089
|
} catch {
|
|
41893
42090
|
}
|
|
41894
42091
|
return unknown(reason);
|
|
@@ -41909,24 +42106,6 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41909
42106
|
} catch {
|
|
41910
42107
|
}
|
|
41911
42108
|
}
|
|
41912
|
-
if (receipt?.state === "enqueued" && !superseded) {
|
|
41913
|
-
try {
|
|
41914
|
-
const observed = await readPrMergeQueue(pr, repo, read);
|
|
41915
|
-
if (observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === receipt.head) {
|
|
41916
|
-
const events = await readArrayPages(
|
|
41917
|
-
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
41918
|
-
options.timelineRead ?? read,
|
|
41919
|
-
"queue ejection timeline unreadable"
|
|
41920
|
-
);
|
|
41921
|
-
const admittedAt = receipt.queueEntry ? Date.parse(receipt.queueEntry.enqueuedAt) : receipt.observedAt;
|
|
41922
|
-
if (events.some((event) => event.event === "removed_from_merge_queue" && Date.parse(event.created_at) > admittedAt)) {
|
|
41923
|
-
journal.save("ejected");
|
|
41924
|
-
return rejected("the previous admission was ejected by GitHub; rerun to submit a new intent");
|
|
41925
|
-
}
|
|
41926
|
-
}
|
|
41927
|
-
} catch {
|
|
41928
|
-
}
|
|
41929
|
-
}
|
|
41930
42109
|
if (superseded && receipt?.state === "enqueued") {
|
|
41931
42110
|
try {
|
|
41932
42111
|
journal.save("ejected");
|
|
@@ -41935,6 +42114,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41935
42114
|
}
|
|
41936
42115
|
}
|
|
41937
42116
|
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
|
|
42117
|
+
if (options.selection) {
|
|
42118
|
+
options.selection.selected = true;
|
|
42119
|
+
options.selection.intentId = receipt.intentId;
|
|
42120
|
+
}
|
|
41938
42121
|
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");
|
|
41939
42122
|
if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
|
|
41940
42123
|
} else {
|
|
@@ -41943,12 +42126,17 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41943
42126
|
if (!journal.create(head)) return submitQueueAdmission(pr, repo, head, read, write, options, true);
|
|
41944
42127
|
journal = admissionJournal(repo, pr, options.receiptDirectory);
|
|
41945
42128
|
receipt = journal.receipt;
|
|
42129
|
+
if (options.selection) {
|
|
42130
|
+
options.selection.selected = true;
|
|
42131
|
+
options.selection.intentId = receipt.intentId;
|
|
42132
|
+
}
|
|
41946
42133
|
} catch {
|
|
41947
42134
|
return unknown("the pre-send checkpoint failed; no request was dispatched");
|
|
41948
42135
|
}
|
|
41949
42136
|
let output;
|
|
42137
|
+
let credential;
|
|
41950
42138
|
try {
|
|
41951
|
-
|
|
42139
|
+
const written = await write([
|
|
41952
42140
|
`repos/${repo}/pulls/${pr}/merge-async`,
|
|
41953
42141
|
"--method",
|
|
41954
42142
|
"PUT",
|
|
@@ -41961,8 +42149,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41961
42149
|
"-f",
|
|
41962
42150
|
`sha=${head}`
|
|
41963
42151
|
]);
|
|
42152
|
+
({ stdout: output, credential } = typeof written === "string" ? { stdout: written, credential: void 0 } : written);
|
|
41964
42153
|
} catch (error) {
|
|
41965
42154
|
output = error.stdout ?? "";
|
|
42155
|
+
credential = error.credential;
|
|
41966
42156
|
}
|
|
41967
42157
|
try {
|
|
41968
42158
|
response = admissionResponse(output);
|
|
@@ -41972,19 +42162,51 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41972
42162
|
const { code, body } = response;
|
|
41973
42163
|
if (code === 200 && ["enqueued", "merged"].includes(body.status ?? "")) {
|
|
41974
42164
|
try {
|
|
41975
|
-
journal.save(body.status);
|
|
42165
|
+
journal.save(body.status, void 0, void 0, credential);
|
|
41976
42166
|
} catch {
|
|
41977
42167
|
return reconcile("the completed operation could not be checkpointed");
|
|
41978
42168
|
}
|
|
41979
42169
|
return reconcile(`GitHub reported ${body.status}, but matching-head membership or merged state is unconfirmed`, body.status === "enqueued");
|
|
41980
42170
|
}
|
|
42171
|
+
const tryAutoMergeFallback = async (reason, uuid) => {
|
|
42172
|
+
const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
|
|
42173
|
+
if (!/enqueuer is not authorized to merge/i.test(reason) || !autoMerge) return void 0;
|
|
42174
|
+
try {
|
|
42175
|
+
const fallback = await autoMerge(pr, repo, head);
|
|
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
|
+
}
|
|
42196
|
+
} catch {
|
|
42197
|
+
}
|
|
42198
|
+
return void 0;
|
|
42199
|
+
};
|
|
41981
42200
|
if (code === 400 || code === 403 || code === 404 || code === 422) {
|
|
42201
|
+
const reason = body.details?.message ?? body.message ?? `HTTP ${code}`;
|
|
42202
|
+
const fallbackResult = await tryAutoMergeFallback(reason);
|
|
42203
|
+
if (fallbackResult) return fallbackResult;
|
|
41982
42204
|
try {
|
|
41983
|
-
journal.save("failed");
|
|
42205
|
+
journal.save("failed", void 0, void 0, credential);
|
|
41984
42206
|
} catch {
|
|
41985
42207
|
return unknown("the explicit rejection could not be checkpointed");
|
|
41986
42208
|
}
|
|
41987
|
-
return rejected(
|
|
42209
|
+
return rejected(reason);
|
|
41988
42210
|
}
|
|
41989
42211
|
const details = body.details;
|
|
41990
42212
|
if (code !== 202 && code !== 409 || body.status !== "pending" || !UUID.test(details?.uuid ?? "")) return reconcile("the send returned no recognized admission result");
|
|
@@ -41992,7 +42214,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41992
42214
|
return unknown("an existing operation has different or missing queue/head/no-bypass options; inspect that operation before continuing");
|
|
41993
42215
|
}
|
|
41994
42216
|
try {
|
|
41995
|
-
journal.save("pending", details.uuid);
|
|
42217
|
+
journal.save("pending", details.uuid, void 0, credential);
|
|
41996
42218
|
} catch {
|
|
41997
42219
|
return reconcile("the accepted operation UUID could not be checkpointed");
|
|
41998
42220
|
}
|
|
@@ -42015,6 +42237,36 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
42015
42237
|
continue;
|
|
42016
42238
|
}
|
|
42017
42239
|
if (!["failed", "enqueued", "merged"].includes(status ?? "")) return reconcile("the operation returned an unrecognized state");
|
|
42240
|
+
if (status === "failed") {
|
|
42241
|
+
const reason = details?.message ?? "GitHub reported a failed operation";
|
|
42242
|
+
const autoMerge = options.autoMerge ?? (write === writeApi ? autoMergeApi : void 0);
|
|
42243
|
+
if (/enqueuer is not authorized to merge/i.test(reason) && autoMerge && receipt.head === head) {
|
|
42244
|
+
try {
|
|
42245
|
+
const fallback = await autoMerge(pr, repo, head);
|
|
42246
|
+
const fallbackCredential = fallback?.credential ?? receipt?.credential;
|
|
42247
|
+
const observed = await readPrMergeQueue(pr, repo, read);
|
|
42248
|
+
if (observed.state === "MERGED" && observed.headRefOid === head) {
|
|
42249
|
+
try {
|
|
42250
|
+
journal.save("merged", receipt.uuid, void 0, fallbackCredential);
|
|
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" };
|
|
42265
|
+
}
|
|
42266
|
+
} catch {
|
|
42267
|
+
}
|
|
42268
|
+
}
|
|
42269
|
+
}
|
|
42018
42270
|
try {
|
|
42019
42271
|
journal.save(status, receipt.uuid);
|
|
42020
42272
|
} catch {
|
|
@@ -42047,8 +42299,12 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42047
42299
|
if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
|
|
42048
42300
|
const head = options.expectedHeadOid ?? pull.headRefOid;
|
|
42049
42301
|
if (head !== pull.headRefOid) return { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` };
|
|
42050
|
-
|
|
42051
|
-
|
|
42302
|
+
const stamp = (result2, selected = false, intentId) => {
|
|
42303
|
+
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
42304
|
+
return credential === "unknown" ? result2 : { ...result2, credential };
|
|
42305
|
+
};
|
|
42306
|
+
if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
|
|
42307
|
+
if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
|
|
42052
42308
|
if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
|
|
42053
42309
|
const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
|
|
42054
42310
|
let page = pull.closingIssuesReferences.pageInfo;
|
|
@@ -42144,7 +42400,20 @@ ${pull.body}`;
|
|
|
42144
42400
|
return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}`);
|
|
42145
42401
|
}
|
|
42146
42402
|
const requiredBody = options.bodyText ?? composedBody;
|
|
42147
|
-
|
|
42403
|
+
const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
|
|
42404
|
+
if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
|
|
42405
|
+
if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
|
|
42406
|
+
return {
|
|
42407
|
+
mergeStatus: "failed",
|
|
42408
|
+
preparation: {
|
|
42409
|
+
status: "preparation-needed",
|
|
42410
|
+
reason: "queue-message-override",
|
|
42411
|
+
issueTargets: [],
|
|
42412
|
+
nextAction: "PR author: this repository squashes with PR_BODY, so place the required Test-Policy-Override trailer(s) in the PR body final trailer block (e.g. mmi-cli devops pr edit <n> --body-file <path>) without a body override, then rerun pr land. Keep repository settings unchanged."
|
|
42413
|
+
},
|
|
42414
|
+
error: "queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block"
|
|
42415
|
+
};
|
|
42416
|
+
}
|
|
42148
42417
|
return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
|
|
42149
42418
|
}
|
|
42150
42419
|
const current = await queueGraphql(
|
|
@@ -42155,8 +42424,9 @@ ${pull.body}`;
|
|
|
42155
42424
|
if (current.pullRequest.headRefOid !== head || current.pullRequest.title !== pull.title || current.pullRequest.body !== pull.body) {
|
|
42156
42425
|
return { mergeStatus: "failed", retryable: false, error: "queue source or PR text changed during preparation; rerun pr land against the new revision" };
|
|
42157
42426
|
}
|
|
42158
|
-
const
|
|
42159
|
-
|
|
42427
|
+
const selection = { selected: false };
|
|
42428
|
+
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
42429
|
+
return { ...stamp(result, selection.selected, selection.intentId), ...verdict.message ? { warning: verdict.message } : {} };
|
|
42160
42430
|
}
|
|
42161
42431
|
function decideQueueMergeRoute(hasQueue, method, headRef) {
|
|
42162
42432
|
if (!hasQueue) return "normal";
|
|
@@ -42194,8 +42464,50 @@ function prMergeQueueLine(result) {
|
|
|
42194
42464
|
return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
|
|
42195
42465
|
}
|
|
42196
42466
|
}
|
|
42467
|
+
function admissionCredential(repo, pr, head, receiptDirectory, selected = false, intentId) {
|
|
42468
|
+
try {
|
|
42469
|
+
const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
|
|
42470
|
+
const saved = intentId ? journal.findByIntentId(intentId) : journal.receipt;
|
|
42471
|
+
const live = saved && ["pending", "enqueued", "merged"].includes(saved.state);
|
|
42472
|
+
return saved && saved.head.toLowerCase() === head.toLowerCase() && (selected || live) ? saved.credential ?? "unknown" : "unknown";
|
|
42473
|
+
} catch {
|
|
42474
|
+
return "unknown";
|
|
42475
|
+
}
|
|
42476
|
+
}
|
|
42477
|
+
function observeQueuedAdmission(repo, pr, snapshot, receiptDirectory) {
|
|
42478
|
+
try {
|
|
42479
|
+
const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
|
|
42480
|
+
const saved = journal.receipt;
|
|
42481
|
+
if (!saved || !["pending", "enqueued", "merged"].includes(saved.state) || snapshot.headRefOid?.toLowerCase() !== saved.head.toLowerCase()) return {};
|
|
42482
|
+
const entry = snapshot.mergeQueueEntry;
|
|
42483
|
+
if (saved.queueEntry) {
|
|
42484
|
+
if (entry?.enqueuedAt && entry.enqueuedAt !== saved.queueEntry.enqueuedAt) return { mismatched: true };
|
|
42485
|
+
} else if ((saved.state === "enqueued" || saved.state === "pending") && snapshot.isInMergeQueue && entry?.headCommit && entry.enqueuedAt) {
|
|
42486
|
+
try {
|
|
42487
|
+
journal.save("enqueued", saved.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
|
|
42488
|
+
} catch {
|
|
42489
|
+
return {};
|
|
42490
|
+
}
|
|
42491
|
+
}
|
|
42492
|
+
return saved.credential ? { credential: saved.credential } : {};
|
|
42493
|
+
} catch {
|
|
42494
|
+
return {};
|
|
42495
|
+
}
|
|
42496
|
+
}
|
|
42197
42497
|
|
|
42198
42498
|
// src/merge-group-orphans.ts
|
|
42499
|
+
var SETTLE_POLLS = 6;
|
|
42500
|
+
var SETTLE_MS = 5e3;
|
|
42501
|
+
async function settled(repo, id, deps) {
|
|
42502
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
42503
|
+
let status = "unknown";
|
|
42504
|
+
for (let poll = 0; poll < SETTLE_POLLS; poll++) {
|
|
42505
|
+
await sleep2(SETTLE_MS);
|
|
42506
|
+
status = JSON.parse(await deps.read([`repos/${repo}/actions/runs/${id}`])).status ?? "unknown";
|
|
42507
|
+
if (status === "completed") break;
|
|
42508
|
+
}
|
|
42509
|
+
return status;
|
|
42510
|
+
}
|
|
42199
42511
|
async function isOrphan(run, repo, deps) {
|
|
42200
42512
|
const match = /^gh-readonly-queue\/(.+)\/pr-([1-9][0-9]*)-([a-f0-9]+)$/i.exec(run.head_branch);
|
|
42201
42513
|
if (!match || run.event !== "merge_group") return false;
|
|
@@ -42215,7 +42527,7 @@ async function isOrphan(run, repo, deps) {
|
|
|
42215
42527
|
}
|
|
42216
42528
|
}
|
|
42217
42529
|
async function cancelMergeGroupOrphans(repo, apply, deps) {
|
|
42218
|
-
const report = { repo, apply, candidates: [], cancelled: [], errors: [] };
|
|
42530
|
+
const report = { repo, apply, candidates: [], cancelled: [], forceRequested: [], forceCancelled: [], errors: [] };
|
|
42219
42531
|
try {
|
|
42220
42532
|
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error("expected --repo owner/repo");
|
|
42221
42533
|
const runs2 = [];
|
|
@@ -42237,7 +42549,18 @@ async function cancelMergeGroupOrphans(repo, apply, deps) {
|
|
|
42237
42549
|
for (const run of confirmed) {
|
|
42238
42550
|
if (!await isOrphan(run, repo, deps)) continue;
|
|
42239
42551
|
await deps.cancel(repo, run.id);
|
|
42240
|
-
|
|
42552
|
+
let status = await settled(repo, run.id, deps);
|
|
42553
|
+
let forced = false;
|
|
42554
|
+
if (status !== "completed" && await isOrphan(run, repo, deps)) {
|
|
42555
|
+
await deps.forceCancel(repo, run.id);
|
|
42556
|
+
report.forceRequested.push(run.id);
|
|
42557
|
+
forced = true;
|
|
42558
|
+
status = await settled(repo, run.id, deps);
|
|
42559
|
+
}
|
|
42560
|
+
if (status === "completed") {
|
|
42561
|
+
report.cancelled.push(run.id);
|
|
42562
|
+
if (forced) report.forceCancelled.push(run.id);
|
|
42563
|
+
} else report.errors.push(`cancel-orphans: run ${run.id} still ${status} after cancel${forced ? " and force-cancel" : ""}`);
|
|
42241
42564
|
}
|
|
42242
42565
|
}
|
|
42243
42566
|
} catch (error) {
|
|
@@ -42263,17 +42586,68 @@ function registerCancelOrphans(ci2, writeToken) {
|
|
|
42263
42586
|
timeout: 2e4,
|
|
42264
42587
|
env: { ...process.env, GH_TOKEN: token }
|
|
42265
42588
|
});
|
|
42589
|
+
},
|
|
42590
|
+
forceCancel: async (repo, id) => {
|
|
42591
|
+
token ??= await writeToken();
|
|
42592
|
+
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${id}/force-cancel`, "--method", "POST"], {
|
|
42593
|
+
timeout: 2e4,
|
|
42594
|
+
env: { ...process.env, GH_TOKEN: token }
|
|
42595
|
+
});
|
|
42266
42596
|
}
|
|
42267
42597
|
});
|
|
42268
42598
|
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
42269
42599
|
else {
|
|
42270
42600
|
for (const run of report.candidates) console.log(`${run.id} ${run.head_branch} ${run.html_url}`);
|
|
42271
|
-
console.log(`cancel-orphans: ${report.candidates.length} candidates; ${report.cancelled.length} cancelled${options.apply ? "" : " (report only; use --apply)"}`);
|
|
42601
|
+
console.log(`cancel-orphans: ${report.candidates.length} candidates; ${report.cancelled.length} cancelled (verified completed; ${report.forceCancelled.length} of them after force-cancel; ${report.forceRequested.length} force-cancel request(s))${options.apply ? "" : " (report only; use --apply)"}`);
|
|
42272
42602
|
for (const error of report.errors) console.error(error);
|
|
42273
42603
|
}
|
|
42274
42604
|
if (report.errors.length) process.exitCode = 1;
|
|
42275
42605
|
});
|
|
42276
42606
|
}
|
|
42607
|
+
var RUN_CANCEL_STATUS = "cancel-requested";
|
|
42608
|
+
var RUN_FORCE_CANCEL_STATUS = "force-cancel-requested";
|
|
42609
|
+
async function cancelWorkflowRun(repo, id, deps, force = false) {
|
|
42610
|
+
const raw = String(id).trim();
|
|
42611
|
+
if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run cancel: <id> must be a positive workflow run id (received '${String(id)}')`);
|
|
42612
|
+
const runId = Number(raw);
|
|
42613
|
+
if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
|
|
42614
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
|
|
42615
|
+
try {
|
|
42616
|
+
if (force && deps.forceCancel) await deps.forceCancel(repo, runId);
|
|
42617
|
+
else await deps.cancel(repo, runId);
|
|
42618
|
+
} catch (error) {
|
|
42619
|
+
const detail = String(error.stderr ?? error);
|
|
42620
|
+
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
|
+
}
|
|
42622
|
+
return { ok: true, repo, runId, ...force ? { force: true } : {}, status: force ? RUN_FORCE_CANCEL_STATUS : RUN_CANCEL_STATUS };
|
|
42623
|
+
}
|
|
42624
|
+
function registerRunCancel(run, writeToken) {
|
|
42625
|
+
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
|
+
let receipt;
|
|
42627
|
+
try {
|
|
42628
|
+
receipt = await cancelWorkflowRun(options.repo, id, {
|
|
42629
|
+
cancel: async (repo, runId) => {
|
|
42630
|
+
const token = await writeToken();
|
|
42631
|
+
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
|
|
42632
|
+
timeout: 2e4,
|
|
42633
|
+
env: { ...process.env, GH_TOKEN: token }
|
|
42634
|
+
});
|
|
42635
|
+
},
|
|
42636
|
+
forceCancel: async (repo, runId) => {
|
|
42637
|
+
const token = await writeToken();
|
|
42638
|
+
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/force-cancel`, "--method", "POST"], {
|
|
42639
|
+
timeout: 2e4,
|
|
42640
|
+
env: { ...process.env, GH_TOKEN: token }
|
|
42641
|
+
});
|
|
42642
|
+
}
|
|
42643
|
+
}, options.force === true);
|
|
42644
|
+
} catch (error) {
|
|
42645
|
+
return fail(error.message);
|
|
42646
|
+
}
|
|
42647
|
+
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})`);
|
|
42649
|
+
});
|
|
42650
|
+
}
|
|
42277
42651
|
|
|
42278
42652
|
// src/board-commands.ts
|
|
42279
42653
|
init_cli_shared();
|
|
@@ -42690,6 +43064,8 @@ function registerBoardCommands(program3) {
|
|
|
42690
43064
|
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
42691
43065
|
if (result.claimed) for (const line of formatDecisionLines(result.ref, ruled[i].decisions ?? [])) console.log(line);
|
|
42692
43066
|
});
|
|
43067
|
+
const summary = formatBulkClaimSummary(bulk.results);
|
|
43068
|
+
if (summary) console.log(summary);
|
|
42693
43069
|
printClaimWarnings([...bulk.warnings ?? [], ...rulingWarnings]);
|
|
42694
43070
|
}
|
|
42695
43071
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
@@ -46353,13 +46729,14 @@ function filledDocCheck(label, text, path2) {
|
|
|
46353
46729
|
const unfilled = unfilledDocPlaceholders(text);
|
|
46354
46730
|
return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
|
|
46355
46731
|
}
|
|
46356
|
-
var NPMRC_PATHS = [".npmrc", "web/.npmrc"];
|
|
46732
|
+
var NPMRC_PATHS = [".npmrc", "module/.npmrc", "web/.npmrc"];
|
|
46357
46733
|
var SCOPED_GITHUB_PACKAGES_RE = /^\s*@[A-Za-z0-9-]+:registry\s*=\s*https:\/\/npm\.pkg\.github\.com/m;
|
|
46358
46734
|
var GITHUB_PACKAGES_AUTH_RE = /^\s*\/\/npm\.pkg\.github\.com\/:_authToken\s*=(.*)$/m;
|
|
46359
46735
|
function npmrcAuthValueIsPlaceholder(value) {
|
|
46360
46736
|
const trimmed = value.trim();
|
|
46361
46737
|
return trimmed === "" || /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(trimmed);
|
|
46362
46738
|
}
|
|
46739
|
+
var GH_PACKAGES_SCOPE_NOTE = ". Note that `gh auth token` only works as a Packages credential when that login carries the read:packages scope \u2014 without it the same install fails as `E403 ... Permission permission_denied: The token ... does not match expected scopes` (a scope refusal, not a 401/E401). Recoveries: widen the login with `gh auth refresh -h github.com -s read:packages`, or keep the token keyless in the Jerv vault key `GH_PACKAGES_READ_TOKEN` and supply it with `jerv-cli vault use`";
|
|
46363
46740
|
function npmrcGitHubPackagesAuthCheck(path2, text) {
|
|
46364
46741
|
if (text === null || !SCOPED_GITHUB_PACKAGES_RE.test(text)) return null;
|
|
46365
46742
|
const label = `${path2} authenticates GitHub Packages`;
|
|
@@ -46368,7 +46745,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
|
|
|
46368
46745
|
return {
|
|
46369
46746
|
ok: false,
|
|
46370
46747
|
label,
|
|
46371
|
-
detail: `${path2} points a scope at npm.pkg.github.com with no token line \u2014 add \`//npm.pkg.github.com/:_authToken=<token>\` with a real value, or keep the line as \`\${NODE_AUTH_TOKEN}\` and supply the token in the shell for every install (NODE_AUTH_TOKEN=$(gh auth token) npm ci) \u2014 otherwise npm ci 401s outside a setup-node CI job`
|
|
46748
|
+
detail: `${path2} points a scope at npm.pkg.github.com with no token line \u2014 add \`//npm.pkg.github.com/:_authToken=<token>\` with a real value, or keep the line as \`\${NODE_AUTH_TOKEN}\` and supply the token in the shell for every install (NODE_AUTH_TOKEN=$(gh auth token) npm ci) \u2014 otherwise npm ci 401s outside a setup-node CI job` + GH_PACKAGES_SCOPE_NOTE
|
|
46372
46749
|
};
|
|
46373
46750
|
}
|
|
46374
46751
|
if (npmrcAuthValueIsPlaceholder(match[1])) {
|
|
@@ -46376,7 +46753,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
|
|
|
46376
46753
|
return {
|
|
46377
46754
|
ok: false,
|
|
46378
46755
|
label,
|
|
46379
|
-
detail: `${path2} carries only the placeholder //npm.pkg.github.com/:_authToken=${carried} \u2014 declared, not proven: GitHub Packages reads that line only while NODE_AUTH_TOKEN is exported in the same shell, so a lane, laptop or container with it unset 401s before anything names the cause. Two recoveries: put a real token value in the line, or supply the token from the shell (NODE_AUTH_TOKEN=$(gh auth token) npm ci)`
|
|
46756
|
+
detail: `${path2} carries only the placeholder //npm.pkg.github.com/:_authToken=${carried} \u2014 declared, not proven: GitHub Packages reads that line only while NODE_AUTH_TOKEN is exported in the same shell, so a lane, laptop or container with it unset 401s before anything names the cause. Two recoveries: put a real token value in the line, or supply the token from the shell (NODE_AUTH_TOKEN=$(gh auth token) npm ci)` + GH_PACKAGES_SCOPE_NOTE
|
|
46380
46757
|
};
|
|
46381
46758
|
}
|
|
46382
46759
|
return { ok: true, label };
|
|
@@ -46564,7 +46941,15 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
46564
46941
|
});
|
|
46565
46942
|
}
|
|
46566
46943
|
}
|
|
46567
|
-
|
|
46944
|
+
const tree = await restJson3(
|
|
46945
|
+
deps,
|
|
46946
|
+
`repos/${repo}/git/trees/${encodeURIComponent(baseBranch)}?recursive=1`,
|
|
46947
|
+
{}
|
|
46948
|
+
);
|
|
46949
|
+
const discoveredNpmrcPaths = (tree.tree ?? []).filter(
|
|
46950
|
+
(entry) => entry.type === "blob" && typeof entry.path === "string" && (entry.path === ".npmrc" || entry.path.endsWith("/.npmrc"))
|
|
46951
|
+
).map((entry) => entry.path);
|
|
46952
|
+
for (const path2 of /* @__PURE__ */ new Set([...NPMRC_PATHS, ...discoveredNpmrcPaths])) {
|
|
46568
46953
|
const check = npmrcGitHubPackagesAuthCheck(path2, await contentText(deps, repo, baseBranch, path2));
|
|
46569
46954
|
if (check) checks.push(check);
|
|
46570
46955
|
}
|
|
@@ -51778,7 +52163,7 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
|
51778
52163
|
if (now() >= deadline) break;
|
|
51779
52164
|
const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
|
|
51780
52165
|
if (checks.status !== "success" && checks.status !== "skipped") {
|
|
51781
|
-
return { mergeStatus: "failed", error: `${result.error ?? "merge failed"}
|
|
52166
|
+
return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
|
|
51782
52167
|
registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
|
|
51783
52168
|
}
|
|
51784
52169
|
if (now() >= deadline) break;
|
|
@@ -51924,6 +52309,8 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
51924
52309
|
if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
|
|
51925
52310
|
const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
|
|
51926
52311
|
base.mergeStatus = merge.mergeStatus;
|
|
52312
|
+
if (merge.credential) base.credential = merge.credential;
|
|
52313
|
+
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
51927
52314
|
if (merge.mergeStatus === "queue-admission-pending") {
|
|
51928
52315
|
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
51929
52316
|
}
|
|
@@ -51985,9 +52372,19 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
51985
52372
|
const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
|
|
51986
52373
|
const deadline = deadlineMs ?? now() + timeoutMs;
|
|
51987
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
|
+
};
|
|
51988
52384
|
while (now() < deadline) {
|
|
51989
52385
|
const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
|
|
51990
52386
|
snapshot = void 0;
|
|
52387
|
+
if (read.isInMergeQueue || read.state === "MERGED") observe(read);
|
|
51991
52388
|
if (read.state === "MERGED") return queueMerged(base, deps);
|
|
51992
52389
|
if (read.isInMergeQueue) {
|
|
51993
52390
|
base.mergeQueue = {
|
|
@@ -52000,7 +52397,10 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
52000
52397
|
} else if (read.state === "OPEN" && !read.autoMergeRequest) {
|
|
52001
52398
|
await sleep2(PR_LAND_EJECT_CONFIRM_MS);
|
|
52002
52399
|
const again = await deps.readMergeQueue(pr, base.repo);
|
|
52003
|
-
if (again.state === "MERGED")
|
|
52400
|
+
if (again.state === "MERGED") {
|
|
52401
|
+
observe(again);
|
|
52402
|
+
return queueMerged(base, deps);
|
|
52403
|
+
}
|
|
52004
52404
|
if (again.isInMergeQueue || again.autoMergeRequest) {
|
|
52005
52405
|
snapshot = again;
|
|
52006
52406
|
continue;
|
|
@@ -52055,6 +52455,10 @@ function renderPrLandTable(entries) {
|
|
|
52055
52455
|
const width = (i) => Math.max(...rows.map((r) => r[i].length));
|
|
52056
52456
|
return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
|
|
52057
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
|
+
}
|
|
52058
52462
|
|
|
52059
52463
|
// src/git-worktree-observation.ts
|
|
52060
52464
|
function parseGitWorktreePorcelain(text) {
|
|
@@ -52255,6 +52659,12 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).filter((line) => li
|
|
|
52255
52659
|
var defaultGhMergeAutoIo = {
|
|
52256
52660
|
gh: async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout
|
|
52257
52661
|
};
|
|
52662
|
+
var credentialedGh = {
|
|
52663
|
+
gh: async (args, timeoutMs) => {
|
|
52664
|
+
const { stdout, credential } = await execFileP("gh", args, { timeout: timeoutMs });
|
|
52665
|
+
return { stdout, credential };
|
|
52666
|
+
}
|
|
52667
|
+
};
|
|
52258
52668
|
function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
|
|
52259
52669
|
const policy = squashBodyWithOverride(commits, cwd);
|
|
52260
52670
|
const base = policy ?? defaultSquashBodyFromCommits(commits);
|
|
@@ -52316,14 +52726,34 @@ async function ghAdminTrueMergeWhenClean(prNumber, repo, opts = {}) {
|
|
|
52316
52726
|
return { mergeStatus: "merged" };
|
|
52317
52727
|
}
|
|
52318
52728
|
async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
|
|
52319
|
-
|
|
52729
|
+
let credential;
|
|
52730
|
+
let refusedAsMerged = false;
|
|
52731
|
+
const source = extra?.io ?? credentialedGh;
|
|
52732
|
+
const note = (args2, value) => {
|
|
52733
|
+
if (args2[0] === "pr" && args2[1] === "merge" && value) credential = value;
|
|
52734
|
+
};
|
|
52735
|
+
const io = {
|
|
52736
|
+
gh: async (ghArgs, timeoutMs) => {
|
|
52737
|
+
try {
|
|
52738
|
+
const out = await source.gh(ghArgs, timeoutMs);
|
|
52739
|
+
if (typeof out === "string") return out;
|
|
52740
|
+
note(ghArgs, out.credential);
|
|
52741
|
+
return out.stdout;
|
|
52742
|
+
} catch (e) {
|
|
52743
|
+
note(ghArgs, e?.credential);
|
|
52744
|
+
if (ghArgs[0] === "pr" && ghArgs[1] === "merge" && /already been merged/i.test(String(e?.message ?? ""))) refusedAsMerged = true;
|
|
52745
|
+
throw e;
|
|
52746
|
+
}
|
|
52747
|
+
}
|
|
52748
|
+
};
|
|
52320
52749
|
const args = repo ? ["--repo", repo] : [];
|
|
52321
52750
|
const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh, {
|
|
52322
52751
|
allowedClosing: method === "--squash" ? extra?.allowedClosing : void 0,
|
|
52323
52752
|
bodyText: method === "--squash" ? extra?.bodyText : void 0
|
|
52324
52753
|
});
|
|
52325
52754
|
try {
|
|
52326
|
-
|
|
52755
|
+
const result = await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue, extra?.matchHeadCommit);
|
|
52756
|
+
return credential ? { ...result, credential, ...refusedAsMerged && result.mergeStatus === "merged" ? { credentialRole: "attempted-write" } : {} } : result;
|
|
52327
52757
|
} finally {
|
|
52328
52758
|
overrideBody?.cleanup();
|
|
52329
52759
|
}
|
|
@@ -55727,7 +56157,7 @@ async function observePrCreateClaims(body, repoOption, deps = {}) {
|
|
|
55727
56157
|
if (findings.length) throw new Error("pr create: claim findings must be resolved before restamping");
|
|
55728
56158
|
if (attempted) throw new Error("pr create: claim restamp already attempted; observe again before retrying");
|
|
55729
56159
|
attempted = true;
|
|
55730
|
-
const writer = deps.client ?? defaultGitHubClient();
|
|
56160
|
+
const writer = await selectClaimWriteClient(deps.client ?? defaultGitHubClient(), deps.appClient);
|
|
55731
56161
|
for (const number of restamps) {
|
|
55732
56162
|
await writer.rest("POST", `repos/${repo}/issues/${number}/comments`, { body: { body: formatClaimMarker(actor) } });
|
|
55733
56163
|
}
|
|
@@ -55743,11 +56173,11 @@ function catFileBatch(root, ref, paths) {
|
|
|
55743
56173
|
if (paths.length === 0) return Promise.resolve([]);
|
|
55744
56174
|
return new Promise((resolve7) => {
|
|
55745
56175
|
const chunks = [];
|
|
55746
|
-
let
|
|
56176
|
+
let settled2 = false;
|
|
55747
56177
|
const child2 = (0, import_node_child_process15.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
|
|
55748
56178
|
const finish = (ok = false) => {
|
|
55749
|
-
if (
|
|
55750
|
-
|
|
56179
|
+
if (settled2) return;
|
|
56180
|
+
settled2 = true;
|
|
55751
56181
|
clearTimeout(timer);
|
|
55752
56182
|
resolve7(ok ? parseCatFileBatch(Buffer.concat(chunks), paths.length) : []);
|
|
55753
56183
|
};
|
|
@@ -55913,6 +56343,21 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
|
55913
56343
|
}
|
|
55914
56344
|
};
|
|
55915
56345
|
}
|
|
56346
|
+
async function retryPrCreateIdentityDrift(input, deps, notify = (line) => process.stderr.write(line)) {
|
|
56347
|
+
notify("pr create: WARNING \u2014 candidate or policy/config identity changed during preparation; re-reading the head, base and policy/config identity and preparing once more (#9470)\n");
|
|
56348
|
+
const preparation = await preparePrCreate(input, deps);
|
|
56349
|
+
if (preparation.findings.length) {
|
|
56350
|
+
return { preparation, error: new Error(`preparation found ${preparation.findings.length} blocker(s) after re-reading the current identity: ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${finding.detail}`).join("; ")}`) };
|
|
56351
|
+
}
|
|
56352
|
+
try {
|
|
56353
|
+
await preparation.commit();
|
|
56354
|
+
return { preparation };
|
|
56355
|
+
} catch (error) {
|
|
56356
|
+
const message2 = error.message;
|
|
56357
|
+
if (!message2.includes("identity changed during preparation")) return { preparation, error };
|
|
56358
|
+
return { preparation, error: new Error(`${message2}. Automatic re-preparation ran once and the identity drifted again \u2014 this is retryable: re-run \`mmi-cli devops pr create\` (same title/body/head/base) against the current head and base; a concurrent push, rebase or policy/config commit caused it, and re-preparing loses nothing.`) };
|
|
56359
|
+
}
|
|
56360
|
+
}
|
|
55916
56361
|
function createPreparationDeps(input, io = {}) {
|
|
55917
56362
|
const exec3 = io.exec ?? execFileP;
|
|
55918
56363
|
const git3 = async (args) => (await exec3("git", args, { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
@@ -56196,10 +56641,10 @@ function healthPollIntervalMs() {
|
|
|
56196
56641
|
}
|
|
56197
56642
|
function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
|
|
56198
56643
|
return new Promise((resolve7, reject2) => {
|
|
56199
|
-
let
|
|
56644
|
+
let settled2 = false;
|
|
56200
56645
|
const finish = (fn) => {
|
|
56201
|
-
if (
|
|
56202
|
-
|
|
56646
|
+
if (settled2) return;
|
|
56647
|
+
settled2 = true;
|
|
56203
56648
|
clearTimeout(timer);
|
|
56204
56649
|
child2.removeAllListeners("error");
|
|
56205
56650
|
child2.removeAllListeners("exit");
|
|
@@ -58258,7 +58703,15 @@ function registerCollaborationCommands(program3) {
|
|
|
58258
58703
|
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
58259
58704
|
}
|
|
58260
58705
|
});
|
|
58261
|
-
jsonParity(issue.command("comment <ref>").description("post
|
|
58706
|
+
jsonParity(issue.command("comment <ref>").description("post or exact-text edit a Markdown issue comment and print {number,repo,url,commentUrl} JSON").option("--body <body>", "comment body (markdown; prefer --body-file for multiline Markdown)").option("--body-file <path|->", "read comment body from a UTF-8 file, or from stdin with -").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).option("--edit <commentId>", "edit an existing comment on this issue").option("--replace <text>", "exact text to replace (must occur exactly once)").option("--with <text>", "replacement text (may be empty)").action(async (ref, o) => {
|
|
58707
|
+
const editShape = "--edit <commentId> --replace <text> --with <text> (without --body/--body-file)";
|
|
58708
|
+
if (o.edit !== void 0) {
|
|
58709
|
+
if (o.replace === void 0 || o.with === void 0 || o.body !== void 0 || o.bodyFile !== void 0 || o.replace.length === 0) {
|
|
58710
|
+
return fail(`issue comment: use ${editShape}; --replace must not be empty`);
|
|
58711
|
+
}
|
|
58712
|
+
} else if (o.replace !== void 0 || o.with !== void 0) {
|
|
58713
|
+
return fail(`issue comment: --replace/--with require ${editShape}`);
|
|
58714
|
+
}
|
|
58262
58715
|
let parsed;
|
|
58263
58716
|
try {
|
|
58264
58717
|
parsed = parseIssueRef(ref, o.repo);
|
|
@@ -58267,14 +58720,14 @@ function registerCollaborationCommands(program3) {
|
|
|
58267
58720
|
}
|
|
58268
58721
|
let body;
|
|
58269
58722
|
try {
|
|
58270
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
58723
|
+
body = o.edit !== void 0 ? "" : await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
58271
58724
|
} catch (e) {
|
|
58272
58725
|
return fail(`issue comment: ${e.message}`);
|
|
58273
58726
|
}
|
|
58274
58727
|
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
58275
58728
|
if (!repo) return fail("issue comment: could not resolve repo \u2014 pass --repo owner/repo");
|
|
58276
58729
|
try {
|
|
58277
|
-
const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
|
|
58730
|
+
const result = o.edit !== void 0 ? await editIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, commentId: o.edit, replace: o.replace, with: o.with }) : await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
|
|
58278
58731
|
console.log(JSON.stringify(result));
|
|
58279
58732
|
} catch (e) {
|
|
58280
58733
|
const err = e;
|
|
@@ -58483,7 +58936,7 @@ ${list}`);
|
|
|
58483
58936
|
title = safeTitle;
|
|
58484
58937
|
process.stderr.write('pr create: WARNING \u2014 rewrote close-keyword text in the title to "part of #N": the squash commit subject would close it (#7688). Put closing keywords on a "Closes #N" body line.\n');
|
|
58485
58938
|
}
|
|
58486
|
-
|
|
58939
|
+
let preparation = await preparePrCreate({ body, repo: o.repo, head: o.head, base: o.base });
|
|
58487
58940
|
if (preparation.findings.length) return fail(`pr create: preparation found ${preparation.findings.length} blocker(s):
|
|
58488
58941
|
${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${finding.detail}`).join("\n")}`, { code: ERROR_CODES.ERR_STATE_CONFLICT });
|
|
58489
58942
|
for (const observation of preparation.observations.filter((item) => item.state === "skipped")) {
|
|
@@ -58497,7 +58950,16 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
58497
58950
|
try {
|
|
58498
58951
|
await preparation.commit();
|
|
58499
58952
|
} catch (error) {
|
|
58500
|
-
|
|
58953
|
+
const message2 = error.message;
|
|
58954
|
+
if (!message2.includes("identity changed during preparation")) return fail(`pr create: preparation commit failed \u2014 ${message2}`);
|
|
58955
|
+
const retry = await retryPrCreateIdentityDrift({ body, repo: o.repo, head: o.head, base: o.base }, createPreparationDeps({ body, repo: o.repo, head: o.head, base: o.base }));
|
|
58956
|
+
if (retry.error) return fail(`pr create: preparation commit failed after one automatic re-preparation \u2014 ${retry.error.message}`, { code: ERROR_CODES.ERR_STATE_CONFLICT });
|
|
58957
|
+
preparation = retry.preparation;
|
|
58958
|
+
if (preparation.candidate?.head !== createHead) {
|
|
58959
|
+
const retriedHeadWarning = await prCreateMergedHeadWarning({ head: preparation.candidate?.head || void 0, repo: o.repo });
|
|
58960
|
+
if (retriedHeadWarning) process.stderr.write(`${retriedHeadWarning}
|
|
58961
|
+
`);
|
|
58962
|
+
}
|
|
58501
58963
|
}
|
|
58502
58964
|
const created = await ghCreate(buildPrArgs({ title, body, base: preparation.candidate.base, head: preparation.candidate.head, repo: preparation.candidate.repo, draft: o.draft }), { allowHeadPublication: false });
|
|
58503
58965
|
if (isGhCreateRateLimited(created)) {
|
|
@@ -58859,6 +59321,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
58859
59321
|
return receipt;
|
|
58860
59322
|
},
|
|
58861
59323
|
readMergeQueue: readPrMergeQueue,
|
|
59324
|
+
observeQueued: (prNumber, repo, snapshot) => observeQueuedAdmission(repo, prNumber, snapshot),
|
|
58862
59325
|
findMergeGroupRun: findPrMergeGroupRun,
|
|
58863
59326
|
queueProgress: (receipt) => {
|
|
58864
59327
|
const line = prMergeQueueLine(receipt);
|
|
@@ -59047,7 +59510,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59047
59510
|
inFlight = Math.max(1, inFlight - 1);
|
|
59048
59511
|
}
|
|
59049
59512
|
});
|
|
59050
|
-
const credential =
|
|
59513
|
+
const { each: credentials, overall: credential } = landCredentials(entries.map((e) => e.result));
|
|
59051
59514
|
if (numbers.length === 1) {
|
|
59052
59515
|
const { result, cleanup } = entries[0];
|
|
59053
59516
|
if (opts.json) printLine(JSON.stringify({ ...result, ...cleanup ? { cleanup } : {}, credential }));
|
|
@@ -59058,7 +59521,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59058
59521
|
if (cleanup) printLine(`pr land: ${cleanup}`);
|
|
59059
59522
|
}
|
|
59060
59523
|
} else if (opts.json) {
|
|
59061
|
-
printLine(JSON.stringify({ results: entries.map((e) => ({ ...e.result, outcome: e.outcome, ...e.cleanup ? { cleanup: e.cleanup } : {} })), summary: Object.fromEntries(["merged", "ejected", "timeout", "queued", "queue-admission-pending", "failed"].map((k) => [k, entries.filter((e) => e.outcome === k).length])), credential }));
|
|
59524
|
+
printLine(JSON.stringify({ results: entries.map((e, i) => ({ ...e.result, credential: credentials[i], outcome: e.outcome, ...e.cleanup ? { cleanup: e.cleanup } : {} })), summary: Object.fromEntries(["merged", "ejected", "timeout", "queued", "queue-admission-pending", "failed"].map((k) => [k, entries.filter((e) => e.outcome === k).length])), credential }));
|
|
59062
59525
|
} else {
|
|
59063
59526
|
printLine(renderPrLandTable(entries));
|
|
59064
59527
|
printLine(`pr land: ${entries.filter((e) => e.outcome === "merged").length}/${entries.length} merged (credential: ${credential})`);
|
|
@@ -64808,8 +65271,7 @@ function parseBrokerPrState(raw) {
|
|
|
64808
65271
|
}
|
|
64809
65272
|
function buildPrListSpec(o) {
|
|
64810
65273
|
const repo = parseBrokerRepo(o.repo, "--repo");
|
|
64811
|
-
const fields = parseBrokerJsonFields(o.json);
|
|
64812
|
-
rejectPrViewQueueFields(fields, "<number>", repo, "list");
|
|
65274
|
+
const fields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), "<number>", repo, "list");
|
|
64813
65275
|
const args = withJson(withRepo(["pr", "list"], repo), fields, o.json);
|
|
64814
65276
|
const state = parseBrokerPrState(o.state);
|
|
64815
65277
|
if (state) args.push("--state", state);
|
|
@@ -64876,18 +65338,24 @@ function buildRunWatchSpec(id, o) {
|
|
|
64876
65338
|
}
|
|
64877
65339
|
function rejectPrViewQueueFields(fields, number, repo, verb = "view") {
|
|
64878
65340
|
const badField = fields?.split(",").find((field) => field === "mergeQueueEntry" || field === "isInMergeQueue");
|
|
64879
|
-
if (!badField) return;
|
|
65341
|
+
if (!badField) return fields;
|
|
64880
65342
|
const targetRepo2 = repo ?? "<owner/repo>";
|
|
64881
65343
|
const remaining = fields?.split(",").filter((f) => f && f !== "mergeQueueEntry" && f !== "isInMergeQueue").join(",") ?? "";
|
|
64882
|
-
|
|
64883
|
-
|
|
65344
|
+
if (!remaining) {
|
|
65345
|
+
throw new Error(
|
|
65346
|
+
`read pr ${verb}: '${badField}' is not a gh pr ${verb} --json field \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo ${targetRepo2} --json\` and drop it from --json`
|
|
65347
|
+
);
|
|
65348
|
+
}
|
|
65349
|
+
process.stderr.write(
|
|
65350
|
+
`read pr ${verb}: dropped '${badField}' (not a gh pr ${verb} --json field) \u2014 read the merge queue with \`mmi-cli devops read pr queue ${number} --repo ${targetRepo2} --json\`
|
|
65351
|
+
`
|
|
64884
65352
|
);
|
|
65353
|
+
return remaining;
|
|
64885
65354
|
}
|
|
64886
65355
|
function buildPrViewSpec(ref, o) {
|
|
64887
65356
|
const number = parseBrokerId(ref, "PR number");
|
|
64888
|
-
const jsonFields = parseBrokerJsonFields(o.json);
|
|
64889
65357
|
const repo = parseBrokerRepo(o.repo, "--repo");
|
|
64890
|
-
rejectPrViewQueueFields(
|
|
65358
|
+
const jsonFields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), number, repo);
|
|
64891
65359
|
const args = withJson(withRepo(["pr", "view", number], repo), jsonFields, o.json);
|
|
64892
65360
|
if (o.comments) args.push("--comments");
|
|
64893
65361
|
return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
|
|
@@ -64961,7 +65429,34 @@ function ghExitCodeOf(error) {
|
|
|
64961
65429
|
const code = error.code;
|
|
64962
65430
|
return typeof code === "number" ? code : null;
|
|
64963
65431
|
}
|
|
64964
|
-
|
|
65432
|
+
var RUN_LIST_RECOVERY_FLAGS = ["--repo", "--branch", "--commit", "--event", "--workflow", "--status", "--created", "--limit"];
|
|
65433
|
+
function shellWord(value) {
|
|
65434
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
65435
|
+
}
|
|
65436
|
+
function formatBrokerRunListRecovery(spec, jq) {
|
|
65437
|
+
const tokens2 = spec.args.slice(2);
|
|
65438
|
+
const valueOf = (flag) => {
|
|
65439
|
+
const index = tokens2.indexOf(flag);
|
|
65440
|
+
if (index === -1) return void 0;
|
|
65441
|
+
const next = tokens2[index + 1];
|
|
65442
|
+
return next === void 0 || next.startsWith("-") ? void 0 : next;
|
|
65443
|
+
};
|
|
65444
|
+
const words = ["MMI_ACTOR=app", "mmi-cli", "devops", "read", "run", "list"];
|
|
65445
|
+
const repo = valueOf("--repo");
|
|
65446
|
+
words.push("--repo", repo === void 0 ? "<owner/repo>" : shellWord(repo));
|
|
65447
|
+
for (const flag of RUN_LIST_RECOVERY_FLAGS.slice(1)) {
|
|
65448
|
+
const value = valueOf(flag);
|
|
65449
|
+
if (value !== void 0) words.push(flag, shellWord(value));
|
|
65450
|
+
}
|
|
65451
|
+
if (tokens2.includes("--json")) {
|
|
65452
|
+
const fields = valueOf("--json");
|
|
65453
|
+
words.push("--json");
|
|
65454
|
+
if (fields !== void 0) words.push(shellWord(fields));
|
|
65455
|
+
}
|
|
65456
|
+
if (jq !== void 0) words.push("--jq", `'${jq.replace(/'/g, `'\\''`)}'`);
|
|
65457
|
+
return `re-run exactly this read: \`${words.join(" ")}\` \u2014 it carries every filter gh was given, because a reconstructed list that drops them answers a different question`;
|
|
65458
|
+
}
|
|
65459
|
+
function brokerGhFailureHint(failureText, args, jq) {
|
|
64965
65460
|
const target = args[0] === "pr" && args[1] === "view" ? args[2] : void 0;
|
|
64966
65461
|
const unknownField = /Unknown JSON field: "([^"]+)"/.exec(failureText)?.[1] ?? /Unknown JSON field: ([A-Za-z][A-Za-z0-9]*)/.exec(failureText)?.[1];
|
|
64967
65462
|
if (unknownField === "mergeQueueEntry" || unknownField === "isInMergeQueue") {
|
|
@@ -64973,9 +65468,12 @@ function brokerGhFailureHint(failureText, args) {
|
|
|
64973
65468
|
if (/Could not resolve to a PullRequest/i.test(failureText)) {
|
|
64974
65469
|
return `GitHub could not resolve the PR${target ? ` #${target}` : ""} \u2014 verify the number and that --repo names the repo that owns it before retrying; an identical re-run fails the same way`;
|
|
64975
65470
|
}
|
|
65471
|
+
if (args[0] === "run" && args[1] === "list") {
|
|
65472
|
+
return formatBrokerRunListRecovery({ program: "gh", args: [...args], timeoutMs: SINGLE_SHOT_TIMEOUT_MS }, jq);
|
|
65473
|
+
}
|
|
64976
65474
|
return void 0;
|
|
64977
65475
|
}
|
|
64978
|
-
async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk)) {
|
|
65476
|
+
async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk), jq) {
|
|
64979
65477
|
const env = brokerChildEnv();
|
|
64980
65478
|
let output;
|
|
64981
65479
|
try {
|
|
@@ -64987,7 +65485,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
|
|
|
64987
65485
|
if (code !== null) {
|
|
64988
65486
|
if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
|
|
64989
65487
|
const failure = `${typeof err.stdout === "string" ? err.stdout : ""}${typeof err.stderr === "string" ? err.stderr : ""}`;
|
|
64990
|
-
const recovery = brokerGhFailureHint(failure, spec.args) ?? "inspect stderr; retry the same typed read and only parse stdout after success";
|
|
65488
|
+
const recovery = brokerGhFailureHint(failure, spec.args, jq) ?? "inspect stderr; retry the same typed read and only parse stdout after success";
|
|
64991
65489
|
process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} failed on the App pool (exit ${code}) \u2014 ${recovery}
|
|
64992
65490
|
`);
|
|
64993
65491
|
return code;
|
|
@@ -64998,7 +65496,8 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
|
|
|
64998
65496
|
try {
|
|
64999
65497
|
JSON.parse(output);
|
|
65000
65498
|
} catch {
|
|
65001
|
-
|
|
65499
|
+
const recovery = spec.args[0] === "run" && spec.args[1] === "list" ? `${formatBrokerRunListRecovery(spec, jq)}; only parse stdout after success` : "retry the same typed read; only parse stdout after success";
|
|
65500
|
+
process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} returned ${output.trim() ? "invalid" : "empty"} JSON on the App pool \u2014 ${recovery}
|
|
65002
65501
|
`);
|
|
65003
65502
|
return 1;
|
|
65004
65503
|
}
|
|
@@ -65080,13 +65579,13 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
|
|
|
65080
65579
|
requireBrokerApp(label);
|
|
65081
65580
|
try {
|
|
65082
65581
|
if (jq === void 0) {
|
|
65083
|
-
process.exitCode = await runBrokerGh(ghRun, spec);
|
|
65582
|
+
process.exitCode = await runBrokerGh(ghRun, spec, void 0, void 0);
|
|
65084
65583
|
return;
|
|
65085
65584
|
}
|
|
65086
65585
|
let out = "";
|
|
65087
65586
|
const code = await runBrokerGh(ghRun, spec, (chunk) => {
|
|
65088
65587
|
out += chunk;
|
|
65089
|
-
});
|
|
65588
|
+
}, jq);
|
|
65090
65589
|
if (code === 0) await emitJson(out, jq);
|
|
65091
65590
|
process.exitCode = code;
|
|
65092
65591
|
} catch (e) {
|
|
@@ -65309,7 +65808,7 @@ function typedReadHint(path2) {
|
|
|
65309
65808
|
return say(`mmi-cli devops read issue view <issue> --repo ${slug} --json comments`, `; a comment is read through its issue, so match the comment whose url ends in issuecomment-${id}`);
|
|
65310
65809
|
}
|
|
65311
65810
|
if (sub === "events" || sub === "timeline") {
|
|
65312
|
-
return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json state,stateReason,closedAt,labels,assignees,comments`, "; the raw event/timeline history itself has no read");
|
|
65811
|
+
return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json state,stateReason,closedAt,closedBy,labels,assignees,comments`, "; closedBy carries the merge evidence (merged, mergedAt, mergeCommit.oid), while the raw event/timeline history itself has no read");
|
|
65313
65812
|
}
|
|
65314
65813
|
return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json title,body,state,labels,comments`);
|
|
65315
65814
|
}
|
|
@@ -65339,11 +65838,17 @@ function tagsReadHint(path2) {
|
|
|
65339
65838
|
const slug = `${match[1]}/${match[2]}`;
|
|
65340
65839
|
return ` No App-token REST read exists for git tags or refs \u2014 read git tags directly with \`git ls-remote --tags origin 'refs/tags/*'\` (no token needed; \`git ls-remote origin refs/tags/<tag>\` for one tag), or read tag-backed GitHub Releases with \`MMI_ACTOR=app mmi-cli devops api get "repos/${slug}/releases?per_page=20"\`.`;
|
|
65341
65840
|
}
|
|
65841
|
+
function branchRulesReadHint(path2) {
|
|
65842
|
+
const match = new RegExp(`^repos/(${NAME})/(${NAME})/(?:rules/branches/[^/?#]+|branches/[^/?#]+/protection(?:/|$)|rulesets(?:/|$))`).exec(path2.split("?")[0]);
|
|
65843
|
+
if (!match) return "";
|
|
65844
|
+
const slug = `${match[1]}/${match[2]}`;
|
|
65845
|
+
return ` No raw rules, rulesets or branch-protection read is on the App allowlist, so retrying path or query variants cannot work \u2014 inspect a PR's required status contexts with \`MMI_ACTOR=app mmi-cli devops pr checks-required <pr> --repo ${slug} --json\`, or verify repository ruleset and branch-protection health with \`mmi-cli devops bootstrap verify --repo ${slug} --json\` and \`mmi-cli devops ci audit --repo ${slug} --json\`.`;
|
|
65846
|
+
}
|
|
65342
65847
|
function parseApiGetPath(rawPath) {
|
|
65343
65848
|
const path2 = rawPath.trim();
|
|
65344
65849
|
const refuse = () => {
|
|
65345
65850
|
const hint = /^[A-Za-z]:[/\\]/.test(path2) ? " Git Bash/MSYS may have rewritten a leading-slash argument; retry with MSYS_NO_PATHCONV=1 and the slash-free repos/... path. The endpoint must still be allowlisted." : "";
|
|
65346
|
-
const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${hint}`;
|
|
65851
|
+
const routeHints = `${typedReadHint(path2)}${runsReadHint(path2)}${orgScopedHint(path2)}${tagsReadHint(path2)}${branchRulesReadHint(path2)}${hint}`;
|
|
65347
65852
|
return new Error(`api get: refusing '${rawPath}' \u2014${routeHints}${API_GET_RECOVERY} Only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}.`);
|
|
65348
65853
|
};
|
|
65349
65854
|
if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
|
|
@@ -65395,14 +65900,16 @@ async function readAppApi(path2, runner = apiReadRunner) {
|
|
|
65395
65900
|
function registerApiReadCommands(program3, overrides) {
|
|
65396
65901
|
const spawn7 = overrides?.runner ?? apiReadRunner;
|
|
65397
65902
|
const jqRun = overrides?.jqRunner ?? systemJqRunner;
|
|
65398
|
-
const
|
|
65399
|
-
api.command("get").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist").option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
|
|
65400
|
-
acceptHeaderFor(v);
|
|
65401
|
-
return v.trim().toLowerCase();
|
|
65402
|
-
}, "json").option("--jq <expr>", JQ_HELP, (v) => parseBrokerJq(v)).option("--out <file>", "write response bytes to a file (required for, and only valid with, actions artifact zips, job logs, and release assets)").argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o) => {
|
|
65903
|
+
const runGet = async (req, o) => {
|
|
65403
65904
|
if (o.jq !== void 0 && (o.accept === "raw" || o.out !== void 0)) return fail("api get: --jq is only valid with JSON stdout reads (not --accept raw or --out)");
|
|
65404
65905
|
const zip = req.kind === "actions-artifact-zip" || req.kind === "actions-job-logs" || req.kind === "release-asset";
|
|
65405
|
-
if (zip !== (o.out !== void 0))
|
|
65906
|
+
if (zip !== (o.out !== void 0)) {
|
|
65907
|
+
const shapes = "api get: --out <file> is required for, and only valid with, repos/{owner}/{repo}/actions/artifacts/{id}/zip and repos/{owner}/{repo}/actions/jobs/{id}/logs and repos/{owner}/{repo}/releases/assets/{id}";
|
|
65908
|
+
if (!zip && o.out !== void 0) {
|
|
65909
|
+
return fail(`${shapes} \u2014 drop --out and let the JSON body go to stdout (pipe it into node/jq, or use --jq <expr>), or use the typed read (e.g. \`mmi-cli devops read run list --repo <owner/repo> --json databaseId,workflowName,event,status,conclusion\`). This refusal exits 1 on stderr with nothing written: check the exit code and never discard stderr (2>&1 / >/dev/null) before parsing.`);
|
|
65910
|
+
}
|
|
65911
|
+
return fail(shapes);
|
|
65912
|
+
}
|
|
65406
65913
|
const appRoute = req.kind.startsWith("actions-") || req.kind.startsWith("release-");
|
|
65407
65914
|
if (appRoute) requireBrokerApp("api get");
|
|
65408
65915
|
const env = appRoute || process.env.MMI_ACTOR?.trim() !== "personal" ? brokerChildEnv() : void 0;
|
|
@@ -65423,6 +65930,28 @@ function registerApiReadCommands(program3, overrides) {
|
|
|
65423
65930
|
return fail(`api get: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
65424
65931
|
}
|
|
65425
65932
|
process.stdout.write(body);
|
|
65933
|
+
};
|
|
65934
|
+
const withApiGetOptions = (cmd) => cmd.option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
|
|
65935
|
+
acceptHeaderFor(v);
|
|
65936
|
+
return v.trim().toLowerCase();
|
|
65937
|
+
}, "json").option("--jq <expr>", JQ_HELP, (v) => parseBrokerJq(v)).option("--out <file>", "write response bytes to a file (required for, and only valid with, actions artifact zips, job logs, and release assets)");
|
|
65938
|
+
const api = withApiGetOptions(
|
|
65939
|
+
program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents, actions runs/jobs/artifacts, releases)")
|
|
65940
|
+
).argument("[path]", "allowlisted GET path \u2014 shorthand for `api get <path>` (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, _o, cmd) => {
|
|
65941
|
+
if (req === void 0) {
|
|
65942
|
+
return fail(`api: name an allowlisted GET path \u2014 \`mmi-cli devops api <path>\` or \`mmi-cli devops api get <path>\`. Only these GET reads are allowed: ${API_GET_ALLOWED_SHAPES}.`);
|
|
65943
|
+
}
|
|
65944
|
+
return runGet(req, cmd.opts());
|
|
65945
|
+
});
|
|
65946
|
+
withApiGetOptions(
|
|
65947
|
+
api.command("get").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist")
|
|
65948
|
+
).argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o, cmd) => {
|
|
65949
|
+
const parentOpts = cmd.parent?.opts() ?? {};
|
|
65950
|
+
return runGet(req, {
|
|
65951
|
+
accept: o.accept && o.accept !== "json" ? o.accept : parentOpts.accept ?? o.accept ?? "json",
|
|
65952
|
+
jq: o.jq ?? parentOpts.jq,
|
|
65953
|
+
out: o.out ?? parentOpts.out
|
|
65954
|
+
});
|
|
65426
65955
|
});
|
|
65427
65956
|
}
|
|
65428
65957
|
|
|
@@ -69029,6 +69558,7 @@ setGhWriteTokenProvider(() => {
|
|
|
69029
69558
|
}
|
|
69030
69559
|
return filingToken ??= mintFilingWriteToken(appActorDeps()).then((t) => t.token, () => void 0);
|
|
69031
69560
|
});
|
|
69561
|
+
setClaimWriteTokenProvider(() => mintFilingWriteToken(appActorDeps()).then((t) => t.token));
|
|
69032
69562
|
setStatusWriteTokenProvider(() => mintStatusWriteToken(appActorDeps()));
|
|
69033
69563
|
var registerPreAction = Reflect.get(program2, ["h", "ook"].join(""));
|
|
69034
69564
|
registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand) => {
|
|
@@ -69036,7 +69566,7 @@ registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand
|
|
|
69036
69566
|
appActorCommand = commandPath4;
|
|
69037
69567
|
setAuditCommand(commandPath4);
|
|
69038
69568
|
if (commandPath4 !== "whoami diagnose-hub") emitCliCallTelemetry(commandPath4);
|
|
69039
|
-
if (commandPath4 === "ci cancel-orphans") return;
|
|
69569
|
+
if (commandPath4 === "ci cancel-orphans" || commandPath4 === "run cancel") return;
|
|
69040
69570
|
try {
|
|
69041
69571
|
await activateAppActor(commandPath4, process.env, () => APP_WRITE_COMMANDS.has(commandPath4) ? mintCommandWriteToken(appActorDeps(), commandPath4) : mintInstallationToken(appActorDeps()));
|
|
69042
69572
|
} catch (e) {
|
|
@@ -69871,6 +70401,10 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
|
|
|
69871
70401
|
});
|
|
69872
70402
|
registerCiChecksWait(ci);
|
|
69873
70403
|
registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
|
|
70404
|
+
registerRunCancel(
|
|
70405
|
+
program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` is the only one, and it rides the org App actions:write profile"),
|
|
70406
|
+
async () => activeAppToken() ?? (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
|
|
70407
|
+
);
|
|
69874
70408
|
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it \u2014 keeps its required contexts (master role required)").action(async (o) => {
|
|
69875
70409
|
if (o.apply && o.parkRuleset) {
|
|
69876
70410
|
return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");
|