@mutmutco/cli 3.45.0 → 3.47.0

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.
Files changed (2) hide show
  1. package/dist/main.cjs +69 -32
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -14788,6 +14788,9 @@ function deriveHotfixState(f) {
14788
14788
  if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
14789
14789
  return { state: "not-started", next: `mmi-cli hotfix start --from <pr#|sha> (would mint ${f.tag})` };
14790
14790
  }
14791
+ if (!f.pr && !f.branchExists && f.tagPushed && f.releaseExists) {
14792
+ return { state: "complete", next: "nothing \u2014 that version is a completed release, not a hotfix in flight" };
14793
+ }
14791
14794
  if (!f.pr) {
14792
14795
  return { state: "branch-pushed (no PR)", next: `mmi-cli hotfix start --from <pr#|sha> (resumes at the PR step for ${f.tag})` };
14793
14796
  }
@@ -20428,13 +20431,46 @@ async function pollGhPrChecks(prNumber, repoArgs) {
20428
20431
  }
20429
20432
  return state;
20430
20433
  }
20431
- async function ghMergeAutoEnqueue(prNumber, repo, method) {
20434
+ function ghFailureReason(e) {
20435
+ const err = e;
20436
+ const head = String(err?.message ?? "").split("\n")[0]?.trim() ?? "";
20437
+ const detail = `${err?.stderr ?? ""}
20438
+ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
20439
+ if (detail && head) return `${head} \u2014 ${detail}`;
20440
+ return detail || head || "unknown error";
20441
+ }
20442
+ var defaultGhMergeAutoIo = {
20443
+ gh: async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout
20444
+ };
20445
+ async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAutoIo) {
20432
20446
  const args = repo ? ["--repo", repo] : [];
20433
- const headRef = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS2 }).catch(() => ({ stdout: "" }))).stdout.trim();
20447
+ const headRef = (await io.gh(["pr", "view", prNumber, ...args, "--json", "headRefName", "--jq", ".headRefName"], GC_GH_TIMEOUT_MS2).catch(() => "")).trim();
20434
20448
  const deleteBranch = !isProtectedBranch(headRef);
20435
- const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
20449
+ const readMergeState = () => readGhPrStateWithRetry(
20450
+ () => io.gh(["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], GC_GH_TIMEOUT_MS2)
20451
+ );
20452
+ const confirmEnqueueOutcome = async () => {
20453
+ const stateRead = await readMergeState();
20454
+ if (!stateRead.ok) {
20455
+ return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
20456
+ }
20457
+ if (stateRead.state === "MERGED") return { mergeStatus: "merged" };
20458
+ const confirmation = await confirmAutoMergeEnqueued({
20459
+ readAutoMergeRequest: async () => io.gh(["pr", "view", prNumber, ...args, "--json", "autoMergeRequest", "--jq", '.autoMergeRequest // ""'], GC_GH_TIMEOUT_MS2),
20460
+ readMerged: async () => {
20461
+ const s = await readMergeState();
20462
+ return s.ok && s.state === "MERGED";
20463
+ },
20464
+ reEnqueue: async () => {
20465
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
20466
+ }
20467
+ });
20468
+ if (confirmation === "merged") return { mergeStatus: "merged" };
20469
+ if (confirmation === "enqueued") return { mergeStatus: "auto-merge-enqueued" };
20470
+ return { mergeStatus: "failed", error: "auto-merge did not stick \u2014 GitHub reported no autoMergeRequest after enqueue; retry once the PR has a pending check" };
20471
+ };
20436
20472
  try {
20437
- await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), { timeout: GH_MUTATION_TIMEOUT_MS });
20473
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
20438
20474
  } catch (e) {
20439
20475
  const message = String(e.message || "");
20440
20476
  if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
@@ -20442,50 +20478,51 @@ async function ghMergeAutoEnqueue(prNumber, repo, method) {
20442
20478
  if (note) return { mergeStatus: "failed", error: note };
20443
20479
  if (mergeAutoRejectedPrAlreadyClean(message)) {
20444
20480
  try {
20445
- await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false, deleteBranch }), { timeout: GH_MUTATION_TIMEOUT_MS });
20481
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
20446
20482
  } catch (e2) {
20447
20483
  const m2 = String(e2.message || "");
20448
20484
  if (/already been merged/i.test(m2)) return { mergeStatus: "merged" };
20449
- if (!ghPrMergeLocalBranchDeleteWarning(m2)) return { mergeStatus: "failed", error: m2.split("\n")[0] };
20485
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) {
20486
+ const afterDirect = await readMergeState();
20487
+ if (afterDirect.ok && afterDirect.state === "MERGED") return { mergeStatus: "merged" };
20488
+ if (!afterDirect.ok) {
20489
+ return { mergeStatus: "failed", error: `could not confirm PR state after clean-status fallback: ${afterDirect.error}` };
20490
+ }
20491
+ try {
20492
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
20493
+ } catch (e3) {
20494
+ const m3 = String(e3.message || "");
20495
+ if (/already been merged/i.test(m3)) return { mergeStatus: "merged" };
20496
+ const afterRetry = await readMergeState();
20497
+ if (afterRetry.ok && afterRetry.state === "MERGED") return { mergeStatus: "merged" };
20498
+ const stuck = (await io.gh(["pr", "view", prNumber, ...args, "--json", "autoMergeRequest", "--jq", '.autoMergeRequest // ""'], GC_GH_TIMEOUT_MS2).catch(() => "")).trim();
20499
+ if (stuck) return { mergeStatus: "auto-merge-enqueued" };
20500
+ return { mergeStatus: "failed", error: `${ghFailureReason(e2)} (re-enqueue also failed: ${ghFailureReason(e3)})` };
20501
+ }
20502
+ return confirmEnqueueOutcome();
20503
+ }
20450
20504
  }
20451
- const stateRead2 = await readMergeState();
20452
- if (stateRead2.ok && stateRead2.state === "MERGED") return { mergeStatus: "merged" };
20505
+ const stateRead = await readMergeState();
20506
+ if (stateRead.ok && stateRead.state === "MERGED") return { mergeStatus: "merged" };
20453
20507
  return {
20454
20508
  mergeStatus: "failed",
20455
- error: stateRead2.ok ? "direct merge did not land after clean-status fallback" : `could not confirm PR state after clean-status fallback: ${stateRead2.error}`
20509
+ error: stateRead.ok ? "direct merge did not land after clean-status fallback" : `could not confirm PR state after clean-status fallback: ${stateRead.error}`
20456
20510
  };
20457
20511
  }
20458
20512
  if (ghPrMergeLocalBranchDeleteWarning(message)) {
20459
- const stateRead2 = await readMergeState();
20460
- if (stateRead2.ok && stateRead2.state === "MERGED") return { mergeStatus: "merged" };
20513
+ const stateRead = await readMergeState();
20514
+ if (stateRead.ok && stateRead.state === "MERGED") return { mergeStatus: "merged" };
20461
20515
  return {
20462
20516
  mergeStatus: "failed",
20463
- error: stateRead2.ok ? message.split("\n")[0] : `could not confirm PR state after local branch cleanup warning: ${stateRead2.error}`
20517
+ error: stateRead.ok ? message.split("\n")[0] : `could not confirm PR state after local branch cleanup warning: ${stateRead.error}`
20464
20518
  };
20465
20519
  }
20466
20520
  if (!basePolicyBlocksImmediateMerge(message)) {
20467
- return { mergeStatus: "failed", error: message.split("\n")[0] };
20521
+ return { mergeStatus: "failed", error: ghFailureReason(e) };
20468
20522
  }
20469
- return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
20523
+ return { mergeStatus: "failed", error: `merge blocked: ${ghFailureReason(e)} \u2014 ensure checks are green` };
20470
20524
  }
20471
- const stateRead = await readMergeState();
20472
- if (!stateRead.ok) {
20473
- return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
20474
- }
20475
- if (stateRead.state === "MERGED") return { mergeStatus: "merged" };
20476
- const confirmation = await confirmAutoMergeEnqueued({
20477
- readAutoMergeRequest: async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "autoMergeRequest", "--jq", '.autoMergeRequest // ""'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout,
20478
- readMerged: async () => {
20479
- const s = await readMergeState();
20480
- return s.ok && s.state === "MERGED";
20481
- },
20482
- reEnqueue: async () => {
20483
- await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), { timeout: GH_MUTATION_TIMEOUT_MS });
20484
- }
20485
- });
20486
- if (confirmation === "merged") return { mergeStatus: "merged" };
20487
- if (confirmation === "enqueued") return { mergeStatus: "auto-merge-enqueued" };
20488
- return { mergeStatus: "failed", error: "auto-merge did not stick \u2014 GitHub reported no autoMergeRequest after enqueue; retry once the PR has a pending check" };
20525
+ return confirmEnqueueOutcome();
20489
20526
  }
20490
20527
  async function remoteBranchExists2(branch, options = {}) {
20491
20528
  return checkRemoteBranchExists(branch, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.45.0",
3
+ "version": "3.47.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",