@mutmutco/cli 3.7.0 → 3.9.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 +181 -38
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -9270,7 +9270,7 @@ function parseNpmVersion(stdout) {
9270
9270
  return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v) ? v : void 0;
9271
9271
  }
9272
9272
  function staleTrainCliMessage(report, commandName) {
9273
- return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path`;
9273
+ return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path. This gate runs before any train mutation, so this invocation changed nothing; any partial train state (local merge, pushed tag) is from an earlier run and the rerun resumes it safely`;
9274
9274
  }
9275
9275
  async function resolveReleasedVersion(runners) {
9276
9276
  try {
@@ -9750,6 +9750,19 @@ function requireValue(value, label) {
9750
9750
  if (!value) throw new Error(`${label} could not be resolved`);
9751
9751
  return value;
9752
9752
  }
9753
+ function surfaceTrainSubprocessFailure(e, file, args) {
9754
+ if (!(e instanceof Error)) return e;
9755
+ const err = e;
9756
+ const stderr = typeof err.stderr === "string" ? err.stderr.trim() : "";
9757
+ const stdout = typeof err.stdout === "string" ? err.stdout.trim() : "";
9758
+ const cmd = `${file} ${args.join(" ")}`.trim();
9759
+ if (!stderr && !stdout) {
9760
+ err.message = `${cmd} failed with no stderr output (exit ${err.code ?? "?"}) \u2014 likely a transient subprocess/network error. [${err.message}]`;
9761
+ } else if (/^Command failed/i.test(err.message) && stderr) {
9762
+ err.message = `${cmd} failed: ${stderr}`;
9763
+ }
9764
+ return err;
9765
+ }
9753
9766
  function planTrainApplyRepoGuard(applyRepo, cwdRepo, rerun) {
9754
9767
  if (cwdRepo && cwdRepo.toLowerCase() === applyRepo.toLowerCase()) return { ok: true };
9755
9768
  const where = cwdRepo ? `this checkout is ${cwdRepo}` : "this checkout could not be identified";
@@ -9817,14 +9830,14 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
9817
9830
  if (tagSha !== expectedSha) {
9818
9831
  throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
9819
9832
  }
9820
- const branchOut = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
9833
+ const branchOut = clean(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
9821
9834
  const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
9822
9835
  if (branchSha !== expectedSha) {
9823
9836
  throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
9824
9837
  }
9825
9838
  }
9826
9839
  async function ffOnlyPull(deps, branch) {
9827
- await deps.run("git", ["pull", "--ff-only", "origin", branch]);
9840
+ await runGitRemoteRead(deps, ["pull", "--ff-only", "origin", branch]);
9828
9841
  }
9829
9842
  var RELEASE_TOLERATED_PATHS = [".gitignore"];
9830
9843
  var MERGE_TREE_PREFLIGHT_ATTEMPTS = 3;
@@ -9878,7 +9891,7 @@ function ensurePositiveCount(out, emptyMessage) {
9878
9891
  if (count <= 0) throw new Error(emptyMessage);
9879
9892
  }
9880
9893
  async function remoteBranchExists(deps, branch) {
9881
- const out = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
9894
+ const out = clean(await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/heads/${branch}`]));
9882
9895
  return out.length > 0;
9883
9896
  }
9884
9897
  async function loadReleaseTrackBranchHints(deps) {
@@ -10038,7 +10051,33 @@ var defaultSleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
10038
10051
  function resolveSleep(deps) {
10039
10052
  return deps.sleep ?? defaultSleep2;
10040
10053
  }
10041
- var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
10054
+ var GIT_NETWORK_ATTEMPTS = 3;
10055
+ var GIT_NETWORK_RETRY_DELAY_MS = 1e3;
10056
+ function isPersistentGitPushFailure(e) {
10057
+ const msg = `${e instanceof Error ? e.message : String(e)} ${String(e.stderr ?? "")}`;
10058
+ return /non-fast-forward|\[rejected\]|failed to push|protected branch|hook declined|permission denied|could not read Username|authentication failed|Authentication failed|403 Forbidden|GH006|GH013/i.test(msg);
10059
+ }
10060
+ async function runGitWithRetry(deps, args, shouldRetry) {
10061
+ const sleep = resolveSleep(deps);
10062
+ let lastError;
10063
+ for (let attempt = 1; attempt <= GIT_NETWORK_ATTEMPTS; attempt++) {
10064
+ try {
10065
+ return await deps.run("git", args);
10066
+ } catch (e) {
10067
+ lastError = e;
10068
+ if (attempt >= GIT_NETWORK_ATTEMPTS || !shouldRetry(e)) throw e;
10069
+ await sleep(GIT_NETWORK_RETRY_DELAY_MS * attempt);
10070
+ }
10071
+ }
10072
+ throw lastError;
10073
+ }
10074
+ function runGitRemoteRead(deps, args) {
10075
+ return runGitWithRetry(deps, args, () => true);
10076
+ }
10077
+ function runGitPush(deps, args) {
10078
+ return runGitWithRetry(deps, args, (e) => !isPersistentGitPushFailure(e));
10079
+ }
10080
+ var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion,startedAt:.started_at}]";
10042
10081
  var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
10043
10082
  var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
10044
10083
  var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
@@ -10046,6 +10085,7 @@ var TRAIN_CHECK_ATTEMPTS = 40;
10046
10085
  var TRAIN_CHECK_DELAY_MS = 15e3;
10047
10086
  var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
10048
10087
  var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
10088
+ var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
10049
10089
  async function correlateRun(deps, args) {
10050
10090
  const sleep = resolveSleep(deps);
10051
10091
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
@@ -10103,12 +10143,19 @@ async function correlateWorkflowRun(deps, args) {
10103
10143
  }
10104
10144
  async function watchTenantRun(deps, runId, repo = HUB_REPO3) {
10105
10145
  if (runId == null) return "pending";
10146
+ let watchSaidSuccess = true;
10106
10147
  try {
10107
10148
  await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
10108
- return "success";
10109
10149
  } catch {
10110
- return "failure";
10150
+ watchSaidSuccess = false;
10151
+ }
10152
+ try {
10153
+ const out = await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "status,conclusion"]);
10154
+ const parsed = JSON.parse(out);
10155
+ if (parsed.status === "completed") return parsed.conclusion === "success" ? "success" : "failure";
10156
+ } catch {
10111
10157
  }
10158
+ return watchSaidSuccess ? "success" : "failure";
10112
10159
  }
10113
10160
  async function fetchControlRunLog(deps, runId) {
10114
10161
  try {
@@ -10220,10 +10267,16 @@ function resolveContextState(context, checkRuns, statuses) {
10220
10267
  if (sawSuccess) return "success";
10221
10268
  return sawFailure ? "failed" : "pending";
10222
10269
  }
10223
- async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10270
+ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince) {
10224
10271
  if (required.length === 0) {
10225
10272
  return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
10226
10273
  }
10274
+ const freshThreshold = freshSince !== void 0 ? freshSince - CORRELATE_SKEW_SLACK_MS : void 0;
10275
+ const isFreshRun = (r) => {
10276
+ if (freshThreshold === void 0) return true;
10277
+ const t = Date.parse(String(r.startedAt ?? ""));
10278
+ return !Number.isFinite(t) || t >= freshThreshold;
10279
+ };
10227
10280
  const sleep = resolveSleep(deps);
10228
10281
  let lastStatus = "not checked";
10229
10282
  let lastError;
@@ -10256,7 +10309,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10256
10309
  }
10257
10310
  }
10258
10311
  const pending = required.filter((c) => !autoSatisfied.has(c));
10259
- const states = pending.map((c) => [c, resolveContextState(c, checkRuns, statuses)]);
10312
+ const holdStale = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
10313
+ const states = pending.map((c) => {
10314
+ if (holdStale) {
10315
+ const runsFor = checkRuns.filter((r) => r.name === c);
10316
+ if (runsFor.length > 0 && !runsFor.some(isFreshRun)) return [c, "pending"];
10317
+ }
10318
+ return [c, resolveContextState(c, checkRuns, statuses)];
10319
+ });
10260
10320
  const satisfiedNote = autoSatisfied.size ? `, auto-satisfied (never runs on a tag, see #2404): ${[...autoSatisfied].join(", ")}` : "";
10261
10321
  lastStatus = `${states.map(([c, s]) => `${c}=${s}`).join(", ")}${satisfiedNote}`;
10262
10322
  const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
@@ -10291,9 +10351,12 @@ Recovery sequence:
10291
10351
  Do not delete or force-move the pushed tag; rerun the train only after confirming the branch, release, and deploy states above.`
10292
10352
  );
10293
10353
  }
10294
- async function ensureTagPushed(deps, tag, sha) {
10295
- const remoteOut = await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]);
10296
- const remoteSha = clean(remoteOut).split(/\s+/)[0] || "";
10354
+ async function probeRemoteTag(deps, tag) {
10355
+ const remoteOut = await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/tags/${tag}`]);
10356
+ return clean(remoteOut).split(/\s+/)[0] || "";
10357
+ }
10358
+ async function ensureTagPushed(deps, tag, sha, probed) {
10359
+ const remoteSha = probed ? probed.remoteSha : await probeRemoteTag(deps, tag);
10297
10360
  let localSha = "";
10298
10361
  try {
10299
10362
  localSha = clean(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
@@ -10308,17 +10371,20 @@ async function ensureTagPushed(deps, tag, sha) {
10308
10371
  if (localSha && localSha !== sha) {
10309
10372
  throw new Error(`local tag ${tag} points at ${localSha} but origin has it at ${sha} \u2014 delete the stale local tag (git tag -d ${tag}) and rerun`);
10310
10373
  }
10311
- return `tag ${tag} already on origin at ${sha.slice(0, 7)} \u2014 resumed without re-pushing`;
10374
+ return { note: `tag ${tag} already on origin at ${sha.slice(0, 7)} \u2014 resumed without re-pushing`, pushed: false };
10312
10375
  }
10313
10376
  if (localSha && localSha !== sha) {
10314
10377
  throw new Error(`local tag ${tag} already points at ${localSha}, not the intended ${sha} \u2014 delete the stale local tag (git tag -d ${tag}) and rerun`);
10315
10378
  }
10316
10379
  if (!localSha) await deps.run("git", ["tag", tag, sha]);
10317
- await deps.run("git", ["push", "origin", tag]);
10318
- return `tag ${tag} pushed at ${sha.slice(0, 7)}`;
10380
+ await runGitPush(deps, ["push", "origin", tag]);
10381
+ return { note: `tag ${tag} pushed at ${sha.slice(0, 7)}`, pushed: true };
10319
10382
  }
10320
- async function resolveRcResumeTag(deps, base, sha) {
10321
- const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
10383
+ function probeRcResumeTags(deps, base) {
10384
+ return runGitRemoteRead(deps, ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
10385
+ }
10386
+ async function resolveRcResumeTag(deps, base, sha, probed) {
10387
+ const out = probed ?? await runGitRemoteRead(deps, ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
10322
10388
  const onSha = clean(out).split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/)).filter(([refSha]) => refSha === sha).map(([, ref]) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "")).filter((ref) => new RegExp(`^${base.replace(/\./g, "\\.")}-rc\\.\\d+$`).test(ref));
10323
10389
  const unique = [...new Set(onSha)];
10324
10390
  if (unique.length === 0) return { tag: null };
@@ -10569,6 +10635,49 @@ Recovery sequence:
10569
10635
  ${steps.join("\n")}`
10570
10636
  );
10571
10637
  }
10638
+ async function recoverFailedRcand(deps, cause, preRcSha) {
10639
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
10640
+ const branch = await currentBranch(deps).catch(() => "");
10641
+ const mergeInProgress = await deps.run("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).then(() => true).catch(() => false);
10642
+ if (mergeInProgress) {
10643
+ try {
10644
+ await deps.run("git", ["merge", "--abort"]);
10645
+ } catch (e) {
10646
+ return new Error(
10647
+ `${causeMessage}
10648
+
10649
+ rcand failed with an in-progress merge on ${branch || "(unknown branch)"}; automatic \`git merge --abort\` failed: ${e instanceof Error ? e.message : String(e)}. Nothing was pushed to origin.
10650
+ Recovery sequence:
10651
+ 1. git merge --abort
10652
+ 2. git checkout development
10653
+ 3. git branch -f rc origin/rc`
10654
+ );
10655
+ }
10656
+ }
10657
+ const rcSha = await deps.run("git", ["rev-parse", "rc"]).then(clean).catch(() => "");
10658
+ const rcDescends = Boolean(preRcSha) && Boolean(rcSha) ? await deps.run("git", ["merge-base", "--is-ancestor", preRcSha, "rc"]).then(() => true).catch(() => false) : false;
10659
+ if (branch === "rc" && rcDescends) {
10660
+ try {
10661
+ await deps.run("git", ["reset", "--hard", preRcSha]);
10662
+ await deps.run("git", ["checkout", "development"]);
10663
+ return new Error(
10664
+ `${causeMessage}
10665
+
10666
+ rcand failed after \`git checkout rc\`; local rc was reset to its pre-merge state (${shaLabel(preRcSha)}) and the checkout returned to development. Nothing was pushed to origin. Rerun \`mmi-cli rcand --apply\`.`
10667
+ );
10668
+ } catch {
10669
+ }
10670
+ }
10671
+ return new Error(
10672
+ `${causeMessage}
10673
+
10674
+ rcand failed leaving the checkout on ${branch || "(unknown)"} (pre-merge rc=${preRcSha ? shaLabel(preRcSha) : "not captured"}). Nothing was pushed to origin.
10675
+ Recovery sequence:
10676
+ 1. git checkout development
10677
+ 2. git branch -f rc origin/rc # discard the unpushed local rc merge
10678
+ 3. mmi-cli rcand --apply`
10679
+ );
10680
+ }
10572
10681
  async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha) {
10573
10682
  const causeMessage = cause instanceof Error ? cause.message : String(cause);
10574
10683
  const probe = await probeFoldFailureState(deps);
@@ -10620,16 +10729,22 @@ async function runFoldStage(deps, startBranch, preFold, fn) {
10620
10729
  throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
10621
10730
  }
10622
10731
  }
10623
- async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
10624
- await ensureTagPushed(deps, tag, releaseSha);
10732
+ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe) {
10733
+ let tagPush;
10734
+ try {
10735
+ tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe);
10736
+ } catch (e) {
10737
+ throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
10738
+ }
10739
+ const tagPushSince = (deps.now ?? Date.now)();
10625
10740
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
10626
10741
  let checks;
10627
10742
  try {
10628
- checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
10743
+ checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
10629
10744
  } catch (e) {
10630
10745
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
10631
10746
  }
10632
- await deps.run("git", ["push", "origin", "main"]);
10747
+ await runGitPush(deps, ["push", "origin", "main"]);
10633
10748
  const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
10634
10749
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
10635
10750
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
@@ -10670,23 +10785,34 @@ async function runTrainApplyPipeline(mode, input) {
10670
10785
  const deployModel2 = await preflight(deps, ctx, "rc", meta);
10671
10786
  const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
10672
10787
  await releaseRcBranchLock(deps);
10788
+ const rcTagProbe = await probeRcResumeTags(deps, releaseBase);
10673
10789
  await deps.run("git", ["checkout", "rc"]);
10674
10790
  await ffOnlyPull(deps, "rc");
10675
- await deps.run("git", ["merge", "development", "--no-edit"]);
10676
- const rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
10677
- const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
10678
- const tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
10791
+ const preRcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "pre-merge rc sha");
10792
+ let rcSha;
10793
+ let resume;
10794
+ let tag2;
10795
+ let tagPush;
10796
+ try {
10797
+ await deps.run("git", ["merge", "development", "--no-edit"]);
10798
+ rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
10799
+ resume = await resolveRcResumeTag(deps, releaseBase, rcSha, rcTagProbe);
10800
+ tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
10801
+ tagPush = await ensureTagPushed(deps, tag2, rcSha);
10802
+ } catch (e) {
10803
+ throw await recoverFailedRcand(deps, e, preRcSha);
10804
+ }
10679
10805
  const resumeNote = resume.tag ? resume.note : void 0;
10680
- await ensureTagPushed(deps, tag2, rcSha);
10806
+ const tagPushSince = (deps.now ?? Date.now)();
10681
10807
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
10682
10808
  let checks2;
10683
10809
  try {
10684
- checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
10810
+ checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
10685
10811
  } catch (e) {
10686
10812
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
10687
10813
  }
10688
10814
  const autoRunSince = (deps.now ?? Date.now)();
10689
- await deps.run("git", ["push", "origin", "rc"]);
10815
+ await runGitPush(deps, ["push", "origin", "rc"]);
10690
10816
  const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
10691
10817
  return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
10692
10818
  }
@@ -10718,6 +10844,7 @@ async function runTrainApplyPipeline(mode, input) {
10718
10844
  "development -> main merge would conflict on untolerated path(s)",
10719
10845
  "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
10720
10846
  );
10847
+ const tagProbe2 = { remoteSha: await probeRemoteTag(deps, tag2) };
10721
10848
  const preFold2 = {};
10722
10849
  const { versionFold: versionFold2, releaseSha: releaseSha2 } = await runFoldStage(deps, "development", preFold2, async () => {
10723
10850
  await executeMergeToMain(deps, "development", "development -> main", tolerated2, predicted2, preFold2);
@@ -10725,7 +10852,7 @@ async function runTrainApplyPipeline(mode, input) {
10725
10852
  const releaseSha3 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10726
10853
  return { versionFold: versionFold3, releaseSha: releaseSha3 };
10727
10854
  });
10728
- const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
10855
+ const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2, "development", preFold2, tagProbe2);
10729
10856
  const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
10730
10857
  if (directTrack) {
10731
10858
  return {
@@ -10801,15 +10928,16 @@ async function runTrainApplyPipeline(mode, input) {
10801
10928
  );
10802
10929
  }
10803
10930
  const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
10931
+ const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
10932
+ const tagProbe = { remoteSha: await probeRemoteTag(deps, tag) };
10804
10933
  const preFold = {};
10805
- const { tag, versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
10934
+ const { versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
10806
10935
  await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted, preFold);
10807
- const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
10808
- const versionFold2 = await foldReleaseVersion(deps, deployModel, tag2, foldPaths);
10936
+ const versionFold2 = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
10809
10937
  const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
10810
- return { tag: tag2, versionFold: versionFold2, releaseSha: releaseSha2 };
10938
+ return { versionFold: versionFold2, releaseSha: releaseSha2 };
10811
10939
  });
10812
- const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
10940
+ const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, "rc", preFold, tagProbe);
10813
10941
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
10814
10942
  const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
10815
10943
  const rcAlignment = await pushRcAlignment(deps);
@@ -10843,7 +10971,7 @@ async function runTrainApply(command, deps, options = {}) {
10843
10971
  const watch = options.watch ?? false;
10844
10972
  const ctx = await buildTrainApplyContext(deps);
10845
10973
  await requireCleanTree(deps);
10846
- await deps.run("git", ["fetch", "origin"]);
10974
+ await runGitRemoteRead(deps, ["fetch", "origin"]);
10847
10975
  const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
10848
10976
  const branchHints = await loadReleaseTrackBranchHints(deps);
10849
10977
  const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
@@ -11383,9 +11511,11 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
11383
11511
  await deps.run("git", ["merge-base", "--is-ancestor", mergedSha, "origin/main"]).catch(() => {
11384
11512
  throw new Error(`merged hotfix SHA ${mergedSha.slice(0, 7)} is not on origin/main \u2014 refusing to tag`);
11385
11513
  });
11386
- const tagNote = await ensureTagPushed(deps, tag, mergedSha);
11514
+ const tagPush = await ensureTagPushed(deps, tag, mergedSha);
11515
+ const tagPushSince = Date.now();
11516
+ const tagNote = tagPush.note;
11387
11517
  const required = await discoverRequiredCheckContexts(deps, ctx, "main");
11388
- const checks = await waitForRequiredTrainChecks(deps, ctx, mergedSha, required);
11518
+ const checks = await waitForRequiredTrainChecks(deps, ctx, mergedSha, required, tagPush.pushed ? tagPushSince : void 0);
11389
11519
  let releaseNote;
11390
11520
  let releaseExists = false;
11391
11521
  try {
@@ -13634,6 +13764,11 @@ function parseFofuEnabledVar(raw) {
13634
13764
  if (raw === "false") return false;
13635
13765
  throw new Error("project set: fofuEnabled must be true or false");
13636
13766
  }
13767
+ function parseRuntimeVaultOnlyVar(raw) {
13768
+ if (raw === "true") return true;
13769
+ if (raw === "false") return false;
13770
+ throw new Error("project set: runtimeVaultOnly must be true or false");
13771
+ }
13637
13772
  function parseConsumesDesignSystemVar(raw) {
13638
13773
  if (raw === "fofu") return raw;
13639
13774
  throw new Error('project set: consumesDesignSystem must be "fofu"');
@@ -13711,6 +13846,7 @@ var SETTABLE_VAR_KEYS = [
13711
13846
  "dsManifestPath",
13712
13847
  "fofuEnabled",
13713
13848
  "consumesDesignSystem",
13849
+ "runtimeVaultOnly",
13714
13850
  "requiredGcpApis",
13715
13851
  "requiredRuntimeSecrets",
13716
13852
  "requiredBuildSecrets",
@@ -13733,6 +13869,7 @@ var SETTABLE_VAR_HINTS = {
13733
13869
  dsManifestPath: "relative path to a package.json, e.g. web/package.json",
13734
13870
  fofuEnabled: "true|false",
13735
13871
  consumesDesignSystem: '"fofu"',
13872
+ runtimeVaultOnly: "true|false",
13736
13873
  repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
13737
13874
  oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
13738
13875
  requiredGcpApis: "comma-string",
@@ -13814,6 +13951,8 @@ function buildProjectSetPatch(input) {
13814
13951
  patch[key] = parsePublishRequiredVar(raw);
13815
13952
  } else if (key === "fofuEnabled") {
13816
13953
  patch[key] = parseFofuEnabledVar(raw);
13954
+ } else if (key === "runtimeVaultOnly") {
13955
+ patch[key] = parseRuntimeVaultOnlyVar(raw);
13817
13956
  } else if (key === "consumesDesignSystem") {
13818
13957
  patch[key] = parseConsumesDesignSystemVar(raw);
13819
13958
  } else if (key === "publishDir") {
@@ -20862,7 +21001,11 @@ function trainApplyDeps() {
20862
21001
  return {
20863
21002
  run: async (file, args) => {
20864
21003
  const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
20865
- return isWin2 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
21004
+ try {
21005
+ return isWin2 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
21006
+ } catch (e) {
21007
+ throw surfaceTrainSubprocessFailure(e, file, args);
21008
+ }
20866
21009
  },
20867
21010
  runSelf: async (args) => (await execFileP2(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
20868
21011
  trainAuthority: async (repo) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.7.0",
3
+ "version": "3.9.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",