@mutmutco/cli 2.16.0 → 2.17.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/README.md +1 -1
- package/dist/index.cjs +393 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ mmi-cli doctor --json
|
|
|
45
45
|
- `mmi-cli board read|claim|show|move|done|backfill-priority` reads and moves GitHub Project work.
|
|
46
46
|
- `mmi-cli tenant control <owner/repo> <stage> <status|start|stop|restart>` runs bounded dev/rc box control for project-admins through the Hub API; main remains master-only.
|
|
47
47
|
- `mmi-cli stage`, `stage start`, `stage stop`, `stage run`, and `port-range <repo>` manage the local gitignored stage and its port block; `stage-live` explains that remote rc/live move only via `/rcand` · `/release` · `/hotfix`.
|
|
48
|
-
- `mmi-cli rcand`, `release`, and `hotfix` render guarded train plans;
|
|
48
|
+
- `mmi-cli rcand`, `release`, and `hotfix` render guarded train plans; product trains trigger the Hub's central tenant deployer, while MMI-Hub releases directly from `development` to `main`.
|
|
49
49
|
- `mmi-cli bootstrap`, `bootstrap verify`, and `bootstrap apply` plan, audit, and seed repo onboarding.
|
|
50
50
|
- `mmi-cli access audit` checks collaborator roles and train-branch allowlists.
|
|
51
51
|
- `mmi-cli doctor` checks GitHub auth, repo config, CLI availability, plugin install/config/version state, and stale MMI plugin cache dirs, auto-repairing the safe gaps.
|
package/dist/index.cjs
CHANGED
|
@@ -5113,6 +5113,47 @@ function buildRemoteBranchCleanupReport(branch, input) {
|
|
|
5113
5113
|
if (input.existsAfter === false) return { name: branch, status: "deleted" };
|
|
5114
5114
|
return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
|
|
5115
5115
|
}
|
|
5116
|
+
async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input) {
|
|
5117
|
+
const existsAfter = input.attempted ? await deps.exists(branch, { prune: true }) : void 0;
|
|
5118
|
+
return buildRemoteBranchCleanupReport(branch, {
|
|
5119
|
+
attempted: input.attempted,
|
|
5120
|
+
existedBefore: input.existedBefore,
|
|
5121
|
+
existsAfter,
|
|
5122
|
+
reason: input.reason
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
function summarizePrMergeCleanupStatus(input) {
|
|
5126
|
+
if (input.remoteBranch.status === "failed") return "warnings";
|
|
5127
|
+
if (input.localBranch.status === "failed") return "warnings";
|
|
5128
|
+
if (input.localBranch.reason === "worktree-removal-failed") return "warnings";
|
|
5129
|
+
if (input.worktree?.status === "failed") return "warnings";
|
|
5130
|
+
return "clean";
|
|
5131
|
+
}
|
|
5132
|
+
function buildPrMergeResultPayload(input) {
|
|
5133
|
+
return {
|
|
5134
|
+
mergeStatus: "merged",
|
|
5135
|
+
merged: input.number,
|
|
5136
|
+
branch: input.branch,
|
|
5137
|
+
method: input.method,
|
|
5138
|
+
cleanupStatus: summarizePrMergeCleanupStatus({
|
|
5139
|
+
remoteBranch: input.remoteBranch,
|
|
5140
|
+
localBranch: input.localCleanup.localBranch,
|
|
5141
|
+
worktree: input.localCleanup.worktree
|
|
5142
|
+
}),
|
|
5143
|
+
remoteBranch: input.remoteBranch,
|
|
5144
|
+
localBranch: input.localCleanup.localBranch,
|
|
5145
|
+
worktree: input.localCleanup.worktree
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
5148
|
+
async function checkRemoteBranchExists(branch, deps, options = {}) {
|
|
5149
|
+
if (!branch) return void 0;
|
|
5150
|
+
try {
|
|
5151
|
+
if (options.prune) await deps.execGit(["fetch", "origin", "--prune"]).catch(() => void 0);
|
|
5152
|
+
return (await deps.execGit(["ls-remote", "--heads", "origin", branch])).trim().length > 0;
|
|
5153
|
+
} catch {
|
|
5154
|
+
return void 0;
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5116
5157
|
var DEFAULT_PROTECTED = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
|
|
5117
5158
|
function groupedPrs(prs) {
|
|
5118
5159
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -5314,7 +5355,7 @@ function branchMissingFromList(branch, stdout) {
|
|
|
5314
5355
|
return !names.includes(branch);
|
|
5315
5356
|
}
|
|
5316
5357
|
function shellQuote(value) {
|
|
5317
|
-
return `
|
|
5358
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
5318
5359
|
}
|
|
5319
5360
|
function safeWorktreeRemoveCommand(safeCwd, targetPath) {
|
|
5320
5361
|
const prefix = safeCwd ? `git -C ${shellQuote(safeCwd)}` : "git";
|
|
@@ -5340,7 +5381,11 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
5340
5381
|
const wtPath = selectPrMergeCleanupWorktree(branch, beforeWorktrees, afterWorktrees, options.startingPath);
|
|
5341
5382
|
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath);
|
|
5342
5383
|
const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
|
|
5343
|
-
|
|
5384
|
+
const mainWorktreePath = beforeWorktrees[0]?.path ?? afterWorktrees[0]?.path;
|
|
5385
|
+
const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
|
|
5386
|
+
if (wtPath && mainWorktreeTarget) {
|
|
5387
|
+
report.worktree = { path: wtPath, status: "not-attempted", reason: "main-worktree" };
|
|
5388
|
+
} else if (wtPath) {
|
|
5344
5389
|
let stageTeardown;
|
|
5345
5390
|
if (options.teardownWorktreeStage) {
|
|
5346
5391
|
try {
|
|
@@ -5408,6 +5453,43 @@ function formatGcPlan(plan2, apply) {
|
|
|
5408
5453
|
}
|
|
5409
5454
|
|
|
5410
5455
|
// src/command-plans.ts
|
|
5456
|
+
function parseTrainTag(tag) {
|
|
5457
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(tag);
|
|
5458
|
+
return m ? { major: +m[1], minor: +m[2], patch: +m[3], rc: m[4] ? +m[4] : null } : null;
|
|
5459
|
+
}
|
|
5460
|
+
function cmpTrainRelease(a, b) {
|
|
5461
|
+
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
5462
|
+
}
|
|
5463
|
+
function trainPlanTargetsFromTags(tags, explicitRaw) {
|
|
5464
|
+
const all = tags.map(parseTrainTag).filter((v) => Boolean(v));
|
|
5465
|
+
const release = all.find((v) => v.rc === null) ?? { major: 0, minor: 0, patch: 0, rc: null };
|
|
5466
|
+
const targets = {};
|
|
5467
|
+
let explicit = null;
|
|
5468
|
+
if (explicitRaw) {
|
|
5469
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(String(explicitRaw).trim());
|
|
5470
|
+
if (!m) {
|
|
5471
|
+
targets.plannedReleaseError = `MMI_RELEASE_VERSION must be X.Y.Z (got ${explicitRaw})`;
|
|
5472
|
+
} else {
|
|
5473
|
+
explicit = { major: +m[1], minor: +m[2], patch: +m[3], rc: null };
|
|
5474
|
+
if (cmpTrainRelease(explicit, release) <= 0) {
|
|
5475
|
+
targets.plannedReleaseError = `MMI_RELEASE_VERSION ${explicitRaw} must be ahead of the latest release v${release.major}.${release.minor}.${release.patch}`;
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
if (!targets.plannedReleaseError) {
|
|
5480
|
+
const planned = explicit ?? { major: release.major, minor: release.minor + 1, patch: 0, rc: null };
|
|
5481
|
+
targets.plannedRelease = `v${planned.major}.${planned.minor}.${planned.patch}`;
|
|
5482
|
+
}
|
|
5483
|
+
let rcs = all.filter((v) => v.rc !== null);
|
|
5484
|
+
if (explicit) rcs = rcs.filter((v) => v.major === explicit.major && v.minor === explicit.minor && v.patch === explicit.patch);
|
|
5485
|
+
rcs.sort((a, b) => cmpTrainRelease(b, a) || (b.rc ?? 0) - (a.rc ?? 0));
|
|
5486
|
+
if (rcs[0]) {
|
|
5487
|
+
targets.existingRcRelease = `v${rcs[0].major}.${rcs[0].minor}.${rcs[0].patch}`;
|
|
5488
|
+
} else {
|
|
5489
|
+
targets.existingRcReleaseError = explicit ? `no rc tag for ${explicit.major}.${explicit.minor}.${explicit.patch} to release` : "no rc tag to release";
|
|
5490
|
+
}
|
|
5491
|
+
return targets;
|
|
5492
|
+
}
|
|
5411
5493
|
function stagePlan(stage2 = {}, stops = true) {
|
|
5412
5494
|
return [
|
|
5413
5495
|
...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
|
|
@@ -5436,20 +5518,49 @@ function stageLivePlan() {
|
|
|
5436
5518
|
{ label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli rcand && mmi-cli release && mmi-cli hotfix", gated: true }
|
|
5437
5519
|
];
|
|
5438
5520
|
}
|
|
5439
|
-
function
|
|
5521
|
+
function rcandVersionStep(targets) {
|
|
5522
|
+
const planned = targets.plannedRelease ? `plannedRelease=${targets.plannedRelease}` : "plannedRelease=(unresolved)";
|
|
5523
|
+
const existing = targets.existingRcRelease ? `existingRcRelease=${targets.existingRcRelease}` : "existingRcRelease=(none resolved yet)";
|
|
5524
|
+
return {
|
|
5525
|
+
label: `resolve train version targets (${planned}; ${existing})`,
|
|
5526
|
+
command: "plannedRelease: node scripts/next-version.mjs cycle; existing rc release only: node scripts/next-version.mjs release",
|
|
5527
|
+
gated: true
|
|
5528
|
+
};
|
|
5529
|
+
}
|
|
5530
|
+
function trainPlan(command, options = {}) {
|
|
5531
|
+
const isHub = options.repo?.toLowerCase() === "mutmutco/mmi-hub";
|
|
5440
5532
|
if (command === "rcand") {
|
|
5533
|
+
if (isHub) {
|
|
5534
|
+
return [
|
|
5535
|
+
{ label: "MMI-Hub releases skip rc; use /release from development instead", command: "mmi-cli release --apply", gated: true }
|
|
5536
|
+
];
|
|
5537
|
+
}
|
|
5441
5538
|
return [
|
|
5442
5539
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
5443
5540
|
{ label: "verify current branch is development", gated: true },
|
|
5541
|
+
rcandVersionStep(options),
|
|
5444
5542
|
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5445
5543
|
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
5446
|
-
{ 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", gated: true },
|
|
5544
|
+
{ 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 },
|
|
5447
5545
|
{ label: "merge development to rc", gated: true },
|
|
5448
|
-
{ label: "trigger the deploy path for this repo model, returning
|
|
5546
|
+
{ 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 },
|
|
5449
5547
|
{ 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 }
|
|
5450
5548
|
];
|
|
5451
5549
|
}
|
|
5452
5550
|
if (command === "release") {
|
|
5551
|
+
if (isHub) {
|
|
5552
|
+
return [
|
|
5553
|
+
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
5554
|
+
{ label: "verify current branch is development", gated: true },
|
|
5555
|
+
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
5556
|
+
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5557
|
+
{ label: "Hub releases skip rc; verify the distribution bump is already landed on development", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5558
|
+
{ label: "merge development to main", gated: true },
|
|
5559
|
+
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5560
|
+
{ label: "trigger the Hub deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release", gated: true },
|
|
5561
|
+
{ label: "roll development forward", gated: true }
|
|
5562
|
+
];
|
|
5563
|
+
}
|
|
5453
5564
|
return [
|
|
5454
5565
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
5455
5566
|
{ label: "verify current branch is rc", gated: true },
|
|
@@ -5457,9 +5568,9 @@ function trainPlan(command) {
|
|
|
5457
5568
|
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
5458
5569
|
{ label: "verify every main-only hotfix commit is covered by the rc candidate", command: "node scripts/hotfix-coverage.mjs", gated: true },
|
|
5459
5570
|
{ label: "merge rc to main", gated: true },
|
|
5460
|
-
{ label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
|
|
5571
|
+
{ label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view; if missing: mmi-cli train prep --apply", gated: true },
|
|
5461
5572
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
5462
|
-
{ label: "trigger the deploy path for this repo model, returning
|
|
5573
|
+
{ 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 },
|
|
5463
5574
|
{ label: "roll development forward", gated: true }
|
|
5464
5575
|
];
|
|
5465
5576
|
}
|
|
@@ -5546,9 +5657,9 @@ function stalePosixFields(config, shell2) {
|
|
|
5546
5657
|
}
|
|
5547
5658
|
function sanitizeLocalStage(local, stale) {
|
|
5548
5659
|
if (!stale.length) return local;
|
|
5549
|
-
const
|
|
5550
|
-
for (const field of stale) delete
|
|
5551
|
-
return
|
|
5660
|
+
const clean4 = { ...local };
|
|
5661
|
+
for (const field of stale) delete clean4[field];
|
|
5662
|
+
return clean4;
|
|
5552
5663
|
}
|
|
5553
5664
|
function staleNote(staleFields, outcome) {
|
|
5554
5665
|
const list = staleFields.join(", ");
|
|
@@ -6338,6 +6449,30 @@ async function verifyHubDistributionVersion(deps, model, releaseTag) {
|
|
|
6338
6449
|
if (model !== "hub-serverless") return;
|
|
6339
6450
|
await deps.run("node", ["scripts/release-distribution.mjs", "verify", releaseTag, "--skip-npm-view"]);
|
|
6340
6451
|
}
|
|
6452
|
+
async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedSha) {
|
|
6453
|
+
const out = await deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName,targetCommitish"]);
|
|
6454
|
+
let release;
|
|
6455
|
+
try {
|
|
6456
|
+
release = JSON.parse(out);
|
|
6457
|
+
} catch {
|
|
6458
|
+
throw new Error(`Release ${tag} metadata was not valid JSON`);
|
|
6459
|
+
}
|
|
6460
|
+
if (release.tagName !== tag) {
|
|
6461
|
+
throw new Error(`Release metadata tag is ${String(release.tagName || "(missing)")}, expected ${tag}`);
|
|
6462
|
+
}
|
|
6463
|
+
if (release.targetCommitish !== expectedTarget) {
|
|
6464
|
+
throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
|
|
6465
|
+
}
|
|
6466
|
+
const tagSha = requireValue(clean(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
|
|
6467
|
+
if (tagSha !== expectedSha) {
|
|
6468
|
+
throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
|
|
6469
|
+
}
|
|
6470
|
+
const branchOut = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
|
|
6471
|
+
const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
|
|
6472
|
+
if (branchSha !== expectedSha) {
|
|
6473
|
+
throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6341
6476
|
var ORG_SPINE_FILES = [
|
|
6342
6477
|
"AGENTS.md",
|
|
6343
6478
|
"CLAUDE.md",
|
|
@@ -6414,6 +6549,9 @@ async function requireBranch(deps, branch) {
|
|
|
6414
6549
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
6415
6550
|
}
|
|
6416
6551
|
var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
6552
|
+
function isHubControlRepo(repo) {
|
|
6553
|
+
return repo.toLowerCase() === HUB_REPO2.toLowerCase();
|
|
6554
|
+
}
|
|
6417
6555
|
var CORRELATE_ATTEMPTS = 5;
|
|
6418
6556
|
var CORRELATE_DELAY_MS = 1500;
|
|
6419
6557
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
@@ -6451,6 +6589,42 @@ async function correlateTenantRun(deps, since) {
|
|
|
6451
6589
|
}
|
|
6452
6590
|
return {};
|
|
6453
6591
|
}
|
|
6592
|
+
async function correlateWorkflowRun(deps, args) {
|
|
6593
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6594
|
+
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
6595
|
+
let lastError;
|
|
6596
|
+
let parsedAnyResponse = false;
|
|
6597
|
+
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
6598
|
+
if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
|
|
6599
|
+
const listArgs = [
|
|
6600
|
+
"run",
|
|
6601
|
+
"list",
|
|
6602
|
+
"--repo",
|
|
6603
|
+
HUB_REPO2,
|
|
6604
|
+
"--workflow",
|
|
6605
|
+
args.workflow,
|
|
6606
|
+
"--event",
|
|
6607
|
+
args.event,
|
|
6608
|
+
...args.branch ? ["--branch", args.branch] : [],
|
|
6609
|
+
"--limit",
|
|
6610
|
+
"10",
|
|
6611
|
+
"--json",
|
|
6612
|
+
"databaseId,url,event,createdAt,status,conclusion,headSha"
|
|
6613
|
+
];
|
|
6614
|
+
let rows;
|
|
6615
|
+
try {
|
|
6616
|
+
rows = JSON.parse(await deps.run("gh", listArgs));
|
|
6617
|
+
parsedAnyResponse = true;
|
|
6618
|
+
} catch {
|
|
6619
|
+
lastError = new Error(`could not list ${args.workflow} runs`);
|
|
6620
|
+
continue;
|
|
6621
|
+
}
|
|
6622
|
+
const match = rows.filter((r) => r.event === args.event && r.headSha === args.headSha && typeof r.databaseId === "number").map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
|
|
6623
|
+
if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
|
|
6624
|
+
}
|
|
6625
|
+
if (!parsedAnyResponse && lastError) throw lastError;
|
|
6626
|
+
return {};
|
|
6627
|
+
}
|
|
6454
6628
|
async function watchTenantRun(deps, runId) {
|
|
6455
6629
|
if (runId == null) return "pending";
|
|
6456
6630
|
try {
|
|
@@ -6460,6 +6634,16 @@ async function watchTenantRun(deps, runId) {
|
|
|
6460
6634
|
return "failure";
|
|
6461
6635
|
}
|
|
6462
6636
|
}
|
|
6637
|
+
async function watchWorkflowRun(deps, workflow, run) {
|
|
6638
|
+
if (run.runId == null) return { workflow, conclusion: "pending" };
|
|
6639
|
+
const conclusion = await watchTenantRun(deps, run.runId);
|
|
6640
|
+
return { workflow, runId: run.runId, runUrl: run.runUrl, conclusion };
|
|
6641
|
+
}
|
|
6642
|
+
function aggregateWorkflowRuns(runs) {
|
|
6643
|
+
if (runs.some((r) => r.conclusion === "failure")) return "failure";
|
|
6644
|
+
if (runs.some((r) => r.conclusion === "pending")) return "pending";
|
|
6645
|
+
return "success";
|
|
6646
|
+
}
|
|
6463
6647
|
function isNotFoundError(e) {
|
|
6464
6648
|
const msg = `${e.message ?? e} ${String(e.stderr ?? "")}`;
|
|
6465
6649
|
return /HTTP 404|Not Found|\(404\)/i.test(msg);
|
|
@@ -6578,7 +6762,7 @@ async function resolveRcResumeTag(deps, base, sha) {
|
|
|
6578
6762
|
const note = leftovers.length > 0 ? `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} (newest of ${sorted.length}); harmless leftover tag(s) on the same SHA: ${leftovers.join(", ")}` : `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} instead of minting a fresh rc`;
|
|
6579
6763
|
return { tag: newest, note };
|
|
6580
6764
|
}
|
|
6581
|
-
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
|
|
6765
|
+
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha) {
|
|
6582
6766
|
if (model === "tenant-container") {
|
|
6583
6767
|
const since = (deps.now ?? Date.now)();
|
|
6584
6768
|
await deps.run("gh", [
|
|
@@ -6601,9 +6785,30 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
|
|
|
6601
6785
|
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
6602
6786
|
}
|
|
6603
6787
|
if (model === "hub-serverless") {
|
|
6788
|
+
const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
|
|
6789
|
+
if (!watch) return { note, deployStatus: "pending" };
|
|
6790
|
+
if (!autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
6791
|
+
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
6792
|
+
const targets = ref === "rc" ? [{ workflow: "deploy.yml", event: "push", branch: "rc" }] : [
|
|
6793
|
+
{ workflow: "deploy.yml", event: "release" },
|
|
6794
|
+
{ workflow: "publish.yml", event: "release" }
|
|
6795
|
+
];
|
|
6796
|
+
const workflowRuns = [];
|
|
6797
|
+
for (const target of targets) {
|
|
6798
|
+
try {
|
|
6799
|
+
const run = await correlateWorkflowRun(deps, { ...target, since, headSha: autoRunHeadSha });
|
|
6800
|
+
workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run));
|
|
6801
|
+
} catch {
|
|
6802
|
+
workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
const primary = workflowRuns[0];
|
|
6604
6806
|
return {
|
|
6605
|
-
note
|
|
6606
|
-
|
|
6807
|
+
note,
|
|
6808
|
+
runId: primary?.runId,
|
|
6809
|
+
runUrl: primary?.runUrl,
|
|
6810
|
+
workflowRuns,
|
|
6811
|
+
deployStatus: aggregateWorkflowRuns(workflowRuns)
|
|
6607
6812
|
};
|
|
6608
6813
|
}
|
|
6609
6814
|
return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
|
|
@@ -6638,6 +6843,9 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6638
6843
|
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
6639
6844
|
);
|
|
6640
6845
|
const deployModel2 = await preflight(deps, ctx, "rc");
|
|
6846
|
+
if (isHubControlRepo(ctx.repo) && deployModel2 === "hub-serverless") {
|
|
6847
|
+
throw new Error("MMI-Hub releases directly from development to main; run mmi-cli release --apply from development instead of rcand");
|
|
6848
|
+
}
|
|
6641
6849
|
const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
|
|
6642
6850
|
await verifyHubDistributionVersion(deps, deployModel2, releaseBase);
|
|
6643
6851
|
await deps.run("git", ["checkout", "rc"]);
|
|
@@ -6650,9 +6858,70 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6650
6858
|
await ensureTagPushed(deps, tag2, rcSha);
|
|
6651
6859
|
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
|
|
6652
6860
|
const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks2);
|
|
6861
|
+
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
6653
6862
|
await deps.run("git", ["push", "origin", "rc"]);
|
|
6654
|
-
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch);
|
|
6655
|
-
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, deployStatus: d2.deployStatus };
|
|
6863
|
+
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince2, rcSha);
|
|
6864
|
+
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 };
|
|
6865
|
+
}
|
|
6866
|
+
if (isHubControlRepo(ctx.repo)) {
|
|
6867
|
+
await requireBranch(deps, "development");
|
|
6868
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6869
|
+
ensurePositiveCount(
|
|
6870
|
+
await deps.run("git", ["rev-list", "--count", "origin/main..origin/development"]),
|
|
6871
|
+
"nothing to release: origin/development is not ahead of origin/main"
|
|
6872
|
+
);
|
|
6873
|
+
const deployModel2 = await preflight(deps, ctx, "main");
|
|
6874
|
+
if (deployModel2 !== "hub-serverless") {
|
|
6875
|
+
throw new Error(`MMI-Hub direct release requires deployModel=hub-serverless, got ${deployModel2}`);
|
|
6876
|
+
}
|
|
6877
|
+
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
6878
|
+
await verifyHubDistributionVersion(deps, deployModel2, tag2);
|
|
6879
|
+
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
6880
|
+
const predictedNonSpine2 = predicted2.filter((f) => !isSpinePath(f));
|
|
6881
|
+
if (predictedNonSpine2.length > 0) {
|
|
6882
|
+
throw new Error(
|
|
6883
|
+
`development -> main merge would conflict on non-spine path(s): ${predictedNonSpine2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
|
|
6884
|
+
);
|
|
6885
|
+
}
|
|
6886
|
+
await deps.run("git", ["checkout", "main"]);
|
|
6887
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
6888
|
+
if (predicted2.length === 0) {
|
|
6889
|
+
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
6890
|
+
} else {
|
|
6891
|
+
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs");
|
|
6892
|
+
}
|
|
6893
|
+
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
6894
|
+
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
6895
|
+
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
6896
|
+
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
6897
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
6898
|
+
const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
6899
|
+
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
6900
|
+
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
6901
|
+
const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2);
|
|
6902
|
+
await deps.run("git", ["checkout", "development"]);
|
|
6903
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6904
|
+
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
6905
|
+
await deps.run("git", ["push", "origin", "development"]);
|
|
6906
|
+
return {
|
|
6907
|
+
...ctx,
|
|
6908
|
+
command,
|
|
6909
|
+
stage: "main",
|
|
6910
|
+
ref: "main",
|
|
6911
|
+
tag: tag2,
|
|
6912
|
+
deployModel: deployModel2,
|
|
6913
|
+
promoted: true,
|
|
6914
|
+
checks: checks2,
|
|
6915
|
+
dispatch: d2.note,
|
|
6916
|
+
runId: d2.runId,
|
|
6917
|
+
runUrl: d2.runUrl,
|
|
6918
|
+
workflowRuns: d2.workflowRuns,
|
|
6919
|
+
deployStatus: d2.deployStatus,
|
|
6920
|
+
rcRetirement: "not-applicable",
|
|
6921
|
+
rcRetirementNote: "Hub releases skip rc; no rc runtime to retire",
|
|
6922
|
+
announceNote: announceNote2,
|
|
6923
|
+
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
6924
|
+
};
|
|
6656
6925
|
}
|
|
6657
6926
|
await requireBranch(deps, "rc");
|
|
6658
6927
|
ensurePositiveCount(
|
|
@@ -6682,9 +6951,11 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6682
6951
|
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
6683
6952
|
const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
6684
6953
|
await deps.run("git", ["push", "origin", "main"]);
|
|
6685
|
-
const
|
|
6954
|
+
const autoRunSince = (deps.now ?? Date.now)();
|
|
6955
|
+
const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
6956
|
+
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
6686
6957
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
6687
|
-
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch);
|
|
6958
|
+
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha);
|
|
6688
6959
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
6689
6960
|
await deps.run("git", ["checkout", "development"]);
|
|
6690
6961
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
@@ -6703,6 +6974,7 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6703
6974
|
dispatch: d.note,
|
|
6704
6975
|
runId: d.runId,
|
|
6705
6976
|
runUrl: d.runUrl,
|
|
6977
|
+
workflowRuns: d.workflowRuns,
|
|
6706
6978
|
deployStatus: d.deployStatus,
|
|
6707
6979
|
rcRetirement: retirement.status,
|
|
6708
6980
|
rcRetirementNote: retirement.note,
|
|
@@ -6814,14 +7086,42 @@ async function runTenantRedeploy(deps, options) {
|
|
|
6814
7086
|
throw new Error(`${repo} is ${deployModel}, not tenant-container \u2014 there is no central tenant-deploy run to retry (its deploy fires from its own workflow)`);
|
|
6815
7087
|
}
|
|
6816
7088
|
const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
|
|
6817
|
-
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, deployStatus: d.deployStatus };
|
|
7089
|
+
return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
|
|
7090
|
+
}
|
|
7091
|
+
|
|
7092
|
+
// src/train-prep.ts
|
|
7093
|
+
function clean2(text) {
|
|
7094
|
+
return text.trim();
|
|
7095
|
+
}
|
|
7096
|
+
function normalizeVersion2(target) {
|
|
7097
|
+
const version = target.replace(/^v/, "");
|
|
7098
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
7099
|
+
throw new Error(`invalid train prep target ${target || "(empty)"}`);
|
|
7100
|
+
}
|
|
7101
|
+
return version;
|
|
7102
|
+
}
|
|
7103
|
+
function parseChangedFiles(out) {
|
|
7104
|
+
return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
7105
|
+
}
|
|
7106
|
+
async function runTrainPrep(deps, options = {}) {
|
|
7107
|
+
const target = clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"]));
|
|
7108
|
+
const version = normalizeVersion2(target);
|
|
7109
|
+
if (!options.apply) {
|
|
7110
|
+
return { target, version, applied: false, command: "mmi-cli train prep --apply", stagedFiles: [] };
|
|
7111
|
+
}
|
|
7112
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", target]);
|
|
7113
|
+
const stagedFiles = parseChangedFiles(await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"]));
|
|
7114
|
+
if (stagedFiles.length === 0) throw new Error("no locked distribution files returned by release-distribution changed-files");
|
|
7115
|
+
await deps.run("git", ["add", "--", ...stagedFiles]);
|
|
7116
|
+
await deps.run("node", ["scripts/release-distribution.mjs", "verify", target, "--skip-npm-view"]);
|
|
7117
|
+
return { target, version, applied: true, command: "mmi-cli pr create --base development --head <current-branch>", stagedFiles };
|
|
6818
7118
|
}
|
|
6819
7119
|
|
|
6820
7120
|
// src/hotfix-apply.ts
|
|
6821
7121
|
var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
|
|
6822
7122
|
var HOTFIX_RUN_FIND_ATTEMPTS = 10;
|
|
6823
7123
|
var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
|
|
6824
|
-
function
|
|
7124
|
+
function clean3(out) {
|
|
6825
7125
|
return out.trim();
|
|
6826
7126
|
}
|
|
6827
7127
|
function sleeper(deps) {
|
|
@@ -6875,7 +7175,7 @@ async function resolveHotfixSource(deps, ctx, from) {
|
|
|
6875
7175
|
if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
|
|
6876
7176
|
return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
|
|
6877
7177
|
}
|
|
6878
|
-
const sha =
|
|
7178
|
+
const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
|
|
6879
7179
|
if (!sha) throw new Error(`could not resolve commit ${from}`);
|
|
6880
7180
|
return { sha, label: sha.slice(0, 7) };
|
|
6881
7181
|
}
|
|
@@ -6902,7 +7202,7 @@ async function runHotfixStart(deps, options) {
|
|
|
6902
7202
|
};
|
|
6903
7203
|
}
|
|
6904
7204
|
const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
|
|
6905
|
-
const remoteBranch =
|
|
7205
|
+
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
6906
7206
|
if (remoteBranch) {
|
|
6907
7207
|
await deps.run("git", ["checkout", branch]);
|
|
6908
7208
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
@@ -6928,7 +7228,7 @@ async function runHotfixStart(deps, options) {
|
|
|
6928
7228
|
}
|
|
6929
7229
|
await deps.run("git", ["push", "-u", "origin", branch]);
|
|
6930
7230
|
}
|
|
6931
|
-
const prUrl =
|
|
7231
|
+
const prUrl = clean3(await deps.run("gh", [
|
|
6932
7232
|
"pr",
|
|
6933
7233
|
"create",
|
|
6934
7234
|
"--repo",
|
|
@@ -7011,7 +7311,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
7011
7311
|
if (releaseExists) {
|
|
7012
7312
|
releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
|
|
7013
7313
|
} else {
|
|
7014
|
-
const tagCommit =
|
|
7314
|
+
const tagCommit = clean3(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
|
|
7015
7315
|
await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
|
|
7016
7316
|
releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
|
|
7017
7317
|
if (deps.announce) {
|
|
@@ -7022,7 +7322,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
7022
7322
|
for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
|
|
7023
7323
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
7024
7324
|
}
|
|
7025
|
-
const previousRef =
|
|
7325
|
+
const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
7026
7326
|
let verifyNote;
|
|
7027
7327
|
const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
|
|
7028
7328
|
try {
|
|
@@ -7090,9 +7390,9 @@ async function runHotfixStatus(deps, versionInput) {
|
|
|
7090
7390
|
return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
|
|
7091
7391
|
}
|
|
7092
7392
|
async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
7093
|
-
const branchExists = Boolean(
|
|
7393
|
+
const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
|
|
7094
7394
|
const pr2 = await findHotfixPr(deps, ctx, tag);
|
|
7095
|
-
const remoteTag =
|
|
7395
|
+
const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
|
|
7096
7396
|
const tagPushed = Boolean(remoteTag);
|
|
7097
7397
|
const tagSha = remoteTag.split(/\s+/)[0] || "";
|
|
7098
7398
|
let releaseExists = false;
|
|
@@ -7126,7 +7426,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
|
|
|
7126
7426
|
}
|
|
7127
7427
|
}
|
|
7128
7428
|
}
|
|
7129
|
-
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(
|
|
7429
|
+
const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
|
|
7130
7430
|
const devVersion = await deps.run("git", ["show", "origin/development:cli/package.json"]).then(
|
|
7131
7431
|
(out) => {
|
|
7132
7432
|
try {
|
|
@@ -8697,8 +8997,8 @@ function resolveKbSource(rawBase) {
|
|
|
8697
8997
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
8698
8998
|
}
|
|
8699
8999
|
function buildKbGetArgs(src, path2) {
|
|
8700
|
-
const
|
|
8701
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
9000
|
+
const clean4 = path2.replace(/^\/+/, "");
|
|
9001
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
8702
9002
|
}
|
|
8703
9003
|
function buildKbTreeArgs(src) {
|
|
8704
9004
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -10832,14 +11132,10 @@ pr.command("create").description("create a PR and print {number,url} JSON").requ
|
|
|
10832
11132
|
const created = await ghCreate(buildPrArgs({ title: o.title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
10833
11133
|
console.log(JSON.stringify(created));
|
|
10834
11134
|
});
|
|
10835
|
-
async function remoteBranchExists(branch) {
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
return stdout.trim().length > 0;
|
|
10840
|
-
} catch {
|
|
10841
|
-
return void 0;
|
|
10842
|
-
}
|
|
11135
|
+
async function remoteBranchExists(branch, options = {}) {
|
|
11136
|
+
return checkRemoteBranchExists(branch, {
|
|
11137
|
+
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
|
|
11138
|
+
}, options);
|
|
10843
11139
|
}
|
|
10844
11140
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
10845
11141
|
function teardownWorktreeStage(worktreePath) {
|
|
@@ -10877,11 +11173,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
10877
11173
|
if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
|
|
10878
11174
|
});
|
|
10879
11175
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
10880
|
-
const
|
|
10881
|
-
|
|
11176
|
+
const remoteBranch = repoArgs.length ? buildRemoteBranchCleanupReport(headRef, {
|
|
11177
|
+
attempted: false,
|
|
11178
|
+
reason: remoteNotAttemptedReason
|
|
11179
|
+
}) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
11180
|
+
exists: remoteBranchExists
|
|
11181
|
+
}, {
|
|
10882
11182
|
attempted: remoteDeleteAttempted,
|
|
10883
11183
|
existedBefore: remoteBefore,
|
|
10884
|
-
existsAfter: remoteAfter,
|
|
10885
11184
|
reason: remoteNotAttemptedReason
|
|
10886
11185
|
});
|
|
10887
11186
|
const localCleanup = repoArgs.length ? {
|
|
@@ -10894,14 +11193,13 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
10894
11193
|
execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
10895
11194
|
teardownWorktreeStage
|
|
10896
11195
|
});
|
|
10897
|
-
console.log(JSON.stringify({
|
|
10898
|
-
|
|
11196
|
+
console.log(JSON.stringify(buildPrMergeResultPayload({
|
|
11197
|
+
number,
|
|
10899
11198
|
branch: headRef,
|
|
10900
11199
|
method: method.slice(2),
|
|
10901
11200
|
remoteBranch,
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
}));
|
|
11201
|
+
localCleanup
|
|
11202
|
+
})));
|
|
10905
11203
|
});
|
|
10906
11204
|
async function runBoardRead(o) {
|
|
10907
11205
|
try {
|
|
@@ -11218,9 +11516,22 @@ function trainApplyDeps() {
|
|
|
11218
11516
|
}
|
|
11219
11517
|
};
|
|
11220
11518
|
}
|
|
11519
|
+
function trainPrepDeps() {
|
|
11520
|
+
return {
|
|
11521
|
+
run: async (file, args) => {
|
|
11522
|
+
const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : GIT_TIMEOUT_MS;
|
|
11523
|
+
return (await execFileP4(file, args, { timeout })).stdout;
|
|
11524
|
+
}
|
|
11525
|
+
};
|
|
11526
|
+
}
|
|
11527
|
+
function formatWorkflowRun(r) {
|
|
11528
|
+
const ref = r.runUrl ?? (r.runId != null ? String(r.runId) : "unresolved");
|
|
11529
|
+
return `${r.workflow} ${ref} ${r.conclusion.toUpperCase()}`;
|
|
11530
|
+
}
|
|
11221
11531
|
function renderDeployLine(d) {
|
|
11222
11532
|
const parts = [d.dispatch];
|
|
11223
|
-
if (d.
|
|
11533
|
+
if (d.workflowRuns?.length) parts.push(`runs: ${d.workflowRuns.map(formatWorkflowRun).join(", ")}`);
|
|
11534
|
+
else if (d.runUrl) parts.push(`run ${d.runUrl}`);
|
|
11224
11535
|
if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
|
|
11225
11536
|
else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
|
|
11226
11537
|
else if (d.runId != null) parts.push(`deploy: dispatched (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
@@ -11235,8 +11546,37 @@ function renderTrainApply(commandName, r) {
|
|
|
11235
11546
|
function renderTenantRedeploy(r) {
|
|
11236
11547
|
return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
11237
11548
|
}
|
|
11549
|
+
async function resolveRcandPlanTargets() {
|
|
11550
|
+
try {
|
|
11551
|
+
const tags = (await execFileP4("git", ["tag", "--list", "v*", "--sort=-v:refname"], { timeout: 1e4 })).stdout.trim().split("\n").filter(Boolean);
|
|
11552
|
+
return trainPlanTargetsFromTags(tags, process.env.MMI_RELEASE_VERSION);
|
|
11553
|
+
} catch (e) {
|
|
11554
|
+
return { plannedReleaseError: e.message, existingRcReleaseError: e.message };
|
|
11555
|
+
}
|
|
11556
|
+
}
|
|
11557
|
+
function renderTrainPrep(r) {
|
|
11558
|
+
if (!r.applied) {
|
|
11559
|
+
return `mmi-cli train prep: target ${r.target}; run ${r.command} to prepare and stage locked distribution files`;
|
|
11560
|
+
}
|
|
11561
|
+
return [
|
|
11562
|
+
`mmi-cli train prep: prepared ${r.target}`,
|
|
11563
|
+
` - staged locked files: ${r.stagedFiles.join(", ")}`,
|
|
11564
|
+
" - stopped before promotion; no push, merge, tag, release, or deploy was run",
|
|
11565
|
+
` - next: ${r.command}`
|
|
11566
|
+
].join("\n");
|
|
11567
|
+
}
|
|
11568
|
+
var trainCmd = program2.command("train").description("release train helpers that stop before promotion");
|
|
11569
|
+
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) => {
|
|
11570
|
+
try {
|
|
11571
|
+
await requireFreshTrainCli("train prep");
|
|
11572
|
+
const result = await runTrainPrep(trainPrepDeps(), { apply: o.apply });
|
|
11573
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainPrep(result));
|
|
11574
|
+
} catch (e) {
|
|
11575
|
+
fail(`train prep: ${e.message}`);
|
|
11576
|
+
}
|
|
11577
|
+
});
|
|
11238
11578
|
for (const commandName of ["rcand", "release"]) {
|
|
11239
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the
|
|
11579
|
+
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)").action(async (o) => {
|
|
11240
11580
|
try {
|
|
11241
11581
|
await requireFreshTrainCli(commandName);
|
|
11242
11582
|
} catch (e) {
|
|
@@ -11250,8 +11590,12 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
11250
11590
|
return fail(`${commandName}: ${e.message}`);
|
|
11251
11591
|
}
|
|
11252
11592
|
}
|
|
11253
|
-
const
|
|
11254
|
-
|
|
11593
|
+
const repo = await resolveRepo();
|
|
11594
|
+
const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
|
|
11595
|
+
const steps = trainPlan(commandName, { ...targets ?? {}, repo });
|
|
11596
|
+
console.log(
|
|
11597
|
+
o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps)
|
|
11598
|
+
);
|
|
11255
11599
|
});
|
|
11256
11600
|
}
|
|
11257
11601
|
function renderHotfixStart(r) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.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",
|