@mutmutco/cli 4.0.15 → 4.0.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.
- package/dist/main.cjs +317 -23
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5035,16 +5035,26 @@ function isAgentScratchPath(path2) {
|
|
|
5035
5035
|
return /^tmp_[^/]+$/.test(normalized);
|
|
5036
5036
|
}
|
|
5037
5037
|
var PUSH_WALL_AGENT_CONFIG_DIRS = [".claude/", ".cursor/rules/", ".codex/", ".agents/"];
|
|
5038
|
+
var LIVE_SESSION_DIRS = [".jerv/", ".pi/"];
|
|
5039
|
+
function pathUnderDirPrefix(path2, dir) {
|
|
5040
|
+
return path2 === dir || path2 === dir.slice(0, -1) || path2.startsWith(dir) || path2.includes(`/${dir}`);
|
|
5041
|
+
}
|
|
5038
5042
|
function isUncommittableAgentConfigPath(path2) {
|
|
5039
5043
|
const normalized = path2.replace(/\\/g, "/").trim();
|
|
5040
|
-
return PUSH_WALL_AGENT_CONFIG_DIRS.some((dir) => normalized
|
|
5044
|
+
return PUSH_WALL_AGENT_CONFIG_DIRS.some((dir) => pathUnderDirPrefix(normalized, dir));
|
|
5045
|
+
}
|
|
5046
|
+
function isLiveSessionPath(path2) {
|
|
5047
|
+
const normalized = path2.replace(/\\/g, "/").trim();
|
|
5048
|
+
return LIVE_SESSION_DIRS.some((dir) => pathUnderDirPrefix(normalized, dir));
|
|
5041
5049
|
}
|
|
5042
5050
|
function splitPorcelainLine(line) {
|
|
5043
5051
|
return { status: line.slice(0, 2), path: line.slice(3).split(" -> ")[0]?.trim() ?? "" };
|
|
5044
5052
|
}
|
|
5045
5053
|
function isExemptPorcelainLine(status, path2) {
|
|
5046
5054
|
if (isAgentScratchPath(path2)) return true;
|
|
5047
|
-
|
|
5055
|
+
if (status !== "??") return false;
|
|
5056
|
+
if (isUncommittableAgentConfigPath(path2)) return true;
|
|
5057
|
+
return isLiveSessionPath(path2);
|
|
5048
5058
|
}
|
|
5049
5059
|
function porcelainBlockingPaths(porcelain) {
|
|
5050
5060
|
const blocking = [];
|
|
@@ -5059,12 +5069,21 @@ function porcelainHasBlockingChanges(porcelain) {
|
|
|
5059
5069
|
return porcelainBlockingPaths(porcelain).length > 0;
|
|
5060
5070
|
}
|
|
5061
5071
|
var BLOCKING_PATH_PREVIEW_LIMIT = 10;
|
|
5072
|
+
function porcelainHasBlockingUntracked(porcelain) {
|
|
5073
|
+
for (const line of porcelain.split("\n")) {
|
|
5074
|
+
if (!line.trim()) continue;
|
|
5075
|
+
const { status, path: path2 } = splitPorcelainLine(line);
|
|
5076
|
+
if (path2 !== "" && status === "??" && !isExemptPorcelainLine(status, path2)) return true;
|
|
5077
|
+
}
|
|
5078
|
+
return false;
|
|
5079
|
+
}
|
|
5062
5080
|
function describeBlockingPaths(porcelain) {
|
|
5063
5081
|
const paths = porcelainBlockingPaths(porcelain);
|
|
5064
5082
|
if (paths.length === 0) return "";
|
|
5065
5083
|
const shown = paths.slice(0, BLOCKING_PATH_PREVIEW_LIMIT);
|
|
5066
5084
|
const rest = paths.length - shown.length;
|
|
5067
|
-
|
|
5085
|
+
const hint = porcelainHasBlockingUntracked(porcelain) ? " (untracked: gitignore it, or add it to .git/info/exclude)" : "";
|
|
5086
|
+
return ` \u2014 blocking: ${shown.join(", ")}${rest > 0 ? `, \u2026 and ${rest} more` : ""}${hint}`;
|
|
5068
5087
|
}
|
|
5069
5088
|
|
|
5070
5089
|
// src/local-train-sync.ts
|
|
@@ -5614,6 +5633,12 @@ var PR_CHECKS_FAILURE_CONFIRMATIONS = 2;
|
|
|
5614
5633
|
var PR_CHECKS_RATELIMIT_FLOOR = 50;
|
|
5615
5634
|
var PR_CHECKS_RATELIMIT_JITTER_MAX_MS = 5e3;
|
|
5616
5635
|
var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
|
|
5636
|
+
var PR_CHECKS_ZERO_RUNS_GRACE_MS = 2 * 6e4;
|
|
5637
|
+
var PR_CHECKS_ZERO_RUNS_CONFIRMATIONS = 2;
|
|
5638
|
+
function zeroRunsDeliveryMessage(headSha) {
|
|
5639
|
+
const tip = headSha ? ` (${headSha.slice(0, 7)})` : "";
|
|
5640
|
+
return `GitHub delivered ZERO workflow runs for this PR head${tip} \u2014 not "checks still pending". Sibling PRs on the same repo can still be healthy while this head is silent. Close/reopen is insufficient. Recourse: push an empty commit on the same branch (\`git commit --allow-empty -m "ci: re-trigger gates" && git push\`) or re-branch onto a new head and open a fresh PR. See docs/Guides/gh-runner-runbook.md \xA7 Zero workflow runs on a PR head.`;
|
|
5641
|
+
}
|
|
5617
5642
|
function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none") {
|
|
5618
5643
|
if (checks === "no-checks-reported") return { action: "proceed" };
|
|
5619
5644
|
const staleNote = `merge CI policy says no-ci (${policyReason}), but live PR checks exist`;
|
|
@@ -5661,6 +5686,7 @@ async function waitForPrChecks(deps) {
|
|
|
5661
5686
|
let lastDetail = "pending";
|
|
5662
5687
|
let successStreak = 0;
|
|
5663
5688
|
let failureStreak = 0;
|
|
5689
|
+
let zeroRunsStreak = 0;
|
|
5664
5690
|
report("starting");
|
|
5665
5691
|
while (now() < deadline) {
|
|
5666
5692
|
if (deps.pollRateLimit) {
|
|
@@ -5694,6 +5720,7 @@ async function waitForPrChecks(deps) {
|
|
|
5694
5720
|
report(state);
|
|
5695
5721
|
if (state !== "success") successStreak = 0;
|
|
5696
5722
|
if (state !== "failure") failureStreak = 0;
|
|
5723
|
+
if (state !== "no-checks-reported") zeroRunsStreak = 0;
|
|
5697
5724
|
if (state === "failure") {
|
|
5698
5725
|
failureStreak += 1;
|
|
5699
5726
|
if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
|
|
@@ -5726,6 +5753,30 @@ async function waitForPrChecks(deps) {
|
|
|
5726
5753
|
}
|
|
5727
5754
|
if (state === "no-checks-reported") {
|
|
5728
5755
|
lastDetail = "no-checks-reported (waiting for workflow to queue)";
|
|
5756
|
+
const elapsed = now() - started;
|
|
5757
|
+
if (deps.pollHeadWorkflowRunCount && elapsed >= PR_CHECKS_ZERO_RUNS_GRACE_MS) {
|
|
5758
|
+
const count = await deps.pollHeadWorkflowRunCount().catch(() => null);
|
|
5759
|
+
if (count === 0) {
|
|
5760
|
+
zeroRunsStreak += 1;
|
|
5761
|
+
if (zeroRunsStreak >= PR_CHECKS_ZERO_RUNS_CONFIRMATIONS) {
|
|
5762
|
+
return {
|
|
5763
|
+
policy,
|
|
5764
|
+
status: "failure",
|
|
5765
|
+
reason: zeroRunsDeliveryMessage(),
|
|
5766
|
+
detail: "zero-runs",
|
|
5767
|
+
waitedMs: elapsed
|
|
5768
|
+
};
|
|
5769
|
+
}
|
|
5770
|
+
lastDetail = "no-checks-reported (confirming zero workflow runs on head)";
|
|
5771
|
+
} else {
|
|
5772
|
+
zeroRunsStreak = 0;
|
|
5773
|
+
if (count !== null && count > 0) {
|
|
5774
|
+
lastDetail = `no-checks-reported (${count} workflow run(s) on head; waiting for check-runs)`;
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
} else {
|
|
5778
|
+
zeroRunsStreak = 0;
|
|
5779
|
+
}
|
|
5729
5780
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
5730
5781
|
continue;
|
|
5731
5782
|
}
|
|
@@ -10832,10 +10883,10 @@ var rollout_plan_default = {
|
|
|
10832
10883
|
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)."
|
|
10833
10884
|
},
|
|
10834
10885
|
baseline: {
|
|
10835
|
-
version: "4.0.
|
|
10836
|
-
tag: "v4.0.
|
|
10837
|
-
commit: "
|
|
10838
|
-
npm: "@mutmutco/cli@4.0.
|
|
10886
|
+
version: "4.0.17",
|
|
10887
|
+
tag: "v4.0.17",
|
|
10888
|
+
commit: "df1e866df4f8",
|
|
10889
|
+
npm: "@mutmutco/cli@4.0.17"
|
|
10839
10890
|
},
|
|
10840
10891
|
exitCriterion: "fleet-n-of-n",
|
|
10841
10892
|
hubOnlyShortcut: "forbidden",
|
|
@@ -10852,14 +10903,14 @@ var rollout_plan_default = {
|
|
|
10852
10903
|
repo: "mutmutco/mmi-hub",
|
|
10853
10904
|
role: "canary",
|
|
10854
10905
|
schedule: "train",
|
|
10855
|
-
v3Target: "v4.0.
|
|
10906
|
+
v3Target: "v4.0.17"
|
|
10856
10907
|
}
|
|
10857
10908
|
],
|
|
10858
10909
|
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.",
|
|
10859
10910
|
rollback: {
|
|
10860
10911
|
independent: true,
|
|
10861
|
-
mechanism: "npm dist-tag latest -> 4.0.
|
|
10862
|
-
v3Target: "v4.0.
|
|
10912
|
+
mechanism: "npm dist-tag latest -> 4.0.17 and redeploy the Hub Lambda from tag v4.0.17 (df1e866df4f8); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
10913
|
+
v3Target: "v4.0.17 (@mutmutco/cli@4.0.17, tag commit df1e866df4f8 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
10863
10914
|
}
|
|
10864
10915
|
},
|
|
10865
10916
|
{
|
|
@@ -13926,8 +13977,39 @@ async function isStrayUnreleasedTag(deps, tag, remoteSha, repo) {
|
|
|
13926
13977
|
}
|
|
13927
13978
|
return deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName"]).then(() => false).catch((e) => /not found|HTTP 404/i.test(e instanceof Error ? e.message : String(e)));
|
|
13928
13979
|
}
|
|
13980
|
+
function cmpReleaseTag(a, b) {
|
|
13981
|
+
const parse = (t) => {
|
|
13982
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(t.trim());
|
|
13983
|
+
return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
|
|
13984
|
+
};
|
|
13985
|
+
const left = parse(a);
|
|
13986
|
+
const right = parse(b);
|
|
13987
|
+
if (!left || !right) throw new Error(`cmpReleaseTag expects vX.Y.Z tags, got ${a} vs ${b}`);
|
|
13988
|
+
return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
|
|
13989
|
+
}
|
|
13990
|
+
async function listRemoteReleaseTags(deps) {
|
|
13991
|
+
const out = await runGitRemoteRead(deps, ["ls-remote", "--tags", "origin", "refs/tags/v*"]);
|
|
13992
|
+
const tags = /* @__PURE__ */ new Set();
|
|
13993
|
+
for (const line of clean2(out).split("\n")) {
|
|
13994
|
+
const ref = line.trim().split(/\s+/)[1] ?? "";
|
|
13995
|
+
const tag = ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, "");
|
|
13996
|
+
if (/^v\d+\.\d+\.\d+$/.test(tag)) tags.add(tag);
|
|
13997
|
+
}
|
|
13998
|
+
return [...tags].sort(cmpReleaseTag);
|
|
13999
|
+
}
|
|
14000
|
+
async function assertComputedReleaseTagStillNext(deps, tag) {
|
|
14001
|
+
if (!/^v\d+\.\d+\.\d+$/.test(tag)) return;
|
|
14002
|
+
const remote = await listRemoteReleaseTags(deps);
|
|
14003
|
+
const newer = remote.filter((t) => cmpReleaseTag(t, tag) > 0);
|
|
14004
|
+
if (newer.length === 0) return;
|
|
14005
|
+
const latest = newer[newer.length - 1];
|
|
14006
|
+
throw new Error(
|
|
14007
|
+
`origin already has release tag ${latest} ahead of this run's computed ${tag} \u2014 another /release landed mid-flight. Refusing to mint a skip-version or hole-fill tag. Stop; let the other train finish (or abort its stray tag/Release with the authorized human's go if verify failed), then rerun mmi-cli devops release --apply so next-version re-derives from the settled tags. Do not run two /release trains on the same repo concurrently.`
|
|
14008
|
+
);
|
|
14009
|
+
}
|
|
13929
14010
|
async function ensureTagPushed(deps, tag, sha, probed, releaseRepo) {
|
|
13930
|
-
const remoteSha =
|
|
14011
|
+
const remoteSha = await probeRemoteTag(deps, tag);
|
|
14012
|
+
const planRemoteSha = probed?.remoteSha ?? "";
|
|
13931
14013
|
let localSha = "";
|
|
13932
14014
|
try {
|
|
13933
14015
|
localSha = clean2(await deps.run("git", ["rev-parse", "--verify", `refs/tags/${tag}^{commit}`]));
|
|
@@ -13936,6 +14018,11 @@ async function ensureTagPushed(deps, tag, sha, probed, releaseRepo) {
|
|
|
13936
14018
|
if (remoteSha) {
|
|
13937
14019
|
if (remoteSha !== sha) {
|
|
13938
14020
|
const mismatch = `tag ${tag} already exists on origin at ${remoteSha}, but this run intends ${sha}`;
|
|
14021
|
+
if (!planRemoteSha && releaseRepo) {
|
|
14022
|
+
throw new Error(
|
|
14023
|
+
`${mismatch}. The tag appeared on origin while this train was promoting \u2014 another /release claimed this version mid-flight. Refusing to mint a competing or skip-version tag. Stop; let the other train finish, or if its tag/Release is a stray (publish verify failed, tagged SHA lacks the fold commit), delete that stray tag and its GitHub Release with the authorized human's go, then rerun mmi-cli devops release --apply. Do not start a second /release while one is still folding or tagging.`
|
|
14024
|
+
);
|
|
14025
|
+
}
|
|
13939
14026
|
if (releaseRepo && await isStrayUnreleasedTag(deps, tag, remoteSha, releaseRepo)) {
|
|
13940
14027
|
throw new Error(
|
|
13941
14028
|
`${mismatch}. The existing tag is not reachable from origin/main and has no GitHub Release \u2014 it was pushed outside the train, not by a completed release. Sanctioned recovery: delete the stray tag (git push origin --delete ${tag}; git tag -d ${tag} if it exists locally), check the repo's Actions for any workflow the stray tag already triggered, then rerun mmi-cli devops release --apply (if a publish already ran off the stray tag, mint the next version instead of reusing this one). Never complete the release by hand \u2014 a manual GitHub Release or branch push bypasses the train and leaves main behind.`
|
|
@@ -14610,6 +14697,7 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
14610
14697
|
}
|
|
14611
14698
|
let tagPush;
|
|
14612
14699
|
try {
|
|
14700
|
+
await assertComputedReleaseTagStillNext(deps, tag);
|
|
14613
14701
|
tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe, ctx.repo);
|
|
14614
14702
|
} catch (e) {
|
|
14615
14703
|
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha, resumeCommand);
|
|
@@ -14622,7 +14710,11 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
14622
14710
|
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main", deployModel });
|
|
14623
14711
|
}
|
|
14624
14712
|
if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
|
|
14625
|
-
|
|
14713
|
+
try {
|
|
14714
|
+
await runGitPush(deps, ["push", "origin", "main"]);
|
|
14715
|
+
} catch (e) {
|
|
14716
|
+
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main", deployModel });
|
|
14717
|
+
}
|
|
14626
14718
|
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
14627
14719
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
14628
14720
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
@@ -15008,7 +15100,11 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
15008
15100
|
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc", deployModel: deployModel2 });
|
|
15009
15101
|
}
|
|
15010
15102
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
15011
|
-
|
|
15103
|
+
try {
|
|
15104
|
+
await runGitPush(deps, ["push", "origin", "rc"]);
|
|
15105
|
+
} catch (e) {
|
|
15106
|
+
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc", deployModel: deployModel2 });
|
|
15107
|
+
}
|
|
15012
15108
|
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince, rcSha);
|
|
15013
15109
|
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, bumpIntent, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
|
|
15014
15110
|
}
|
|
@@ -17194,7 +17290,12 @@ async function runStage(config = {}, opts = {}) {
|
|
|
17194
17290
|
const ranBuild = Boolean(build);
|
|
17195
17291
|
try {
|
|
17196
17292
|
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
17197
|
-
if (build)
|
|
17293
|
+
if (build) {
|
|
17294
|
+
await shell(sub(build), cwd, timeoutMs, {
|
|
17295
|
+
...stageProcessEnv(stagePort, extraEnv),
|
|
17296
|
+
...opts.buildEnvMerge ?? {}
|
|
17297
|
+
});
|
|
17298
|
+
}
|
|
17198
17299
|
} catch (e) {
|
|
17199
17300
|
(0, import_node_fs16.rmSync)(statePath2, { force: true });
|
|
17200
17301
|
if (globalStatePath && globalStatePath !== statePath2) (0, import_node_fs16.rmSync)(globalStatePath, { force: true });
|
|
@@ -29221,6 +29322,85 @@ async function runStageLiveDown(deps, t) {
|
|
|
29221
29322
|
};
|
|
29222
29323
|
}
|
|
29223
29324
|
|
|
29325
|
+
// src/stage-build-secrets.ts
|
|
29326
|
+
var GITHUB_PACKAGES_TOKEN_REF = "@github-packages-token";
|
|
29327
|
+
var ENV_ID_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
29328
|
+
var BARE_VAULT_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
29329
|
+
var CROSS_SLUG_REF_RE = /^(?:mm-fofu|_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*$/;
|
|
29330
|
+
function parseStageBuildSecretEntry(entry) {
|
|
29331
|
+
const trimmed = entry.trim();
|
|
29332
|
+
if (!trimmed) return null;
|
|
29333
|
+
const eq = trimmed.indexOf("=");
|
|
29334
|
+
if (eq === -1) {
|
|
29335
|
+
if (!BARE_VAULT_KEY_RE.test(trimmed) && !CROSS_SLUG_REF_RE.test(trimmed)) return null;
|
|
29336
|
+
const envKey2 = trimmed.includes(":") ? trimmed.slice(trimmed.lastIndexOf(":") + 1) : trimmed;
|
|
29337
|
+
return { envKey: envKey2, ref: trimmed };
|
|
29338
|
+
}
|
|
29339
|
+
const envKey = trimmed.slice(0, eq);
|
|
29340
|
+
const ref = trimmed.slice(eq + 1);
|
|
29341
|
+
if (!ENV_ID_RE.test(envKey) || !ref) return null;
|
|
29342
|
+
if (ref === GITHUB_PACKAGES_TOKEN_REF) return { envKey, ref };
|
|
29343
|
+
if (!BARE_VAULT_KEY_RE.test(ref) && !CROSS_SLUG_REF_RE.test(ref)) return null;
|
|
29344
|
+
return { envKey, ref };
|
|
29345
|
+
}
|
|
29346
|
+
async function resolveStageBuildSecrets(input) {
|
|
29347
|
+
const entries = input.requiredBuildSecrets;
|
|
29348
|
+
if (!entries?.length) return {};
|
|
29349
|
+
const env = input.env ?? process.env;
|
|
29350
|
+
const out = {};
|
|
29351
|
+
const missing = [];
|
|
29352
|
+
for (const raw of entries) {
|
|
29353
|
+
const parsed = parseStageBuildSecretEntry(raw);
|
|
29354
|
+
if (!parsed) {
|
|
29355
|
+
missing.push(`${raw} (malformed requiredBuildSecrets entry)`);
|
|
29356
|
+
continue;
|
|
29357
|
+
}
|
|
29358
|
+
const { envKey, ref } = parsed;
|
|
29359
|
+
const fromEnv = env[envKey];
|
|
29360
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
29361
|
+
out[envKey] = fromEnv;
|
|
29362
|
+
continue;
|
|
29363
|
+
}
|
|
29364
|
+
if (ref === GITHUB_PACKAGES_TOKEN_REF) {
|
|
29365
|
+
const fromVault = await input.fetchVault(envKey);
|
|
29366
|
+
if (fromVault) {
|
|
29367
|
+
out[envKey] = fromVault;
|
|
29368
|
+
continue;
|
|
29369
|
+
}
|
|
29370
|
+
missing.push(
|
|
29371
|
+
`${envKey}=${GITHUB_PACKAGES_TOKEN_REF} (central deploy mints this; local /stage needs process env ${envKey} or a stageless project vault secret ${envKey} \u2014 a GitHub PAT with read:packages for npm.pkg.github.com; declare+set via vault secrets, never commit the value)`
|
|
29372
|
+
);
|
|
29373
|
+
continue;
|
|
29374
|
+
}
|
|
29375
|
+
const vault = await fetchVaultRef(input.fetchVault, ref);
|
|
29376
|
+
if (vault) {
|
|
29377
|
+
out[envKey] = vault;
|
|
29378
|
+
continue;
|
|
29379
|
+
}
|
|
29380
|
+
missing.push(
|
|
29381
|
+
`${envKey}=${ref} (not in process env and vault read returned nothing \u2014 declare+set the stageless secret, or export ${envKey} before mmi-cli stage run --apply)`
|
|
29382
|
+
);
|
|
29383
|
+
}
|
|
29384
|
+
if (missing.length) {
|
|
29385
|
+
throw new Error(
|
|
29386
|
+
`stage build secrets unresolved: ${missing.join("; ")}. Compose BuildKit mounts (e.g. NODE_AUTH_TOKEN) read the build process env only.`
|
|
29387
|
+
);
|
|
29388
|
+
}
|
|
29389
|
+
return out;
|
|
29390
|
+
}
|
|
29391
|
+
async function fetchVaultRef(fetchVault, ref) {
|
|
29392
|
+
const colon = ref.indexOf(":");
|
|
29393
|
+
if (colon === -1) return fetchVault(ref);
|
|
29394
|
+
const prefix = ref.slice(0, colon);
|
|
29395
|
+
const key = ref.slice(colon + 1);
|
|
29396
|
+
if (prefix === "mm-fofu") return fetchVault(key, { slug: "mm-fofu" });
|
|
29397
|
+
if (prefix.startsWith("_org/")) {
|
|
29398
|
+
const provider = prefix.slice("_org/".length);
|
|
29399
|
+
return fetchVault(`${provider}/${key}`, { slug: "_org" });
|
|
29400
|
+
}
|
|
29401
|
+
return null;
|
|
29402
|
+
}
|
|
29403
|
+
|
|
29224
29404
|
// src/stage-commands.ts
|
|
29225
29405
|
function registerStageCommands(program3) {
|
|
29226
29406
|
function stagePortFromArgv() {
|
|
@@ -29270,6 +29450,30 @@ function registerStageCommands(program3) {
|
|
|
29270
29450
|
}
|
|
29271
29451
|
return Object.keys(merge).length ? merge : void 0;
|
|
29272
29452
|
}
|
|
29453
|
+
async function fetchStageBuildEnvMerge() {
|
|
29454
|
+
const cfg = await loadConfig();
|
|
29455
|
+
if (!cfg.sagaApiUrl) return void 0;
|
|
29456
|
+
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch(() => null);
|
|
29457
|
+
if (!read?.ok || !read.project) return void 0;
|
|
29458
|
+
const required = read.project.requiredBuildSecrets;
|
|
29459
|
+
if (!required?.length) return void 0;
|
|
29460
|
+
const d = makeSecretsDeps(cfg);
|
|
29461
|
+
const merge = await resolveStageBuildSecrets({
|
|
29462
|
+
requiredBuildSecrets: required,
|
|
29463
|
+
fetchVault: (key, opts) => fetchSecretValue(d, key, opts ?? {})
|
|
29464
|
+
});
|
|
29465
|
+
return Object.keys(merge).length ? merge : void 0;
|
|
29466
|
+
}
|
|
29467
|
+
async function stageVaultOpts() {
|
|
29468
|
+
const [vaultEnvMerge, buildEnvMerge] = await Promise.all([
|
|
29469
|
+
fetchStageVaultEnvMerge(),
|
|
29470
|
+
fetchStageBuildEnvMerge()
|
|
29471
|
+
]);
|
|
29472
|
+
return {
|
|
29473
|
+
...vaultEnvMerge ? { vaultEnvMerge } : {},
|
|
29474
|
+
...buildEnvMerge ? { buildEnvMerge } : {}
|
|
29475
|
+
};
|
|
29476
|
+
}
|
|
29273
29477
|
function stageStepsFor(res, stops = true) {
|
|
29274
29478
|
if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
|
|
29275
29479
|
return [{ label: `no local stage to run \u2014 ${res.gap ?? "stage config gap"}` }];
|
|
@@ -29367,7 +29571,8 @@ function registerStageCommands(program3) {
|
|
|
29367
29571
|
const cfg = res.derived.config;
|
|
29368
29572
|
const hold = stageKeepAlive();
|
|
29369
29573
|
try {
|
|
29370
|
-
const
|
|
29574
|
+
const vaultOpts = await stageVaultOpts();
|
|
29575
|
+
const result = await runStage(cfg, { ...stageScopedRunOpts({ timeoutMs: o.timeoutMs }), ...vaultOpts });
|
|
29371
29576
|
const reportUrl = reportedStageUrl(res, result);
|
|
29372
29577
|
const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
|
|
29373
29578
|
return printLine(o.json ? JSON.stringify({ ...result, source: res.source, url: reportUrl }) : `mmi-cli stage: ${result.message}${url}`);
|
|
@@ -29437,14 +29642,14 @@ function registerStageCommands(program3) {
|
|
|
29437
29642
|
}
|
|
29438
29643
|
if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
|
|
29439
29644
|
const cfg = res.derived.config;
|
|
29440
|
-
const
|
|
29645
|
+
const vaultOpts = await stageVaultOpts();
|
|
29441
29646
|
try {
|
|
29442
29647
|
const hold = stageKeepAlive();
|
|
29443
29648
|
let printed = false;
|
|
29444
29649
|
try {
|
|
29445
29650
|
const result = await runStage(cfg, {
|
|
29446
29651
|
...stageScopedRunOpts({ timeoutMs: o.timeoutMs, allowStaleEnv: o.allowStaleEnv }),
|
|
29447
|
-
|
|
29652
|
+
...vaultOpts,
|
|
29448
29653
|
onReady: (ready) => {
|
|
29449
29654
|
const reportUrl = reportedStageUrl(res, ready);
|
|
29450
29655
|
const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
|
|
@@ -29626,6 +29831,14 @@ function ghPrMergeLocalBranchDeleteWarning(message) {
|
|
|
29626
29831
|
function mergeAutoRejectedPrAlreadyClean(message) {
|
|
29627
29832
|
return /clean status \(enablePullRequestAutoMerge\)|is in clean status/i.test(message);
|
|
29628
29833
|
}
|
|
29834
|
+
function mergeMethodFromParentCount(parentCount) {
|
|
29835
|
+
if (parentCount >= 2) return "merge";
|
|
29836
|
+
return void 0;
|
|
29837
|
+
}
|
|
29838
|
+
function reportedMergeMethod(input) {
|
|
29839
|
+
if (!input.alreadyMerged) return input.requestedMethod;
|
|
29840
|
+
return input.observedMethod;
|
|
29841
|
+
}
|
|
29629
29842
|
|
|
29630
29843
|
// src/merge-cleanup.ts
|
|
29631
29844
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
@@ -30264,6 +30477,25 @@ async function pollRestPrMergeable(prNumber, repo, gh = defaultGhApi) {
|
|
|
30264
30477
|
return "UNKNOWN";
|
|
30265
30478
|
}
|
|
30266
30479
|
}
|
|
30480
|
+
async function countHeadWorkflowRuns(headSha, repo, gh = defaultGhApi) {
|
|
30481
|
+
if (!headSha) return { state: "failed", error: "no head SHA" };
|
|
30482
|
+
let raw;
|
|
30483
|
+
try {
|
|
30484
|
+
raw = await gh([`repos/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&per_page=1`]);
|
|
30485
|
+
} catch (e) {
|
|
30486
|
+
return { state: "failed", error: `actions/runs read failed for ${headSha.slice(0, 7)} on ${repo}: ${readErrorText(e)}` };
|
|
30487
|
+
}
|
|
30488
|
+
let count;
|
|
30489
|
+
try {
|
|
30490
|
+
count = JSON.parse(raw).total_count;
|
|
30491
|
+
} catch (e) {
|
|
30492
|
+
return { state: "failed", error: `actions/runs payload for ${headSha.slice(0, 7)} was not JSON: ${readErrorText(e)}` };
|
|
30493
|
+
}
|
|
30494
|
+
if (typeof count !== "number" || !Number.isFinite(count) || count < 0) {
|
|
30495
|
+
return { state: "failed", error: `actions/runs payload for ${headSha.slice(0, 7)} carried no usable total_count` };
|
|
30496
|
+
}
|
|
30497
|
+
return { state: "ok", count };
|
|
30498
|
+
}
|
|
30267
30499
|
async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
|
|
30268
30500
|
try {
|
|
30269
30501
|
return (await fetchRestPrSnapshot(prNumber, repo, gh)).merged ? { state: "merged" } : { state: "open" };
|
|
@@ -33854,7 +34086,27 @@ var surfaces_default = {
|
|
|
33854
34086
|
kind: "npm-pack",
|
|
33855
34087
|
packagePath: "updater"
|
|
33856
34088
|
},
|
|
33857
|
-
publishVisibility: "public"
|
|
34089
|
+
publishVisibility: "public",
|
|
34090
|
+
prepare: [
|
|
34091
|
+
{
|
|
34092
|
+
command: "npm",
|
|
34093
|
+
args: [
|
|
34094
|
+
"--prefix",
|
|
34095
|
+
"updater",
|
|
34096
|
+
"run",
|
|
34097
|
+
"build"
|
|
34098
|
+
],
|
|
34099
|
+
inputs: [
|
|
34100
|
+
"updater/src",
|
|
34101
|
+
"updater/build.mjs",
|
|
34102
|
+
"updater/package.json",
|
|
34103
|
+
"updater/tsconfig.json"
|
|
34104
|
+
],
|
|
34105
|
+
outputs: [
|
|
34106
|
+
"updater/dist/index.cjs"
|
|
34107
|
+
]
|
|
34108
|
+
}
|
|
34109
|
+
]
|
|
33858
34110
|
},
|
|
33859
34111
|
{
|
|
33860
34112
|
id: "mmi-cli-lock",
|
|
@@ -37891,12 +38143,29 @@ async function waitLoopDiagnosis(label, prNumber, repo) {
|
|
|
37891
38143
|
}
|
|
37892
38144
|
return null;
|
|
37893
38145
|
}
|
|
38146
|
+
async function waitLoopHeadWorkflowRunCount(label, prNumber, repo) {
|
|
38147
|
+
let snapshot;
|
|
38148
|
+
try {
|
|
38149
|
+
snapshot = await fetchRestPrSnapshot(prNumber, repo);
|
|
38150
|
+
} catch (e) {
|
|
38151
|
+
console.warn(
|
|
38152
|
+
`${label}: zero-runs probe FAILED (pulls read: ${e?.message ?? e}) \u2014 not treating as delivery failure this cycle`
|
|
38153
|
+
);
|
|
38154
|
+
return null;
|
|
38155
|
+
}
|
|
38156
|
+
const read = await countHeadWorkflowRuns(snapshot.headSha, repo);
|
|
38157
|
+
if (read.state === "ok") return read.count;
|
|
38158
|
+
console.warn(
|
|
38159
|
+
`${label}: zero-runs probe FAILED (${read.error}) \u2014 not treating as delivery failure this cycle`
|
|
38160
|
+
);
|
|
38161
|
+
return null;
|
|
38162
|
+
}
|
|
37894
38163
|
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
37895
38164
|
const result = await resolveMergeCiPolicyForCheckout(o.repo);
|
|
37896
38165
|
if (o.json) return printLine(JSON.stringify(result));
|
|
37897
38166
|
printLine(`merge CI policy: ${result.policy} (${result.reason})`);
|
|
37898
38167
|
});
|
|
37899
|
-
pr.command("checks-wait <number>").description(`bounded wait for ALL checks on the PR head, required or not (#5336); skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR \u2014 GitHub never queues checks for one (#2970). REST-only polling with a pool floor (#3024). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED
|
|
38168
|
+
pr.command("checks-wait <number>").description(`bounded wait for ALL checks on the PR head, required or not (#5336); skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR \u2014 GitHub never queues checks for one (#2970), and fails early on a confirmed zero-workflow-runs head instead of burning the full budget as pending (#5400). REST-only polling with a pool floor (#3024). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED, the PR is CONFLICTING, or GitHub delivered zero runs; exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired or the API pool ran dry (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) \u2014 raise it for serial self-hosted e2e queues`).action(async (number, o) => {
|
|
37900
38169
|
let timeoutMs;
|
|
37901
38170
|
if (o.timeout !== void 0) {
|
|
37902
38171
|
const minutes = Number(o.timeout);
|
|
@@ -37922,6 +38191,8 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
|
|
|
37922
38191
|
// #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
|
|
37923
38192
|
// reading as "your tests failed". One call per failing run, only at the verdict.
|
|
37924
38193
|
diagnoseFailure: () => waitLoopDiagnosis("pr checks-wait", number, repo),
|
|
38194
|
+
// #5400: after grace, name "GitHub delivered zero runs" instead of burning the full budget as pending.
|
|
38195
|
+
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr checks-wait", number, repo),
|
|
37925
38196
|
baseBranch,
|
|
37926
38197
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
37927
38198
|
log: (message) => console.warn(message),
|
|
@@ -37936,7 +38207,7 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
|
|
|
37936
38207
|
} else if (result.status === "timeout") {
|
|
37937
38208
|
const stuckQueuing = /no-checks-reported/i.test(result.detail ?? "");
|
|
37938
38209
|
printLine(
|
|
37939
|
-
`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.` + (stuckQueuing ? " Tip:
|
|
38210
|
+
`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.` + (stuckQueuing ? " Tip: check-runs never appeared \u2014 if `gh api repos/<owner>/<repo>/actions/runs?head_sha=<sha>` shows total_count=0, see docs/Guides/gh-runner-runbook.md \xA7 Zero workflow runs; otherwise mmi-live may be saturated or Actions may be failing before checkout (docs/Guides/gh-runner-runbook.md)." : "")
|
|
37940
38211
|
);
|
|
37941
38212
|
} else if (result.status === "rate-limited") {
|
|
37942
38213
|
printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
|
|
@@ -37944,6 +38215,8 @@ pr.command("checks-wait <number>").description(`bounded wait for ALL checks on t
|
|
|
37944
38215
|
printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure) \u2014 ${result.reason}`);
|
|
37945
38216
|
} else if (result.detail === "stale-head") {
|
|
37946
38217
|
printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
|
|
38218
|
+
} else if (result.detail === "zero-runs") {
|
|
38219
|
+
printLine(`pr checks-wait: failure (zero workflow runs delivered, NOT pending checks) \u2014 ${result.reason}`);
|
|
37947
38220
|
} else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
37948
38221
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
37949
38222
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
@@ -38014,6 +38287,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
38014
38287
|
// #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
|
|
38015
38288
|
// then read its own wall-clock kill as a broken diff.
|
|
38016
38289
|
diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
|
|
38290
|
+
// #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
|
|
38291
|
+
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
|
|
38017
38292
|
baseBranch: "development",
|
|
38018
38293
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
38019
38294
|
log: (message) => console.warn(message),
|
|
@@ -38136,6 +38411,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
38136
38411
|
pollMergeable: () => pollRestPrMergeable(number, repo),
|
|
38137
38412
|
pollRateLimit: () => waitLoopCorePool("pr merge --wait"),
|
|
38138
38413
|
diagnoseFailure: () => waitLoopDiagnosis("pr merge --wait", number, repo),
|
|
38414
|
+
pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr merge --wait", number, repo),
|
|
38139
38415
|
baseBranch,
|
|
38140
38416
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
38141
38417
|
log: (message) => console.warn(message),
|
|
@@ -38255,11 +38531,27 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
38255
38531
|
invalidateStatuslineBoardCache();
|
|
38256
38532
|
const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
|
|
38257
38533
|
const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
|
|
38534
|
+
let observedMethod;
|
|
38535
|
+
if (remoteNotAttemptedReason === "pr-already-merged" && repoForPostCleanup) {
|
|
38536
|
+
try {
|
|
38537
|
+
const mergeSha = (await defaultGitHubClient().rest("GET", `repos/${repoForPostCleanup}/pulls/${number}`)).merge_commit_sha;
|
|
38538
|
+
if (mergeSha) {
|
|
38539
|
+
const commit = await defaultGitHubClient().rest("GET", `repos/${repoForPostCleanup}/commits/${mergeSha}`);
|
|
38540
|
+
observedMethod = mergeMethodFromParentCount(Array.isArray(commit.parents) ? commit.parents.length : 0);
|
|
38541
|
+
}
|
|
38542
|
+
} catch {
|
|
38543
|
+
}
|
|
38544
|
+
}
|
|
38545
|
+
const methodField = reportedMergeMethod({
|
|
38546
|
+
requestedMethod: method.slice(2),
|
|
38547
|
+
alreadyMerged: remoteNotAttemptedReason === "pr-already-merged",
|
|
38548
|
+
observedMethod
|
|
38549
|
+
});
|
|
38258
38550
|
console.log(JSON.stringify({
|
|
38259
38551
|
mergeStatus: "merged",
|
|
38260
38552
|
merged: number,
|
|
38261
38553
|
branch: headRef,
|
|
38262
|
-
method:
|
|
38554
|
+
...methodField ? { method: methodField } : {},
|
|
38263
38555
|
remoteBranch,
|
|
38264
38556
|
housekeeping,
|
|
38265
38557
|
// `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
|
|
@@ -38297,11 +38589,12 @@ registerStageCommands(program2);
|
|
|
38297
38589
|
var GH_TRAIN_TIMEOUT_MS = 3e4;
|
|
38298
38590
|
var GH_RUN_WATCH_TIMEOUT_MS = 20 * 6e4;
|
|
38299
38591
|
var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
38592
|
+
var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
|
|
38300
38593
|
var NPM_TRAIN_TIMEOUT_MS = 6e4;
|
|
38301
38594
|
function trainApplyDeps() {
|
|
38302
38595
|
return {
|
|
38303
38596
|
run: async (file, args) => {
|
|
38304
|
-
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;
|
|
38597
|
+
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_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;
|
|
38305
38598
|
try {
|
|
38306
38599
|
return isWin2 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
|
|
38307
38600
|
} catch (e) {
|
|
@@ -38640,7 +38933,8 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
38640
38933
|
applyTrainFollowUpExit(followUpStatus);
|
|
38641
38934
|
return;
|
|
38642
38935
|
} catch (e) {
|
|
38643
|
-
|
|
38936
|
+
process.exitCode = 1;
|
|
38937
|
+
return await failGraceful(`${commandName}: ${e.message}`);
|
|
38644
38938
|
}
|
|
38645
38939
|
}
|
|
38646
38940
|
const repo = o.repo ?? await resolveRepo();
|
package/package.json
CHANGED