@mutmutco/cli 4.2.5 → 4.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +476 -115
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5844,13 +5844,20 @@ async function waitForPrChecks(deps) {
|
|
|
5844
5844
|
failureStreak += 1;
|
|
5845
5845
|
if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
|
|
5846
5846
|
const diagnosis = deps.diagnoseFailure ? await deps.diagnoseFailure().catch(() => null) : null;
|
|
5847
|
+
const diagnosticCommand = diagnosis?.failedChecks.find((check) => check.diagnosticCommand)?.diagnosticCommand ?? deps.checksDiagnosticCommand;
|
|
5848
|
+
const failureReceipt = {
|
|
5849
|
+
failedChecks: diagnosis?.failedChecks ?? [],
|
|
5850
|
+
...deps.checksUrl ? { checksUrl: deps.checksUrl } : {},
|
|
5851
|
+
...diagnosticCommand ? { diagnosticCommand } : {},
|
|
5852
|
+
...diagnosis?.checksFailureDetail ? { checksFailureDetail: diagnosis.checksFailureDetail } : {}
|
|
5853
|
+
};
|
|
5847
5854
|
if (diagnosis?.cause === "stale-head") {
|
|
5848
|
-
return { policy, status: "failure", reason: diagnosis.reason, detail: "stale-head" };
|
|
5855
|
+
return { policy, status: "failure", reason: diagnosis.reason, detail: "stale-head", ...failureReceipt };
|
|
5849
5856
|
}
|
|
5850
5857
|
if (diagnosis?.cause === "runner-infra") {
|
|
5851
|
-
return { policy, status: "failure", reason: diagnosis.reason, detail: "runner-infra" };
|
|
5858
|
+
return { policy, status: "failure", reason: diagnosis.reason, detail: "runner-infra", ...failureReceipt };
|
|
5852
5859
|
}
|
|
5853
|
-
return { policy, status: "failure", reason, detail: "checks-failure" };
|
|
5860
|
+
return { policy, status: "failure", reason, detail: "checks-failure", ...failureReceipt };
|
|
5854
5861
|
}
|
|
5855
5862
|
lastDetail = "confirming-failure";
|
|
5856
5863
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
@@ -9340,6 +9347,27 @@ function boardConfigFromProject(meta, floor = {}) {
|
|
|
9340
9347
|
};
|
|
9341
9348
|
}
|
|
9342
9349
|
|
|
9350
|
+
// src/issue-ref.ts
|
|
9351
|
+
var ISSUE_REF_SHAPES = "123, #123, owner/repo#123, or a GitHub issue or PR URL";
|
|
9352
|
+
function invalidReference(ref) {
|
|
9353
|
+
return new Error(`invalid reference "${ref}" \u2014 expected ${ISSUE_REF_SHAPES}`);
|
|
9354
|
+
}
|
|
9355
|
+
function parseIssueRef(ref, expectedRepo) {
|
|
9356
|
+
const trimmed = ref.trim();
|
|
9357
|
+
const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)$/i);
|
|
9358
|
+
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
9359
|
+
const bare = trimmed.match(/^#?(\d+)$/);
|
|
9360
|
+
const match = url ?? qualified ?? bare;
|
|
9361
|
+
if (!match) throw invalidReference(ref);
|
|
9362
|
+
const number = Number(match[match.length - 1]);
|
|
9363
|
+
if (!Number.isInteger(number) || number <= 0) throw invalidReference(ref);
|
|
9364
|
+
const repo = (url ?? qualified)?.[1];
|
|
9365
|
+
if (repo && expectedRepo && repo.toLowerCase() !== expectedRepo.toLowerCase()) {
|
|
9366
|
+
throw new Error(`reference "${ref}" names ${repo}, which does not match --repo ${expectedRepo} \u2014 expected ${ISSUE_REF_SHAPES}`);
|
|
9367
|
+
}
|
|
9368
|
+
return repo ? { repo, number } : { number };
|
|
9369
|
+
}
|
|
9370
|
+
|
|
9343
9371
|
// src/repo-resolve.ts
|
|
9344
9372
|
function slugOf(repoOrSlug) {
|
|
9345
9373
|
return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
|
|
@@ -9349,11 +9377,11 @@ function repoFromRemoteUrl(remoteUrl) {
|
|
|
9349
9377
|
return m ? `${m[1]}/${m[2]}` : void 0;
|
|
9350
9378
|
}
|
|
9351
9379
|
function repoFromSelector(selector) {
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9380
|
+
try {
|
|
9381
|
+
return parseIssueRef(selector).repo;
|
|
9382
|
+
} catch {
|
|
9383
|
+
return void 0;
|
|
9384
|
+
}
|
|
9357
9385
|
}
|
|
9358
9386
|
async function resolveRepo(repo) {
|
|
9359
9387
|
if (repo) return repo;
|
|
@@ -11802,10 +11830,10 @@ var rollout_plan_default = {
|
|
|
11802
11830
|
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)."
|
|
11803
11831
|
},
|
|
11804
11832
|
baseline: {
|
|
11805
|
-
version: "4.2.
|
|
11806
|
-
tag: "v4.2.
|
|
11807
|
-
commit: "
|
|
11808
|
-
npm: "@mutmutco/cli@4.2.
|
|
11833
|
+
version: "4.2.7",
|
|
11834
|
+
tag: "v4.2.7",
|
|
11835
|
+
commit: "bb37250e186c",
|
|
11836
|
+
npm: "@mutmutco/cli@4.2.7"
|
|
11809
11837
|
},
|
|
11810
11838
|
exitCriterion: "fleet-n-of-n",
|
|
11811
11839
|
hubOnlyShortcut: "forbidden",
|
|
@@ -11822,14 +11850,14 @@ var rollout_plan_default = {
|
|
|
11822
11850
|
repo: "mutmutco/mmi-hub",
|
|
11823
11851
|
role: "canary",
|
|
11824
11852
|
schedule: "train",
|
|
11825
|
-
v3Target: "v4.2.
|
|
11853
|
+
v3Target: "v4.2.7"
|
|
11826
11854
|
}
|
|
11827
11855
|
],
|
|
11828
11856
|
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.",
|
|
11829
11857
|
rollback: {
|
|
11830
11858
|
independent: true,
|
|
11831
|
-
mechanism: "npm dist-tag latest -> 4.2.
|
|
11832
|
-
v3Target: "v4.2.
|
|
11859
|
+
mechanism: "npm dist-tag latest -> 4.2.7 and redeploy the Hub Lambda from tag v4.2.7 (bb37250e186c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
11860
|
+
v3Target: "v4.2.7 (@mutmutco/cli@4.2.7, tag commit bb37250e186c \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
11833
11861
|
}
|
|
11834
11862
|
},
|
|
11835
11863
|
{
|
|
@@ -12136,16 +12164,6 @@ function formatOrgVersions(report) {
|
|
|
12136
12164
|
}
|
|
12137
12165
|
|
|
12138
12166
|
// src/sub-issue.ts
|
|
12139
|
-
function parseIssueRef(ref) {
|
|
12140
|
-
const trimmed = ref.trim();
|
|
12141
|
-
const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/i);
|
|
12142
|
-
if (url) return { repo: url[1], number: Number(url[2]) };
|
|
12143
|
-
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
12144
|
-
if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
|
|
12145
|
-
const bare = trimmed.match(/^#?(\d+)$/);
|
|
12146
|
-
if (bare) return { number: Number(bare[1]) };
|
|
12147
|
-
throw new Error(`invalid issue reference "${ref}" \u2014 expected #123, 123, owner/repo#123, or an issue URL`);
|
|
12148
|
-
}
|
|
12149
12167
|
function buildResolveIdArgs(ref) {
|
|
12150
12168
|
const args = ["issue", "view", String(ref.number), "--json", "id", "--jq", ".id"];
|
|
12151
12169
|
if (ref.repo) args.push("--repo", ref.repo);
|
|
@@ -14855,19 +14873,19 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
|
|
|
14855
14873
|
}
|
|
14856
14874
|
return [...contexts];
|
|
14857
14875
|
}
|
|
14858
|
-
async function
|
|
14859
|
-
const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", target, "--head", "main", "--state", "
|
|
14876
|
+
async function findAlignmentPr(deps, ctx, target) {
|
|
14877
|
+
const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", target, "--head", "main", "--state", "all", "--json", "number,url,state"]));
|
|
14860
14878
|
if (!out) return void 0;
|
|
14861
14879
|
const rows = JSON.parse(out);
|
|
14862
|
-
const row = rows.
|
|
14863
|
-
return row
|
|
14880
|
+
const row = rows.filter((r) => typeof r.number === "number" && typeof r.url === "string" && (r.state === "OPEN" || r.state === "MERGED")).sort((a, b) => b.number - a.number)[0];
|
|
14881
|
+
return row;
|
|
14864
14882
|
}
|
|
14865
14883
|
function parsePrNumber(url) {
|
|
14866
14884
|
const n = Number.parseInt(url.split("/").pop() ?? "", 10);
|
|
14867
14885
|
return Number.isFinite(n) ? n : void 0;
|
|
14868
14886
|
}
|
|
14869
14887
|
async function rollDevelopmentForward(deps, ctx, tag) {
|
|
14870
|
-
await runGitRemoteRead(deps, ["fetch", "origin", "main"]);
|
|
14888
|
+
await runGitRemoteRead(deps, ["fetch", "origin", "main", "development"]);
|
|
14871
14889
|
const required = await discoverRequiredCheckContexts(deps, ctx, "development");
|
|
14872
14890
|
if (required.length === 0) {
|
|
14873
14891
|
await deps.run("git", ["checkout", "development"]);
|
|
@@ -14876,13 +14894,17 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
14876
14894
|
await deps.run("git", ["push", "origin", "development"]);
|
|
14877
14895
|
return { status: "pushed", note: "development rolled forward to the released main (development has no required checks)" };
|
|
14878
14896
|
}
|
|
14879
|
-
const existing = await findOpenAlignmentPr(deps, ctx, "development");
|
|
14880
|
-
if (existing) {
|
|
14881
|
-
return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `alignment PR already open: ${existing.url}`);
|
|
14882
|
-
}
|
|
14883
14897
|
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..origin/main"]));
|
|
14898
|
+
const existing = await findAlignmentPr(deps, ctx, "development");
|
|
14884
14899
|
if (ahead === "0") {
|
|
14885
|
-
return {
|
|
14900
|
+
return {
|
|
14901
|
+
status: "aligned",
|
|
14902
|
+
alignment: "already-merged",
|
|
14903
|
+
note: existing?.state === "MERGED" ? `alignment: already-merged \u2014 development already contains main via alignment PR #${existing.number}` : "alignment: already-merged \u2014 development already contains the released main; nothing to roll forward"
|
|
14904
|
+
};
|
|
14905
|
+
}
|
|
14906
|
+
if (existing?.state === "OPEN") {
|
|
14907
|
+
return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `alignment PR already open: ${existing.url}`);
|
|
14886
14908
|
}
|
|
14887
14909
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
14888
14910
|
|
|
@@ -14919,7 +14941,7 @@ async function enqueueAlignmentAutoMerge(deps, ctx, prNumber, prUrl, openedNote)
|
|
|
14919
14941
|
return { ...base, note: manual };
|
|
14920
14942
|
}
|
|
14921
14943
|
async function alignRcForward(deps, ctx, tag) {
|
|
14922
|
-
await runGitRemoteRead(deps, ["fetch", "origin", "main"]);
|
|
14944
|
+
await runGitRemoteRead(deps, ["fetch", "origin", "main", "rc"]);
|
|
14923
14945
|
const required = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
14924
14946
|
if (required.length === 0) {
|
|
14925
14947
|
const ahead2 = clean2(await deps.run("git", ["rev-list", "--count", "origin/rc..origin/main"]));
|
|
@@ -14929,13 +14951,17 @@ async function alignRcForward(deps, ctx, tag) {
|
|
|
14929
14951
|
await runGitPush(deps, ["push", "origin", "origin/main:refs/heads/rc"]);
|
|
14930
14952
|
return { status: "pushed", note: "rc aligned to the released main (rc has no required checks)" };
|
|
14931
14953
|
}
|
|
14932
|
-
const existing = await findOpenAlignmentPr(deps, ctx, "rc");
|
|
14933
|
-
if (existing) {
|
|
14934
|
-
return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `rc alignment PR already open: ${existing.url}`);
|
|
14935
|
-
}
|
|
14936
14954
|
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/rc..origin/main"]));
|
|
14955
|
+
const existing = await findAlignmentPr(deps, ctx, "rc");
|
|
14937
14956
|
if (ahead === "0") {
|
|
14938
|
-
return {
|
|
14957
|
+
return {
|
|
14958
|
+
status: "aligned",
|
|
14959
|
+
alignment: "already-merged",
|
|
14960
|
+
note: existing?.state === "MERGED" ? `alignment: already-merged \u2014 rc already contains main via alignment PR #${existing.number}` : "alignment: already-merged \u2014 rc already contains the released main; nothing to align"
|
|
14961
|
+
};
|
|
14962
|
+
}
|
|
14963
|
+
if (existing?.state === "OPEN") {
|
|
14964
|
+
return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `rc alignment PR already open: ${existing.url}`);
|
|
14939
14965
|
}
|
|
14940
14966
|
const body = `Carries the ${tag} release from \`main\` back to \`rc\`.
|
|
14941
14967
|
|
|
@@ -15847,16 +15873,42 @@ function resolveGreenGateNpmFromGateWorkflows(files) {
|
|
|
15847
15873
|
return pin ? bundledNpmForNodePin(pin) : void 0;
|
|
15848
15874
|
}
|
|
15849
15875
|
function parsePublishWorkflowNpmPin(workflowBody) {
|
|
15850
|
-
const match = /(?:^|[^\w])npm@(\d
|
|
15876
|
+
const match = /(?:^|[^\w])npm@(\d+(?:\.\d+\.\d+)?)/m.exec(workflowBody);
|
|
15851
15877
|
return match?.[1];
|
|
15852
15878
|
}
|
|
15879
|
+
function parseGateNpmVersionPins(workflowBody) {
|
|
15880
|
+
if (!/runner-node-toolchain/.test(workflowBody)) return [];
|
|
15881
|
+
return [...workflowBody.matchAll(/^\s*npm-version:\s*['"]?(\d+(?:\.\d+\.\d+)?)/gm)].map((m) => m[1]);
|
|
15882
|
+
}
|
|
15883
|
+
function parseGateNpmInstallPins(workflowBody) {
|
|
15884
|
+
return [...workflowBody.matchAll(/npm\s+install\s+-g\s+npm@(\d+(?:\.\d+\.\d+)?)/g)].map((m) => m[1]);
|
|
15885
|
+
}
|
|
15886
|
+
function gateDeclaredNpmPins(gateFiles) {
|
|
15887
|
+
const files = gateFiles ?? [];
|
|
15888
|
+
return [
|
|
15889
|
+
...files.flatMap((file) => parseGateNpmVersionPins(file.body)),
|
|
15890
|
+
...files.flatMap((file) => parseGateNpmInstallPins(file.body))
|
|
15891
|
+
];
|
|
15892
|
+
}
|
|
15893
|
+
function publishWorkflowNpm(files) {
|
|
15894
|
+
const publish = files?.find((f) => /(?:^|\/)publish\.ya?ml$/i.test(f.path));
|
|
15895
|
+
return publish ? parsePublishWorkflowNpmPin(publish.body) : void 0;
|
|
15896
|
+
}
|
|
15853
15897
|
function resolveExpectedCiNpmFromWorkflows(gateFiles, allFiles) {
|
|
15898
|
+
const declared = gateDeclaredNpmPins(gateFiles);
|
|
15899
|
+
if (declared.length) return declared[0];
|
|
15854
15900
|
const fromGate = resolveGreenGateNpmFromGateWorkflows(gateFiles);
|
|
15855
15901
|
if (fromGate) return fromGate;
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
15859
|
-
|
|
15902
|
+
return publishWorkflowNpm(allFiles ?? gateFiles);
|
|
15903
|
+
}
|
|
15904
|
+
function detectNpmPinDisagreement(gateFiles, allFiles) {
|
|
15905
|
+
const gateNpm = gateDeclaredNpmPins(gateFiles)[0];
|
|
15906
|
+
const publishNpm = publishWorkflowNpm(allFiles ?? gateFiles);
|
|
15907
|
+
if (!gateNpm || !publishNpm) return null;
|
|
15908
|
+
const gateMajor = npmMajor(gateNpm);
|
|
15909
|
+
const publishMajor = npmMajor(publishNpm);
|
|
15910
|
+
if (gateMajor === void 0 || publishMajor === void 0) return null;
|
|
15911
|
+
return gateMajor === publishMajor ? null : { gateNpm, publishNpm };
|
|
15860
15912
|
}
|
|
15861
15913
|
function npmMajor(version) {
|
|
15862
15914
|
const match = /^(\d+)/.exec(version.trim());
|
|
@@ -15871,7 +15923,7 @@ function assertNpmMajorPreflight(opts) {
|
|
|
15871
15923
|
if (localMajor === void 0 || expectedMajor === void 0) return;
|
|
15872
15924
|
if (localMajor === expectedMajor) return;
|
|
15873
15925
|
throw new Error(
|
|
15874
|
-
`${opts.repo}: Step 0a npm-major preflight failed \u2014 local npm ${localNpm} (major ${localMajor}) \u2260 CI npm ${expectedNpm} (major ${expectedMajor}). Supported repair (preserves Node): \`npm install -g npm@${expectedNpm}\`; then verify with \`node -v && npm -v\` and rerun the same release command. This is an npm-major mismatch (#5666/#5616/#4578), not a lockfile or publish-surface problem.
|
|
15926
|
+
`${opts.repo}: Step 0a npm-major preflight failed \u2014 local npm ${localNpm} (major ${localMajor}) \u2260 CI npm ${expectedNpm} (major ${expectedMajor}). Supported repair (preserves Node): \`npm install -g npm@${expectedNpm}\`; then verify with \`node -v && npm -v\` and rerun the same release command. This is an npm-major mismatch (#5666/#5616/#4578), not a lockfile or publish-surface problem. The expected npm is the gate's own declared npm \u2014 an \`npm-version:\` input or an explicit \`npm install -g npm@X\` step (#5893) \u2014 and only a gate that declares neither falls back to a bundled-npm mapping or the publish workflow's pin; otherwise compare \`npm -v\` with the green gate log's toolchain-preflight line (#3446).`
|
|
15875
15927
|
);
|
|
15876
15928
|
}
|
|
15877
15929
|
async function assertNpmMajorPreflightFromWorkflows(deps, repo) {
|
|
@@ -15883,6 +15935,12 @@ async function assertNpmMajorPreflightFromWorkflows(deps, repo) {
|
|
|
15883
15935
|
} catch {
|
|
15884
15936
|
return;
|
|
15885
15937
|
}
|
|
15938
|
+
const disagreement = detectNpmPinDisagreement(gateFiles, allFiles);
|
|
15939
|
+
if (disagreement) {
|
|
15940
|
+
(deps.warn ?? ((m) => console.warn(m)))(
|
|
15941
|
+
`${repo}: repo defect \u2014 the gate declares npm ${disagreement.gateNpm} while the publish workflow pins npm ${disagreement.publishNpm}. The gate's declaration wins this preflight, but the publish lane will still run npm ${disagreement.publishNpm}. Align both pins in one PR.`
|
|
15942
|
+
);
|
|
15943
|
+
}
|
|
15886
15944
|
assertNpmMajorPreflight({
|
|
15887
15945
|
repo,
|
|
15888
15946
|
localNpm,
|
|
@@ -20683,15 +20741,9 @@ function resolveBoardConfig(cfg) {
|
|
|
20683
20741
|
priorityOptions: cfg.priorityOptions
|
|
20684
20742
|
};
|
|
20685
20743
|
}
|
|
20686
|
-
function parseIssueSelector(selector, defaultRepo) {
|
|
20687
|
-
const
|
|
20688
|
-
|
|
20689
|
-
if (url) return { repo: url[1], number: Number(url[2]) };
|
|
20690
|
-
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
20691
|
-
if (qualified) return { repo: qualified[1], number: Number(qualified[2]) };
|
|
20692
|
-
const local = trimmed.match(/^#?(\d+)$/);
|
|
20693
|
-
if (local) return { repo: defaultRepo, number: Number(local[1]) };
|
|
20694
|
-
throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
|
|
20744
|
+
function parseIssueSelector(selector, defaultRepo, expectedRepo) {
|
|
20745
|
+
const parsed = parseIssueRef(selector, expectedRepo);
|
|
20746
|
+
return { repo: parsed.repo ?? defaultRepo, number: parsed.number };
|
|
20695
20747
|
}
|
|
20696
20748
|
function sameRepo(itemRepo, selectorKey) {
|
|
20697
20749
|
const repo = itemRepo.toLowerCase();
|
|
@@ -21016,7 +21068,7 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
21016
21068
|
const cfg = resolveBoardConfig(options.config);
|
|
21017
21069
|
const client = deps.client ?? defaultGitHubClient();
|
|
21018
21070
|
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
21019
|
-
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
21071
|
+
const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
|
|
21020
21072
|
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
21021
21073
|
const item = lookup.item;
|
|
21022
21074
|
if (!item) {
|
|
@@ -21090,7 +21142,7 @@ async function showBoardItem(options, deps = {}) {
|
|
|
21090
21142
|
const cfg = resolveBoardConfig(options.config);
|
|
21091
21143
|
const client = deps.client ?? defaultGitHubClient();
|
|
21092
21144
|
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
21093
|
-
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
21145
|
+
const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
|
|
21094
21146
|
const { item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
21095
21147
|
if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
21096
21148
|
if (item.contentType === "Issue") {
|
|
@@ -21181,6 +21233,15 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
21181
21233
|
}
|
|
21182
21234
|
const assignee = options.assignee ?? "@me";
|
|
21183
21235
|
const assignedLogin = assignee === "@me" ? report.viewer : assignee.replace(/^@/, "");
|
|
21236
|
+
const holder = {
|
|
21237
|
+
login: assignedLogin,
|
|
21238
|
+
session: ctx.session.session,
|
|
21239
|
+
surface: ctx.session.surface,
|
|
21240
|
+
host: ctx.session.host
|
|
21241
|
+
};
|
|
21242
|
+
let previousHolder;
|
|
21243
|
+
const claimedReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
|
|
21244
|
+
const heldReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
|
|
21184
21245
|
if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
|
|
21185
21246
|
const pre = evaluateClaim(flatItem, assignedLogin);
|
|
21186
21247
|
if (!pre.ok) throw new Error(pre.reason);
|
|
@@ -21191,20 +21252,28 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
21191
21252
|
if (!verdict.ok) throw new Error(verdict.reason);
|
|
21192
21253
|
item = fresh;
|
|
21193
21254
|
const refuseIfContested = async () => {
|
|
21194
|
-
if (options.force) return;
|
|
21195
21255
|
const contest = await checkLaneContest(client, item, ctx.session);
|
|
21196
|
-
if (contest.contested)
|
|
21256
|
+
if (!contest.contested) return;
|
|
21257
|
+
if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
|
|
21258
|
+
previousHolder = {
|
|
21259
|
+
login: assignedLogin,
|
|
21260
|
+
...contest.marker ? {
|
|
21261
|
+
session: contest.marker.session,
|
|
21262
|
+
surface: contest.marker.surface,
|
|
21263
|
+
host: contest.marker.host
|
|
21264
|
+
} : {}
|
|
21265
|
+
};
|
|
21197
21266
|
};
|
|
21198
21267
|
await refuseIfContested();
|
|
21199
21268
|
if (verdict.alreadyClaimed) {
|
|
21200
21269
|
if (options.check) {
|
|
21201
|
-
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true, checked: true };
|
|
21270
|
+
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
|
|
21202
21271
|
}
|
|
21203
21272
|
await postClaimMarkerComment(client, item, ctx.session);
|
|
21204
|
-
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, alreadyClaimed: true };
|
|
21273
|
+
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true };
|
|
21205
21274
|
}
|
|
21206
21275
|
if (options.check) {
|
|
21207
|
-
return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, checked: true };
|
|
21276
|
+
return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: false, ...claimedReceipt(), checked: true };
|
|
21208
21277
|
}
|
|
21209
21278
|
await refuseIfContested();
|
|
21210
21279
|
try {
|
|
@@ -21218,7 +21287,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
21218
21287
|
} catch (e) {
|
|
21219
21288
|
const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
|
|
21220
21289
|
if (!options.allowPartial) throw new Error(warning);
|
|
21221
|
-
return { item, viewer: report.viewer, repo: report.repo, status: "Todo", partial: true, warning };
|
|
21290
|
+
return { item, viewer: report.viewer, repo: report.repo, status: "Todo", partial: true, warning, ...claimedReceipt() };
|
|
21222
21291
|
}
|
|
21223
21292
|
return {
|
|
21224
21293
|
item: {
|
|
@@ -21230,13 +21299,14 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
21230
21299
|
viewer: report.viewer,
|
|
21231
21300
|
repo: report.repo,
|
|
21232
21301
|
status: "In Progress",
|
|
21233
|
-
partial: false
|
|
21302
|
+
partial: false,
|
|
21303
|
+
...claimedReceipt()
|
|
21234
21304
|
};
|
|
21235
21305
|
}
|
|
21236
21306
|
async function claimBoardIssue(options, deps = {}) {
|
|
21237
21307
|
const cfg = resolveBoardConfig(options.config);
|
|
21238
21308
|
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
|
|
21239
|
-
const selector = parseIssueSelector(options.selector, collected.repo);
|
|
21309
|
+
const selector = parseIssueSelector(options.selector, collected.repo, options.repo);
|
|
21240
21310
|
const ctx = await prepareClaimContext(options, [selector], deps, collected);
|
|
21241
21311
|
return claimOneBoardItem(ctx, selector, options);
|
|
21242
21312
|
}
|
|
@@ -21246,7 +21316,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
21246
21316
|
const selectors = [];
|
|
21247
21317
|
const seen = /* @__PURE__ */ new Set();
|
|
21248
21318
|
for (const raw of options.selectors) {
|
|
21249
|
-
const selector = parseIssueSelector(raw, collected.repo);
|
|
21319
|
+
const selector = parseIssueSelector(raw, collected.repo, options.repo);
|
|
21250
21320
|
const key = `${selector.repo.toLowerCase()}#${selector.number}`;
|
|
21251
21321
|
if (seen.has(key)) continue;
|
|
21252
21322
|
seen.add(key);
|
|
@@ -21262,7 +21332,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
21262
21332
|
const ref = `${selector.repo}#${selector.number}`;
|
|
21263
21333
|
try {
|
|
21264
21334
|
const result = await claimOneBoardItem(ctx, selector, options);
|
|
21265
|
-
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
21335
|
+
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
21266
21336
|
} catch (e) {
|
|
21267
21337
|
results[index] = { ref, claimed: false, reason: e.message };
|
|
21268
21338
|
}
|
|
@@ -21283,7 +21353,7 @@ async function moveBoardIssues(options, deps = {}) {
|
|
|
21283
21353
|
const selectors = [];
|
|
21284
21354
|
const seen = /* @__PURE__ */ new Set();
|
|
21285
21355
|
for (const raw of options.selectors) {
|
|
21286
|
-
const selector = parseIssueSelector(raw, currentRepo);
|
|
21356
|
+
const selector = parseIssueSelector(raw, currentRepo, options.repo);
|
|
21287
21357
|
const key = `${selector.repo.toLowerCase()}#${selector.number}`;
|
|
21288
21358
|
if (seen.has(key)) continue;
|
|
21289
21359
|
seen.add(key);
|
|
@@ -21363,7 +21433,7 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
21363
21433
|
const cfg = resolveBoardConfig(options.config);
|
|
21364
21434
|
const client = deps.client ?? defaultGitHubClient();
|
|
21365
21435
|
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
21366
|
-
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
21436
|
+
const selector = parseIssueSelector(options.selector, currentRepo, options.repo);
|
|
21367
21437
|
const { viewer, item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
21368
21438
|
if (!item) {
|
|
21369
21439
|
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
@@ -22485,6 +22555,20 @@ var REQUIRED_OPTION_HELP = {
|
|
|
22485
22555
|
optionDescription(option) {
|
|
22486
22556
|
const described = Help.prototype.optionDescription.call(this, option);
|
|
22487
22557
|
return option.mandatory ? `(required) ${described}` : described;
|
|
22558
|
+
},
|
|
22559
|
+
// #5874: resolveHouseShim removes the house token before Commander renders help. Its stock usage
|
|
22560
|
+
// therefore teaches a removed flat alias; render the manifest's canonical house-qualified path instead.
|
|
22561
|
+
commandUsage(command) {
|
|
22562
|
+
const parts = [];
|
|
22563
|
+
for (let node = command; node && node.parent; node = node.parent) {
|
|
22564
|
+
parts.unshift(node.name());
|
|
22565
|
+
}
|
|
22566
|
+
if (!parts.length) return Help.prototype.commandUsage.call(this, command);
|
|
22567
|
+
const flatPath = parts.join(" ");
|
|
22568
|
+
const canonicalPath = canonicalPathFor(flatPath) ?? flatPath;
|
|
22569
|
+
const alias = command.alias();
|
|
22570
|
+
const name = alias ? `${command.name()}|${alias}` : command.name();
|
|
22571
|
+
return `mmi-cli ${canonicalPath.slice(0, -command.name().length)}${name} ${command.usage()}`;
|
|
22488
22572
|
}
|
|
22489
22573
|
};
|
|
22490
22574
|
function classifyTree(command, path2, inherited, hideFromParent) {
|
|
@@ -31286,15 +31370,81 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
|
|
|
31286
31370
|
}
|
|
31287
31371
|
return confirmEnqueueOutcome();
|
|
31288
31372
|
}
|
|
31373
|
+
function cleanupGitArgs(cwd, args) {
|
|
31374
|
+
return cwd ? ["-C", cwd, ...args] : args;
|
|
31375
|
+
}
|
|
31289
31376
|
async function remoteBranchExists2(branch, options = {}) {
|
|
31290
31377
|
if (!branch) return void 0;
|
|
31291
31378
|
try {
|
|
31292
|
-
if (options.prune) await execFileP2("git", ["fetch", "origin", "--prune"], { timeout: GIT_TIMEOUT_MS });
|
|
31293
|
-
return (await execFileP2("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
|
|
31379
|
+
if (options.prune) await execFileP2("git", cleanupGitArgs(options.cwd, ["fetch", "origin", "--prune"]), { timeout: GIT_TIMEOUT_MS });
|
|
31380
|
+
return (await execFileP2("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads", "origin", branch]), { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
|
|
31294
31381
|
} catch {
|
|
31295
31382
|
return void 0;
|
|
31296
31383
|
}
|
|
31297
31384
|
}
|
|
31385
|
+
async function deleteMergedRemoteBranch(options) {
|
|
31386
|
+
const remediation = `git push origin --delete ${options.branch}`;
|
|
31387
|
+
if (!options.branch) {
|
|
31388
|
+
return {
|
|
31389
|
+
branch: options.branch,
|
|
31390
|
+
existedBefore: options.existedBefore,
|
|
31391
|
+
attempted: false,
|
|
31392
|
+
status: "failed",
|
|
31393
|
+
error: "missing PR head branch",
|
|
31394
|
+
remediation
|
|
31395
|
+
};
|
|
31396
|
+
}
|
|
31397
|
+
if (options.existedBefore === false) {
|
|
31398
|
+
const exists2 = await options.branchExists(options.branch);
|
|
31399
|
+
if (exists2 === false) return { branch: options.branch, existedBefore: false, attempted: false, status: "already-gone" };
|
|
31400
|
+
return {
|
|
31401
|
+
branch: options.branch,
|
|
31402
|
+
existedBefore: false,
|
|
31403
|
+
attempted: false,
|
|
31404
|
+
status: "failed",
|
|
31405
|
+
error: exists2 ? `origin reports ${options.branch} after merge` : `could not verify absence of origin/${options.branch}`,
|
|
31406
|
+
remediation
|
|
31407
|
+
};
|
|
31408
|
+
}
|
|
31409
|
+
try {
|
|
31410
|
+
await options.execGit(["push", "origin", "--delete", options.branch]);
|
|
31411
|
+
} catch (e) {
|
|
31412
|
+
const exists2 = await options.branchExists(options.branch);
|
|
31413
|
+
if (exists2 === false) {
|
|
31414
|
+
return {
|
|
31415
|
+
branch: options.branch,
|
|
31416
|
+
existedBefore: options.existedBefore,
|
|
31417
|
+
attempted: true,
|
|
31418
|
+
status: "deleted"
|
|
31419
|
+
};
|
|
31420
|
+
}
|
|
31421
|
+
return {
|
|
31422
|
+
branch: options.branch,
|
|
31423
|
+
existedBefore: options.existedBefore,
|
|
31424
|
+
attempted: true,
|
|
31425
|
+
status: "failed",
|
|
31426
|
+
error: e instanceof Error ? e.message : String(e),
|
|
31427
|
+
remediation
|
|
31428
|
+
};
|
|
31429
|
+
}
|
|
31430
|
+
const exists = await options.branchExists(options.branch);
|
|
31431
|
+
if (exists === false) {
|
|
31432
|
+
return {
|
|
31433
|
+
branch: options.branch,
|
|
31434
|
+
existedBefore: options.existedBefore,
|
|
31435
|
+
attempted: true,
|
|
31436
|
+
status: "deleted"
|
|
31437
|
+
};
|
|
31438
|
+
}
|
|
31439
|
+
return {
|
|
31440
|
+
branch: options.branch,
|
|
31441
|
+
existedBefore: options.existedBefore,
|
|
31442
|
+
attempted: true,
|
|
31443
|
+
status: "failed",
|
|
31444
|
+
error: exists ? `origin still reports ${options.branch} after deletion` : `could not verify deletion of origin/${options.branch}`,
|
|
31445
|
+
remediation
|
|
31446
|
+
};
|
|
31447
|
+
}
|
|
31298
31448
|
|
|
31299
31449
|
// src/worktree-merge-cleanup.ts
|
|
31300
31450
|
var import_node_fs36 = require("node:fs");
|
|
@@ -31484,6 +31634,7 @@ function removeResidueDirectory(wtPath) {
|
|
|
31484
31634
|
} catch {
|
|
31485
31635
|
}
|
|
31486
31636
|
(0, import_node_fs36.rmSync)(wtPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
31637
|
+
if ((0, import_node_fs36.existsSync)(wtPath)) return { ok: false, error: `directory remains after removal: ${wtPath}` };
|
|
31487
31638
|
return { ok: true };
|
|
31488
31639
|
} catch (e) {
|
|
31489
31640
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
@@ -31776,8 +31927,11 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
31776
31927
|
if (residue.ok) {
|
|
31777
31928
|
report.worktree.residue = "swept";
|
|
31778
31929
|
} else {
|
|
31930
|
+
report.worktree.status = "failed";
|
|
31931
|
+
report.worktree.reason = "residue-remains";
|
|
31779
31932
|
report.worktree.residue = "left";
|
|
31780
31933
|
report.worktree.residueError = residue.error;
|
|
31934
|
+
report.worktree.remediation = `Remove-Item -LiteralPath '${wtPath.replace(/'/g, "''")}' -Recurse -Force`;
|
|
31781
31935
|
}
|
|
31782
31936
|
}
|
|
31783
31937
|
try {
|
|
@@ -31808,7 +31962,7 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
31808
31962
|
lines.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
|
|
31809
31963
|
}
|
|
31810
31964
|
if (wt.residue === "left") {
|
|
31811
|
-
lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014
|
|
31965
|
+
lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
|
|
31812
31966
|
}
|
|
31813
31967
|
if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
|
|
31814
31968
|
lines.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
|
|
@@ -31863,8 +32017,24 @@ function registerBoardCommands(program3) {
|
|
|
31863
32017
|
return failGraceful(`board read failed: ${withDiscoverMissDetail(e.message)}`);
|
|
31864
32018
|
}
|
|
31865
32019
|
}
|
|
31866
|
-
function
|
|
31867
|
-
|
|
32020
|
+
function formatClaimHolder(holder) {
|
|
32021
|
+
const lane = holder.surface && holder.session && holder.host ? ` (${holder.surface}/${holder.session}@${holder.host})` : "";
|
|
32022
|
+
return `@${holder.login}${lane}`;
|
|
32023
|
+
}
|
|
32024
|
+
function claimVerdict(ref, result) {
|
|
32025
|
+
const holder = formatClaimHolder(result.holder);
|
|
32026
|
+
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
32027
|
+
if (result.checked) {
|
|
32028
|
+
if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
|
|
32029
|
+
if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
|
|
32030
|
+
return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
|
|
32031
|
+
}
|
|
32032
|
+
if (result.partial) {
|
|
32033
|
+
return result.outcome === "took-over" ? `Partially took over ${ref} from ${previousHolder}: ${result.warning}` : `Partially claimed ${ref} for ${holder}: ${result.warning}`;
|
|
32034
|
+
}
|
|
32035
|
+
if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
|
|
32036
|
+
if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
|
|
32037
|
+
return `Claimed ${ref} for ${holder} - In Progress`;
|
|
31868
32038
|
}
|
|
31869
32039
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
31870
32040
|
board.command("read", { isDefault: true }).description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
|
|
@@ -31886,7 +32056,7 @@ function registerBoardCommands(program3) {
|
|
|
31886
32056
|
});
|
|
31887
32057
|
if (!result.checked) invalidateStatuslineBoardCache();
|
|
31888
32058
|
if (o.json) return console.log(JSON.stringify(result));
|
|
31889
|
-
console.log(
|
|
32059
|
+
console.log(claimVerdict(result.item.ref, result));
|
|
31890
32060
|
} catch (e) {
|
|
31891
32061
|
if (refuseRateLimited(e, o.json)) return;
|
|
31892
32062
|
return failGraceful(`board claim failed: ${e.message}`);
|
|
@@ -31908,7 +32078,7 @@ function registerBoardCommands(program3) {
|
|
|
31908
32078
|
console.log(JSON.stringify(bulk.results));
|
|
31909
32079
|
} else {
|
|
31910
32080
|
for (const result of bulk.results) {
|
|
31911
|
-
console.log(result.claimed ?
|
|
32081
|
+
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
31912
32082
|
}
|
|
31913
32083
|
}
|
|
31914
32084
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
@@ -32004,7 +32174,7 @@ function registerBoardCommands(program3) {
|
|
|
32004
32174
|
}
|
|
32005
32175
|
try {
|
|
32006
32176
|
const defaultRepo = await resolveRepo(o.repo) ?? "";
|
|
32007
|
-
const selector = parseIssueSelector(issueRef, defaultRepo);
|
|
32177
|
+
const selector = parseIssueSelector(issueRef, defaultRepo, o.repo);
|
|
32008
32178
|
if (!selector.repo) {
|
|
32009
32179
|
return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
|
|
32010
32180
|
}
|
|
@@ -32249,7 +32419,7 @@ async function fetchRestClosingGuardPayload(prNumber, repo, gh = defaultGhApi) {
|
|
|
32249
32419
|
return { pr: pr2, commits };
|
|
32250
32420
|
}
|
|
32251
32421
|
async function fetchHeadCheckRuns(headSha, repo, gh) {
|
|
32252
|
-
const runsOut = await gh(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id}"]);
|
|
32422
|
+
const runsOut = await gh(["--paginate", `repos/${repo}/commits/${headSha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app_id: .app.id, details_url}"]);
|
|
32253
32423
|
return dedupeLatestCheckRuns(parseNdjsonLines(runsOut));
|
|
32254
32424
|
}
|
|
32255
32425
|
async function fetchHeadCheckEntries(headSha, repo, gh) {
|
|
@@ -32321,6 +32491,29 @@ var RUNNER_INFRA_PRESTART_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
|
32321
32491
|
"cancelled",
|
|
32322
32492
|
"startup_failure"
|
|
32323
32493
|
]);
|
|
32494
|
+
var BARE_CHECKS_FAILURE_MESSAGE = "GitHub reported a checks failure but exposed no per-check data.";
|
|
32495
|
+
function actionRunIdFromDetailsUrl(detailsUrl) {
|
|
32496
|
+
const match = detailsUrl?.match(/\/actions\/runs\/(\d+)(?:\/|$)/);
|
|
32497
|
+
if (!match) return void 0;
|
|
32498
|
+
const runId = Number(match[1]);
|
|
32499
|
+
return Number.isSafeInteger(runId) ? runId : void 0;
|
|
32500
|
+
}
|
|
32501
|
+
function failedCheckReceipt(run, repo) {
|
|
32502
|
+
const name = run.name ?? `check-run ${run.id ?? "unknown"}`;
|
|
32503
|
+
const conclusion = run.conclusion ?? "unknown";
|
|
32504
|
+
const detailsUrl = typeof run.details_url === "string" && run.details_url ? run.details_url : void 0;
|
|
32505
|
+
const runId = actionRunIdFromDetailsUrl(detailsUrl);
|
|
32506
|
+
return {
|
|
32507
|
+
name,
|
|
32508
|
+
conclusion,
|
|
32509
|
+
...detailsUrl ? { detailsUrl } : {},
|
|
32510
|
+
...runId !== void 0 ? {
|
|
32511
|
+
runId,
|
|
32512
|
+
runUrl: `https://github.com/${repo}/actions/runs/${runId}`,
|
|
32513
|
+
diagnosticCommand: `gh run view ${runId} --log-failed --repo ${repo}`
|
|
32514
|
+
} : {}
|
|
32515
|
+
};
|
|
32516
|
+
}
|
|
32324
32517
|
function isErrorAnnotation(a) {
|
|
32325
32518
|
const level = a.annotation_level?.toLowerCase();
|
|
32326
32519
|
return level === "failure" || level === "error";
|
|
@@ -32353,6 +32546,7 @@ function classifyFailedChecks(failing) {
|
|
|
32353
32546
|
cause: "runner-infra",
|
|
32354
32547
|
infraFailures,
|
|
32355
32548
|
otherFailures,
|
|
32549
|
+
failedChecks: [],
|
|
32356
32550
|
reason: `${infraFailures.length} check(s) failed on RUNNER INFRASTRUCTURE, not your diff: ${infraFailures.join(", ")}. No test failed \u2014 the shared runner hit a wall-clock budget kill, ran out of disk, was missing a toolchain, could not acquire an mmi-live lane, or Actions returned Service Unavailable before checkout (the check annotation / conclusion names which). Re-run once the runner drains/heals (gh run rerun --failed); do not debug the diff.`
|
|
32357
32551
|
};
|
|
32358
32552
|
}
|
|
@@ -32360,6 +32554,7 @@ function classifyFailedChecks(failing) {
|
|
|
32360
32554
|
cause: "checks-failure",
|
|
32361
32555
|
infraFailures,
|
|
32362
32556
|
otherFailures,
|
|
32557
|
+
failedChecks: [],
|
|
32363
32558
|
reason: infraFailures.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${infraFailures.join(", ")} failed on runner infrastructure, not a test.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
|
|
32364
32559
|
};
|
|
32365
32560
|
}
|
|
@@ -32380,6 +32575,7 @@ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
|
|
|
32380
32575
|
cause: "stale-head",
|
|
32381
32576
|
infraFailures: [],
|
|
32382
32577
|
otherFailures: [],
|
|
32578
|
+
failedChecks: [],
|
|
32383
32579
|
reason: `the PR head (${snapshot.headSha.slice(0, 7)}) is BEHIND the branch tip (${tip.sha.slice(0, 7)}) \u2014 GitHub likely dropped a \`synchronize\` event, so the reported red is a superseded commit and NO run exists for your current push. This is not your diff failing. Re-push (an empty commit is enough) or close and reopen the PR to re-sync the head and trigger a fresh run.`
|
|
32384
32580
|
}
|
|
32385
32581
|
};
|
|
@@ -32392,15 +32588,20 @@ async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
|
|
|
32392
32588
|
return { state: "failed", error: `check-runs read failed for ${snapshot.headSha.slice(0, 7)} on ${repo}: ${readErrorText(e)}` };
|
|
32393
32589
|
}
|
|
32394
32590
|
if (!failing.length) return { state: "absent", reason: "no failing check-run on the current head to diagnose" };
|
|
32591
|
+
const failedChecks = failing.map((run) => failedCheckReceipt(run, repo));
|
|
32395
32592
|
try {
|
|
32396
32593
|
const annotated = await Promise.all(failing.map(async (run) => ({
|
|
32397
32594
|
name: run.name ?? `check-run ${run.id}`,
|
|
32398
32595
|
conclusion: run.conclusion ?? null,
|
|
32399
32596
|
annotations: JSON.parse(await gh([`repos/${repo}/check-runs/${run.id}/annotations`]))
|
|
32400
32597
|
})));
|
|
32401
|
-
return { state: "ok", diagnosis: classifyFailedChecks(annotated) };
|
|
32598
|
+
return { state: "ok", diagnosis: { ...classifyFailedChecks(annotated), failedChecks } };
|
|
32402
32599
|
} catch (e) {
|
|
32403
|
-
return {
|
|
32600
|
+
return {
|
|
32601
|
+
state: "failed",
|
|
32602
|
+
error: `annotations read failed for a failing check-run on ${repo}: ${readErrorText(e)}`,
|
|
32603
|
+
failedChecks
|
|
32604
|
+
};
|
|
32404
32605
|
}
|
|
32405
32606
|
}
|
|
32406
32607
|
function isNotFoundError2(e) {
|
|
@@ -33240,6 +33441,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
33240
33441
|
(opts, args) => ({ command: "issue edit", ref: args[0], fields: Object.keys(opts).filter((k) => k !== "repo" && opts[k] !== void 0) })
|
|
33241
33442
|
).action(async (ref, o) => {
|
|
33242
33443
|
try {
|
|
33444
|
+
parseIssueRef(ref, o.repo);
|
|
33243
33445
|
const defaultRepo = await resolveRepo(o.repo);
|
|
33244
33446
|
const result = await editIssue(defaultGitHubClient(), {
|
|
33245
33447
|
ref,
|
|
@@ -33284,6 +33486,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
33284
33486
|
).action(async (ref, o) => {
|
|
33285
33487
|
const parsed = parseCloseReason(o.reason);
|
|
33286
33488
|
try {
|
|
33489
|
+
parseIssueRef(ref, o.repo);
|
|
33287
33490
|
const defaultRepo = await resolveRepo(o.repo);
|
|
33288
33491
|
const result = await closeIssue(defaultGitHubClient(), {
|
|
33289
33492
|
ref,
|
|
@@ -33303,6 +33506,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
33303
33506
|
(_opts, args) => ({ command: "issue reopen", ref: args[0] })
|
|
33304
33507
|
).action(async (ref, o) => {
|
|
33305
33508
|
try {
|
|
33509
|
+
parseIssueRef(ref, o.repo);
|
|
33306
33510
|
const defaultRepo = await resolveRepo(o.repo);
|
|
33307
33511
|
const result = await reopenIssue(defaultGitHubClient(), ref, defaultRepo);
|
|
33308
33512
|
console.log(JSON.stringify(result));
|
|
@@ -33315,6 +33519,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
33315
33519
|
(_opts, args) => ({ command: "issue assign", ref: args[0], login: args[1] })
|
|
33316
33520
|
).action(async (ref, login, o) => {
|
|
33317
33521
|
try {
|
|
33522
|
+
parseIssueRef(ref, o.repo);
|
|
33318
33523
|
const defaultRepo = await resolveRepo(o.repo);
|
|
33319
33524
|
const result = await assignIssue(defaultGitHubClient(), ref, login, defaultRepo);
|
|
33320
33525
|
console.log(JSON.stringify(result));
|
|
@@ -33327,6 +33532,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
33327
33532
|
(_opts, args) => ({ command: "issue unassign", ref: args[0], login: args[1] })
|
|
33328
33533
|
).action(async (ref, login, o) => {
|
|
33329
33534
|
try {
|
|
33535
|
+
parseIssueRef(ref, o.repo);
|
|
33330
33536
|
const defaultRepo = await resolveRepo(o.repo);
|
|
33331
33537
|
const result = await unassignIssue(defaultGitHubClient(), ref, login, defaultRepo);
|
|
33332
33538
|
console.log(JSON.stringify(result));
|
|
@@ -33339,6 +33545,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
33339
33545
|
(opts, args) => ({ command: "issue relocate", ref: args[0], to: opts.to })
|
|
33340
33546
|
).action(async (ref, o) => {
|
|
33341
33547
|
try {
|
|
33548
|
+
parseIssueRef(ref, o.repo);
|
|
33342
33549
|
const defaultRepo = await resolveRepo(o.repo);
|
|
33343
33550
|
const result = await relocateIssue(defaultGitHubClient(), ref, o.to, defaultRepo);
|
|
33344
33551
|
console.log(JSON.stringify(result));
|
|
@@ -33350,8 +33557,10 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
33350
33557
|
issue2.command("unlink-child <parent> <child>").description("unlink a child issue from its parent (inverse of link-child) and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)"),
|
|
33351
33558
|
(_opts, args) => ({ command: "issue unlink-child", parent: args[0], child: args[1] })
|
|
33352
33559
|
).action(async (parentRef, childRef, o) => {
|
|
33353
|
-
const defaultRepo = await resolveRepo(o.repo);
|
|
33354
33560
|
try {
|
|
33561
|
+
parseIssueRef(parentRef, o.repo);
|
|
33562
|
+
parseIssueRef(childRef, o.repo);
|
|
33563
|
+
const defaultRepo = await resolveRepo(o.repo);
|
|
33355
33564
|
const result = await unlinkSubIssue(ghRunner, parentRef, childRef, defaultRepo);
|
|
33356
33565
|
console.log(JSON.stringify(result));
|
|
33357
33566
|
} catch (e) {
|
|
@@ -34985,7 +35194,7 @@ function registerLearningPickupCommand(program3) {
|
|
|
34985
35194
|
let selector;
|
|
34986
35195
|
try {
|
|
34987
35196
|
if (ref) {
|
|
34988
|
-
selector = parseIssueSelector(ref, defaultRepo);
|
|
35197
|
+
selector = parseIssueSelector(ref, defaultRepo, o.repo);
|
|
34989
35198
|
} else if (o.number) {
|
|
34990
35199
|
selector = { repo: defaultRepo, number: Number.parseInt(o.number, 10) };
|
|
34991
35200
|
if (!Number.isFinite(selector.number)) throw new Error("invalid --number");
|
|
@@ -39659,7 +39868,7 @@ withExamples(mutating(
|
|
|
39659
39868
|
repo: targetRepo2,
|
|
39660
39869
|
labels: extraLabels.length ? extraLabels : void 0
|
|
39661
39870
|
});
|
|
39662
|
-
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
39871
|
+
if (o.parent !== void 0) parseIssueRef(o.parent, o.repo);
|
|
39663
39872
|
} catch (e) {
|
|
39664
39873
|
return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
39665
39874
|
}
|
|
@@ -39741,10 +39950,15 @@ async function readParentField(number, repo) {
|
|
|
39741
39950
|
}
|
|
39742
39951
|
return resolveParentField(payload);
|
|
39743
39952
|
}
|
|
39744
|
-
issue.command("view <
|
|
39745
|
-
|
|
39746
|
-
|
|
39747
|
-
|
|
39953
|
+
issue.command("view <ref>").aliases(["show", "get"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show` and `get` alias view (#5483/#5706)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
|
|
39954
|
+
let parsed;
|
|
39955
|
+
try {
|
|
39956
|
+
parsed = parseIssueRef(ref, o.repo);
|
|
39957
|
+
} catch (e) {
|
|
39958
|
+
return fail(`issue view: ${e.message}`);
|
|
39959
|
+
}
|
|
39960
|
+
const n = parsed.number;
|
|
39961
|
+
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
39748
39962
|
if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
|
|
39749
39963
|
const fields = normalizeIssueViewJsonFields(o.json);
|
|
39750
39964
|
try {
|
|
@@ -39804,12 +40018,14 @@ issue.command("discover-related").description("find related issues for an existi
|
|
|
39804
40018
|
jsonParity(issue.command("link-child <parent> <child>").description("link an existing issue as a native sub-issue of a parent and print {parentNumber,subIssueNumber,totalCount} JSON").option("--repo <owner/repo>", "repo for bare refs on either side (defaults to the current repo)")).action(async (parentRef, childRef, o) => {
|
|
39805
40019
|
const defaultRepo = await resolveRepo(o.repo);
|
|
39806
40020
|
try {
|
|
40021
|
+
parseIssueRef(parentRef, o.repo);
|
|
40022
|
+
parseIssueRef(childRef, o.repo);
|
|
39807
40023
|
const result = await linkSubIssue(ghRunner2, parentRef, childRef, defaultRepo);
|
|
39808
40024
|
console.log(JSON.stringify(result));
|
|
39809
40025
|
} catch (e) {
|
|
39810
40026
|
let conflict;
|
|
39811
40027
|
try {
|
|
39812
|
-
const child2 = parseIssueRef(childRef);
|
|
40028
|
+
const child2 = parseIssueRef(childRef, o.repo);
|
|
39813
40029
|
const childRepo = child2.repo ?? defaultRepo;
|
|
39814
40030
|
if (childRepo) conflict = await classifyReparentFailure(e, ghRunner2, childRepo, child2.number, parentRef);
|
|
39815
40031
|
} catch {
|
|
@@ -39824,7 +40040,7 @@ jsonParity(issue.command("link-child <parent> <child>").description("link an exi
|
|
|
39824
40040
|
jsonParity(issue.command("comment <ref>").description("post a Markdown comment to an issue 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)")).action(async (ref, o) => {
|
|
39825
40041
|
let parsed;
|
|
39826
40042
|
try {
|
|
39827
|
-
parsed = parseIssueRef(ref);
|
|
40043
|
+
parsed = parseIssueRef(ref, o.repo);
|
|
39828
40044
|
} catch (e) {
|
|
39829
40045
|
return fail(`issue comment: ${e.message}`);
|
|
39830
40046
|
}
|
|
@@ -39847,7 +40063,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
|
|
|
39847
40063
|
jsonParity(issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2014 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
|
|
39848
40064
|
let parsed;
|
|
39849
40065
|
try {
|
|
39850
|
-
parsed = parseIssueRef(ref);
|
|
40066
|
+
parsed = parseIssueRef(ref, o.repo);
|
|
39851
40067
|
} catch (e) {
|
|
39852
40068
|
return fail(`issue check: ${e.message}`);
|
|
39853
40069
|
}
|
|
@@ -40034,10 +40250,15 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
40034
40250
|
"Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
|
|
40035
40251
|
"Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files."
|
|
40036
40252
|
]);
|
|
40037
|
-
pr.command("view <
|
|
40038
|
-
|
|
40039
|
-
|
|
40040
|
-
|
|
40253
|
+
pr.command("view <ref>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) \u2014 read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (ref, o) => {
|
|
40254
|
+
let parsed;
|
|
40255
|
+
try {
|
|
40256
|
+
parsed = parseIssueRef(ref, o.repo);
|
|
40257
|
+
} catch (e) {
|
|
40258
|
+
return fail(`pr view: ${e.message}`);
|
|
40259
|
+
}
|
|
40260
|
+
const n = parsed.number;
|
|
40261
|
+
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
40041
40262
|
if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
|
|
40042
40263
|
const defaultPrFields = "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
|
|
40043
40264
|
const effective = resolvePrViewFields(o.json, defaultPrFields);
|
|
@@ -40146,13 +40367,44 @@ async function waitLoopCorePool(label) {
|
|
|
40146
40367
|
async function waitLoopDiagnosis(label, prNumber, repo) {
|
|
40147
40368
|
const read = await diagnoseFailedRestChecks(prNumber, repo);
|
|
40148
40369
|
if (read.state === "ok") return read.diagnosis;
|
|
40370
|
+
if (read.state === "absent") {
|
|
40371
|
+
return {
|
|
40372
|
+
cause: "checks-failure",
|
|
40373
|
+
infraFailures: [],
|
|
40374
|
+
otherFailures: [],
|
|
40375
|
+
failedChecks: [],
|
|
40376
|
+
checksFailureDetail: BARE_CHECKS_FAILURE_MESSAGE,
|
|
40377
|
+
reason: BARE_CHECKS_FAILURE_MESSAGE
|
|
40378
|
+
};
|
|
40379
|
+
}
|
|
40149
40380
|
if (read.state === "failed") {
|
|
40150
40381
|
console.warn(
|
|
40151
40382
|
`${label}: CI failure diagnosis read FAILED (${read.error}) \u2014 the red verdict stands UNDIAGNOSED; this is not evidence the failure is a real test failure. Re-read it with \`gh pr checks ${prNumber} --repo ${repo}\`.`
|
|
40152
40383
|
);
|
|
40384
|
+
return {
|
|
40385
|
+
cause: "checks-failure",
|
|
40386
|
+
infraFailures: [],
|
|
40387
|
+
otherFailures: [],
|
|
40388
|
+
failedChecks: read.failedChecks ?? [],
|
|
40389
|
+
reason: "checks failure diagnosis unavailable"
|
|
40390
|
+
};
|
|
40153
40391
|
}
|
|
40154
40392
|
return null;
|
|
40155
40393
|
}
|
|
40394
|
+
function failedChecksReceiptLines(context, wait) {
|
|
40395
|
+
if (wait.detail !== "checks-failure") return [];
|
|
40396
|
+
const lines = [];
|
|
40397
|
+
for (const check of wait.failedChecks ?? []) {
|
|
40398
|
+
lines.push(`${context}: failed check: ${check.name} (${check.conclusion})${check.detailsUrl ? ` \u2014 ${check.detailsUrl}` : ""}`);
|
|
40399
|
+
if (check.diagnosticCommand) lines.push(`${context}: next diagnostic: ${check.diagnosticCommand}`);
|
|
40400
|
+
}
|
|
40401
|
+
if (!wait.failedChecks?.length && wait.checksFailureDetail) lines.push(`${context}: ${wait.checksFailureDetail}`);
|
|
40402
|
+
if (!wait.failedChecks?.some((check) => check.diagnosticCommand) && wait.diagnosticCommand) {
|
|
40403
|
+
lines.push(`${context}: next diagnostic: ${wait.diagnosticCommand}`);
|
|
40404
|
+
}
|
|
40405
|
+
if (wait.checksUrl) lines.push(`${context}: PR checks: ${wait.checksUrl}`);
|
|
40406
|
+
return lines;
|
|
40407
|
+
}
|
|
40156
40408
|
async function waitLoopHeadWorkflowRunCount(label, prNumber, repo) {
|
|
40157
40409
|
let snapshot;
|
|
40158
40410
|
try {
|
|
@@ -40206,6 +40458,8 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
|
|
|
40206
40458
|
// #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
|
|
40207
40459
|
// reading as "your tests failed". One call per failing run, only at the verdict.
|
|
40208
40460
|
diagnoseFailure: () => waitLoopDiagnosis("pr checks-wait", number, repo),
|
|
40461
|
+
checksUrl: `https://github.com/${repo}/pull/${number}/checks`,
|
|
40462
|
+
checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
|
|
40209
40463
|
// #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
|
|
40210
40464
|
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
|
|
40211
40465
|
baseBranch,
|
|
@@ -40232,21 +40486,21 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
|
|
|
40232
40486
|
printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
|
|
40233
40487
|
} else if (result.detail === "zero-runs") {
|
|
40234
40488
|
printLine(`pr checks-wait: failure (zero workflow runs delivered, NOT pending checks) \u2014 ${result.reason}`);
|
|
40235
|
-
} else
|
|
40489
|
+
} else {
|
|
40490
|
+
printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
40491
|
+
for (const line of failedChecksReceiptLines("pr checks-wait", result)) printLine(line);
|
|
40492
|
+
}
|
|
40236
40493
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
40237
40494
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
40238
40495
|
});
|
|
40239
40496
|
pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").action(async (number, o) => {
|
|
40240
|
-
|
|
40241
|
-
|
|
40242
|
-
|
|
40243
|
-
|
|
40244
|
-
if (
|
|
40245
|
-
|
|
40246
|
-
|
|
40247
|
-
return;
|
|
40248
|
-
}
|
|
40249
|
-
o.repo = refMatch[1];
|
|
40497
|
+
if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
|
|
40498
|
+
try {
|
|
40499
|
+
const parsed = parseIssueRef(number, o.repo);
|
|
40500
|
+
number = String(parsed.number);
|
|
40501
|
+
if (parsed.repo) o.repo = parsed.repo;
|
|
40502
|
+
} catch (e) {
|
|
40503
|
+
return fail(`pr land: ${e.message}`);
|
|
40250
40504
|
}
|
|
40251
40505
|
}
|
|
40252
40506
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
@@ -40308,6 +40562,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
40308
40562
|
// #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
|
|
40309
40563
|
// then read its own wall-clock kill as a broken diff.
|
|
40310
40564
|
diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
|
|
40565
|
+
checksUrl: `https://github.com/${repo}/pull/${prNumber}/checks`,
|
|
40566
|
+
checksDiagnosticCommand: `gh pr checks ${prNumber} --repo ${repo}`,
|
|
40311
40567
|
// #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
|
|
40312
40568
|
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
|
|
40313
40569
|
baseBranch: "development",
|
|
@@ -40490,6 +40746,8 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
40490
40746
|
pollMergeable: () => pollRestPrMergeable(number, repo),
|
|
40491
40747
|
pollRateLimit: () => waitLoopCorePool("pr merge --wait"),
|
|
40492
40748
|
diagnoseFailure: () => waitLoopDiagnosis("pr merge --wait", number, repo),
|
|
40749
|
+
checksUrl: `https://github.com/${repo}/pull/${number}/checks`,
|
|
40750
|
+
checksDiagnosticCommand: `gh pr checks ${number} --repo ${repo}`,
|
|
40493
40751
|
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
|
|
40494
40752
|
baseBranch,
|
|
40495
40753
|
sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
|
|
@@ -40498,7 +40756,20 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
40498
40756
|
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
40499
40757
|
});
|
|
40500
40758
|
if (wait.status !== "success" && wait.status !== "skipped") {
|
|
40759
|
+
if (wait.detail === "checks-failure") {
|
|
40760
|
+
console.log(JSON.stringify({
|
|
40761
|
+
mergeStatus: "not-merged",
|
|
40762
|
+
reason: "checks-failure",
|
|
40763
|
+
pr: number,
|
|
40764
|
+
repo,
|
|
40765
|
+
failedChecks: wait.failedChecks ?? [],
|
|
40766
|
+
...wait.checksUrl ? { checksUrl: wait.checksUrl } : {},
|
|
40767
|
+
...wait.diagnosticCommand ? { diagnosticCommand: wait.diagnosticCommand } : {},
|
|
40768
|
+
...wait.checksFailureDetail ? { checksFailureDetail: wait.checksFailureDetail } : {}
|
|
40769
|
+
}));
|
|
40770
|
+
}
|
|
40501
40771
|
console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
|
|
40772
|
+
for (const line of failedChecksReceiptLines("pr merge", wait)) console.warn(line);
|
|
40502
40773
|
process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
|
|
40503
40774
|
return;
|
|
40504
40775
|
}
|
|
@@ -40610,7 +40881,6 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
40610
40881
|
process.exitCode = 1;
|
|
40611
40882
|
return;
|
|
40612
40883
|
}
|
|
40613
|
-
const remoteBranch = { branch: headRef, existedBefore: remoteBefore, attempted: false, reason: remoteNotAttemptedReason };
|
|
40614
40884
|
const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
|
|
40615
40885
|
let localCleanup;
|
|
40616
40886
|
try {
|
|
@@ -40623,7 +40893,10 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
40623
40893
|
gcAcknowledged: o.gc,
|
|
40624
40894
|
expectedHeadOid: headRefOid,
|
|
40625
40895
|
pathExists: (p) => (0, import_node_fs43.existsSync)(p),
|
|
40626
|
-
|
|
40896
|
+
// #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
|
|
40897
|
+
// standing in is removed mid-cleanup, so a cwd-relative invocation fails with
|
|
40898
|
+
// 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
|
|
40899
|
+
execGit: async (args) => (await execFileP2("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout
|
|
40627
40900
|
});
|
|
40628
40901
|
} catch (e) {
|
|
40629
40902
|
localCleanup = {
|
|
@@ -40635,6 +40908,30 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
40635
40908
|
}
|
|
40636
40909
|
};
|
|
40637
40910
|
}
|
|
40911
|
+
const remoteBranch = await deleteMergedRemoteBranch({
|
|
40912
|
+
branch: headRef,
|
|
40913
|
+
existedBefore: remoteBefore,
|
|
40914
|
+
// #5899: this leg runs after worktree teardown — anchor it to the main checkout so a cwd inside
|
|
40915
|
+
// the removed worktree cannot turn the delete into 'fatal: not a git repository' + manual remediation.
|
|
40916
|
+
execGit: async (args) => (await execFileP2("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
40917
|
+
branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
|
|
40918
|
+
});
|
|
40919
|
+
const worktreePartial = localCleanup?.worktree?.path && localCleanup.worktree.status !== "removed" ? {
|
|
40920
|
+
kind: "worktree-directory",
|
|
40921
|
+
path: localCleanup.worktree.path,
|
|
40922
|
+
reason: localCleanup.worktree.reason ?? localCleanup.worktree.status,
|
|
40923
|
+
error: localCleanup.worktree.error ?? localCleanup.worktree.residueError,
|
|
40924
|
+
remediation: localCleanup.worktree.remediation ?? `Remove-Item -LiteralPath '${localCleanup.worktree.path?.replace(/'/g, "''")}' -Recurse -Force`
|
|
40925
|
+
} : void 0;
|
|
40926
|
+
const partialCleanup = [
|
|
40927
|
+
...worktreePartial ? [worktreePartial] : [],
|
|
40928
|
+
...remoteBranch.status === "failed" ? [{
|
|
40929
|
+
kind: "remote-branch",
|
|
40930
|
+
branch: remoteBranch.branch,
|
|
40931
|
+
error: remoteBranch.error,
|
|
40932
|
+
remediation: remoteBranch.remediation
|
|
40933
|
+
}] : []
|
|
40934
|
+
];
|
|
40638
40935
|
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
40639
40936
|
const crossRepoFilingIssue = repoForPostCleanup ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), repoForPostCleanup, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
|
|
40640
40937
|
const recovery = repoForPostCleanup ? buildPostMergeReconRecovery({
|
|
@@ -40672,12 +40969,13 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
40672
40969
|
observedMethod
|
|
40673
40970
|
});
|
|
40674
40971
|
console.log(JSON.stringify({
|
|
40675
|
-
mergeStatus: "merged",
|
|
40972
|
+
mergeStatus: partialCleanup.length ? "partial-cleanup" : "merged",
|
|
40676
40973
|
merged: number,
|
|
40677
40974
|
branch: headRef,
|
|
40678
40975
|
...methodField ? { method: methodField } : {},
|
|
40679
40976
|
remoteBranch,
|
|
40680
40977
|
housekeeping,
|
|
40978
|
+
...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
|
|
40681
40979
|
...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
|
|
40682
40980
|
...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
|
|
40683
40981
|
// `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
|
|
@@ -40701,7 +40999,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
40701
40999
|
})) {
|
|
40702
41000
|
console.error(line);
|
|
40703
41001
|
}
|
|
40704
|
-
process.exitCode = prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
|
|
41002
|
+
process.exitCode = (partialCleanup.length ? 1 : void 0) ?? prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
|
|
40705
41003
|
});
|
|
40706
41004
|
registerQueryCommands(program2);
|
|
40707
41005
|
registerIssueLifecycleCommands(program2, { attach: attachToProject });
|
|
@@ -40844,6 +41142,52 @@ function renderAlignment(label, alignment) {
|
|
|
40844
41142
|
}
|
|
40845
41143
|
return `${label}: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli devops pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
|
|
40846
41144
|
}
|
|
41145
|
+
function followUpLegStatus(status) {
|
|
41146
|
+
return status === "failure" ? "failed" : status;
|
|
41147
|
+
}
|
|
41148
|
+
function releaseFollowUpLegs(result, projectInfoSync) {
|
|
41149
|
+
const legs = [
|
|
41150
|
+
{
|
|
41151
|
+
leg: "project-info",
|
|
41152
|
+
status: projectInfoSync && "error" in projectInfoSync ? "failed" : "success",
|
|
41153
|
+
...projectInfoSync && "error" in projectInfoSync ? { error: projectInfoSync.error } : {}
|
|
41154
|
+
}
|
|
41155
|
+
];
|
|
41156
|
+
const runs = result.workflowRuns ?? [];
|
|
41157
|
+
if (runs.length > 0) {
|
|
41158
|
+
legs.push(...runs.map((run) => ({
|
|
41159
|
+
leg: run.workflow,
|
|
41160
|
+
status: followUpLegStatus(run.conclusion),
|
|
41161
|
+
...run.conclusion === "failure" ? { error: run.workflow === "jerv-gateway" ? result.dispatch : `${run.workflow} reported failure` } : {}
|
|
41162
|
+
})));
|
|
41163
|
+
} else if (result.deployStatus !== "success") {
|
|
41164
|
+
legs.push({
|
|
41165
|
+
leg: "deploy",
|
|
41166
|
+
status: followUpLegStatus(result.deployStatus),
|
|
41167
|
+
...result.deployStatus === "failure" ? { error: result.dispatch } : {}
|
|
41168
|
+
});
|
|
41169
|
+
}
|
|
41170
|
+
if (result.rcRetirement) {
|
|
41171
|
+
legs.push({
|
|
41172
|
+
leg: "rc-retirement",
|
|
41173
|
+
status: result.rcRetirement === "failed" ? "failed" : "success",
|
|
41174
|
+
...result.rcRetirement === "failed" ? { error: result.rcRetirementNote ?? "rc retirement failed" } : {}
|
|
41175
|
+
});
|
|
41176
|
+
}
|
|
41177
|
+
if (result.devRollForward) {
|
|
41178
|
+
legs.push({
|
|
41179
|
+
leg: "development-alignment",
|
|
41180
|
+
status: result.devRollForward.status === "pr-pending" ? "pending" : "success"
|
|
41181
|
+
});
|
|
41182
|
+
}
|
|
41183
|
+
if (result.rcAlignment) {
|
|
41184
|
+
legs.push({
|
|
41185
|
+
leg: "rc-alignment",
|
|
41186
|
+
status: result.rcAlignment.status === "pr-pending" ? "pending" : "success"
|
|
41187
|
+
});
|
|
41188
|
+
}
|
|
41189
|
+
return legs;
|
|
41190
|
+
}
|
|
40847
41191
|
var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
|
|
40848
41192
|
async function runPostReleaseJervDoctor(repo) {
|
|
40849
41193
|
if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
|
|
@@ -40884,7 +41228,11 @@ function renderTrainApply(commandName, r) {
|
|
|
40884
41228
|
if (r.releaseVerdict) {
|
|
40885
41229
|
const v = r.releaseVerdict;
|
|
40886
41230
|
const verdict = v.followUpStatus === "failed" ? "PROMOTED, FOLLOW-UP FAILED" : v.followUpStatus === "pending" ? "PROMOTED, FOLLOW-UP PENDING" : "SUCCEEDED";
|
|
40887
|
-
|
|
41231
|
+
const failedLegs = v.legs.filter((leg) => leg.status === "failed");
|
|
41232
|
+
return [
|
|
41233
|
+
`${base}; release verdict: ${verdict}; promoted=${v.promoted}; tag=${v.tag}; release=${v.releaseUrl ?? "unreported"}; follow-up: ${v.followUpStatus.toUpperCase()}`,
|
|
41234
|
+
...failedLegs.map((leg) => ` - follow-up leg ${leg.leg}: FAILED \u2014 ${leg.error ?? "no diagnostic recorded"}`)
|
|
41235
|
+
].join("\n");
|
|
40888
41236
|
}
|
|
40889
41237
|
return base;
|
|
40890
41238
|
}
|
|
@@ -41055,7 +41403,8 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
41055
41403
|
followUpStatus,
|
|
41056
41404
|
promoted: result.promoted,
|
|
41057
41405
|
tag: result.tag,
|
|
41058
|
-
releaseUrl: result.release?.url ?? null
|
|
41406
|
+
releaseUrl: result.release?.url ?? null,
|
|
41407
|
+
legs: releaseFollowUpLegs(result, projectInfoSync)
|
|
41059
41408
|
} : void 0;
|
|
41060
41409
|
const reported = {
|
|
41061
41410
|
...result,
|
|
@@ -41105,7 +41454,7 @@ function renderHotfixRelease(r) {
|
|
|
41105
41454
|
` - ${r.tagNote}`,
|
|
41106
41455
|
` - ${r.releaseNote}`,
|
|
41107
41456
|
` - deploy: ${r.deployNote}`,
|
|
41108
|
-
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
41457
|
+
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""}${run.url ? ` (${run.url})` : ""}`),
|
|
41109
41458
|
` - ${r.verifyNote}`,
|
|
41110
41459
|
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
41111
41460
|
` - fold: ${r.foldNote}`,
|
|
@@ -41116,7 +41465,7 @@ function renderHotfixStatus(r) {
|
|
|
41116
41465
|
return [
|
|
41117
41466
|
`mmi-cli devops hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
|
|
41118
41467
|
` - branch: ${r.branchExists ? "pushed" : "absent"} \u2014 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \u2014 tag: ${r.tagPushed ? "pushed" : "absent"} \u2014 Release: ${r.releaseExists ? "exists" : "absent"}`,
|
|
41119
|
-
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
41468
|
+
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""}${run.url ? ` (${run.url})` : ""}`),
|
|
41120
41469
|
` - npm @mutmutco/cli: ${r.npmVersion}`,
|
|
41121
41470
|
` - next: ${r.next}`,
|
|
41122
41471
|
...r.warnings.map((w) => ` - warning: ${w}`)
|
|
@@ -41127,13 +41476,25 @@ function hotfixRunOutcome(conclusion) {
|
|
|
41127
41476
|
if (conclusion === "failure") return "failure";
|
|
41128
41477
|
return "unresolved";
|
|
41129
41478
|
}
|
|
41479
|
+
function hotfixFollowUpLegs(runs, foldPort, foldNote) {
|
|
41480
|
+
const legs = runs.map((run) => ({
|
|
41481
|
+
leg: run.workflow,
|
|
41482
|
+
status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
|
|
41483
|
+
...run.conclusion === "failure" ? { error: "workflow reported failure" } : {}
|
|
41484
|
+
}));
|
|
41485
|
+
if (foldPort === "failure") {
|
|
41486
|
+
legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
|
|
41487
|
+
}
|
|
41488
|
+
return legs;
|
|
41489
|
+
}
|
|
41130
41490
|
async function runHotfixSub(sub, body, json, render) {
|
|
41131
41491
|
try {
|
|
41132
41492
|
await requireFreshTrainCli("hotfix");
|
|
41133
41493
|
const result = await body();
|
|
41134
|
-
printLine(json ? JSON.stringify(result, null, 2) : render(result));
|
|
41135
41494
|
const runs = result.runs;
|
|
41136
41495
|
const foldPort = result.foldStatus ?? "ok";
|
|
41496
|
+
const legs = runs ? hotfixFollowUpLegs(runs, foldPort, result.foldNote) : void 0;
|
|
41497
|
+
printLine(json ? JSON.stringify(legs ? Object.assign({}, result, { legs }) : result, null, 2) : render(result));
|
|
41137
41498
|
if (runs) applyTrainFollowUpExit(deriveTrainFollowUpStatus({
|
|
41138
41499
|
projectInfo: "ok",
|
|
41139
41500
|
deploy: reduceFollowUpOutcomes(runs.map((r) => hotfixRunOutcome(r.conclusion))),
|
package/package.json
CHANGED