@mutmutco/claude-plugin 4.6.3 → 4.6.4
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 +544 -119
- 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 +6 -297
- 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",
|
|
@@ -1599,16 +1601,26 @@ var init_cli_shared = __esm({
|
|
|
1599
1601
|
rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options, ...env ? { env } : {} })
|
|
1600
1602
|
);
|
|
1601
1603
|
const appEnv = options.env ? void 0 : await ghReadAppEnv(file, args) ?? await ghFilingAppEnv(file, args);
|
|
1604
|
+
const reportsCredential = !options.env && /(^|[\\/])gh(\.exe)?$/i.test(file) && isAppFilingWrite(args);
|
|
1605
|
+
const tag = async (call, credential) => {
|
|
1606
|
+
if (!reportsCredential) return call;
|
|
1607
|
+
try {
|
|
1608
|
+
return { ...await call, credential };
|
|
1609
|
+
} catch (e) {
|
|
1610
|
+
if (e && typeof e === "object") e.credential = credential;
|
|
1611
|
+
throw e;
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1602
1614
|
if (/(^|[\\/])gh(\.exe)?$/i.test(file) && args[0] !== "auth") {
|
|
1603
1615
|
const callerEnv = options.env;
|
|
1604
1616
|
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
1617
|
recordGhUsage("gh", pool, args.filter((a) => !a.startsWith("-")).slice(0, 2).join(" "));
|
|
1606
1618
|
}
|
|
1607
|
-
if (!appEnv) return run();
|
|
1619
|
+
if (!appEnv) return tag(run(), "user");
|
|
1608
1620
|
try {
|
|
1609
|
-
return await run(appEnv);
|
|
1621
|
+
return await tag(run(appEnv), "app_installation");
|
|
1610
1622
|
} catch (e) {
|
|
1611
|
-
if (shouldRetryIntegrationRefusalOnAmbient(e)) return run();
|
|
1623
|
+
if (shouldRetryIntegrationRefusalOnAmbient(e)) return tag(run(), "user");
|
|
1612
1624
|
throw e;
|
|
1613
1625
|
}
|
|
1614
1626
|
};
|
|
@@ -15263,6 +15275,8 @@ async function mintInstallationToken(deps) {
|
|
|
15263
15275
|
}
|
|
15264
15276
|
var APP_WRITE_COMMANDS = /* @__PURE__ */ new Map([
|
|
15265
15277
|
["ci cancel-orphans", { metadata: "read", actions: "write" }],
|
|
15278
|
+
// #9479: one named run cancelled under the same actions:write profile as the orphan sweep.
|
|
15279
|
+
["run cancel", { metadata: "read", actions: "write" }],
|
|
15266
15280
|
["issue comment", { metadata: "read", issues: "write", pull_requests: "write" }],
|
|
15267
15281
|
// #8401: the detached related-issue lookup (issue list/view reads + one marker comment) — no identity contract.
|
|
15268
15282
|
["issue discover-related", { metadata: "read", issues: "write" }]
|
|
@@ -15953,9 +15967,6 @@ var REST_GH_TIMEOUT_MS = 2e4;
|
|
|
15953
15967
|
var pollTokenProvider;
|
|
15954
15968
|
var pollTokenOnce;
|
|
15955
15969
|
var lastPollSource;
|
|
15956
|
-
function lastRestPollSource() {
|
|
15957
|
-
return lastPollSource;
|
|
15958
|
-
}
|
|
15959
15970
|
function setPollTokenProvider(provider) {
|
|
15960
15971
|
pollTokenProvider = provider;
|
|
15961
15972
|
pollTokenOnce = void 0;
|
|
@@ -16798,6 +16809,30 @@ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
|
|
|
16798
16809
|
}
|
|
16799
16810
|
|
|
16800
16811
|
// src/claim-marker-client.ts
|
|
16812
|
+
var claimWriteTokenProvider;
|
|
16813
|
+
var claimWriteTokenOnce;
|
|
16814
|
+
function setClaimWriteTokenProvider(provider) {
|
|
16815
|
+
claimWriteTokenProvider = provider;
|
|
16816
|
+
claimWriteTokenOnce = void 0;
|
|
16817
|
+
}
|
|
16818
|
+
async function selectClaimWriteClient(client, appClient) {
|
|
16819
|
+
if (appClient) return appClient;
|
|
16820
|
+
if (!claimWriteTokenProvider) return client;
|
|
16821
|
+
let token;
|
|
16822
|
+
try {
|
|
16823
|
+
claimWriteTokenOnce ??= claimWriteTokenProvider();
|
|
16824
|
+
token = await claimWriteTokenOnce;
|
|
16825
|
+
} catch (e) {
|
|
16826
|
+
claimWriteTokenOnce = void 0;
|
|
16827
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
16828
|
+
throw new Error(`${detail} \u2014 refusing personal-token fallback for claim write (#9511)`);
|
|
16829
|
+
}
|
|
16830
|
+
if (!token) {
|
|
16831
|
+
claimWriteTokenOnce = void 0;
|
|
16832
|
+
throw new Error("app actor: claim write token unavailable \u2014 refusing personal-token fallback for claim write (#9511)");
|
|
16833
|
+
}
|
|
16834
|
+
return createGitHubClient({ token: async () => token });
|
|
16835
|
+
}
|
|
16801
16836
|
async function selectLivenessClient(client, appClient) {
|
|
16802
16837
|
if (appClient) return appClient;
|
|
16803
16838
|
let token;
|
|
@@ -21309,7 +21344,7 @@ async function loadConfigForBoardSelector(selector, repoOption) {
|
|
|
21309
21344
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
21310
21345
|
var SKILL_LESSON_FILE_LABELS = [SKILL_LESSON_LABEL, LEARNING_LABEL2];
|
|
21311
21346
|
var SKILL_LESSON_LOOP_KIND = "lesson";
|
|
21312
|
-
var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "rcand", "release", "secrets"];
|
|
21347
|
+
var SKILL_NAMES = ["board", "bootstrap", "epic", "hotfix", "intercom-guest", "mmi-help", "org-guide", "rcand", "release", "secrets"];
|
|
21313
21348
|
function assertSkillName(name) {
|
|
21314
21349
|
const match = SKILL_NAMES.find((skill) => skill === name);
|
|
21315
21350
|
if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
|
|
@@ -21342,6 +21377,9 @@ function findDuplicateLesson(source, openLessons) {
|
|
|
21342
21377
|
// src/board-doctor-config.ts
|
|
21343
21378
|
init_github_client();
|
|
21344
21379
|
|
|
21380
|
+
// src/issue-comment.ts
|
|
21381
|
+
init_github_client();
|
|
21382
|
+
|
|
21345
21383
|
// src/sub-issue.ts
|
|
21346
21384
|
init_error_codes();
|
|
21347
21385
|
init_github_quota();
|
|
@@ -21551,6 +21589,40 @@ function parentLinkFields(result, error, parentRef, childRef, reason = "failed")
|
|
|
21551
21589
|
}
|
|
21552
21590
|
|
|
21553
21591
|
// src/issue-comment.ts
|
|
21592
|
+
async function editIssueComment(client, input) {
|
|
21593
|
+
const parsed = parseIssueRef(input.ref);
|
|
21594
|
+
const repo = parsed.repo ?? input.defaultRepo;
|
|
21595
|
+
if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
|
|
21596
|
+
if (!input.replace.length) throw new Error("--replace must not be empty; use --edit <commentId> --replace <text> --with <text>");
|
|
21597
|
+
if (!/^\d+$/.test(input.commentId)) throw new Error(`--edit takes a numeric comment id, got: ${input.commentId}`);
|
|
21598
|
+
const path2 = `repos/${repo}/issues/comments/${input.commentId}`;
|
|
21599
|
+
let comment;
|
|
21600
|
+
try {
|
|
21601
|
+
comment = await client.rest("GET", path2);
|
|
21602
|
+
} catch (e) {
|
|
21603
|
+
if (e instanceof GitHubApiError && e.status === 404) {
|
|
21604
|
+
throw new Error(`comment ${input.commentId} does not exist on ${repo}`);
|
|
21605
|
+
}
|
|
21606
|
+
throw e;
|
|
21607
|
+
}
|
|
21608
|
+
const expectedAnchor = `/issues/${parsed.number}#issuecomment-${input.commentId}`;
|
|
21609
|
+
if (!comment.html_url?.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
|
|
21610
|
+
throw new Error(`comment ${input.commentId} is not a comment on ${repo}#${parsed.number} (it lives at ${comment.html_url ?? "unknown"})`);
|
|
21611
|
+
}
|
|
21612
|
+
const body = comment.body ?? "";
|
|
21613
|
+
const count = body.split(input.replace).length - 1;
|
|
21614
|
+
if (count !== 1) throw new Error(`--replace must match exactly once; found ${count} occurrences in comment ${input.commentId}`);
|
|
21615
|
+
const index = body.indexOf(input.replace);
|
|
21616
|
+
const newBody = body.slice(0, index) + input.with + body.slice(index + input.replace.length);
|
|
21617
|
+
await client.rest("PATCH", path2, { body: { body: newBody } });
|
|
21618
|
+
return {
|
|
21619
|
+
number: parsed.number,
|
|
21620
|
+
repo,
|
|
21621
|
+
url: `https://github.com/${repo}/issues/${parsed.number}`,
|
|
21622
|
+
commentUrl: comment.html_url,
|
|
21623
|
+
edited: true
|
|
21624
|
+
};
|
|
21625
|
+
}
|
|
21554
21626
|
async function postIssueComment(client, input) {
|
|
21555
21627
|
const parsed = parseIssueRef(input.ref);
|
|
21556
21628
|
const repo = parsed.repo ?? input.defaultRepo;
|
|
@@ -22059,9 +22131,12 @@ function detailCandidates(report, options = {}) {
|
|
|
22059
22131
|
const scopes = [report.primary, report.secondary];
|
|
22060
22132
|
return scopes.flatMap((scope) => options.all ? [...scope.userOwned, ...scope.claimable, ...scope.taken, ...scope.unownedInFlight] : [...scope.userOwned, ...scope.claimable]).filter((item) => item.contentType === "Issue");
|
|
22061
22133
|
}
|
|
22134
|
+
function boardNotFoundRemedy(board, repoExplicit) {
|
|
22135
|
+
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";
|
|
22136
|
+
}
|
|
22062
22137
|
function boardNotFoundError(ref, board, opts = {}) {
|
|
22063
22138
|
const verb = opts.verb ?? "is not on";
|
|
22064
|
-
const remedy = opts.remedy ??
|
|
22139
|
+
const remedy = opts.remedy ?? boardNotFoundRemedy(board, opts.repoExplicit === true);
|
|
22065
22140
|
return new Error(`${ref} ${verb} the ${board.owner} #${board.number} board; ${remedy}`);
|
|
22066
22141
|
}
|
|
22067
22142
|
var NotClaimableError = class extends Error {
|
|
@@ -22489,7 +22564,7 @@ async function showBoardItem(options, deps = {}) {
|
|
|
22489
22564
|
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
22490
22565
|
const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
|
|
22491
22566
|
const { item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
22492
|
-
if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
22567
|
+
if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
|
|
22493
22568
|
if (item.contentType === "Issue") {
|
|
22494
22569
|
try {
|
|
22495
22570
|
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
@@ -23319,7 +23394,7 @@ function isSameBranchClaimResume(contest, repo, landing = {}) {
|
|
|
23319
23394
|
const headBranch = landing.headBranch?.replace(/^refs\/heads\//, "").replace(/^[^/:]+:/, "").trim();
|
|
23320
23395
|
const headSha = landing.headSha?.trim();
|
|
23321
23396
|
if (!headBranch && !headSha) return false;
|
|
23322
|
-
if (
|
|
23397
|
+
if (contest.unverifiable.length > 0 || contest.live.length === 0) return false;
|
|
23323
23398
|
const branchLine = /^live branch ([^\s]+) on /;
|
|
23324
23399
|
const fromContest = contest.branch?.replace(/ \(head of open PR .*\)$/, "").trim();
|
|
23325
23400
|
const fromLive = contest.live.map((line) => branchLine.exec(line)?.[1]).find(Boolean);
|
|
@@ -23360,7 +23435,9 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
23360
23435
|
branchSha: evidence.branchSha
|
|
23361
23436
|
}, item.repository, landing)) {
|
|
23362
23437
|
contested = false;
|
|
23363
|
-
|
|
23438
|
+
if (evidence.marker) {
|
|
23439
|
+
resume = `same-branch resume over live branch ${evidence.branch ?? landing.headBranch}`;
|
|
23440
|
+
}
|
|
23364
23441
|
}
|
|
23365
23442
|
const carry = staleWorkLines(evidence);
|
|
23366
23443
|
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 +23503,7 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
23426
23503
|
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
23427
23504
|
const item = lookup.item;
|
|
23428
23505
|
if (!item) {
|
|
23429
|
-
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
23506
|
+
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
|
|
23430
23507
|
}
|
|
23431
23508
|
const optionId = cfg.statusOptions[options.status];
|
|
23432
23509
|
let credential = "user";
|
|
@@ -23529,6 +23606,9 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
|
|
|
23529
23606
|
return {
|
|
23530
23607
|
cfg,
|
|
23531
23608
|
client,
|
|
23609
|
+
appClient: deps.appClient,
|
|
23610
|
+
git: deps.git ?? defaultGit,
|
|
23611
|
+
landing: deps.landing,
|
|
23532
23612
|
items: collected.items,
|
|
23533
23613
|
selectedRefs: new Set(selectors.map((selector) => `${selector.repo}#${selector.number}`.toLowerCase())),
|
|
23534
23614
|
writable: writableOrUnknown(writable),
|
|
@@ -23621,7 +23701,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23621
23701
|
if (!verdict.ok) throw new NotClaimableError(verdict.reason, { ref: fresh.ref, status: fresh.status });
|
|
23622
23702
|
item = fresh;
|
|
23623
23703
|
const refuseIfContested = async () => {
|
|
23624
|
-
const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
|
|
23704
|
+
const contest = await checkLaneContest(client, item, ctx.session, report.viewer, ctx.appClient, ctx.landing);
|
|
23625
23705
|
resumeEvidence = void 0;
|
|
23626
23706
|
staleStamp = void 0;
|
|
23627
23707
|
const displaced = () => ({
|
|
@@ -23632,6 +23712,24 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23632
23712
|
host: contest.marker.host
|
|
23633
23713
|
} : {}
|
|
23634
23714
|
});
|
|
23715
|
+
if (contest.contested && !ctx.landing && contest.branch) {
|
|
23716
|
+
const candidateBranch = contest.branch.replace(/ \(head of open PR .*\)$/, "").trim();
|
|
23717
|
+
const originRepo = repoFromRemoteUrl(await ctx.git(["remote", "get-url", "origin"]).catch(() => ""));
|
|
23718
|
+
if (originRepo && originRepo.toLowerCase() === item.repository.toLowerCase()) {
|
|
23719
|
+
const localSha = (await ctx.git(["rev-parse", "--verify", `refs/heads/${candidateBranch}^{commit}`]).catch(() => "")).trim();
|
|
23720
|
+
const currentBranch2 = (await ctx.git(["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => "")).trim();
|
|
23721
|
+
if (localSha && (!contest.branchSha || localSha === contest.branchSha) || currentBranch2 === candidateBranch) {
|
|
23722
|
+
const localLanding = { headBranch: candidateBranch, ...localSha ? { headSha: localSha } : {} };
|
|
23723
|
+
if (isSameBranchClaimResume(contest, item.repository, localLanding)) {
|
|
23724
|
+
if (contest.marker) {
|
|
23725
|
+
resumeEvidence = `same-branch resume over live branch ${contest.branch}`;
|
|
23726
|
+
previousHolder = displaced();
|
|
23727
|
+
}
|
|
23728
|
+
return;
|
|
23729
|
+
}
|
|
23730
|
+
}
|
|
23731
|
+
}
|
|
23732
|
+
}
|
|
23635
23733
|
if (!contest.contested) {
|
|
23636
23734
|
if (contest.resume && contest.marker) {
|
|
23637
23735
|
resumeEvidence = contest.resume;
|
|
@@ -23655,19 +23753,21 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
23655
23753
|
if (options.check) {
|
|
23656
23754
|
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
|
|
23657
23755
|
}
|
|
23658
|
-
const
|
|
23756
|
+
const appWriter2 = await selectClaimWriteClient(client, ctx.appClient);
|
|
23757
|
+
const renewed = await postClaimMarkerComment(appWriter2, item, ctx.session, staleStamp);
|
|
23659
23758
|
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, renewed };
|
|
23660
23759
|
}
|
|
23661
23760
|
if (options.check) {
|
|
23662
23761
|
return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, ...claimedReceipt(), checked: true };
|
|
23663
23762
|
}
|
|
23664
23763
|
await refuseIfContested();
|
|
23764
|
+
const appWriter = await selectClaimWriteClient(client, ctx.appClient);
|
|
23665
23765
|
try {
|
|
23666
|
-
await
|
|
23766
|
+
await appWriter.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
|
|
23667
23767
|
} catch (e) {
|
|
23668
23768
|
throw new Error(`claim failed before board status changed: ${ghError(e)}`);
|
|
23669
23769
|
}
|
|
23670
|
-
await postClaimMarkerComment(
|
|
23770
|
+
await postClaimMarkerComment(appWriter, item, ctx.session, staleStamp);
|
|
23671
23771
|
let credential = "user";
|
|
23672
23772
|
try {
|
|
23673
23773
|
({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
|
|
@@ -23828,7 +23928,7 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
23828
23928
|
const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
|
|
23829
23929
|
const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
23830
23930
|
if (!item) {
|
|
23831
|
-
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
23931
|
+
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber }, { repoExplicit: options.repo !== void 0 });
|
|
23832
23932
|
}
|
|
23833
23933
|
const toStatus = options.toStatus ?? "Todo";
|
|
23834
23934
|
if (item.contentType === "Issue") {
|
|
@@ -23846,8 +23946,9 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
23846
23946
|
}
|
|
23847
23947
|
}
|
|
23848
23948
|
}
|
|
23949
|
+
const appWriter = await selectClaimWriteClient(client, deps.appClient);
|
|
23849
23950
|
try {
|
|
23850
|
-
await
|
|
23951
|
+
await appWriter.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
|
|
23851
23952
|
body: { assignees: item.assignees }
|
|
23852
23953
|
});
|
|
23853
23954
|
} catch (e) {
|
|
@@ -23864,7 +23965,7 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
23864
23965
|
host: actor.host,
|
|
23865
23966
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
23866
23967
|
};
|
|
23867
|
-
await postIssueComment(
|
|
23968
|
+
await postIssueComment(appWriter, { ref: item.ref, body: renderClaimReleaseBody(release) });
|
|
23868
23969
|
} catch (e) {
|
|
23869
23970
|
process.stderr.write(`warning: ${item.ref} was unclaimed, but recording the release marker comment failed: ${ghError(e)}
|
|
23870
23971
|
`);
|
|
@@ -27287,10 +27388,10 @@ var rollout_plan_default = {
|
|
|
27287
27388
|
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
27389
|
},
|
|
27289
27390
|
baseline: {
|
|
27290
|
-
version: "4.6.
|
|
27291
|
-
tag: "v4.6.
|
|
27292
|
-
commit: "
|
|
27293
|
-
npm: "@mutmutco/cli@4.6.
|
|
27391
|
+
version: "4.6.4",
|
|
27392
|
+
tag: "v4.6.4",
|
|
27393
|
+
commit: "db079140cdfb",
|
|
27394
|
+
npm: "@mutmutco/cli@4.6.4"
|
|
27294
27395
|
},
|
|
27295
27396
|
exitCriterion: "fleet-n-of-n",
|
|
27296
27397
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27307,14 +27408,14 @@ var rollout_plan_default = {
|
|
|
27307
27408
|
repo: "mutmutco/mmi-hub",
|
|
27308
27409
|
role: "canary",
|
|
27309
27410
|
schedule: "train",
|
|
27310
|
-
v3Target: "v4.6.
|
|
27411
|
+
v3Target: "v4.6.4"
|
|
27311
27412
|
}
|
|
27312
27413
|
],
|
|
27313
27414
|
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
27415
|
rollback: {
|
|
27315
27416
|
independent: true,
|
|
27316
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27317
|
-
v3Target: "v4.6.
|
|
27417
|
+
mechanism: "npm dist-tag latest -> 4.6.4 and redeploy the Hub Lambda from tag v4.6.4 (db079140cdfb); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
27418
|
+
v3Target: "v4.6.4 (@mutmutco/cli@4.6.4, tag commit db079140cdfb \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
27318
27419
|
}
|
|
27319
27420
|
},
|
|
27320
27421
|
{
|
|
@@ -28993,16 +29094,16 @@ async function writeFleetLockstepRuleset(client, body, liveId, target, org) {
|
|
|
28993
29094
|
await client.rest("PUT", `orgs/${org}/rulesets/${liveId}`, { body, timeoutMs: 2e4 });
|
|
28994
29095
|
}
|
|
28995
29096
|
const read = await fetchFleetLockstepRuleset(client, org);
|
|
28996
|
-
const
|
|
28997
|
-
const shapeDrift =
|
|
28998
|
-
const includeSettled =
|
|
28999
|
-
const enforcementSettled =
|
|
29097
|
+
const settled2 = read.kind === "ok" ? read.ruleset : void 0;
|
|
29098
|
+
const shapeDrift = settled2 ? diffFleetLockstepShape(settled2) : [`ruleset '${FLEET_LOCKSTEP_RULESET_NAME}' absent after apply`];
|
|
29099
|
+
const includeSettled = settled2 ? sameSetCi(settled2.conditions?.repository_name?.include, body.conditions.repository_name.include) : false;
|
|
29100
|
+
const enforcementSettled = settled2?.enforcement === body.enforcement;
|
|
29000
29101
|
if (shapeDrift.length || !includeSettled || !enforcementSettled) {
|
|
29001
29102
|
throw new Error(`fleet lockstep ruleset apply did not settle: ${[...shapeDrift, includeSettled ? "" : "include list differs", enforcementSettled ? "" : "enforcement differs"].filter(Boolean).join("; ")}`);
|
|
29002
29103
|
}
|
|
29003
29104
|
return {
|
|
29004
29105
|
action: liveId == null ? "created" : "updated",
|
|
29005
|
-
id:
|
|
29106
|
+
id: settled2?.id,
|
|
29006
29107
|
target,
|
|
29007
29108
|
include: body.conditions.repository_name.include,
|
|
29008
29109
|
verified: true
|
|
@@ -40015,7 +40116,7 @@ var PRIMARY_GROUPS = [
|
|
|
40015
40116
|
["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
|
|
40016
40117
|
];
|
|
40017
40118
|
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"]);
|
|
40119
|
+
var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read", "run"]);
|
|
40019
40120
|
var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
|
|
40020
40121
|
var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
|
|
40021
40122
|
var HELP_GROUP_ORDER = /* @__PURE__ */ new Map();
|
|
@@ -40061,6 +40162,8 @@ var COMMAND_OWNERSHIP = {
|
|
|
40061
40162
|
api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
|
|
40062
40163
|
github: { module_owner: "cli/src/github-budget.ts", consumer: "release-operator" },
|
|
40063
40164
|
read: { module_owner: "cli/src/app-read-broker.ts", consumer: "agent-workflow" },
|
|
40165
|
+
run: { module_owner: "cli/src/merge-group-orphans.ts", consumer: "agent-workflow" },
|
|
40166
|
+
// #9479
|
|
40064
40167
|
ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
|
|
40065
40168
|
rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
40066
40169
|
release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
@@ -41025,14 +41128,23 @@ function parseScope(value) {
|
|
|
41025
41128
|
function trailerBlockBreak(body, line) {
|
|
41026
41129
|
const lines2 = String(body ?? "").split(/\r?\n/);
|
|
41027
41130
|
const at = lines2.findIndex((candidate) => candidate.trim() === line);
|
|
41028
|
-
if (at === -1) return "";
|
|
41131
|
+
if (at === -1) return { after: "", before: "" };
|
|
41029
41132
|
for (let i = at + 1; i < lines2.length; i += 1) {
|
|
41030
41133
|
const next = lines2[i];
|
|
41031
41134
|
if (!next.trim()) continue;
|
|
41032
|
-
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return "";
|
|
41033
|
-
return next.trim();
|
|
41135
|
+
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(next)) return { after: "", before: "" };
|
|
41136
|
+
return { after: next.trim(), before: "" };
|
|
41034
41137
|
}
|
|
41035
|
-
|
|
41138
|
+
let paraStart = at;
|
|
41139
|
+
while (paraStart > 0 && lines2[paraStart - 1].trim() !== "") {
|
|
41140
|
+
paraStart -= 1;
|
|
41141
|
+
}
|
|
41142
|
+
for (let i = paraStart; i < at; i += 1) {
|
|
41143
|
+
const prev = lines2[i];
|
|
41144
|
+
if (/^[A-Za-z][A-Za-z0-9-]*:\s/.test(prev) || /^[ \t]+\S/.test(prev)) continue;
|
|
41145
|
+
return { after: "", before: prev.trim() };
|
|
41146
|
+
}
|
|
41147
|
+
return { after: "", before: "" };
|
|
41036
41148
|
}
|
|
41037
41149
|
function clipParagraph(line) {
|
|
41038
41150
|
const flat = String(line).trim().replace(/\s+/g, " ");
|
|
@@ -41040,6 +41152,11 @@ function clipParagraph(line) {
|
|
|
41040
41152
|
}
|
|
41041
41153
|
function invisibleTrailer(sha, line, body = "") {
|
|
41042
41154
|
const broken = trailerBlockBreak(body, line);
|
|
41155
|
+
const brokenDiagnostic = broken.after ? ` Broken by the paragraph after the trailer-shaped line, which ends git's final block:
|
|
41156
|
+
${clipParagraph(broken.after)}
|
|
41157
|
+
` : broken.before ? ` Broken by the non-trailer line before the trailer-shaped line, which prevents git from recognizing the final block:
|
|
41158
|
+
${clipParagraph(broken.before)}
|
|
41159
|
+
` : " 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
41160
|
return {
|
|
41044
41161
|
kind: "malformed-override-trailer",
|
|
41045
41162
|
paths: [],
|
|
@@ -41055,9 +41172,7 @@ function invisibleTrailer(sha, line, body = "") {
|
|
|
41055
41172
|
The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.
|
|
41056
41173
|
Diagnosis: git parsed no ${TRAILER_KEY} trailer for ${sha.slice(0, 8)} at all \u2014 read its own view first:
|
|
41057
41174
|
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:
|
|
41175
|
+
` + brokenDiagnostic + ` Forward fix, no hand-reformatting \u2014 let git build the block from one message:
|
|
41061
41176
|
git commit --allow-empty --trailer "${TRAILER_KEY}: <reason>"
|
|
41062
41177
|
That commit's message IS a valid final trailer block, so it is the nearest (winning) waiver.`
|
|
41063
41178
|
};
|
|
@@ -41595,6 +41710,31 @@ ${commit.body}`.trim();
|
|
|
41595
41710
|
if (!overrides.length) return null;
|
|
41596
41711
|
return [...bodies, [...overrides, ...coauthors].join("\n")].join("\n\n");
|
|
41597
41712
|
}
|
|
41713
|
+
function prBodyTrailerSource(prBody) {
|
|
41714
|
+
const lines2 = prBody.split("\n");
|
|
41715
|
+
const separator = lines2.findIndex((line) => GH_MESSAGE_SEPARATOR.test(line.trim()));
|
|
41716
|
+
return separator === -1 ? prBody : lines2.slice(0, separator).join("\n");
|
|
41717
|
+
}
|
|
41718
|
+
function prBodyPreservesCommitOverrides(commits, headline, prBody, cwd) {
|
|
41719
|
+
const key = `${TRAILER_KEY.toLowerCase()}:`;
|
|
41720
|
+
const required = [];
|
|
41721
|
+
for (const commit of commits) {
|
|
41722
|
+
for (const trailer of parseTrailers(`${commit.headline}
|
|
41723
|
+
|
|
41724
|
+
${commit.body}`.trim(), cwd)) {
|
|
41725
|
+
const normalized = trailer.toLowerCase();
|
|
41726
|
+
if (normalized.startsWith(key) && !required.includes(normalized)) required.push(normalized);
|
|
41727
|
+
}
|
|
41728
|
+
}
|
|
41729
|
+
if (!required.length) return true;
|
|
41730
|
+
const source = prBodyTrailerSource(prBody);
|
|
41731
|
+
const recognized = parseTrailers(`${headline}
|
|
41732
|
+
|
|
41733
|
+
${source}`.trim(), cwd).map((trailer) => trailer.toLowerCase()).filter((trailer) => trailer.startsWith(key));
|
|
41734
|
+
const textual = (source.match(/^Test-Policy-Override:\s*\S/gim) ?? []).length;
|
|
41735
|
+
if (textual !== recognized.length) return false;
|
|
41736
|
+
return required.every((trailer) => recognized.includes(trailer));
|
|
41737
|
+
}
|
|
41598
41738
|
function resolveBase(cwd, explicit) {
|
|
41599
41739
|
const candidates = [explicit, process.env.TEST_POLICY_BASE, "origin/development", "origin/main"].filter(Boolean);
|
|
41600
41740
|
for (const ref of candidates) {
|
|
@@ -41797,7 +41937,10 @@ async function hasPrMergeQueue(repo, base, gh2 = readApi) {
|
|
|
41797
41937
|
if (result.mergeQueue === void 0) throw new Error(`merge queue policy unreadable for ${repo}:${base}`);
|
|
41798
41938
|
return result.mergeQueue !== null;
|
|
41799
41939
|
}
|
|
41800
|
-
var writeApi = async (args) =>
|
|
41940
|
+
var writeApi = async (args) => {
|
|
41941
|
+
const { stdout, credential } = await execFileP("gh", ["api", ...args], { timeout: GH_MUTATION_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
|
|
41942
|
+
return { stdout, credential };
|
|
41943
|
+
};
|
|
41801
41944
|
var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
|
|
41802
41945
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
41803
41946
|
function admissionJournal(repo, pr, root) {
|
|
@@ -41813,23 +41956,31 @@ function admissionJournal(repo, pr, root) {
|
|
|
41813
41956
|
const path2 = (number) => (0, import_node_path30.join)(directory, `${number}.jsonl`);
|
|
41814
41957
|
const validate = (value, previous) => {
|
|
41815
41958
|
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)))) {
|
|
41959
|
+
if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
|
|
41817
41960
|
throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
|
|
41818
41961
|
}
|
|
41819
41962
|
if (!previous) {
|
|
41820
41963
|
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)) {
|
|
41964
|
+
} 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
41965
|
throw new Error("queue admission receipt has an illegal transition or changed operation identity");
|
|
41823
41966
|
}
|
|
41824
41967
|
return row;
|
|
41825
41968
|
};
|
|
41826
|
-
const load = () => {
|
|
41969
|
+
const load = (number = attempt) => {
|
|
41827
41970
|
let previous;
|
|
41828
|
-
for (const line of (0, import_node_fs33.readFileSync)(path2(
|
|
41971
|
+
for (const line of (0, import_node_fs33.readFileSync)(path2(number), "utf8").trimEnd().split("\n")) previous = validate(JSON.parse(line), previous);
|
|
41829
41972
|
return previous;
|
|
41830
41973
|
};
|
|
41831
41974
|
return {
|
|
41832
41975
|
receipt: attempt ? load() : void 0,
|
|
41976
|
+
/** #9504: the exact attempt this invocation selected, so a later attempt recorded at the same head can never supply its credential. */
|
|
41977
|
+
findByIntentId(intentId) {
|
|
41978
|
+
for (let number = 1; number <= attempt; number++) {
|
|
41979
|
+
const receipt = load(number);
|
|
41980
|
+
if (receipt.intentId === intentId) return receipt;
|
|
41981
|
+
}
|
|
41982
|
+
return void 0;
|
|
41983
|
+
},
|
|
41833
41984
|
create(head) {
|
|
41834
41985
|
const createdAt = Date.now();
|
|
41835
41986
|
const initial = { version: 1, repo: repo.toLowerCase(), pr, head, intentId: (0, import_node_crypto10.randomUUID)(), createdAt, observedAt: createdAt, state: "dispatching" };
|
|
@@ -41841,9 +41992,9 @@ function admissionJournal(repo, pr, root) {
|
|
|
41841
41992
|
}
|
|
41842
41993
|
return true;
|
|
41843
41994
|
},
|
|
41844
|
-
save(state, uuid, queueEntry) {
|
|
41995
|
+
save(state, uuid, queueEntry, credential) {
|
|
41845
41996
|
const previous = load();
|
|
41846
|
-
const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {} }, previous);
|
|
41997
|
+
const receipt = validate({ ...previous, state, observedAt: Date.now(), ...uuid ? { uuid } : {}, ...queueEntry ? { queueEntry } : {}, ...credential && previous.credential === void 0 ? { credential } : {} }, previous);
|
|
41847
41998
|
(0, import_node_fs33.appendFileSync)(path2(attempt), JSON.stringify(receipt) + "\n", { flush: true });
|
|
41848
41999
|
load();
|
|
41849
42000
|
}
|
|
@@ -41889,6 +42040,22 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41889
42040
|
}
|
|
41890
42041
|
return { mergeStatus: "auto-merge-enqueued" };
|
|
41891
42042
|
}
|
|
42043
|
+
const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
|
|
42044
|
+
if (saved?.queueEntry && observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid === saved.head) {
|
|
42045
|
+
const enqueuedAt = Date.parse(saved.queueEntry.enqueuedAt);
|
|
42046
|
+
const events = await readArrayPages(
|
|
42047
|
+
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
42048
|
+
options.timelineRead ?? read,
|
|
42049
|
+
"queue ejection timeline unreadable"
|
|
42050
|
+
);
|
|
42051
|
+
const times = (name) => events.filter((event) => event.event === name).map((event) => Date.parse(event.created_at));
|
|
42052
|
+
const adds = times("added_to_merge_queue").filter((at) => at >= enqueuedAt);
|
|
42053
|
+
const removals = adds.length === 1 ? times("removed_from_merge_queue").filter((at) => at > adds[0]) : [];
|
|
42054
|
+
if (removals.length === 1) {
|
|
42055
|
+
journal.save("ejected");
|
|
42056
|
+
return rejected("the previous admission was ejected by GitHub; rerun to submit a new intent");
|
|
42057
|
+
}
|
|
42058
|
+
}
|
|
41892
42059
|
} catch {
|
|
41893
42060
|
}
|
|
41894
42061
|
return unknown(reason);
|
|
@@ -41909,24 +42076,6 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41909
42076
|
} catch {
|
|
41910
42077
|
}
|
|
41911
42078
|
}
|
|
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
42079
|
if (superseded && receipt?.state === "enqueued") {
|
|
41931
42080
|
try {
|
|
41932
42081
|
journal.save("ejected");
|
|
@@ -41935,6 +42084,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41935
42084
|
}
|
|
41936
42085
|
}
|
|
41937
42086
|
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
|
|
42087
|
+
if (options.selection) {
|
|
42088
|
+
options.selection.selected = true;
|
|
42089
|
+
options.selection.intentId = receipt.intentId;
|
|
42090
|
+
}
|
|
41938
42091
|
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
42092
|
if (!receipt.uuid) return reconcile(`the previous send for ${repo}#${pr}, submitted head ${receipt.head}, has no saved operation UUID`);
|
|
41940
42093
|
} else {
|
|
@@ -41943,12 +42096,17 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41943
42096
|
if (!journal.create(head)) return submitQueueAdmission(pr, repo, head, read, write, options, true);
|
|
41944
42097
|
journal = admissionJournal(repo, pr, options.receiptDirectory);
|
|
41945
42098
|
receipt = journal.receipt;
|
|
42099
|
+
if (options.selection) {
|
|
42100
|
+
options.selection.selected = true;
|
|
42101
|
+
options.selection.intentId = receipt.intentId;
|
|
42102
|
+
}
|
|
41946
42103
|
} catch {
|
|
41947
42104
|
return unknown("the pre-send checkpoint failed; no request was dispatched");
|
|
41948
42105
|
}
|
|
41949
42106
|
let output;
|
|
42107
|
+
let credential;
|
|
41950
42108
|
try {
|
|
41951
|
-
|
|
42109
|
+
const written = await write([
|
|
41952
42110
|
`repos/${repo}/pulls/${pr}/merge-async`,
|
|
41953
42111
|
"--method",
|
|
41954
42112
|
"PUT",
|
|
@@ -41961,8 +42119,10 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41961
42119
|
"-f",
|
|
41962
42120
|
`sha=${head}`
|
|
41963
42121
|
]);
|
|
42122
|
+
({ stdout: output, credential } = typeof written === "string" ? { stdout: written, credential: void 0 } : written);
|
|
41964
42123
|
} catch (error) {
|
|
41965
42124
|
output = error.stdout ?? "";
|
|
42125
|
+
credential = error.credential;
|
|
41966
42126
|
}
|
|
41967
42127
|
try {
|
|
41968
42128
|
response = admissionResponse(output);
|
|
@@ -41972,7 +42132,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41972
42132
|
const { code, body } = response;
|
|
41973
42133
|
if (code === 200 && ["enqueued", "merged"].includes(body.status ?? "")) {
|
|
41974
42134
|
try {
|
|
41975
|
-
journal.save(body.status);
|
|
42135
|
+
journal.save(body.status, void 0, void 0, credential);
|
|
41976
42136
|
} catch {
|
|
41977
42137
|
return reconcile("the completed operation could not be checkpointed");
|
|
41978
42138
|
}
|
|
@@ -41980,7 +42140,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41980
42140
|
}
|
|
41981
42141
|
if (code === 400 || code === 403 || code === 404 || code === 422) {
|
|
41982
42142
|
try {
|
|
41983
|
-
journal.save("failed");
|
|
42143
|
+
journal.save("failed", void 0, void 0, credential);
|
|
41984
42144
|
} catch {
|
|
41985
42145
|
return unknown("the explicit rejection could not be checkpointed");
|
|
41986
42146
|
}
|
|
@@ -41992,7 +42152,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
41992
42152
|
return unknown("an existing operation has different or missing queue/head/no-bypass options; inspect that operation before continuing");
|
|
41993
42153
|
}
|
|
41994
42154
|
try {
|
|
41995
|
-
journal.save("pending", details.uuid);
|
|
42155
|
+
journal.save("pending", details.uuid, void 0, credential);
|
|
41996
42156
|
} catch {
|
|
41997
42157
|
return reconcile("the accepted operation UUID could not be checkpointed");
|
|
41998
42158
|
}
|
|
@@ -42047,8 +42207,12 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
42047
42207
|
if (!pull || !pull.id || !/^[a-f0-9]{40}$/i.test(pull.headRefOid)) throw new Error(`merge queue PR #${pr} identity unreadable`);
|
|
42048
42208
|
const head = options.expectedHeadOid ?? pull.headRefOid;
|
|
42049
42209
|
if (head !== pull.headRefOid) return { mergeStatus: "failed", retryable: false, error: `merge queue head changed: expected ${head}, observed ${pull.headRefOid}` };
|
|
42050
|
-
|
|
42051
|
-
|
|
42210
|
+
const stamp = (result2, selected = false, intentId) => {
|
|
42211
|
+
const credential = admissionCredential(repo, pr, head, options.receiptDirectory, selected, intentId);
|
|
42212
|
+
return credential === "unknown" ? result2 : { ...result2, credential };
|
|
42213
|
+
};
|
|
42214
|
+
if (pull.state === "MERGED") return stamp({ mergeStatus: "merged" });
|
|
42215
|
+
if (pull.isInMergeQueue) return stamp({ mergeStatus: "auto-merge-enqueued" });
|
|
42052
42216
|
if (pull.state !== "OPEN") return { mergeStatus: "failed", error: `PR #${pr} is ${pull.state}; no queue submission` };
|
|
42053
42217
|
const closing = [...pull.closingIssuesReferences.nodes.map((node) => node.number)];
|
|
42054
42218
|
let page = pull.closingIssuesReferences.pageInfo;
|
|
@@ -42144,7 +42308,20 @@ ${pull.body}`;
|
|
|
42144
42308
|
return preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${verdict.message}`);
|
|
42145
42309
|
}
|
|
42146
42310
|
const requiredBody = options.bodyText ?? composedBody;
|
|
42147
|
-
|
|
42311
|
+
const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
|
|
42312
|
+
if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
|
|
42313
|
+
if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
|
|
42314
|
+
return {
|
|
42315
|
+
mergeStatus: "failed",
|
|
42316
|
+
preparation: {
|
|
42317
|
+
status: "preparation-needed",
|
|
42318
|
+
reason: "queue-message-override",
|
|
42319
|
+
issueTargets: [],
|
|
42320
|
+
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."
|
|
42321
|
+
},
|
|
42322
|
+
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"
|
|
42323
|
+
};
|
|
42324
|
+
}
|
|
42148
42325
|
return preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission");
|
|
42149
42326
|
}
|
|
42150
42327
|
const current = await queueGraphql(
|
|
@@ -42155,8 +42332,9 @@ ${pull.body}`;
|
|
|
42155
42332
|
if (current.pullRequest.headRefOid !== head || current.pullRequest.title !== pull.title || current.pullRequest.body !== pull.body) {
|
|
42156
42333
|
return { mergeStatus: "failed", retryable: false, error: "queue source or PR text changed during preparation; rerun pr land against the new revision" };
|
|
42157
42334
|
}
|
|
42158
|
-
const
|
|
42159
|
-
|
|
42335
|
+
const selection = { selected: false };
|
|
42336
|
+
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
42337
|
+
return { ...stamp(result, selection.selected, selection.intentId), ...verdict.message ? { warning: verdict.message } : {} };
|
|
42160
42338
|
}
|
|
42161
42339
|
function decideQueueMergeRoute(hasQueue, method, headRef) {
|
|
42162
42340
|
if (!hasQueue) return "normal";
|
|
@@ -42194,8 +42372,50 @@ function prMergeQueueLine(result) {
|
|
|
42194
42372
|
return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
|
|
42195
42373
|
}
|
|
42196
42374
|
}
|
|
42375
|
+
function admissionCredential(repo, pr, head, receiptDirectory, selected = false, intentId) {
|
|
42376
|
+
try {
|
|
42377
|
+
const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
|
|
42378
|
+
const saved = intentId ? journal.findByIntentId(intentId) : journal.receipt;
|
|
42379
|
+
const live = saved && ["pending", "enqueued", "merged"].includes(saved.state);
|
|
42380
|
+
return saved && saved.head.toLowerCase() === head.toLowerCase() && (selected || live) ? saved.credential ?? "unknown" : "unknown";
|
|
42381
|
+
} catch {
|
|
42382
|
+
return "unknown";
|
|
42383
|
+
}
|
|
42384
|
+
}
|
|
42385
|
+
function observeQueuedAdmission(repo, pr, snapshot, receiptDirectory) {
|
|
42386
|
+
try {
|
|
42387
|
+
const journal = admissionJournal(repo, pr.replace(/^0+(?=\d)/, ""), receiptDirectory);
|
|
42388
|
+
const saved = journal.receipt;
|
|
42389
|
+
if (!saved || !["pending", "enqueued", "merged"].includes(saved.state) || snapshot.headRefOid?.toLowerCase() !== saved.head.toLowerCase()) return {};
|
|
42390
|
+
const entry = snapshot.mergeQueueEntry;
|
|
42391
|
+
if (saved.queueEntry) {
|
|
42392
|
+
if (entry?.enqueuedAt && entry.enqueuedAt !== saved.queueEntry.enqueuedAt) return { mismatched: true };
|
|
42393
|
+
} else if ((saved.state === "enqueued" || saved.state === "pending") && snapshot.isInMergeQueue && entry?.headCommit && entry.enqueuedAt) {
|
|
42394
|
+
try {
|
|
42395
|
+
journal.save("enqueued", saved.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
|
|
42396
|
+
} catch {
|
|
42397
|
+
return {};
|
|
42398
|
+
}
|
|
42399
|
+
}
|
|
42400
|
+
return saved.credential ? { credential: saved.credential } : {};
|
|
42401
|
+
} catch {
|
|
42402
|
+
return {};
|
|
42403
|
+
}
|
|
42404
|
+
}
|
|
42197
42405
|
|
|
42198
42406
|
// src/merge-group-orphans.ts
|
|
42407
|
+
var SETTLE_POLLS = 6;
|
|
42408
|
+
var SETTLE_MS = 5e3;
|
|
42409
|
+
async function settled(repo, id, deps) {
|
|
42410
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
42411
|
+
let status = "unknown";
|
|
42412
|
+
for (let poll = 0; poll < SETTLE_POLLS; poll++) {
|
|
42413
|
+
await sleep2(SETTLE_MS);
|
|
42414
|
+
status = JSON.parse(await deps.read([`repos/${repo}/actions/runs/${id}`])).status ?? "unknown";
|
|
42415
|
+
if (status === "completed") break;
|
|
42416
|
+
}
|
|
42417
|
+
return status;
|
|
42418
|
+
}
|
|
42199
42419
|
async function isOrphan(run, repo, deps) {
|
|
42200
42420
|
const match = /^gh-readonly-queue\/(.+)\/pr-([1-9][0-9]*)-([a-f0-9]+)$/i.exec(run.head_branch);
|
|
42201
42421
|
if (!match || run.event !== "merge_group") return false;
|
|
@@ -42215,7 +42435,7 @@ async function isOrphan(run, repo, deps) {
|
|
|
42215
42435
|
}
|
|
42216
42436
|
}
|
|
42217
42437
|
async function cancelMergeGroupOrphans(repo, apply, deps) {
|
|
42218
|
-
const report = { repo, apply, candidates: [], cancelled: [], errors: [] };
|
|
42438
|
+
const report = { repo, apply, candidates: [], cancelled: [], forceRequested: [], forceCancelled: [], errors: [] };
|
|
42219
42439
|
try {
|
|
42220
42440
|
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error("expected --repo owner/repo");
|
|
42221
42441
|
const runs2 = [];
|
|
@@ -42237,7 +42457,18 @@ async function cancelMergeGroupOrphans(repo, apply, deps) {
|
|
|
42237
42457
|
for (const run of confirmed) {
|
|
42238
42458
|
if (!await isOrphan(run, repo, deps)) continue;
|
|
42239
42459
|
await deps.cancel(repo, run.id);
|
|
42240
|
-
|
|
42460
|
+
let status = await settled(repo, run.id, deps);
|
|
42461
|
+
let forced = false;
|
|
42462
|
+
if (status !== "completed" && await isOrphan(run, repo, deps)) {
|
|
42463
|
+
await deps.forceCancel(repo, run.id);
|
|
42464
|
+
report.forceRequested.push(run.id);
|
|
42465
|
+
forced = true;
|
|
42466
|
+
status = await settled(repo, run.id, deps);
|
|
42467
|
+
}
|
|
42468
|
+
if (status === "completed") {
|
|
42469
|
+
report.cancelled.push(run.id);
|
|
42470
|
+
if (forced) report.forceCancelled.push(run.id);
|
|
42471
|
+
} else report.errors.push(`cancel-orphans: run ${run.id} still ${status} after cancel${forced ? " and force-cancel" : ""}`);
|
|
42241
42472
|
}
|
|
42242
42473
|
}
|
|
42243
42474
|
} catch (error) {
|
|
@@ -42263,17 +42494,59 @@ function registerCancelOrphans(ci2, writeToken) {
|
|
|
42263
42494
|
timeout: 2e4,
|
|
42264
42495
|
env: { ...process.env, GH_TOKEN: token }
|
|
42265
42496
|
});
|
|
42497
|
+
},
|
|
42498
|
+
forceCancel: async (repo, id) => {
|
|
42499
|
+
token ??= await writeToken();
|
|
42500
|
+
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${id}/force-cancel`, "--method", "POST"], {
|
|
42501
|
+
timeout: 2e4,
|
|
42502
|
+
env: { ...process.env, GH_TOKEN: token }
|
|
42503
|
+
});
|
|
42266
42504
|
}
|
|
42267
42505
|
});
|
|
42268
42506
|
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
42269
42507
|
else {
|
|
42270
42508
|
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)"}`);
|
|
42509
|
+
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
42510
|
for (const error of report.errors) console.error(error);
|
|
42273
42511
|
}
|
|
42274
42512
|
if (report.errors.length) process.exitCode = 1;
|
|
42275
42513
|
});
|
|
42276
42514
|
}
|
|
42515
|
+
var RUN_CANCEL_STATUS = "cancel-requested";
|
|
42516
|
+
async function cancelWorkflowRun(repo, id, deps) {
|
|
42517
|
+
const raw = String(id).trim();
|
|
42518
|
+
if (!/^[1-9]\d*$/.test(raw)) throw new Error(`run cancel: <id> must be a positive workflow run id (received '${String(id)}')`);
|
|
42519
|
+
const runId = Number(raw);
|
|
42520
|
+
if (!Number.isSafeInteger(runId)) throw new Error(`run cancel: <id> ${raw} is beyond the safe integer range; refusing to address a different run`);
|
|
42521
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new Error(`run cancel: --repo must be owner/repo (received '${repo}')`);
|
|
42522
|
+
try {
|
|
42523
|
+
await deps.cancel(repo, runId);
|
|
42524
|
+
} catch (error) {
|
|
42525
|
+
const detail = String(error.stderr ?? error);
|
|
42526
|
+
throw new Error(/\b403\b/.test(detail) ? "run cancel: GitHub App denied (HTTP 403); actions:write permission is required; no personal-token fallback" : `run cancel: ${detail}`);
|
|
42527
|
+
}
|
|
42528
|
+
return { ok: true, repo, runId, status: RUN_CANCEL_STATUS };
|
|
42529
|
+
}
|
|
42530
|
+
function registerRunCancel(run, writeToken) {
|
|
42531
|
+
run.command("cancel <id>").description("cancel one queued or in-progress workflow run on the org App (actions:write); requests the cancel and prints a receipt").requiredOption("--repo <owner/repo>", "repository that owns the run").option("--json", "machine-readable receipt").action(async (id, options) => {
|
|
42532
|
+
let receipt;
|
|
42533
|
+
try {
|
|
42534
|
+
receipt = await cancelWorkflowRun(options.repo, id, {
|
|
42535
|
+
cancel: async (repo, runId) => {
|
|
42536
|
+
const token = await writeToken();
|
|
42537
|
+
await execFileP("gh", ["api", `repos/${repo}/actions/runs/${runId}/cancel`, "--method", "POST"], {
|
|
42538
|
+
timeout: 2e4,
|
|
42539
|
+
env: { ...process.env, GH_TOKEN: token }
|
|
42540
|
+
});
|
|
42541
|
+
}
|
|
42542
|
+
});
|
|
42543
|
+
} catch (error) {
|
|
42544
|
+
return fail(error.message);
|
|
42545
|
+
}
|
|
42546
|
+
if (options.json) console.log(JSON.stringify(receipt));
|
|
42547
|
+
else console.log(`run cancel: ${receipt.repo} run ${receipt.runId} \u2014 cancel requested (status: ${receipt.status})`);
|
|
42548
|
+
});
|
|
42549
|
+
}
|
|
42277
42550
|
|
|
42278
42551
|
// src/board-commands.ts
|
|
42279
42552
|
init_cli_shared();
|
|
@@ -46353,13 +46626,14 @@ function filledDocCheck(label, text, path2) {
|
|
|
46353
46626
|
const unfilled = unfilledDocPlaceholders(text);
|
|
46354
46627
|
return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
|
|
46355
46628
|
}
|
|
46356
|
-
var NPMRC_PATHS = [".npmrc", "web/.npmrc"];
|
|
46629
|
+
var NPMRC_PATHS = [".npmrc", "module/.npmrc", "web/.npmrc"];
|
|
46357
46630
|
var SCOPED_GITHUB_PACKAGES_RE = /^\s*@[A-Za-z0-9-]+:registry\s*=\s*https:\/\/npm\.pkg\.github\.com/m;
|
|
46358
46631
|
var GITHUB_PACKAGES_AUTH_RE = /^\s*\/\/npm\.pkg\.github\.com\/:_authToken\s*=(.*)$/m;
|
|
46359
46632
|
function npmrcAuthValueIsPlaceholder(value) {
|
|
46360
46633
|
const trimmed = value.trim();
|
|
46361
46634
|
return trimmed === "" || /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(trimmed);
|
|
46362
46635
|
}
|
|
46636
|
+
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
46637
|
function npmrcGitHubPackagesAuthCheck(path2, text) {
|
|
46364
46638
|
if (text === null || !SCOPED_GITHUB_PACKAGES_RE.test(text)) return null;
|
|
46365
46639
|
const label = `${path2} authenticates GitHub Packages`;
|
|
@@ -46368,7 +46642,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
|
|
|
46368
46642
|
return {
|
|
46369
46643
|
ok: false,
|
|
46370
46644
|
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`
|
|
46645
|
+
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
46646
|
};
|
|
46373
46647
|
}
|
|
46374
46648
|
if (npmrcAuthValueIsPlaceholder(match[1])) {
|
|
@@ -46376,7 +46650,7 @@ function npmrcGitHubPackagesAuthCheck(path2, text) {
|
|
|
46376
46650
|
return {
|
|
46377
46651
|
ok: false,
|
|
46378
46652
|
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)`
|
|
46653
|
+
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
46654
|
};
|
|
46381
46655
|
}
|
|
46382
46656
|
return { ok: true, label };
|
|
@@ -46564,7 +46838,15 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
46564
46838
|
});
|
|
46565
46839
|
}
|
|
46566
46840
|
}
|
|
46567
|
-
|
|
46841
|
+
const tree = await restJson3(
|
|
46842
|
+
deps,
|
|
46843
|
+
`repos/${repo}/git/trees/${encodeURIComponent(baseBranch)}?recursive=1`,
|
|
46844
|
+
{}
|
|
46845
|
+
);
|
|
46846
|
+
const discoveredNpmrcPaths = (tree.tree ?? []).filter(
|
|
46847
|
+
(entry) => entry.type === "blob" && typeof entry.path === "string" && (entry.path === ".npmrc" || entry.path.endsWith("/.npmrc"))
|
|
46848
|
+
).map((entry) => entry.path);
|
|
46849
|
+
for (const path2 of /* @__PURE__ */ new Set([...NPMRC_PATHS, ...discoveredNpmrcPaths])) {
|
|
46568
46850
|
const check = npmrcGitHubPackagesAuthCheck(path2, await contentText(deps, repo, baseBranch, path2));
|
|
46569
46851
|
if (check) checks.push(check);
|
|
46570
46852
|
}
|
|
@@ -51778,7 +52060,7 @@ async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
|
51778
52060
|
if (now() >= deadline) break;
|
|
51779
52061
|
const checks = await deps.waitForChecks(prNumber, repo, deadline - now());
|
|
51780
52062
|
if (checks.status !== "success" && checks.status !== "skipped") {
|
|
51781
|
-
return { mergeStatus: "failed", error: `${result.error ?? "merge failed"}
|
|
52063
|
+
return { ...result, mergeStatus: "failed", error: `${result.error ?? "merge failed"}
|
|
51782
52064
|
registration wait: checks-wait ${checks.status}: ${checks.detail ?? checks.reason ?? "unknown"}` };
|
|
51783
52065
|
}
|
|
51784
52066
|
if (now() >= deadline) break;
|
|
@@ -51924,6 +52206,8 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
51924
52206
|
if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
|
|
51925
52207
|
const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
|
|
51926
52208
|
base.mergeStatus = merge.mergeStatus;
|
|
52209
|
+
if (merge.credential) base.credential = merge.credential;
|
|
52210
|
+
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
51927
52211
|
if (merge.mergeStatus === "queue-admission-pending") {
|
|
51928
52212
|
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
51929
52213
|
}
|
|
@@ -51985,9 +52269,19 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
51985
52269
|
const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
|
|
51986
52270
|
const deadline = deadlineMs ?? now() + timeoutMs;
|
|
51987
52271
|
let attempt = 0;
|
|
52272
|
+
let credentialMismatched = false;
|
|
52273
|
+
const observe = (read) => {
|
|
52274
|
+
const seen = deps.observeQueued?.(pr, base.repo, read);
|
|
52275
|
+
if (seen?.mismatched) {
|
|
52276
|
+
credentialMismatched = true;
|
|
52277
|
+
delete base.credential;
|
|
52278
|
+
}
|
|
52279
|
+
if (!credentialMismatched && seen?.credential && !base.credential) base.credential = seen.credential;
|
|
52280
|
+
};
|
|
51988
52281
|
while (now() < deadline) {
|
|
51989
52282
|
const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
|
|
51990
52283
|
snapshot = void 0;
|
|
52284
|
+
if (read.isInMergeQueue || read.state === "MERGED") observe(read);
|
|
51991
52285
|
if (read.state === "MERGED") return queueMerged(base, deps);
|
|
51992
52286
|
if (read.isInMergeQueue) {
|
|
51993
52287
|
base.mergeQueue = {
|
|
@@ -52000,7 +52294,10 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
|
52000
52294
|
} else if (read.state === "OPEN" && !read.autoMergeRequest) {
|
|
52001
52295
|
await sleep2(PR_LAND_EJECT_CONFIRM_MS);
|
|
52002
52296
|
const again = await deps.readMergeQueue(pr, base.repo);
|
|
52003
|
-
if (again.state === "MERGED")
|
|
52297
|
+
if (again.state === "MERGED") {
|
|
52298
|
+
observe(again);
|
|
52299
|
+
return queueMerged(base, deps);
|
|
52300
|
+
}
|
|
52004
52301
|
if (again.isInMergeQueue || again.autoMergeRequest) {
|
|
52005
52302
|
snapshot = again;
|
|
52006
52303
|
continue;
|
|
@@ -52055,6 +52352,10 @@ function renderPrLandTable(entries) {
|
|
|
52055
52352
|
const width = (i) => Math.max(...rows.map((r) => r[i].length));
|
|
52056
52353
|
return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
|
|
52057
52354
|
}
|
|
52355
|
+
function landCredentials(results) {
|
|
52356
|
+
const each = results.map((r) => r.credential ?? "unknown");
|
|
52357
|
+
return { each, overall: each.length === 0 ? "unknown" : new Set(each).size === 1 ? each[0] : "mixed" };
|
|
52358
|
+
}
|
|
52058
52359
|
|
|
52059
52360
|
// src/git-worktree-observation.ts
|
|
52060
52361
|
function parseGitWorktreePorcelain(text) {
|
|
@@ -52255,6 +52556,12 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).filter((line) => li
|
|
|
52255
52556
|
var defaultGhMergeAutoIo = {
|
|
52256
52557
|
gh: async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout
|
|
52257
52558
|
};
|
|
52559
|
+
var credentialedGh = {
|
|
52560
|
+
gh: async (args, timeoutMs) => {
|
|
52561
|
+
const { stdout, credential } = await execFileP("gh", args, { timeout: timeoutMs });
|
|
52562
|
+
return { stdout, credential };
|
|
52563
|
+
}
|
|
52564
|
+
};
|
|
52258
52565
|
function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
|
|
52259
52566
|
const policy = squashBodyWithOverride(commits, cwd);
|
|
52260
52567
|
const base = policy ?? defaultSquashBodyFromCommits(commits);
|
|
@@ -52316,14 +52623,34 @@ async function ghAdminTrueMergeWhenClean(prNumber, repo, opts = {}) {
|
|
|
52316
52623
|
return { mergeStatus: "merged" };
|
|
52317
52624
|
}
|
|
52318
52625
|
async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
|
|
52319
|
-
|
|
52626
|
+
let credential;
|
|
52627
|
+
let refusedAsMerged = false;
|
|
52628
|
+
const source = extra?.io ?? credentialedGh;
|
|
52629
|
+
const note = (args2, value) => {
|
|
52630
|
+
if (args2[0] === "pr" && args2[1] === "merge" && value) credential = value;
|
|
52631
|
+
};
|
|
52632
|
+
const io = {
|
|
52633
|
+
gh: async (ghArgs, timeoutMs) => {
|
|
52634
|
+
try {
|
|
52635
|
+
const out = await source.gh(ghArgs, timeoutMs);
|
|
52636
|
+
if (typeof out === "string") return out;
|
|
52637
|
+
note(ghArgs, out.credential);
|
|
52638
|
+
return out.stdout;
|
|
52639
|
+
} catch (e) {
|
|
52640
|
+
note(ghArgs, e?.credential);
|
|
52641
|
+
if (ghArgs[0] === "pr" && ghArgs[1] === "merge" && /already been merged/i.test(String(e?.message ?? ""))) refusedAsMerged = true;
|
|
52642
|
+
throw e;
|
|
52643
|
+
}
|
|
52644
|
+
}
|
|
52645
|
+
};
|
|
52320
52646
|
const args = repo ? ["--repo", repo] : [];
|
|
52321
52647
|
const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh, {
|
|
52322
52648
|
allowedClosing: method === "--squash" ? extra?.allowedClosing : void 0,
|
|
52323
52649
|
bodyText: method === "--squash" ? extra?.bodyText : void 0
|
|
52324
52650
|
});
|
|
52325
52651
|
try {
|
|
52326
|
-
|
|
52652
|
+
const result = await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue, extra?.matchHeadCommit);
|
|
52653
|
+
return credential ? { ...result, credential, ...refusedAsMerged && result.mergeStatus === "merged" ? { credentialRole: "attempted-write" } : {} } : result;
|
|
52327
52654
|
} finally {
|
|
52328
52655
|
overrideBody?.cleanup();
|
|
52329
52656
|
}
|
|
@@ -55727,7 +56054,7 @@ async function observePrCreateClaims(body, repoOption, deps = {}) {
|
|
|
55727
56054
|
if (findings.length) throw new Error("pr create: claim findings must be resolved before restamping");
|
|
55728
56055
|
if (attempted) throw new Error("pr create: claim restamp already attempted; observe again before retrying");
|
|
55729
56056
|
attempted = true;
|
|
55730
|
-
const writer = deps.client ?? defaultGitHubClient();
|
|
56057
|
+
const writer = await selectClaimWriteClient(deps.client ?? defaultGitHubClient(), deps.appClient);
|
|
55731
56058
|
for (const number of restamps) {
|
|
55732
56059
|
await writer.rest("POST", `repos/${repo}/issues/${number}/comments`, { body: { body: formatClaimMarker(actor) } });
|
|
55733
56060
|
}
|
|
@@ -55743,11 +56070,11 @@ function catFileBatch(root, ref, paths) {
|
|
|
55743
56070
|
if (paths.length === 0) return Promise.resolve([]);
|
|
55744
56071
|
return new Promise((resolve7) => {
|
|
55745
56072
|
const chunks = [];
|
|
55746
|
-
let
|
|
56073
|
+
let settled2 = false;
|
|
55747
56074
|
const child2 = (0, import_node_child_process15.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
|
|
55748
56075
|
const finish = (ok = false) => {
|
|
55749
|
-
if (
|
|
55750
|
-
|
|
56076
|
+
if (settled2) return;
|
|
56077
|
+
settled2 = true;
|
|
55751
56078
|
clearTimeout(timer);
|
|
55752
56079
|
resolve7(ok ? parseCatFileBatch(Buffer.concat(chunks), paths.length) : []);
|
|
55753
56080
|
};
|
|
@@ -55913,6 +56240,21 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
|
55913
56240
|
}
|
|
55914
56241
|
};
|
|
55915
56242
|
}
|
|
56243
|
+
async function retryPrCreateIdentityDrift(input, deps, notify = (line) => process.stderr.write(line)) {
|
|
56244
|
+
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");
|
|
56245
|
+
const preparation = await preparePrCreate(input, deps);
|
|
56246
|
+
if (preparation.findings.length) {
|
|
56247
|
+
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("; ")}`) };
|
|
56248
|
+
}
|
|
56249
|
+
try {
|
|
56250
|
+
await preparation.commit();
|
|
56251
|
+
return { preparation };
|
|
56252
|
+
} catch (error) {
|
|
56253
|
+
const message2 = error.message;
|
|
56254
|
+
if (!message2.includes("identity changed during preparation")) return { preparation, error };
|
|
56255
|
+
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.`) };
|
|
56256
|
+
}
|
|
56257
|
+
}
|
|
55916
56258
|
function createPreparationDeps(input, io = {}) {
|
|
55917
56259
|
const exec3 = io.exec ?? execFileP;
|
|
55918
56260
|
const git3 = async (args) => (await exec3("git", args, { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
@@ -56196,10 +56538,10 @@ function healthPollIntervalMs() {
|
|
|
56196
56538
|
}
|
|
56197
56539
|
function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
|
|
56198
56540
|
return new Promise((resolve7, reject2) => {
|
|
56199
|
-
let
|
|
56541
|
+
let settled2 = false;
|
|
56200
56542
|
const finish = (fn) => {
|
|
56201
|
-
if (
|
|
56202
|
-
|
|
56543
|
+
if (settled2) return;
|
|
56544
|
+
settled2 = true;
|
|
56203
56545
|
clearTimeout(timer);
|
|
56204
56546
|
child2.removeAllListeners("error");
|
|
56205
56547
|
child2.removeAllListeners("exit");
|
|
@@ -58258,7 +58600,15 @@ function registerCollaborationCommands(program3) {
|
|
|
58258
58600
|
return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
58259
58601
|
}
|
|
58260
58602
|
});
|
|
58261
|
-
jsonParity(issue.command("comment <ref>").description("post
|
|
58603
|
+
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) => {
|
|
58604
|
+
const editShape = "--edit <commentId> --replace <text> --with <text> (without --body/--body-file)";
|
|
58605
|
+
if (o.edit !== void 0) {
|
|
58606
|
+
if (o.replace === void 0 || o.with === void 0 || o.body !== void 0 || o.bodyFile !== void 0 || o.replace.length === 0) {
|
|
58607
|
+
return fail(`issue comment: use ${editShape}; --replace must not be empty`);
|
|
58608
|
+
}
|
|
58609
|
+
} else if (o.replace !== void 0 || o.with !== void 0) {
|
|
58610
|
+
return fail(`issue comment: --replace/--with require ${editShape}`);
|
|
58611
|
+
}
|
|
58262
58612
|
let parsed;
|
|
58263
58613
|
try {
|
|
58264
58614
|
parsed = parseIssueRef(ref, o.repo);
|
|
@@ -58267,14 +58617,14 @@ function registerCollaborationCommands(program3) {
|
|
|
58267
58617
|
}
|
|
58268
58618
|
let body;
|
|
58269
58619
|
try {
|
|
58270
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
58620
|
+
body = o.edit !== void 0 ? "" : await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises8.readFile, readStdin });
|
|
58271
58621
|
} catch (e) {
|
|
58272
58622
|
return fail(`issue comment: ${e.message}`);
|
|
58273
58623
|
}
|
|
58274
58624
|
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
58275
58625
|
if (!repo) return fail("issue comment: could not resolve repo \u2014 pass --repo owner/repo");
|
|
58276
58626
|
try {
|
|
58277
|
-
const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
|
|
58627
|
+
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
58628
|
console.log(JSON.stringify(result));
|
|
58279
58629
|
} catch (e) {
|
|
58280
58630
|
const err = e;
|
|
@@ -58483,7 +58833,7 @@ ${list}`);
|
|
|
58483
58833
|
title = safeTitle;
|
|
58484
58834
|
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
58835
|
}
|
|
58486
|
-
|
|
58836
|
+
let preparation = await preparePrCreate({ body, repo: o.repo, head: o.head, base: o.base });
|
|
58487
58837
|
if (preparation.findings.length) return fail(`pr create: preparation found ${preparation.findings.length} blocker(s):
|
|
58488
58838
|
${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${finding.detail}`).join("\n")}`, { code: ERROR_CODES.ERR_STATE_CONFLICT });
|
|
58489
58839
|
for (const observation of preparation.observations.filter((item) => item.state === "skipped")) {
|
|
@@ -58497,7 +58847,16 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
58497
58847
|
try {
|
|
58498
58848
|
await preparation.commit();
|
|
58499
58849
|
} catch (error) {
|
|
58500
|
-
|
|
58850
|
+
const message2 = error.message;
|
|
58851
|
+
if (!message2.includes("identity changed during preparation")) return fail(`pr create: preparation commit failed \u2014 ${message2}`);
|
|
58852
|
+
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 }));
|
|
58853
|
+
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 });
|
|
58854
|
+
preparation = retry.preparation;
|
|
58855
|
+
if (preparation.candidate?.head !== createHead) {
|
|
58856
|
+
const retriedHeadWarning = await prCreateMergedHeadWarning({ head: preparation.candidate?.head || void 0, repo: o.repo });
|
|
58857
|
+
if (retriedHeadWarning) process.stderr.write(`${retriedHeadWarning}
|
|
58858
|
+
`);
|
|
58859
|
+
}
|
|
58501
58860
|
}
|
|
58502
58861
|
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
58862
|
if (isGhCreateRateLimited(created)) {
|
|
@@ -58859,6 +59218,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
58859
59218
|
return receipt;
|
|
58860
59219
|
},
|
|
58861
59220
|
readMergeQueue: readPrMergeQueue,
|
|
59221
|
+
observeQueued: (prNumber, repo, snapshot) => observeQueuedAdmission(repo, prNumber, snapshot),
|
|
58862
59222
|
findMergeGroupRun: findPrMergeGroupRun,
|
|
58863
59223
|
queueProgress: (receipt) => {
|
|
58864
59224
|
const line = prMergeQueueLine(receipt);
|
|
@@ -59047,7 +59407,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59047
59407
|
inFlight = Math.max(1, inFlight - 1);
|
|
59048
59408
|
}
|
|
59049
59409
|
});
|
|
59050
|
-
const credential =
|
|
59410
|
+
const { each: credentials, overall: credential } = landCredentials(entries.map((e) => e.result));
|
|
59051
59411
|
if (numbers.length === 1) {
|
|
59052
59412
|
const { result, cleanup } = entries[0];
|
|
59053
59413
|
if (opts.json) printLine(JSON.stringify({ ...result, ...cleanup ? { cleanup } : {}, credential }));
|
|
@@ -59058,7 +59418,7 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
59058
59418
|
if (cleanup) printLine(`pr land: ${cleanup}`);
|
|
59059
59419
|
}
|
|
59060
59420
|
} 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 }));
|
|
59421
|
+
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
59422
|
} else {
|
|
59063
59423
|
printLine(renderPrLandTable(entries));
|
|
59064
59424
|
printLine(`pr land: ${entries.filter((e) => e.outcome === "merged").length}/${entries.length} merged (credential: ${credential})`);
|
|
@@ -64808,8 +65168,7 @@ function parseBrokerPrState(raw) {
|
|
|
64808
65168
|
}
|
|
64809
65169
|
function buildPrListSpec(o) {
|
|
64810
65170
|
const repo = parseBrokerRepo(o.repo, "--repo");
|
|
64811
|
-
const fields = parseBrokerJsonFields(o.json);
|
|
64812
|
-
rejectPrViewQueueFields(fields, "<number>", repo, "list");
|
|
65171
|
+
const fields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), "<number>", repo, "list");
|
|
64813
65172
|
const args = withJson(withRepo(["pr", "list"], repo), fields, o.json);
|
|
64814
65173
|
const state = parseBrokerPrState(o.state);
|
|
64815
65174
|
if (state) args.push("--state", state);
|
|
@@ -64876,18 +65235,24 @@ function buildRunWatchSpec(id, o) {
|
|
|
64876
65235
|
}
|
|
64877
65236
|
function rejectPrViewQueueFields(fields, number, repo, verb = "view") {
|
|
64878
65237
|
const badField = fields?.split(",").find((field) => field === "mergeQueueEntry" || field === "isInMergeQueue");
|
|
64879
|
-
if (!badField) return;
|
|
65238
|
+
if (!badField) return fields;
|
|
64880
65239
|
const targetRepo2 = repo ?? "<owner/repo>";
|
|
64881
65240
|
const remaining = fields?.split(",").filter((f) => f && f !== "mergeQueueEntry" && f !== "isInMergeQueue").join(",") ?? "";
|
|
64882
|
-
|
|
64883
|
-
|
|
65241
|
+
if (!remaining) {
|
|
65242
|
+
throw new Error(
|
|
65243
|
+
`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`
|
|
65244
|
+
);
|
|
65245
|
+
}
|
|
65246
|
+
process.stderr.write(
|
|
65247
|
+
`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\`
|
|
65248
|
+
`
|
|
64884
65249
|
);
|
|
65250
|
+
return remaining;
|
|
64885
65251
|
}
|
|
64886
65252
|
function buildPrViewSpec(ref, o) {
|
|
64887
65253
|
const number = parseBrokerId(ref, "PR number");
|
|
64888
|
-
const jsonFields = parseBrokerJsonFields(o.json);
|
|
64889
65254
|
const repo = parseBrokerRepo(o.repo, "--repo");
|
|
64890
|
-
rejectPrViewQueueFields(
|
|
65255
|
+
const jsonFields = rejectPrViewQueueFields(parseBrokerJsonFields(o.json), number, repo);
|
|
64891
65256
|
const args = withJson(withRepo(["pr", "view", number], repo), jsonFields, o.json);
|
|
64892
65257
|
if (o.comments) args.push("--comments");
|
|
64893
65258
|
return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
|
|
@@ -64961,7 +65326,34 @@ function ghExitCodeOf(error) {
|
|
|
64961
65326
|
const code = error.code;
|
|
64962
65327
|
return typeof code === "number" ? code : null;
|
|
64963
65328
|
}
|
|
64964
|
-
|
|
65329
|
+
var RUN_LIST_RECOVERY_FLAGS = ["--repo", "--branch", "--commit", "--event", "--workflow", "--status", "--created", "--limit"];
|
|
65330
|
+
function shellWord(value) {
|
|
65331
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
65332
|
+
}
|
|
65333
|
+
function formatBrokerRunListRecovery(spec, jq) {
|
|
65334
|
+
const tokens2 = spec.args.slice(2);
|
|
65335
|
+
const valueOf = (flag) => {
|
|
65336
|
+
const index = tokens2.indexOf(flag);
|
|
65337
|
+
if (index === -1) return void 0;
|
|
65338
|
+
const next = tokens2[index + 1];
|
|
65339
|
+
return next === void 0 || next.startsWith("-") ? void 0 : next;
|
|
65340
|
+
};
|
|
65341
|
+
const words = ["MMI_ACTOR=app", "mmi-cli", "devops", "read", "run", "list"];
|
|
65342
|
+
const repo = valueOf("--repo");
|
|
65343
|
+
words.push("--repo", repo === void 0 ? "<owner/repo>" : shellWord(repo));
|
|
65344
|
+
for (const flag of RUN_LIST_RECOVERY_FLAGS.slice(1)) {
|
|
65345
|
+
const value = valueOf(flag);
|
|
65346
|
+
if (value !== void 0) words.push(flag, shellWord(value));
|
|
65347
|
+
}
|
|
65348
|
+
if (tokens2.includes("--json")) {
|
|
65349
|
+
const fields = valueOf("--json");
|
|
65350
|
+
words.push("--json");
|
|
65351
|
+
if (fields !== void 0) words.push(shellWord(fields));
|
|
65352
|
+
}
|
|
65353
|
+
if (jq !== void 0) words.push("--jq", `'${jq.replace(/'/g, `'\\''`)}'`);
|
|
65354
|
+
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`;
|
|
65355
|
+
}
|
|
65356
|
+
function brokerGhFailureHint(failureText, args, jq) {
|
|
64965
65357
|
const target = args[0] === "pr" && args[1] === "view" ? args[2] : void 0;
|
|
64966
65358
|
const unknownField = /Unknown JSON field: "([^"]+)"/.exec(failureText)?.[1] ?? /Unknown JSON field: ([A-Za-z][A-Za-z0-9]*)/.exec(failureText)?.[1];
|
|
64967
65359
|
if (unknownField === "mergeQueueEntry" || unknownField === "isInMergeQueue") {
|
|
@@ -64973,9 +65365,12 @@ function brokerGhFailureHint(failureText, args) {
|
|
|
64973
65365
|
if (/Could not resolve to a PullRequest/i.test(failureText)) {
|
|
64974
65366
|
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
65367
|
}
|
|
65368
|
+
if (args[0] === "run" && args[1] === "list") {
|
|
65369
|
+
return formatBrokerRunListRecovery({ program: "gh", args: [...args], timeoutMs: SINGLE_SHOT_TIMEOUT_MS }, jq);
|
|
65370
|
+
}
|
|
64976
65371
|
return void 0;
|
|
64977
65372
|
}
|
|
64978
|
-
async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk)) {
|
|
65373
|
+
async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(chunk), jq) {
|
|
64979
65374
|
const env = brokerChildEnv();
|
|
64980
65375
|
let output;
|
|
64981
65376
|
try {
|
|
@@ -64987,7 +65382,7 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
|
|
|
64987
65382
|
if (code !== null) {
|
|
64988
65383
|
if (typeof err.stderr === "string" && err.stderr) process.stderr.write(redactPrivateCredential(err.stderr, env));
|
|
64989
65384
|
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";
|
|
65385
|
+
const recovery = brokerGhFailureHint(failure, spec.args, jq) ?? "inspect stderr; retry the same typed read and only parse stdout after success";
|
|
64991
65386
|
process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} failed on the App pool (exit ${code}) \u2014 ${recovery}
|
|
64992
65387
|
`);
|
|
64993
65388
|
return code;
|
|
@@ -64998,7 +65393,8 @@ async function runBrokerGh(run, spec, write = (chunk) => process.stdout.write(ch
|
|
|
64998
65393
|
try {
|
|
64999
65394
|
JSON.parse(output);
|
|
65000
65395
|
} catch {
|
|
65001
|
-
|
|
65396
|
+
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";
|
|
65397
|
+
process.stderr.write(`read: gh ${spec.args.slice(0, 2).join(" ")} returned ${output.trim() ? "invalid" : "empty"} JSON on the App pool \u2014 ${recovery}
|
|
65002
65398
|
`);
|
|
65003
65399
|
return 1;
|
|
65004
65400
|
}
|
|
@@ -65080,13 +65476,13 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
|
|
|
65080
65476
|
requireBrokerApp(label);
|
|
65081
65477
|
try {
|
|
65082
65478
|
if (jq === void 0) {
|
|
65083
|
-
process.exitCode = await runBrokerGh(ghRun, spec);
|
|
65479
|
+
process.exitCode = await runBrokerGh(ghRun, spec, void 0, void 0);
|
|
65084
65480
|
return;
|
|
65085
65481
|
}
|
|
65086
65482
|
let out = "";
|
|
65087
65483
|
const code = await runBrokerGh(ghRun, spec, (chunk) => {
|
|
65088
65484
|
out += chunk;
|
|
65089
|
-
});
|
|
65485
|
+
}, jq);
|
|
65090
65486
|
if (code === 0) await emitJson(out, jq);
|
|
65091
65487
|
process.exitCode = code;
|
|
65092
65488
|
} catch (e) {
|
|
@@ -65309,7 +65705,7 @@ function typedReadHint(path2) {
|
|
|
65309
65705
|
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
65706
|
}
|
|
65311
65707
|
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");
|
|
65708
|
+
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
65709
|
}
|
|
65314
65710
|
return say(`mmi-cli devops read issue view ${item} --repo ${slug} --json title,body,state,labels,comments`);
|
|
65315
65711
|
}
|
|
@@ -65395,14 +65791,16 @@ async function readAppApi(path2, runner = apiReadRunner) {
|
|
|
65395
65791
|
function registerApiReadCommands(program3, overrides) {
|
|
65396
65792
|
const spawn7 = overrides?.runner ?? apiReadRunner;
|
|
65397
65793
|
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) => {
|
|
65794
|
+
const runGet = async (req, o) => {
|
|
65403
65795
|
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
65796
|
const zip = req.kind === "actions-artifact-zip" || req.kind === "actions-job-logs" || req.kind === "release-asset";
|
|
65405
|
-
if (zip !== (o.out !== void 0))
|
|
65797
|
+
if (zip !== (o.out !== void 0)) {
|
|
65798
|
+
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}";
|
|
65799
|
+
if (!zip && o.out !== void 0) {
|
|
65800
|
+
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.`);
|
|
65801
|
+
}
|
|
65802
|
+
return fail(shapes);
|
|
65803
|
+
}
|
|
65406
65804
|
const appRoute = req.kind.startsWith("actions-") || req.kind.startsWith("release-");
|
|
65407
65805
|
if (appRoute) requireBrokerApp("api get");
|
|
65408
65806
|
const env = appRoute || process.env.MMI_ACTOR?.trim() !== "personal" ? brokerChildEnv() : void 0;
|
|
@@ -65423,6 +65821,28 @@ function registerApiReadCommands(program3, overrides) {
|
|
|
65423
65821
|
return fail(`api get: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
65424
65822
|
}
|
|
65425
65823
|
process.stdout.write(body);
|
|
65824
|
+
};
|
|
65825
|
+
const withApiGetOptions = (cmd) => cmd.option("--accept <type>", "response media type: json (default) or raw (file bytes)", (v) => {
|
|
65826
|
+
acceptHeaderFor(v);
|
|
65827
|
+
return v.trim().toLowerCase();
|
|
65828
|
+
}, "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)");
|
|
65829
|
+
const api = withApiGetOptions(
|
|
65830
|
+
program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents, actions runs/jobs/artifacts, releases)")
|
|
65831
|
+
).argument("[path]", "allowlisted GET path \u2014 shorthand for `api get <path>` (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, _o, cmd) => {
|
|
65832
|
+
if (req === void 0) {
|
|
65833
|
+
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}.`);
|
|
65834
|
+
}
|
|
65835
|
+
return runGet(req, cmd.opts());
|
|
65836
|
+
});
|
|
65837
|
+
withApiGetOptions(
|
|
65838
|
+
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")
|
|
65839
|
+
).argument("<path>", "allowlisted GET path (shapes named on refusal)", (v) => parseApiGetPath(v)).action(async (req, o, cmd) => {
|
|
65840
|
+
const parentOpts = cmd.parent?.opts() ?? {};
|
|
65841
|
+
return runGet(req, {
|
|
65842
|
+
accept: o.accept && o.accept !== "json" ? o.accept : parentOpts.accept ?? o.accept ?? "json",
|
|
65843
|
+
jq: o.jq ?? parentOpts.jq,
|
|
65844
|
+
out: o.out ?? parentOpts.out
|
|
65845
|
+
});
|
|
65426
65846
|
});
|
|
65427
65847
|
}
|
|
65428
65848
|
|
|
@@ -69029,6 +69449,7 @@ setGhWriteTokenProvider(() => {
|
|
|
69029
69449
|
}
|
|
69030
69450
|
return filingToken ??= mintFilingWriteToken(appActorDeps()).then((t) => t.token, () => void 0);
|
|
69031
69451
|
});
|
|
69452
|
+
setClaimWriteTokenProvider(() => mintFilingWriteToken(appActorDeps()).then((t) => t.token));
|
|
69032
69453
|
setStatusWriteTokenProvider(() => mintStatusWriteToken(appActorDeps()));
|
|
69033
69454
|
var registerPreAction = Reflect.get(program2, ["h", "ook"].join(""));
|
|
69034
69455
|
registerPreAction.call(program2, "preAction", async (_thisCommand, actionCommand) => {
|
|
@@ -69871,6 +70292,10 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
|
|
|
69871
70292
|
});
|
|
69872
70293
|
registerCiChecksWait(ci);
|
|
69873
70294
|
registerCancelOrphans(ci, async () => (await mintCommandWriteToken(appActorDeps(), "ci cancel-orphans")).token);
|
|
70295
|
+
registerRunCancel(
|
|
70296
|
+
program2.command("run").description("bounded workflow-run writes \u2014 `run cancel <id>` is the only one, and it rides the org App actions:write profile"),
|
|
70297
|
+
async () => (await mintCommandWriteToken(appActorDeps(), "run cancel")).token
|
|
70298
|
+
);
|
|
69874
70299
|
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
70300
|
if (o.apply && o.parkRuleset) {
|
|
69876
70301
|
return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");
|