@mutmutco/cli 2.18.0 → 2.19.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.
- package/dist/index.cjs +79 -133
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5590,7 +5590,6 @@ function trainPlan(command, options = {}) {
|
|
|
5590
5590
|
rcandVersionStep(options),
|
|
5591
5591
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5592
5592
|
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
5593
|
-
{ label: "for Hub distribution changes, verify the release bump is already landed before touching rc", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view; if missing: mmi-cli train prep --apply", gated: true },
|
|
5594
5593
|
{ label: "merge development to rc", gated: true },
|
|
5595
5594
|
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml auto-fires on rc push, correlate/watch that run", gated: true },
|
|
5596
5595
|
{ label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli tenant redeploy <owner/repo> rc --watch", gated: true }
|
|
@@ -5603,8 +5602,8 @@ function trainPlan(command, options = {}) {
|
|
|
5603
5602
|
{ label: "verify current branch is development", gated: true },
|
|
5604
5603
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5605
5604
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5606
|
-
{ label: "direct-track release skips rc; verify any distribution bump is already landed on development", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5607
5605
|
{ label: "merge development to main", gated: true },
|
|
5606
|
+
{ label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
5608
5607
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5609
5608
|
{ label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; other models deploy via their own workflow", gated: true },
|
|
5610
5609
|
{ label: "roll development forward", gated: true }
|
|
@@ -5617,7 +5616,7 @@ function trainPlan(command, options = {}) {
|
|
|
5617
5616
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5618
5617
|
{ label: "verify every main-only hotfix commit is covered by the rc candidate (the guard runs automatically inside the apply step below; --ack <sha> overrides a verified, trailer-less port)", command: "mmi-cli release --apply [--ack <sha>]", gated: true },
|
|
5619
5618
|
{ label: "merge rc to main", gated: true },
|
|
5620
|
-
{ label: "
|
|
5619
|
+
{ label: "fold the version bump into the release commit (app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
5621
5620
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5622
5621
|
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs", gated: true },
|
|
5623
5622
|
{ label: "roll development forward", gated: true }
|
|
@@ -5627,7 +5626,7 @@ function trainPlan(command, options = {}) {
|
|
|
5627
5626
|
{ label: "verify the fix is merged on development (the only hotfix origin)", gated: true },
|
|
5628
5627
|
{ label: "branch hotfix from main and cherry-pick the dev commits", command: "git cherry-pick -x <dev-sha>", gated: true },
|
|
5629
5628
|
{ label: "land on main via PR, tag, deploy prod", gated: true },
|
|
5630
|
-
{ label: "
|
|
5629
|
+
{ label: "no residue: development already has the fix; the next /release fold + back-merge re-aligns version manifests" }
|
|
5631
5630
|
];
|
|
5632
5631
|
}
|
|
5633
5632
|
function bootstrapPlan(repo, repoClass) {
|
|
@@ -5709,9 +5708,9 @@ function stalePosixFields(config, shell2) {
|
|
|
5709
5708
|
}
|
|
5710
5709
|
function sanitizeLocalStage(local, stale) {
|
|
5711
5710
|
if (!stale.length) return local;
|
|
5712
|
-
const
|
|
5713
|
-
for (const field of stale) delete
|
|
5714
|
-
return
|
|
5711
|
+
const clean3 = { ...local };
|
|
5712
|
+
for (const field of stale) delete clean3[field];
|
|
5713
|
+
return clean3;
|
|
5715
5714
|
}
|
|
5716
5715
|
function staleNote(staleFields, outcome) {
|
|
5717
5716
|
const list = staleFields.join(", ");
|
|
@@ -6516,9 +6515,35 @@ function requireValue(value, label) {
|
|
|
6516
6515
|
if (!value) throw new Error(`${label} could not be resolved`);
|
|
6517
6516
|
return value;
|
|
6518
6517
|
}
|
|
6519
|
-
async function
|
|
6520
|
-
if (model
|
|
6521
|
-
|
|
6518
|
+
async function resolveFoldPaths(deps, model) {
|
|
6519
|
+
if (model === "hub-serverless") {
|
|
6520
|
+
return (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
6521
|
+
}
|
|
6522
|
+
try {
|
|
6523
|
+
await deps.run("git", ["cat-file", "-e", "origin/main:package.json"]);
|
|
6524
|
+
} catch {
|
|
6525
|
+
return [];
|
|
6526
|
+
}
|
|
6527
|
+
return ["package.json", "package-lock.json"];
|
|
6528
|
+
}
|
|
6529
|
+
async function foldReleaseVersion(deps, model, tag, foldPaths) {
|
|
6530
|
+
if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
|
|
6531
|
+
const version = tag.replace(/^v/, "");
|
|
6532
|
+
if (model === "hub-serverless") {
|
|
6533
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
|
|
6534
|
+
} else {
|
|
6535
|
+
await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
|
|
6536
|
+
}
|
|
6537
|
+
for (const path2 of foldPaths) {
|
|
6538
|
+
await deps.run("git", ["add", "--", path2]).catch(() => void 0);
|
|
6539
|
+
}
|
|
6540
|
+
const staged = (await deps.run("git", ["diff", "--cached", "--name-only"])).trim();
|
|
6541
|
+
if (!staged) return `version fold: manifests already at ${version} \u2014 nothing committed`;
|
|
6542
|
+
await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
|
|
6543
|
+
if (model === "hub-serverless") {
|
|
6544
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]);
|
|
6545
|
+
}
|
|
6546
|
+
return `version fold: distribution bumped to ${version} in the release commit`;
|
|
6522
6547
|
}
|
|
6523
6548
|
async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedSha) {
|
|
6524
6549
|
const out = await deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName,targetCommitish"]);
|
|
@@ -6568,18 +6593,18 @@ async function predictMergeConflicts(deps, ours, theirs) {
|
|
|
6568
6593
|
return files;
|
|
6569
6594
|
}
|
|
6570
6595
|
}
|
|
6571
|
-
async function mergeWithSpineResolution(deps, sourceRef, label, resolve) {
|
|
6596
|
+
async function mergeWithSpineResolution(deps, sourceRef, label, resolve, extraTolerated = []) {
|
|
6572
6597
|
try {
|
|
6573
6598
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
6574
6599
|
return;
|
|
6575
6600
|
} catch {
|
|
6576
6601
|
}
|
|
6577
6602
|
const unmerged = (await deps.run("git", ["diff", "--name-only", "--diff-filter=U"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
6578
|
-
const
|
|
6579
|
-
if (unmerged.length === 0 ||
|
|
6603
|
+
const blocking = unmerged.filter((f) => !isSpinePath(f) && !extraTolerated.includes(f));
|
|
6604
|
+
if (unmerged.length === 0 || blocking.length > 0) {
|
|
6580
6605
|
await deps.run("git", ["merge", "--abort"]);
|
|
6581
6606
|
throw new Error(
|
|
6582
|
-
unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on non-spine path(s): ${
|
|
6607
|
+
unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on non-spine path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
|
|
6583
6608
|
);
|
|
6584
6609
|
}
|
|
6585
6610
|
await deps.run("git", ["checkout", `--${resolve}`, "--", ...unmerged]);
|
|
@@ -6934,7 +6959,6 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6934
6959
|
);
|
|
6935
6960
|
const deployModel2 = await preflight(deps, ctx, "rc", meta);
|
|
6936
6961
|
const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
6937
|
-
await verifyHubDistributionVersion(deps, deployModel2, releaseBase);
|
|
6938
6962
|
await deps.run("git", ["checkout", "rc"]);
|
|
6939
6963
|
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
6940
6964
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
@@ -6959,12 +6983,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6959
6983
|
);
|
|
6960
6984
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
6961
6985
|
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
6962
|
-
await
|
|
6986
|
+
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
6963
6987
|
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
6964
|
-
const
|
|
6965
|
-
if (
|
|
6988
|
+
const predictedBlocking2 = predicted2.filter((f) => !isSpinePath(f) && !foldPaths2.includes(f));
|
|
6989
|
+
if (predictedBlocking2.length > 0) {
|
|
6966
6990
|
throw new Error(
|
|
6967
|
-
`development -> main merge would conflict on non-spine path(s): ${
|
|
6991
|
+
`development -> main merge would conflict on non-spine path(s): ${predictedBlocking2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
|
|
6968
6992
|
);
|
|
6969
6993
|
}
|
|
6970
6994
|
await deps.run("git", ["checkout", "main"]);
|
|
@@ -6972,8 +6996,9 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6972
6996
|
if (predicted2.length === 0) {
|
|
6973
6997
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
6974
6998
|
} else {
|
|
6975
|
-
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs");
|
|
6999
|
+
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", foldPaths2);
|
|
6976
7000
|
}
|
|
7001
|
+
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
6977
7002
|
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
6978
7003
|
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
6979
7004
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
@@ -6996,6 +7021,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6996
7021
|
deployModel: deployModel2,
|
|
6997
7022
|
promoted: true,
|
|
6998
7023
|
checks: checks2,
|
|
7024
|
+
versionFold: versionFold2,
|
|
6999
7025
|
dispatch: d2.note,
|
|
7000
7026
|
runId: d2.runId,
|
|
7001
7027
|
runUrl: d2.runUrl,
|
|
@@ -7013,11 +7039,12 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
7013
7039
|
"nothing to release: origin/rc is not ahead of origin/main"
|
|
7014
7040
|
);
|
|
7015
7041
|
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
7042
|
+
const foldPaths = await resolveFoldPaths(deps, deployModel);
|
|
7016
7043
|
const predicted = await predictMergeConflicts(deps, "origin/main", "origin/rc");
|
|
7017
|
-
const
|
|
7018
|
-
if (
|
|
7044
|
+
const predictedBlocking = predicted.filter((f) => !isSpinePath(f) && !foldPaths.includes(f));
|
|
7045
|
+
if (predictedBlocking.length > 0) {
|
|
7019
7046
|
throw new Error(
|
|
7020
|
-
`rc -> main merge would conflict on non-spine path(s): ${
|
|
7047
|
+
`rc -> main merge would conflict on non-spine path(s): ${predictedBlocking.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release.`
|
|
7021
7048
|
);
|
|
7022
7049
|
}
|
|
7023
7050
|
const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
|
|
@@ -7033,10 +7060,10 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
7033
7060
|
if (predicted.length === 0) {
|
|
7034
7061
|
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
7035
7062
|
} else {
|
|
7036
|
-
await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs");
|
|
7063
|
+
await mergeWithSpineResolution(deps, "rc", "rc -> main", "theirs", foldPaths);
|
|
7037
7064
|
}
|
|
7038
7065
|
const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
7039
|
-
await
|
|
7066
|
+
const versionFold = await foldReleaseVersion(deps, deployModel, tag, foldPaths);
|
|
7040
7067
|
const releaseSha = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
7041
7068
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
7042
7069
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
@@ -7062,6 +7089,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
7062
7089
|
deployModel,
|
|
7063
7090
|
promoted: true,
|
|
7064
7091
|
checks,
|
|
7092
|
+
versionFold,
|
|
7065
7093
|
dispatch: d.note,
|
|
7066
7094
|
runId: d.runId,
|
|
7067
7095
|
runUrl: d.runUrl,
|
|
@@ -7288,34 +7316,6 @@ function checkHotfixCoverage(options = {}) {
|
|
|
7288
7316
|
return { ok: uncovered.length === 0, mainRef, rcRef, commits, uncovered };
|
|
7289
7317
|
}
|
|
7290
7318
|
|
|
7291
|
-
// src/train-prep.ts
|
|
7292
|
-
function clean2(text) {
|
|
7293
|
-
return text.trim();
|
|
7294
|
-
}
|
|
7295
|
-
function normalizeVersion2(target) {
|
|
7296
|
-
const version = target.replace(/^v/, "");
|
|
7297
|
-
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
7298
|
-
throw new Error(`invalid train prep target ${target || "(empty)"}`);
|
|
7299
|
-
}
|
|
7300
|
-
return version;
|
|
7301
|
-
}
|
|
7302
|
-
function parseChangedFiles(out) {
|
|
7303
|
-
return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
7304
|
-
}
|
|
7305
|
-
async function runTrainPrep(deps, options = {}) {
|
|
7306
|
-
const target = clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"]));
|
|
7307
|
-
const version = normalizeVersion2(target);
|
|
7308
|
-
if (!options.apply) {
|
|
7309
|
-
return { target, version, applied: false, command: "mmi-cli train prep --apply", stagedFiles: [] };
|
|
7310
|
-
}
|
|
7311
|
-
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", target]);
|
|
7312
|
-
const stagedFiles = parseChangedFiles(await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"]));
|
|
7313
|
-
if (stagedFiles.length === 0) throw new Error("no locked distribution files returned by release-distribution changed-files");
|
|
7314
|
-
await deps.run("git", ["add", "--", ...stagedFiles]);
|
|
7315
|
-
await deps.run("node", ["scripts/release-distribution.mjs", "verify", target, "--skip-npm-view"]);
|
|
7316
|
-
return { target, version, applied: true, command: "mmi-cli pr create --base development --head <current-branch>", stagedFiles };
|
|
7317
|
-
}
|
|
7318
|
-
|
|
7319
7319
|
// src/tenant-sweep.ts
|
|
7320
7320
|
function isRcBearingTenant(p) {
|
|
7321
7321
|
if ((p.deployModel ?? "") !== "tenant-container") return false;
|
|
@@ -7383,7 +7383,7 @@ function renderSweep(r) {
|
|
|
7383
7383
|
var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
|
|
7384
7384
|
var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
7385
7385
|
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
7386
|
-
function
|
|
7386
|
+
function clean2(out) {
|
|
7387
7387
|
return out.trim();
|
|
7388
7388
|
}
|
|
7389
7389
|
function sleeper(deps) {
|
|
@@ -7437,7 +7437,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
7437
7437
|
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
7438
7438
|
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
7439
7439
|
}
|
|
7440
|
-
const sha =
|
|
7440
|
+
const sha = clean2(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
7441
7441
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
7442
7442
|
return { sha, label: sha.slice(0, 7) };
|
|
7443
7443
|
}
|
|
@@ -7464,7 +7464,7 @@ async function runHotfixStart(deps, options) {
|
|
|
7464
7464
|
};
|
|
7465
7465
|
}
|
|
7466
7466
|
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
7467
|
-
const remoteBranch =
|
|
7467
|
+
const remoteBranch = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
7468
7468
|
if (remoteBranch) {
|
|
7469
7469
|
await deps.run("git", ["checkout", branch]);
|
|
7470
7470
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
@@ -7490,7 +7490,7 @@ async function runHotfixStart(deps, options) {
|
|
|
7490
7490
|
}
|
|
7491
7491
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
7492
7492
|
}
|
|
7493
|
-
const prUrl =
|
|
7493
|
+
const prUrl = clean2(await deps.run("gh", [
|
|
7494
7494
|
"pr",
|
|
7495
7495
|
"create",
|
|
7496
7496
|
"--repo",
|
|
@@ -7573,7 +7573,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
7573
7573
|
if (releaseExists) {
|
|
7574
7574
|
releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
|
|
7575
7575
|
} else {
|
|
7576
|
-
const tagCommit =
|
|
7576
|
+
const tagCommit = clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
|
|
7577
7577
|
await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
|
|
7578
7578
|
releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
|
|
7579
7579
|
if (deps.announce) {
|
|
@@ -7584,7 +7584,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
7584
7584
|
for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
|
|
7585
7585
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
7586
7586
|
}
|
|
7587
|
-
const previousRef =
|
|
7587
|
+
const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
7588
7588
|
let verifyNote;
|
|
7589
7589
|
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
7590
7590
|
try {
|
|
@@ -7597,15 +7597,6 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
7597
7597
|
}
|
|
7598
7598
|
return { ...ctx, command: "hotfix-release", tag, mergedSha, checks, tagNote, releaseNote, runs, verifyNote, announceNote };
|
|
7599
7599
|
}
|
|
7600
|
-
function versionAtLeast2(actual, wanted) {
|
|
7601
|
-
const pa = actual.split(".").map(Number);
|
|
7602
|
-
const pw = wanted.split(".").map(Number);
|
|
7603
|
-
if (pa.length < 3 || pa.some(Number.isNaN) || pw.length < 3 || pw.some(Number.isNaN)) return false;
|
|
7604
|
-
for (let i = 0; i < 3; i += 1) {
|
|
7605
|
-
if (pa[i] !== pw[i]) return pa[i] > pw[i];
|
|
7606
|
-
}
|
|
7607
|
-
return true;
|
|
7608
|
-
}
|
|
7609
7600
|
function deriveHotfixState(f) {
|
|
7610
7601
|
if (!f.branchExists && !f.pr && !f.tagPushed && !f.releaseExists) {
|
|
7611
7602
|
return { state: "not-started", next: `mmi-cli hotfix start --from <pr#|sha> (would mint ${f.tag})` };
|
|
@@ -7622,13 +7613,7 @@ function deriveHotfixState(f) {
|
|
|
7622
7613
|
if (!f.tagPushed || !f.releaseExists) {
|
|
7623
7614
|
return { state: "pr-merged (not released)", next: `mmi-cli hotfix release ${f.tag}` };
|
|
7624
7615
|
}
|
|
7625
|
-
|
|
7626
|
-
return {
|
|
7627
|
-
state: `UNFINISHED \u2014 released but development distribution manifests behind (dev ${f.devDistribution.version} < ${f.version})`,
|
|
7628
|
-
next: `forward-bump development (never a back-merge): branch from origin/development, node scripts/release-distribution.mjs prepare ${f.version}, land by PR (/hotfix Step 5)`
|
|
7629
|
-
};
|
|
7630
|
-
}
|
|
7631
|
-
return { state: "complete", next: "nothing \u2014 pipeline complete (rc absorbs the fix at the next /rcand; /release guards coverage)" };
|
|
7616
|
+
return { state: "complete", next: "nothing \u2014 pipeline complete (rc absorbs the fix at the next /rcand; /release guards coverage and re-aligns the version manifests via its fold + back-merge)" };
|
|
7632
7617
|
}
|
|
7633
7618
|
async function runHotfixStatus(deps, versionInput) {
|
|
7634
7619
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -7652,9 +7637,9 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
7652
7637
|
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
7653
7638
|
}
|
|
7654
7639
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
7655
|
-
const branchExists = Boolean(
|
|
7640
|
+
const branchExists = Boolean(clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
7656
7641
|
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
7657
|
-
const remoteTag =
|
|
7642
|
+
const remoteTag = clean2(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
7658
7643
|
const tagPushed = Boolean(remoteTag);
|
|
7659
7644
|
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
7660
7645
|
let releaseExists = false;
|
|
@@ -7688,19 +7673,8 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
7688
7673
|
}
|
|
7689
7674
|
}
|
|
7690
7675
|
}
|
|
7691
|
-
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(
|
|
7692
|
-
|
|
7693
|
-
(out) => {
|
|
7694
|
-
try {
|
|
7695
|
-
return JSON.parse(out).version ?? "unknown";
|
|
7696
|
-
} catch {
|
|
7697
|
-
return "unknown";
|
|
7698
|
-
}
|
|
7699
|
-
},
|
|
7700
|
-
() => "unknown"
|
|
7701
|
-
);
|
|
7702
|
-
const devDistribution = { version: devVersion, aligned: versionAtLeast2(devVersion, version) };
|
|
7703
|
-
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion, devDistribution };
|
|
7676
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
|
|
7677
|
+
return { tag, version, branchExists, pr: pr2 ? { number: pr2.number, state: pr2.state, url: pr2.url } : null, tagPushed, releaseExists, runs, npmVersion };
|
|
7704
7678
|
}
|
|
7705
7679
|
|
|
7706
7680
|
// src/release-announce.ts
|
|
@@ -9601,8 +9575,8 @@ function resolveKbSource(rawBase) {
|
|
|
9601
9575
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
9602
9576
|
}
|
|
9603
9577
|
function buildKbGetArgs(src, path2) {
|
|
9604
|
-
const
|
|
9605
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
9578
|
+
const clean3 = path2.replace(/^\/+/, "");
|
|
9579
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean3}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
9606
9580
|
}
|
|
9607
9581
|
function buildKbTreeArgs(src) {
|
|
9608
9582
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -10690,12 +10664,10 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
10690
10664
|
async function requireFreshTrainCli(commandName) {
|
|
10691
10665
|
const report = buildVersionLagReport({
|
|
10692
10666
|
currentVersion: resolveClientVersion(),
|
|
10693
|
-
repoVersion: readRepoVersion(),
|
|
10694
10667
|
releasedVersion: await fetchReleasedVersion()
|
|
10695
10668
|
});
|
|
10696
10669
|
if (report.ok) return;
|
|
10697
|
-
|
|
10698
|
-
throw new Error(`running mmi-cli ${report.currentVersion} is stale against ${target}; run doctor/update first so ${commandName} uses the current train path`);
|
|
10670
|
+
throw new Error(`running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; run doctor/update first so ${commandName} uses the current train path`);
|
|
10699
10671
|
}
|
|
10700
10672
|
var consoleIo = { log: (m) => console.log(m), err: (m) => console.error(m) };
|
|
10701
10673
|
var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
@@ -12163,11 +12135,12 @@ program2.command("stage-live").description("explain that remote rc/live environm
|
|
|
12163
12135
|
var GH_TRAIN_TIMEOUT_MS = 3e4;
|
|
12164
12136
|
var GH_RUN_WATCH_TIMEOUT_MS = 20 * 6e4;
|
|
12165
12137
|
var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
12138
|
+
var NPM_TRAIN_TIMEOUT_MS = 6e4;
|
|
12166
12139
|
function trainApplyDeps() {
|
|
12167
12140
|
return {
|
|
12168
12141
|
run: async (file, args) => {
|
|
12169
|
-
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
|
|
12170
|
-
return (await execFileP4(file, args, { timeout })).stdout;
|
|
12142
|
+
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;
|
|
12143
|
+
return isWin && file === "npm" ? (await execFileP4("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP4(file, args, { timeout })).stdout;
|
|
12171
12144
|
},
|
|
12172
12145
|
runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
|
|
12173
12146
|
trainAuthority: async (repo) => {
|
|
@@ -12183,10 +12156,11 @@ function trainApplyDeps() {
|
|
|
12183
12156
|
throw new Error(`tenant deploy dispatch failed: ${detail}`);
|
|
12184
12157
|
}
|
|
12185
12158
|
},
|
|
12186
|
-
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths
|
|
12187
|
-
//
|
|
12188
|
-
// the
|
|
12189
|
-
|
|
12159
|
+
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
12160
|
+
// release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
|
|
12161
|
+
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
12162
|
+
// set never reaches this guard: the Hub is direct-track.)
|
|
12163
|
+
hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: ["package.json", "package-lock.json"] }),
|
|
12190
12164
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
12191
12165
|
announce: (args) => announceRelease({
|
|
12192
12166
|
run: async (file, cmdArgs) => (await execFileP4(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
@@ -12201,14 +12175,6 @@ function trainApplyDeps() {
|
|
|
12201
12175
|
}
|
|
12202
12176
|
};
|
|
12203
12177
|
}
|
|
12204
|
-
function trainPrepDeps() {
|
|
12205
|
-
return {
|
|
12206
|
-
run: async (file, args) => {
|
|
12207
|
-
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : GIT_TIMEOUT_MS;
|
|
12208
|
-
return (await execFileP4(file, args, { timeout })).stdout;
|
|
12209
|
-
}
|
|
12210
|
-
};
|
|
12211
|
-
}
|
|
12212
12178
|
function formatWorkflowRun(r) {
|
|
12213
12179
|
const ref = r.runUrl ?? (r.runId != null ? String(r.runId) : "unresolved");
|
|
12214
12180
|
return `${r.workflow} ${ref} ${r.conclusion.toUpperCase()}`;
|
|
@@ -12224,6 +12190,7 @@ function renderDeployLine(d) {
|
|
|
12224
12190
|
}
|
|
12225
12191
|
function renderTrainApply(commandName, r) {
|
|
12226
12192
|
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
12193
|
+
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
12227
12194
|
if (r.resumeNote) base = `${base}; ${r.resumeNote}`;
|
|
12228
12195
|
if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
|
|
12229
12196
|
return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
|
|
@@ -12239,27 +12206,6 @@ async function resolveRcandPlanTargets() {
|
|
|
12239
12206
|
return { plannedReleaseError: e.message, existingRcReleaseError: e.message };
|
|
12240
12207
|
}
|
|
12241
12208
|
}
|
|
12242
|
-
function renderTrainPrep(r) {
|
|
12243
|
-
if (!r.applied) {
|
|
12244
|
-
return `mmi-cli train prep: target ${r.target}; run ${r.command} to prepare and stage locked distribution files`;
|
|
12245
|
-
}
|
|
12246
|
-
return [
|
|
12247
|
-
`mmi-cli train prep: prepared ${r.target}`,
|
|
12248
|
-
` - staged locked files: ${r.stagedFiles.join(", ")}`,
|
|
12249
|
-
" - stopped before promotion; no push, merge, tag, release, or deploy was run",
|
|
12250
|
-
` - next: ${r.command}`
|
|
12251
|
-
].join("\n");
|
|
12252
|
-
}
|
|
12253
|
-
var trainCmd = program2.command("train").description("release train helpers that stop before promotion");
|
|
12254
|
-
trainCmd.command("prep").description("prepare and stage the Hub distribution bump for the next release train").option("--json", "machine-readable output").option("--apply", "run release-distribution prepare, exact-file stage, verify, then stop").action(async (o) => {
|
|
12255
|
-
try {
|
|
12256
|
-
await requireFreshTrainCli("train prep");
|
|
12257
|
-
const result = await runTrainPrep(trainPrepDeps(), { apply: o.apply });
|
|
12258
|
-
printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainPrep(result));
|
|
12259
|
-
} catch (e) {
|
|
12260
|
-
fail(`train prep: ${e.message}`);
|
|
12261
|
-
}
|
|
12262
|
-
});
|
|
12263
12209
|
for (const commandName of ["rcand", "release"]) {
|
|
12264
12210
|
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").action(async (o) => {
|
|
12265
12211
|
try {
|
|
@@ -12299,7 +12245,7 @@ function renderHotfixRelease(r) {
|
|
|
12299
12245
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
12300
12246
|
` - ${r.verifyNote}`,
|
|
12301
12247
|
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
12302
|
-
` - next: mmi-cli hotfix status ${r.tag} (no back-merge \u2014 development already has the fix;
|
|
12248
|
+
` - next: mmi-cli hotfix status ${r.tag} (no back-merge \u2014 development already has the fix; the next /release back-merge aligns the version manifests)`
|
|
12303
12249
|
].join("\n");
|
|
12304
12250
|
}
|
|
12305
12251
|
function renderHotfixStatus(r) {
|
|
@@ -12307,7 +12253,7 @@ function renderHotfixStatus(r) {
|
|
|
12307
12253
|
`mmi-cli hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
|
|
12308
12254
|
` - branch: ${r.branchExists ? "pushed" : "absent"} \xB7 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \xB7 tag: ${r.tagPushed ? "pushed" : "absent"} \xB7 Release: ${r.releaseExists ? "exists" : "absent"}`,
|
|
12309
12255
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
12310
|
-
` - npm @mutmutco/cli: ${r.npmVersion}
|
|
12256
|
+
` - npm @mutmutco/cli: ${r.npmVersion}`,
|
|
12311
12257
|
` - next: ${r.next}`
|
|
12312
12258
|
].join("\n");
|
|
12313
12259
|
}
|
|
@@ -12971,7 +12917,7 @@ async function runDoctor(opts, io = consoleIo) {
|
|
|
12971
12917
|
io.log(gaps.length ? `
|
|
12972
12918
|
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
12973
12919
|
}
|
|
12974
|
-
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for
|
|
12920
|
+
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version, Cursor Team Marketplace plugin install), print fixes, and report per-surface versions + update recipes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").action((opts) => (
|
|
12975
12921
|
// Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
|
|
12976
12922
|
runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
|
|
12977
12923
|
));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|