@mutmutco/cli 4.3.16 → 4.3.17

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 +220 -101
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -12917,6 +12917,18 @@ function rulesetRequiredContexts(ruleset) {
12917
12917
  }
12918
12918
  return contexts;
12919
12919
  }
12920
+ function rulesetStrictPolicy(ruleset) {
12921
+ const rules = (ruleset.rules ?? []).filter((rule) => rule.type === "required_status_checks");
12922
+ return rules.length > 0 && rules.every((rule) => rule.parameters?.strict_required_status_checks_policy === true);
12923
+ }
12924
+ function patchRulesetStrictPolicy(body) {
12925
+ const rules = (body.rules ?? []).map((rule) => {
12926
+ const r = rule;
12927
+ if (r.type !== "required_status_checks") return rule;
12928
+ return { ...r, parameters: { ...r.parameters, strict_required_status_checks_policy: true } };
12929
+ });
12930
+ return { ...body, rules };
12931
+ }
12920
12932
  function rulesetBranchIncludes(ruleset) {
12921
12933
  const raw = ruleset.conditions?.ref_name?.include;
12922
12934
  return Array.isArray(raw) ? [...new Set(raw.filter((ref) => typeof ref === "string" && ref.length > 0))].sort((a, b) => a.localeCompare(b)) : [];
@@ -12936,7 +12948,8 @@ function patchRulesetRequiredContexts(body, contexts) {
12936
12948
  ...r,
12937
12949
  parameters: {
12938
12950
  ...r.parameters,
12939
- strict_required_status_checks_policy: r.parameters?.strict_required_status_checks_policy ?? false,
12951
+ strict_required_status_checks_policy: true,
12952
+ // #6263: org policy — see patchRulesetStrictPolicy
12940
12953
  required_status_checks: sorted.map((context) => ({ context }))
12941
12954
  }
12942
12955
  };
@@ -13023,6 +13036,7 @@ async function gateIsProvenGreen(repo, client, baseBranch, gateFiles = DEFAULT_G
13023
13036
  }
13024
13037
  async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
13025
13038
  const want = new Set(rulesetRequiredContexts({ rules: rulesetBody.rules }));
13039
+ const wantStrict = rulesetStrictPolicy({ rules: rulesetBody.rules });
13026
13040
  const wantBranches = rulesetBranchIncludes(rulesetBody);
13027
13041
  const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
13028
13042
  const existing = findProductRuleset(list ?? []);
@@ -13034,8 +13048,9 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
13034
13048
  const have = new Set(rulesetRequiredContexts(detail));
13035
13049
  const haveBranches = rulesetBranchIncludes(detail);
13036
13050
  const branchesMatch = haveBranches.length === wantBranches.length && wantBranches.every((ref, index) => ref === haveBranches[index]);
13037
- if (detail.enforcement === effective && branchesMatch && have.size === want.size && [...want].every((c) => have.has(c))) {
13038
- return { action: "skipped", enforcement: effective, detail: `${effective} ruleset already matches required contexts and branches${kept}` };
13051
+ const strictMatch = rulesetStrictPolicy(detail) === wantStrict;
13052
+ if (detail.enforcement === effective && branchesMatch && strictMatch && have.size === want.size && [...want].every((c) => have.has(c))) {
13053
+ return { action: "skipped", enforcement: effective, detail: `${effective} ruleset already matches required contexts, branches and up-to-date policy${kept}` };
13039
13054
  }
13040
13055
  await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
13041
13056
  return { action: "updated", enforcement: effective, detail: `ruleset ${existing.id}${kept}` };
@@ -15487,10 +15502,10 @@ var rollout_plan_default = {
15487
15502
  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)."
15488
15503
  },
15489
15504
  baseline: {
15490
- version: "4.3.16",
15491
- tag: "v4.3.16",
15492
- commit: "5a4b8e29fd1a",
15493
- npm: "@mutmutco/cli@4.3.16"
15505
+ version: "4.3.17",
15506
+ tag: "v4.3.17",
15507
+ commit: "3b07c941910b",
15508
+ npm: "@mutmutco/cli@4.3.17"
15494
15509
  },
15495
15510
  exitCriterion: "fleet-n-of-n",
15496
15511
  hubOnlyShortcut: "forbidden",
@@ -15507,14 +15522,14 @@ var rollout_plan_default = {
15507
15522
  repo: "mutmutco/mmi-hub",
15508
15523
  role: "canary",
15509
15524
  schedule: "train",
15510
- v3Target: "v4.3.16"
15525
+ v3Target: "v4.3.17"
15511
15526
  }
15512
15527
  ],
15513
15528
  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.",
15514
15529
  rollback: {
15515
15530
  independent: true,
15516
- mechanism: "npm dist-tag latest -> 4.3.16 and redeploy the Hub Lambda from tag v4.3.16 (5a4b8e29fd1a); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15517
- v3Target: "v4.3.16 (@mutmutco/cli@4.3.16, tag commit 5a4b8e29fd1a \u2014 last known-good release carrying the repo-index v4-only contract)"
15531
+ mechanism: "npm dist-tag latest -> 4.3.17 and redeploy the Hub Lambda from tag v4.3.17 (3b07c941910b); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15532
+ v3Target: "v4.3.17 (@mutmutco/cli@4.3.17, tag commit 3b07c941910b \u2014 last known-good release carrying the repo-index v4-only contract)"
15518
15533
  }
15519
15534
  },
15520
15535
  {
@@ -23511,12 +23526,13 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
23511
23526
  const registryContexts = registryRequiredContexts(meta);
23512
23527
  const contexts = sortedUnique(registryContexts ?? committedContexts);
23513
23528
  const explicitBranches = Array.isArray(meta?.requiredCheckBranches) && meta.requiredCheckBranches.length > 0 ? resolveRequiredCheckBranches(meta, repo) : null;
23514
- let apiPayload = registryContexts == null ? committedPayload : patchRulesetRequiredContexts(committedPayload, contexts);
23529
+ const committedStrict = rulesetStrictPolicy(committedPayload);
23530
+ let apiPayload = patchRulesetStrictPolicy(registryContexts == null ? committedPayload : patchRulesetRequiredContexts(committedPayload, contexts));
23515
23531
  if (explicitBranches) apiPayload = patchRulesetBranchIncludes(apiPayload, explicitBranches);
23516
23532
  const branchIncludes = rulesetBranchIncludes(apiPayload);
23517
23533
  const authoritativeFilePayload = {
23518
23534
  ...filePayload,
23519
- ...registryContexts == null ? {} : { rules: apiPayload.rules },
23535
+ ...registryContexts == null && committedStrict ? {} : { rules: apiPayload.rules },
23520
23536
  ...explicitBranches == null ? {} : { conditions: apiPayload.conditions }
23521
23537
  };
23522
23538
  return {
@@ -23526,6 +23542,7 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
23526
23542
  contexts,
23527
23543
  branchIncludes,
23528
23544
  committedBranchIncludes,
23545
+ committedStrict,
23529
23546
  branchSource: explicitBranches ? "registry META requiredCheckBranches" : "committed ruleset reference",
23530
23547
  source: registryContexts == null ? "committed ruleset reference" : "registry META requiredChecks",
23531
23548
  coversReleaseBranches: rulesetCoversReleaseBranches(apiPayload, resolveReleaseTrack(meta, void 0, repo))
@@ -23537,8 +23554,9 @@ function sameContexts(left, right) {
23537
23554
  function resolveProductRulesetReconcilePlan(input) {
23538
23555
  const liveNeedsContextConvergence = !sameContexts(input.liveContexts, input.authorityContexts);
23539
23556
  const liveNeedsBranchConvergence = input.liveBranchIncludes !== void 0 && input.authorityBranchIncludes !== void 0 && !sameContexts(input.liveBranchIncludes, input.authorityBranchIncludes);
23557
+ const liveNeedsStrictConvergence = input.liveStrict === false;
23540
23558
  const liveIsActive = input.liveEnforcement === "active";
23541
- if (liveIsActive && !liveNeedsContextConvergence && !liveNeedsBranchConvergence) {
23559
+ if (liveIsActive && !liveNeedsContextConvergence && !liveNeedsBranchConvergence && !liveNeedsStrictConvergence) {
23542
23560
  return { shouldActivate: false, targetEnforcement: "active" };
23543
23561
  }
23544
23562
  if (input.unsafeContexts.length > 0) {
@@ -23726,6 +23744,7 @@ async function auditRepoCi(repo, deps) {
23726
23744
  const productRuleset = rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
23727
23745
  const liveContexts = productRuleset == null ? [] : sortedUnique(rulesetRequiredContexts(productRuleset));
23728
23746
  const liveBranchIncludes = productRuleset == null ? [] : rulesetBranchIncludes(productRuleset);
23747
+ const liveStrict = productRuleset != null && rulesetStrictPolicy(productRuleset);
23729
23748
  const hasRequiredChecks = productRuleset?.enforcement === "active" && liveContexts.length > 0;
23730
23749
  checks.push({
23731
23750
  ok: hasRequiredChecks,
@@ -23746,11 +23765,12 @@ async function auditRepoCi(repo, deps) {
23746
23765
  const liveContextsAligned = sameContexts(liveContexts, authoritativeRuleset.contexts);
23747
23766
  const fileBranchesAligned = sameContexts(authoritativeRuleset.committedBranchIncludes, authoritativeRuleset.branchIncludes);
23748
23767
  const liveBranchesAligned = sameContexts(liveBranchIncludes, authoritativeRuleset.branchIncludes);
23749
- const aligned = fileContextsAligned && liveContextsAligned && fileBranchesAligned && liveBranchesAligned;
23768
+ const strictAligned = authoritativeRuleset.committedStrict && liveStrict;
23769
+ const aligned = fileContextsAligned && liveContextsAligned && fileBranchesAligned && liveBranchesAligned && strictAligned;
23750
23770
  checks.push({
23751
23771
  ok: aligned,
23752
23772
  label: RULESET_REFERENCE_MATCH_LABEL,
23753
- detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}] and branches [${authoritativeRuleset.branchIncludes.join(", ")}] match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]`,
23773
+ detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy must be true (require branches to be up to date before merging, #6263) \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`),
23754
23774
  remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
23755
23775
  });
23756
23776
  }
@@ -24284,7 +24304,7 @@ async function applyCiReconcileRepo(repo, deps) {
24284
24304
  result.errors.push(`${authority.source} declares no required contexts`);
24285
24305
  return finalizeCiReconcile(repo, deps, result, report);
24286
24306
  }
24287
- const sourceNeedsConvergence = !sameContexts(authority.committedContexts, authority.contexts) || !sameContexts(authority.committedBranchIncludes, authority.branchIncludes);
24307
+ const sourceNeedsConvergence = !sameContexts(authority.committedContexts, authority.contexts) || !sameContexts(authority.committedBranchIncludes, authority.branchIncludes) || !authority.committedStrict;
24288
24308
  if (sourceNeedsConvergence) {
24289
24309
  try {
24290
24310
  const delivery = await deliverSeedFile(
@@ -24339,6 +24359,7 @@ async function applyCiReconcileRepo(repo, deps) {
24339
24359
  authorityContexts: authority.contexts,
24340
24360
  liveBranchIncludes,
24341
24361
  authorityBranchIncludes: authority.branchIncludes,
24362
+ liveStrict: live == null ? void 0 : rulesetStrictPolicy(live),
24342
24363
  gateProvenGreen: await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles),
24343
24364
  unsafeContexts: unsafe
24344
24365
  });
@@ -24347,7 +24368,7 @@ async function applyCiReconcileRepo(repo, deps) {
24347
24368
  result.skipped.push(plan.holdReason);
24348
24369
  return finalizeCiReconcile(repo, deps, result, report, plan.holdReason);
24349
24370
  }
24350
- result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts and branches already match declared authority`);
24371
+ result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts, branches and up-to-date policy already match declared authority`);
24351
24372
  return finalizeCiReconcile(repo, deps, result, report);
24352
24373
  }
24353
24374
  try {
@@ -27146,6 +27167,11 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
27146
27167
  label: "product required-check ruleset enforcement active",
27147
27168
  detail: productRuleset?.enforcement !== "active" ? `${PRODUCT_RULESET_NAME} is ${productRuleset?.enforcement ?? "missing"} \u2014 run mmi-cli devops ci reconcile --apply --repo ${repo} once the gate is green` : void 0
27148
27169
  });
27170
+ checks.push({
27171
+ ok: productRuleset != null && rulesetStrictPolicy(productRuleset),
27172
+ label: "product required-check ruleset requires an up-to-date branch",
27173
+ detail: productRuleset != null && rulesetStrictPolicy(productRuleset) ? void 0 : `${PRODUCT_RULESET_NAME} ${productRuleset == null ? "is missing" : "has strict_required_status_checks_policy=false"} \u2014 run mmi-cli devops ci reconcile --apply --repo ${repo} (#6263)`
27174
+ });
27149
27175
  const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
27150
27176
  const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
27151
27177
  checks.push({
@@ -36010,6 +36036,47 @@ function applyChecklistCheck(body, query, checked) {
36010
36036
  }
36011
36037
 
36012
36038
  // src/pr-land.ts
36039
+ function prHeadBehindBase(input) {
36040
+ if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
36041
+ return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
36042
+ }
36043
+ function prHeadUpdateRemedy(head, base) {
36044
+ return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 docs/Guides/train-troubleshooting.md#head-behind-base`;
36045
+ }
36046
+ var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
36047
+ var PR_HEAD_UPDATE_API_READ_DELAY_MS = 2e3;
36048
+ async function updatePrHeadFromBase(input) {
36049
+ const remedy = prHeadUpdateRemedy(input.head, input.base);
36050
+ if (input.localCheckedOut) {
36051
+ const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
36052
+ await input.git(["fetch", "origin", input.base]);
36053
+ try {
36054
+ await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
36055
+ } catch (e) {
36056
+ await input.git(["merge", "--abort"]).catch(() => {
36057
+ });
36058
+ throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
36059
+ }
36060
+ await input.git(["push", "origin", input.head]);
36061
+ const to = (await input.git(["rev-parse", "HEAD"])).trim();
36062
+ return { from: from2, to, base: input.base, via: "local-merge" };
36063
+ }
36064
+ const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
36065
+ const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
36066
+ const from = await readHead2();
36067
+ try {
36068
+ await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
36069
+ } catch (e) {
36070
+ throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
36071
+ }
36072
+ const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
36073
+ for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
36074
+ const to = await readHead2();
36075
+ if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
36076
+ await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
36077
+ }
36078
+ throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
36079
+ }
36013
36080
  var PR_LAND_POLL_MS = 3e4;
36014
36081
  var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
36015
36082
  var PR_LAND_STATE_READ_RETRIES = 3;
@@ -36080,21 +36147,30 @@ async function runPrLand(prNumber, options, deps) {
36080
36147
  }
36081
36148
  const ciPolicy = await deps.resolveCiPolicy(repo);
36082
36149
  base.ciPolicy = ciPolicy;
36083
- const checksWait = await deps.waitForChecks(prNumber, repo);
36084
- base.checksWait = checksWait;
36085
- if (checksWait.status === "conflicting") {
36086
- return {
36087
- ...base,
36088
- error: `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`
36089
- };
36090
- }
36091
- if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
36092
- return {
36093
- ...base,
36094
- error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
36095
- };
36150
+ const checksWaitError = (checksWait) => {
36151
+ if (checksWait.status === "conflicting") {
36152
+ return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
36153
+ }
36154
+ if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
36155
+ return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
36156
+ }
36157
+ return void 0;
36158
+ };
36159
+ base.checksWait = await deps.waitForChecks(prNumber, repo);
36160
+ const waitError = checksWaitError(base.checksWait);
36161
+ if (waitError) return { ...base, error: waitError };
36162
+ let prState = await deps.readPrState(prNumber, repo);
36163
+ if (prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
36164
+ try {
36165
+ base.branchUpdated = await deps.updateBranch(prNumber, repo);
36166
+ } catch (e) {
36167
+ return { ...base, error: `pr land #${prNumber}: ${e.message}` };
36168
+ }
36169
+ base.checksWait = await deps.waitForChecks(prNumber, repo);
36170
+ const reWaitError = checksWaitError(base.checksWait);
36171
+ if (reWaitError) return { ...base, error: reWaitError };
36172
+ prState = await deps.readPrState(prNumber, repo);
36096
36173
  }
36097
- const prState = await deps.readPrState(prNumber, repo);
36098
36174
  if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
36099
36175
  return {
36100
36176
  ...base,
@@ -39739,6 +39815,27 @@ ${list}`);
39739
39815
  }
39740
39816
  printLine(JSON.stringify({ number: Number(number), repo: o.repo, verdict, patch, head, commentUrl }));
39741
39817
  });
39818
+ async function updatePrHeadForMerge(input) {
39819
+ return updatePrHeadFromBase({
39820
+ ...input,
39821
+ git: async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
39822
+ rest: (method, path2, body) => defaultGitHubClient().rest(method, path2, { ...body ? { body } : {}, timeoutMs: GH_MUTATION_TIMEOUT_MS })
39823
+ });
39824
+ }
39825
+ async function prHeadCheckedOutHere(head, repo, explicitRepo) {
39826
+ if (await gitOut(["branch", "--show-current"]) !== head) return false;
39827
+ if (!explicitRepo || !repo) return true;
39828
+ const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
39829
+ return cwdRepo?.toLowerCase() === repo.split("/").slice(-2).join("/").toLowerCase();
39830
+ }
39831
+ async function prLandUpdateBranch(prNumber, repo, explicitRepo) {
39832
+ const viewed = JSON.parse((await execFileP("gh", ["pr", "view", prNumber, "--repo", repo, "--json", "headRefName,baseRefName"], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
39833
+ const localCheckedOut = await prHeadCheckedOutHere(viewed.headRefName, repo, explicitRepo);
39834
+ console.warn(`pr land: PR #${prNumber} is BEHIND ${viewed.baseRefName} \u2014 updating ${viewed.headRefName} from the base (${localCheckedOut ? "local merge commit + push" : "GitHub update-branch"}) and re-waiting the checks once (#6263).`);
39835
+ return updatePrHeadForMerge({ prNumber, repo, head: viewed.headRefName, base: viewed.baseRefName, localCheckedOut });
39836
+ }
39837
+ class PrHeadBehindBaseError extends Error {
39838
+ }
39742
39839
  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").option("--without-review <reason>", "land without a review verdict (#6255): posts a `zeroci-review-waived` comment carrying the reason and actor, then proceeds").action(async (number, o) => {
39743
39840
  if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
39744
39841
  try {
@@ -39857,6 +39954,7 @@ ${list}`);
39857
39954
  allowedClosing: landClosingGuardInput?.closing,
39858
39955
  bodyText: landSquashBody
39859
39956
  }),
39957
+ updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
39860
39958
  pollMerged: async (prNumber, repo, deadlineMs) => {
39861
39959
  let lastFailure;
39862
39960
  while (Date.now() < deadlineMs) {
@@ -40008,7 +40106,7 @@ ${list}`);
40008
40106
  }
40009
40107
  const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
40010
40108
  const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
40011
- if (o.wait) {
40109
+ const runWaitGate = async () => {
40012
40110
  console.warn(`pr merge: --wait can hold the shell up to the full ${PR_CHECKS_TIMEOUT_MS / 6e4}m budget plus the merge \u2014 run under a shell timeout above that budget or as a background/monitor task (#6027)`);
40013
40111
  const repo = await requireRepo(o.repo);
40014
40112
  const budgetMs = PR_CHECKS_TIMEOUT_MS;
@@ -40020,7 +40118,7 @@ ${list}`);
40020
40118
  if (snapshotRead.state === "failed") {
40021
40119
  console.error(`pr merge: cannot resolve PR #${number}'s base branch \u2014 ${snapshotRead.error}. Refusing to wait against an assumed base; retry when the API answers.`);
40022
40120
  process.exitCode = 1;
40023
- return;
40121
+ return false;
40024
40122
  }
40025
40123
  const baseBranch = snapshotRead.snapshot.baseRef;
40026
40124
  const requiredContexts = await fetchRequiredCheckContexts(repo, baseBranch).catch(() => null);
@@ -40055,9 +40153,11 @@ ${list}`);
40055
40153
  console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
40056
40154
  for (const line of failedChecksReceiptLines("pr merge", wait)) console.warn(line);
40057
40155
  process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
40058
- return;
40156
+ return false;
40059
40157
  }
40060
- }
40158
+ return true;
40159
+ };
40160
+ if (o.wait && !await runWaitGate()) return;
40061
40161
  if (ciPolicy.policy === "no-ci") {
40062
40162
  const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
40063
40163
  if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
@@ -40073,73 +40173,90 @@ ${list}`);
40073
40173
  { allowedClosing: method === "--squash" ? closingGuardInput?.closing : void 0 }
40074
40174
  );
40075
40175
  const bodyFile = overrideBody?.path;
40076
- try {
40077
- await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
40078
- const message2 = String(e.message || "");
40079
- if (/already been merged/i.test(message2)) {
40080
- remoteNotAttemptedReason = "pr-already-merged";
40081
- return;
40176
+ let branchUpdated;
40177
+ const mergeOnce = () => execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
40178
+ const message2 = String(e.message || "");
40179
+ if (/already been merged/i.test(message2)) {
40180
+ remoteNotAttemptedReason = "pr-already-merged";
40181
+ return;
40182
+ }
40183
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
40184
+ if (note) throw new Error(`gh pr merge ${number}: ${note}`);
40185
+ if (!o.auto && !branchUpdated) {
40186
+ const mergeState = prHeadBehindBase({ message: message2 }) ? "BEHIND" : (await execFileP("gh", ["pr", "view", number, ...repoArgs, "--json", "mergeStateStatus", "--jq", ".mergeStateStatus"], { timeout: GC_GH_TIMEOUT_MS4 }).catch(() => ({ stdout: "" }))).stdout.trim();
40187
+ if (prHeadBehindBase({ mergeStateStatus: mergeState })) throw new PrHeadBehindBaseError(message2);
40188
+ }
40189
+ if (isGitHubRateLimitError(e)) {
40190
+ if (o.auto) {
40191
+ throw new Error(`gh pr merge ${number}: GraphQL rate limit exhausted, and enabling auto-merge has no REST equivalent \u2014 retry without --auto to merge now via REST, or retry --auto after the pool resets. (${message2.trim()})`);
40082
40192
  }
40083
- const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
40084
- if (note) throw new Error(`gh pr merge ${number}: ${note}`);
40085
- if (isGitHubRateLimitError(e)) {
40086
- if (o.auto) {
40087
- throw new Error(`gh pr merge ${number}: GraphQL rate limit exhausted, and enabling auto-merge has no REST equivalent \u2014 retry without --auto to merge now via REST, or retry --auto after the pool resets. (${message2.trim()})`);
40193
+ if (!repoForPostCleanup) throw e;
40194
+ console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
40195
+ const commitMessage = bodyFile ? (0, import_node_fs43.readFileSync)(bodyFile, "utf8") : void 0;
40196
+ await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
40197
+ body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
40198
+ timeoutMs: GH_MUTATION_TIMEOUT_MS
40199
+ });
40200
+ return;
40201
+ }
40202
+ if (!o.auto && mergeRejectedBaseBranchModified(message2)) {
40203
+ console.warn(`pr merge: the base branch was modified by a concurrent merge \u2014 waiting ${PR_MERGE_BASE_RACE_RETRY_DELAY_MS / 1e3}s and retrying PR #${number}'s merge once (#6052).`);
40204
+ await new Promise((resolve7) => setTimeout(resolve7, PR_MERGE_BASE_RACE_RETRY_DELAY_MS));
40205
+ await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
40206
+ const m2 = String(e2.message || "");
40207
+ if (/already been merged/i.test(m2)) {
40208
+ remoteNotAttemptedReason = "pr-already-merged";
40209
+ return;
40088
40210
  }
40089
- if (!repoForPostCleanup) throw e;
40090
- console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
40091
- const commitMessage = bodyFile ? (0, import_node_fs43.readFileSync)(bodyFile, "utf8") : void 0;
40092
- await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
40093
- body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
40094
- timeoutMs: GH_MUTATION_TIMEOUT_MS
40095
- });
40096
- return;
40097
- }
40098
- if (!o.auto && mergeRejectedBaseBranchModified(message2)) {
40099
- console.warn(`pr merge: the base branch was modified by a concurrent merge \u2014 waiting ${PR_MERGE_BASE_RACE_RETRY_DELAY_MS / 1e3}s and retrying PR #${number}'s merge once (#6052).`);
40100
- await new Promise((resolve7) => setTimeout(resolve7, PR_MERGE_BASE_RACE_RETRY_DELAY_MS));
40101
- await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
40102
- const m2 = String(e2.message || "");
40103
- if (/already been merged/i.test(m2)) {
40104
- remoteNotAttemptedReason = "pr-already-merged";
40105
- return;
40106
- }
40107
- const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
40108
- if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
40109
- if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
40110
- });
40111
- return;
40112
- }
40113
- if (o.auto && mergeAutoRejectedPrAlreadyClean(message2)) {
40114
- await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
40115
- const m2 = String(e2.message || "");
40116
- if (/already been merged/i.test(m2)) {
40117
- remoteNotAttemptedReason = "pr-already-merged";
40118
- return;
40119
- }
40120
- const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
40121
- if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
40122
- if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
40123
- });
40124
- return;
40125
- }
40126
- if (!o.auto && basePolicyBlocksImmediateMerge(message2)) {
40127
- console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
40128
- upgradedToAuto = true;
40129
- await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
40130
- const m2 = String(e2.message || "");
40131
- if (/already been merged/i.test(m2)) {
40132
- remoteNotAttemptedReason = "pr-already-merged";
40133
- return;
40134
- }
40135
- const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
40136
- if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
40137
- if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
40138
- });
40139
- return;
40140
- }
40141
- if (!ghPrMergeLocalBranchDeleteWarning(message2)) throw e;
40142
- });
40211
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
40212
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
40213
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
40214
+ });
40215
+ return;
40216
+ }
40217
+ if (o.auto && mergeAutoRejectedPrAlreadyClean(message2)) {
40218
+ await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
40219
+ const m2 = String(e2.message || "");
40220
+ if (/already been merged/i.test(m2)) {
40221
+ remoteNotAttemptedReason = "pr-already-merged";
40222
+ return;
40223
+ }
40224
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
40225
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
40226
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
40227
+ });
40228
+ return;
40229
+ }
40230
+ if (!o.auto && basePolicyBlocksImmediateMerge(message2)) {
40231
+ console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
40232
+ upgradedToAuto = true;
40233
+ await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
40234
+ const m2 = String(e2.message || "");
40235
+ if (/already been merged/i.test(m2)) {
40236
+ remoteNotAttemptedReason = "pr-already-merged";
40237
+ return;
40238
+ }
40239
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
40240
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
40241
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
40242
+ });
40243
+ return;
40244
+ }
40245
+ if (!ghPrMergeLocalBranchDeleteWarning(message2)) throw e;
40246
+ });
40247
+ try {
40248
+ try {
40249
+ await mergeOnce();
40250
+ } catch (e) {
40251
+ if (!(e instanceof PrHeadBehindBaseError)) throw e;
40252
+ const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
40253
+ console.warn(`pr merge: GitHub refused the merge \u2014 PR #${number}'s head is BEHIND ${baseRef} \u2014 updating ${headRef} from the base (${localCheckedOut ? "local merge commit + push" : "GitHub update-branch"}) and retrying once (#6263).`);
40254
+ branchUpdated = await updatePrHeadForMerge({ prNumber: number, repo: await requireRepo(o.repo), head: headRef, base: baseRef, localCheckedOut }).catch((e2) => {
40255
+ throw new Error(`pr merge: ${e2.message}`);
40256
+ });
40257
+ if (o.wait && !await runWaitGate()) return;
40258
+ await mergeOnce();
40259
+ }
40143
40260
  } finally {
40144
40261
  overrideBody?.cleanup();
40145
40262
  }
@@ -40174,7 +40291,8 @@ ${list}`);
40174
40291
  pr: number,
40175
40292
  branch: headRef,
40176
40293
  state: stateRead.ok ? stateRead.state : "unknown",
40177
- cleanupStatus: "skipped"
40294
+ cleanupStatus: "skipped",
40295
+ ...branchUpdated ? { branchUpdated } : {}
40178
40296
  }));
40179
40297
  console.error(`pr merge: ${gate.message}. Nothing was deleted \u2014 re-check the PR and retry.`);
40180
40298
  process.exitCode = 1;
@@ -40285,6 +40403,7 @@ ${list}`);
40285
40403
  ...methodField ? { method: methodField } : {},
40286
40404
  remoteBranch,
40287
40405
  housekeeping,
40406
+ ...branchUpdated ? { branchUpdated } : {},
40288
40407
  ...foreignCwd ? { foreignCwd: true, ...foreignCheckout ? { foreignCheckout } : {} } : {},
40289
40408
  ...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
40290
40409
  ...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.16",
3
+ "version": "4.3.17",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",