@mutmutco/cli 3.14.0 → 3.15.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 +85 -9
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6456,6 +6456,9 @@ function basePolicyBlocksImmediateMerge(message) {
6456
6456
  function ghPrMergeLocalBranchDeleteWarning(message) {
6457
6457
  return /used by worktree|cannot delete branch/i.test(message);
6458
6458
  }
6459
+ function mergeAutoRejectedPrAlreadyClean(message) {
6460
+ return /clean status \(enablePullRequestAutoMerge\)|is in clean status/i.test(message);
6461
+ }
6459
6462
  async function checkRemoteBranchExists(branch, deps, options = {}) {
6460
6463
  if (!branch) return void 0;
6461
6464
  try {
@@ -11313,6 +11316,17 @@ async function preflight(deps, ctx, stage2, meta) {
11313
11316
  throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
11314
11317
  }
11315
11318
  await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
11319
+ if (stage2 === "main" && (model === "registry-publish" || meta.publishRequired === true)) {
11320
+ const publishDir = typeof meta.publishDir === "string" && meta.publishDir.trim() ? meta.publishDir.trim() : ".";
11321
+ const publishArgs = ["publish", ...publishDir !== "." ? [`./${publishDir}`] : [], "--dry-run", "--ignore-scripts"];
11322
+ try {
11323
+ await deps.run("npm", publishArgs);
11324
+ } catch (e) {
11325
+ throw new Error(
11326
+ `${ctx.repo}: pre-tag publish dry run failed for '${publishDir}' (npm ${publishArgs.join(" ")}) \u2014 refusing to tag a release that would fail to publish (#2753). Fix the publish surface (deleted dir, malformed package.json, missing files), then rerun. Underlying error: ${e instanceof Error ? e.message : String(e)}`
11327
+ );
11328
+ }
11329
+ }
11316
11330
  return model;
11317
11331
  }
11318
11332
  async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
@@ -18584,6 +18598,15 @@ function parseLinkedIssues(body) {
18584
18598
  }
18585
18599
  return [...refs].sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
18586
18600
  }
18601
+ function parseClosingIssues(body) {
18602
+ const refs = /* @__PURE__ */ new Set();
18603
+ const re = /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)\b/gi;
18604
+ let m;
18605
+ while ((m = re.exec(body)) !== null) {
18606
+ refs.add(`#${m[1]}`);
18607
+ }
18608
+ return [...refs].sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
18609
+ }
18587
18610
  function assertPositiveInt(label, n) {
18588
18611
  const num = Number(n);
18589
18612
  if (!Number.isInteger(num) || num <= 0) {
@@ -19286,6 +19309,27 @@ function repoFromSelector2(selector) {
19286
19309
  async function loadConfigForBoardSelector(selector, repoOption) {
19287
19310
  return loadConfigForRepo(repoFromSelector2(selector) ?? repoOption);
19288
19311
  }
19312
+ async function advanceClosedIssuesToDone(prNumber, repoOption) {
19313
+ const repoArgs = repoOption ? ["--repo", repoOption] : [];
19314
+ let body;
19315
+ try {
19316
+ body = (await execFileP2("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "body", "--jq", ".body"], { timeout: GC_GH_TIMEOUT_MS5 })).stdout;
19317
+ } catch {
19318
+ return void 0;
19319
+ }
19320
+ const closing = parseClosingIssues(body ?? "");
19321
+ if (closing.length === 0) return void 0;
19322
+ const results = [];
19323
+ for (const ref of closing) {
19324
+ try {
19325
+ const moved = await moveBoardItem({ config: await loadConfigForBoardSelector(ref, repoOption), selector: ref, status: "Done", repo: repoOption, allowPartial: true });
19326
+ results.push(moved.partial ? { issue: ref, moved: false, error: moved.warning } : { issue: ref, moved: true });
19327
+ } catch (e) {
19328
+ results.push({ issue: ref, moved: false, error: e.message });
19329
+ }
19330
+ }
19331
+ return results;
19332
+ }
19289
19333
  function renderGcApplyResult(result) {
19290
19334
  const lines = ["gc apply result:"];
19291
19335
  lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
@@ -20922,6 +20966,21 @@ async function ghMergeAutoEnqueue(prNumber, repo, method) {
20922
20966
  if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
20923
20967
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
20924
20968
  if (note) return { mergeStatus: "failed", error: note };
20969
+ if (mergeAutoRejectedPrAlreadyClean(message)) {
20970
+ try {
20971
+ await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false }), { timeout: GH_MUTATION_TIMEOUT_MS });
20972
+ } catch (e2) {
20973
+ const m2 = String(e2.message || "");
20974
+ if (/already been merged/i.test(m2)) return { mergeStatus: "merged" };
20975
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) return { mergeStatus: "failed", error: m2.split("\n")[0] };
20976
+ }
20977
+ const stateRead2 = await readMergeState();
20978
+ if (stateRead2.ok && stateRead2.state === "MERGED") return { mergeStatus: "merged" };
20979
+ return {
20980
+ mergeStatus: "failed",
20981
+ error: stateRead2.ok ? "direct merge did not land after clean-status fallback" : `could not confirm PR state after clean-status fallback: ${stateRead2.error}`
20982
+ };
20983
+ }
20925
20984
  if (ghPrMergeLocalBranchDeleteWarning(message)) {
20926
20985
  const stateRead2 = await readMergeState();
20927
20986
  if (stateRead2.ok && stateRead2.state === "MERGED") return { mergeStatus: "merged" };
@@ -21161,7 +21220,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21161
21220
  const remoteBefore = await remoteBranchExists2(headRef);
21162
21221
  let remoteDeleteAttempted = false;
21163
21222
  let remoteNotAttemptedReason;
21164
- await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
21223
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
21165
21224
  const message = String(e.message || "");
21166
21225
  if (/already been merged/i.test(message)) {
21167
21226
  remoteNotAttemptedReason = "pr-already-merged";
@@ -21169,6 +21228,19 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21169
21228
  }
21170
21229
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
21171
21230
  if (note) throw new Error(`gh pr merge ${number}: ${note}`);
21231
+ if (o.auto && mergeAutoRejectedPrAlreadyClean(message)) {
21232
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
21233
+ const m2 = String(e2.message || "");
21234
+ if (/already been merged/i.test(m2)) {
21235
+ remoteNotAttemptedReason = "pr-already-merged";
21236
+ return;
21237
+ }
21238
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
21239
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
21240
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
21241
+ });
21242
+ return;
21243
+ }
21172
21244
  if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
21173
21245
  throw new Error(`gh pr merge ${number}: the base-branch policy blocks an immediate merge \u2014 re-run with --auto to merge once required checks pass.`);
21174
21246
  }
@@ -21217,14 +21289,18 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21217
21289
  }
21218
21290
  };
21219
21291
  }
21220
- console.log(JSON.stringify(buildPrMergeResultPayload({
21221
- number,
21222
- branch: headRef,
21223
- method: method.slice(2),
21224
- remoteBranch,
21225
- localCleanup,
21226
- housekeeping
21227
- })));
21292
+ const boardAdvance = await advanceClosedIssuesToDone(number, o.repo);
21293
+ console.log(JSON.stringify({
21294
+ ...buildPrMergeResultPayload({
21295
+ number,
21296
+ branch: headRef,
21297
+ method: method.slice(2),
21298
+ remoteBranch,
21299
+ localCleanup,
21300
+ housekeeping
21301
+ }),
21302
+ ...boardAdvance ? { boardAdvance } : {}
21303
+ }));
21228
21304
  });
21229
21305
  registerQueryCommands(program2);
21230
21306
  registerWorktreeCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.14.0",
3
+ "version": "3.15.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",