@mutmutco/cli 3.7.0 → 3.8.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 +39 -14
  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 {
@@ -10038,7 +10038,7 @@ var defaultSleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
10038
10038
  function resolveSleep(deps) {
10039
10039
  return deps.sleep ?? defaultSleep2;
10040
10040
  }
10041
- var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
10041
+ var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion,startedAt:.started_at}]";
10042
10042
  var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
10043
10043
  var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
10044
10044
  var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
@@ -10046,6 +10046,7 @@ var TRAIN_CHECK_ATTEMPTS = 40;
10046
10046
  var TRAIN_CHECK_DELAY_MS = 15e3;
10047
10047
  var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
10048
10048
  var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
10049
+ var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
10049
10050
  async function correlateRun(deps, args) {
10050
10051
  const sleep = resolveSleep(deps);
10051
10052
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
@@ -10103,12 +10104,19 @@ async function correlateWorkflowRun(deps, args) {
10103
10104
  }
10104
10105
  async function watchTenantRun(deps, runId, repo = HUB_REPO3) {
10105
10106
  if (runId == null) return "pending";
10107
+ let watchSaidSuccess = true;
10106
10108
  try {
10107
10109
  await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
10108
- return "success";
10109
10110
  } catch {
10110
- return "failure";
10111
+ watchSaidSuccess = false;
10111
10112
  }
10113
+ try {
10114
+ const out = await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "status,conclusion"]);
10115
+ const parsed = JSON.parse(out);
10116
+ if (parsed.status === "completed") return parsed.conclusion === "success" ? "success" : "failure";
10117
+ } catch {
10118
+ }
10119
+ return watchSaidSuccess ? "success" : "failure";
10112
10120
  }
10113
10121
  async function fetchControlRunLog(deps, runId) {
10114
10122
  try {
@@ -10220,10 +10228,16 @@ function resolveContextState(context, checkRuns, statuses) {
10220
10228
  if (sawSuccess) return "success";
10221
10229
  return sawFailure ? "failed" : "pending";
10222
10230
  }
10223
- async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10231
+ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince) {
10224
10232
  if (required.length === 0) {
10225
10233
  return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
10226
10234
  }
10235
+ const freshThreshold = freshSince !== void 0 ? freshSince - CORRELATE_SKEW_SLACK_MS : void 0;
10236
+ const isFreshRun = (r) => {
10237
+ if (freshThreshold === void 0) return true;
10238
+ const t = Date.parse(String(r.startedAt ?? ""));
10239
+ return !Number.isFinite(t) || t >= freshThreshold;
10240
+ };
10227
10241
  const sleep = resolveSleep(deps);
10228
10242
  let lastStatus = "not checked";
10229
10243
  let lastError;
@@ -10256,7 +10270,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10256
10270
  }
10257
10271
  }
10258
10272
  const pending = required.filter((c) => !autoSatisfied.has(c));
10259
- const states = pending.map((c) => [c, resolveContextState(c, checkRuns, statuses)]);
10273
+ const holdStale = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
10274
+ const states = pending.map((c) => {
10275
+ if (holdStale) {
10276
+ const runsFor = checkRuns.filter((r) => r.name === c);
10277
+ if (runsFor.length > 0 && !runsFor.some(isFreshRun)) return [c, "pending"];
10278
+ }
10279
+ return [c, resolveContextState(c, checkRuns, statuses)];
10280
+ });
10260
10281
  const satisfiedNote = autoSatisfied.size ? `, auto-satisfied (never runs on a tag, see #2404): ${[...autoSatisfied].join(", ")}` : "";
10261
10282
  lastStatus = `${states.map(([c, s]) => `${c}=${s}`).join(", ")}${satisfiedNote}`;
10262
10283
  const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
@@ -10308,14 +10329,14 @@ async function ensureTagPushed(deps, tag, sha) {
10308
10329
  if (localSha && localSha !== sha) {
10309
10330
  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
10331
  }
10311
- return `tag ${tag} already on origin at ${sha.slice(0, 7)} \u2014 resumed without re-pushing`;
10332
+ return { note: `tag ${tag} already on origin at ${sha.slice(0, 7)} \u2014 resumed without re-pushing`, pushed: false };
10312
10333
  }
10313
10334
  if (localSha && localSha !== sha) {
10314
10335
  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
10336
  }
10316
10337
  if (!localSha) await deps.run("git", ["tag", tag, sha]);
10317
10338
  await deps.run("git", ["push", "origin", tag]);
10318
- return `tag ${tag} pushed at ${sha.slice(0, 7)}`;
10339
+ return { note: `tag ${tag} pushed at ${sha.slice(0, 7)}`, pushed: true };
10319
10340
  }
10320
10341
  async function resolveRcResumeTag(deps, base, sha) {
10321
10342
  const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
@@ -10621,11 +10642,12 @@ async function runFoldStage(deps, startBranch, preFold, fn) {
10621
10642
  }
10622
10643
  }
10623
10644
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
10624
- await ensureTagPushed(deps, tag, releaseSha);
10645
+ const tagPush = await ensureTagPushed(deps, tag, releaseSha);
10646
+ const tagPushSince = (deps.now ?? Date.now)();
10625
10647
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
10626
10648
  let checks;
10627
10649
  try {
10628
- checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
10650
+ checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
10629
10651
  } catch (e) {
10630
10652
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
10631
10653
  }
@@ -10677,11 +10699,12 @@ async function runTrainApplyPipeline(mode, input) {
10677
10699
  const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
10678
10700
  const tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
10679
10701
  const resumeNote = resume.tag ? resume.note : void 0;
10680
- await ensureTagPushed(deps, tag2, rcSha);
10702
+ const tagPush = await ensureTagPushed(deps, tag2, rcSha);
10703
+ const tagPushSince = (deps.now ?? Date.now)();
10681
10704
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
10682
10705
  let checks2;
10683
10706
  try {
10684
- checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
10707
+ checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
10685
10708
  } catch (e) {
10686
10709
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
10687
10710
  }
@@ -11383,9 +11406,11 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
11383
11406
  await deps.run("git", ["merge-base", "--is-ancestor", mergedSha, "origin/main"]).catch(() => {
11384
11407
  throw new Error(`merged hotfix SHA ${mergedSha.slice(0, 7)} is not on origin/main \u2014 refusing to tag`);
11385
11408
  });
11386
- const tagNote = await ensureTagPushed(deps, tag, mergedSha);
11409
+ const tagPush = await ensureTagPushed(deps, tag, mergedSha);
11410
+ const tagPushSince = Date.now();
11411
+ const tagNote = tagPush.note;
11387
11412
  const required = await discoverRequiredCheckContexts(deps, ctx, "main");
11388
- const checks = await waitForRequiredTrainChecks(deps, ctx, mergedSha, required);
11413
+ const checks = await waitForRequiredTrainChecks(deps, ctx, mergedSha, required, tagPush.pushed ? tagPushSince : void 0);
11389
11414
  let releaseNote;
11390
11415
  let releaseExists = false;
11391
11416
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.7.0",
3
+ "version": "3.8.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",