@mutmutco/cli 4.3.61 → 4.3.63
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 +4 -0
- package/dist/main.cjs +200 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,4 +85,8 @@ node cli/dist/index.cjs --version
|
|
|
85
85
|
node cli/dist/index.cjs doctor --json
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
When reproducing or verifying CLI behavior from a checkout, always run the checkout's built
|
|
89
|
+
`node cli/dist/index.cjs` — the globally installed `mmi-cli` can lag or diverge from this source
|
|
90
|
+
(#6629), and a reproduction against the global install misreports the fix you are validating.
|
|
91
|
+
|
|
88
92
|
The Claude Code plugin also ships its own `bin/mmi-cli` launcher, so plugin-enabled Bash sessions can use the bundled command even without a global npm install. Codex and other non-Claude surfaces use the standalone npm package or this repo-local fallback.
|
package/dist/main.cjs
CHANGED
|
@@ -12070,12 +12070,15 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
12070
12070
|
resume
|
|
12071
12071
|
};
|
|
12072
12072
|
}
|
|
12073
|
-
function
|
|
12073
|
+
function laneContestParts(contest) {
|
|
12074
12074
|
const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : contest.unverifiable.includes("comments") ? "an unreadable lane (its claim-marker comments could not be read, so no holder can be named)" : "another lane";
|
|
12075
12075
|
const seen = contest.live.join("; ");
|
|
12076
12076
|
const unreadable = contest.unverifiable.length ? `liveness could not be fully verified (failed to read: ${contest.unverifiable.join(", ")})` : "";
|
|
12077
|
-
|
|
12078
|
-
|
|
12077
|
+
return { holder, why: [seen, unreadable].filter(Boolean).join("; ") };
|
|
12078
|
+
}
|
|
12079
|
+
function laneContestMessage(ref, contest, verb) {
|
|
12080
|
+
const { holder, why } = laneContestParts(contest);
|
|
12081
|
+
return verb === "claim" ? `${ref} is already claimed by ${holder} with live evidence of active work (${why}); re-run with --force to take it anyway` : `refusing to unclaim ${ref}: ${holder} shows live evidence of active work (${why}); re-run with --force --holder-dead once you have evidence the holder lane is dead, or wait for it to go stale (#6625)`;
|
|
12079
12082
|
}
|
|
12080
12083
|
function ghError(e) {
|
|
12081
12084
|
const err = e;
|
|
@@ -12633,9 +12636,19 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
12633
12636
|
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
12634
12637
|
}
|
|
12635
12638
|
const toStatus = options.toStatus ?? "Todo";
|
|
12636
|
-
if (
|
|
12639
|
+
if (item.contentType === "Issue") {
|
|
12637
12640
|
const contest = await checkLaneContest(client, item);
|
|
12638
|
-
if (contest.contested)
|
|
12641
|
+
if (contest.contested) {
|
|
12642
|
+
if (!options.force) {
|
|
12643
|
+
throw new Error(withGatedWriteChainHint(laneContestMessage(item.ref, contest, "unclaim")));
|
|
12644
|
+
}
|
|
12645
|
+
if (!options.holderDead) {
|
|
12646
|
+
const { holder, why } = laneContestParts(contest);
|
|
12647
|
+
throw new Error(withGatedWriteChainHint(
|
|
12648
|
+
`refusing to unclaim ${item.ref} with --force: ${holder} still shows live evidence of active work (${why}); --force no longer overrides a live lane alone \u2014 re-run with --holder-dead once you have evidence the holder's session is dead, or wait for its lane to go stale (#6625)`
|
|
12649
|
+
));
|
|
12650
|
+
}
|
|
12651
|
+
}
|
|
12639
12652
|
}
|
|
12640
12653
|
try {
|
|
12641
12654
|
await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
|
|
@@ -15788,10 +15801,10 @@ var rollout_plan_default = {
|
|
|
15788
15801
|
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)."
|
|
15789
15802
|
},
|
|
15790
15803
|
baseline: {
|
|
15791
|
-
version: "4.3.
|
|
15792
|
-
tag: "v4.3.
|
|
15793
|
-
commit: "
|
|
15794
|
-
npm: "@mutmutco/cli@4.3.
|
|
15804
|
+
version: "4.3.63",
|
|
15805
|
+
tag: "v4.3.63",
|
|
15806
|
+
commit: "23180ae03611",
|
|
15807
|
+
npm: "@mutmutco/cli@4.3.63"
|
|
15795
15808
|
},
|
|
15796
15809
|
exitCriterion: "fleet-n-of-n",
|
|
15797
15810
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15808,14 +15821,14 @@ var rollout_plan_default = {
|
|
|
15808
15821
|
repo: "mutmutco/mmi-hub",
|
|
15809
15822
|
role: "canary",
|
|
15810
15823
|
schedule: "train",
|
|
15811
|
-
v3Target: "v4.3.
|
|
15824
|
+
v3Target: "v4.3.63"
|
|
15812
15825
|
}
|
|
15813
15826
|
],
|
|
15814
15827
|
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.",
|
|
15815
15828
|
rollback: {
|
|
15816
15829
|
independent: true,
|
|
15817
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15818
|
-
v3Target: "v4.3.
|
|
15830
|
+
mechanism: "npm dist-tag latest -> 4.3.63 and redeploy the Hub Lambda from tag v4.3.63 (23180ae03611); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15831
|
+
v3Target: "v4.3.63 (@mutmutco/cli@4.3.63, tag commit 23180ae03611 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15819
15832
|
}
|
|
15820
15833
|
},
|
|
15821
15834
|
{
|
|
@@ -21451,6 +21464,69 @@ async function supersedeContentCausedDeployLeg(deps, cwd) {
|
|
|
21451
21464
|
};
|
|
21452
21465
|
});
|
|
21453
21466
|
}
|
|
21467
|
+
function supersedeablePublishLegShape(rec) {
|
|
21468
|
+
return rec.state === "failed" || rec.state === "pending" && rec.runId == null;
|
|
21469
|
+
}
|
|
21470
|
+
async function supersedeContentCausedPublishLeg(deps, cwd) {
|
|
21471
|
+
const path2 = releaseLedgerPath(cwd);
|
|
21472
|
+
return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
|
|
21473
|
+
let prior;
|
|
21474
|
+
try {
|
|
21475
|
+
prior = parseReleaseLedger((0, import_node_fs21.readFileSync)(path2, "utf8"));
|
|
21476
|
+
} catch (e) {
|
|
21477
|
+
throw new ReleaseLedgerError(
|
|
21478
|
+
"unreadable",
|
|
21479
|
+
`release --supersede-publish: the active release ledger at ${path2} could not be read (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand before any train; nothing was written (#6638)`
|
|
21480
|
+
);
|
|
21481
|
+
}
|
|
21482
|
+
if (prior.phases.publish.superseded === true) {
|
|
21483
|
+
throw new Error(`release --supersede-publish: the ${prior.tag} publish leg is already superseded \u2014 nothing to do; the next train runs normally`);
|
|
21484
|
+
}
|
|
21485
|
+
if (!supersedeablePublishLegShape(prior.phases.publish)) {
|
|
21486
|
+
throw new Error(
|
|
21487
|
+
`release --supersede-publish: the ${prior.tag} publish leg is ${prior.phases.publish.state}` + (prior.phases.publish.runId != null ? ` with run ${prior.phases.publish.runId} \u2014 a run-backed leg is resolved by watching/reverifying its run, never superseded` : "") + "; the supersession closes only a FAILED publish leg, or a PENDING one no run correlates (the uncorrelatable historical shape); nothing was written (#6638)"
|
|
21488
|
+
);
|
|
21489
|
+
}
|
|
21490
|
+
const phases = { ...prior.phases };
|
|
21491
|
+
if (phases.alignment.state === "pending") {
|
|
21492
|
+
const closed = await alignmentLegClosedLive(deps, prior);
|
|
21493
|
+
if (closed) phases.alignment = closed;
|
|
21494
|
+
}
|
|
21495
|
+
const unresolved = ["promotion", "ordinaryCi", "githubRelease", "deploy", "alignment"].filter((phase) => phases[phase].state === "pending" || phases[phase].state === "failed");
|
|
21496
|
+
if (unresolved.length > 0) {
|
|
21497
|
+
throw new Error(
|
|
21498
|
+
`release --supersede-publish: ${prior.tag} has unresolved legs besides publish (${unresolved.map((p) => `${p}=${phases[p].state}`).join(", ")}) \u2014 the supersession closes only the publish leg of an otherwise-complete release; resolve those first, nothing was written (#6638)`
|
|
21499
|
+
);
|
|
21500
|
+
}
|
|
21501
|
+
let liveSha = "";
|
|
21502
|
+
try {
|
|
21503
|
+
liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
|
|
21504
|
+
} catch (e) {
|
|
21505
|
+
throw new Error(`release --supersede-publish: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to close the leg on an unproven state; retry when origin answers (#6638)`);
|
|
21506
|
+
}
|
|
21507
|
+
if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
|
|
21508
|
+
throw new Error(
|
|
21509
|
+
`release --supersede-publish: origin reports ${prior.tag} at ${liveSha.slice(0, 12) || "no tag"} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 an absent or divergent release is NOT supersedeable (release --abort --apply is the unpublished candidate's own exit); nothing was written (#6638)`
|
|
21510
|
+
);
|
|
21511
|
+
}
|
|
21512
|
+
const note = `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, #6638)`;
|
|
21513
|
+
atomicWriteJson(path2, {
|
|
21514
|
+
...prior,
|
|
21515
|
+
phases: {
|
|
21516
|
+
...phases,
|
|
21517
|
+
publish: { state: "skipped", sha: prior.tagSha, superseded: true, note, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
21518
|
+
},
|
|
21519
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21520
|
+
});
|
|
21521
|
+
return {
|
|
21522
|
+
command: "release-supersede-publish",
|
|
21523
|
+
repo: prior.repo,
|
|
21524
|
+
tag: prior.tag,
|
|
21525
|
+
tagSha: prior.tagSha,
|
|
21526
|
+
note: `closed the ${prior.tag} publish leg as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm, then run the next patch train (hotfix or release) to carry the cause fix`
|
|
21527
|
+
};
|
|
21528
|
+
});
|
|
21529
|
+
}
|
|
21454
21530
|
async function reverifyPriorRunLegs(deps, path2, targetTag) {
|
|
21455
21531
|
let prior;
|
|
21456
21532
|
try {
|
|
@@ -23243,7 +23319,9 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
23243
23319
|
if (persisted.phases.deploy.superseded !== true) {
|
|
23244
23320
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
23245
23321
|
}
|
|
23246
|
-
|
|
23322
|
+
if (persisted.phases.publish.superseded !== true) {
|
|
23323
|
+
await recordPhase(ledger, deps, anchors, "publish", phaseEntry(phaseInputs.publish), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
23324
|
+
}
|
|
23247
23325
|
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: the GitHub Release exists at the immutable tag SHA" }), { strict: true, landed: "the promotion (verified live)" });
|
|
23248
23326
|
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: origin/main contains the immutable tag SHA" }), { strict: true, landed: "nothing beyond the already-public release" });
|
|
23249
23327
|
}
|
|
@@ -23524,6 +23602,17 @@ async function runReleaseSupersedeDeploy(deps, options = {}) {
|
|
|
23524
23602
|
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
23525
23603
|
}
|
|
23526
23604
|
}
|
|
23605
|
+
async function runReleaseSupersedePublish(deps, options = {}) {
|
|
23606
|
+
if (!options.approved) {
|
|
23607
|
+
throw new Error("release --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
23608
|
+
}
|
|
23609
|
+
try {
|
|
23610
|
+
return await supersedeContentCausedPublishLeg(deps, process.cwd());
|
|
23611
|
+
} catch (e) {
|
|
23612
|
+
const message2 = e instanceof Error ? e.message : String(e);
|
|
23613
|
+
throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
|
|
23614
|
+
}
|
|
23615
|
+
}
|
|
23527
23616
|
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
23528
23617
|
if (!options.approved) {
|
|
23529
23618
|
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
@@ -26318,7 +26407,7 @@ function registerBoardCommands(program3) {
|
|
|
26318
26407
|
return failGraceful(`board set-priority failed: ${e.message}`);
|
|
26319
26408
|
}
|
|
26320
26409
|
});
|
|
26321
|
-
board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--force", "release
|
|
26410
|
+
board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--force", "release a claim whose lane has gone stale or shows no live evidence (#3727); a lane that still shows live evidence also needs --holder-dead (#6625)").option("--holder-dead", "acknowledge the holding lane is known dead \u2014 required with --force when the holder still shows live evidence (#6625)").option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
|
|
26322
26411
|
const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
|
|
26323
26412
|
try {
|
|
26324
26413
|
const result = await unclaimBoardIssue({
|
|
@@ -26327,6 +26416,7 @@ function registerBoardCommands(program3) {
|
|
|
26327
26416
|
repo: o.repo,
|
|
26328
26417
|
toStatus,
|
|
26329
26418
|
force: o.force,
|
|
26419
|
+
holderDead: o.holderDead,
|
|
26330
26420
|
allowPartial: o.allowPartial
|
|
26331
26421
|
});
|
|
26332
26422
|
if (o.json) return console.log(JSON.stringify(result));
|
|
@@ -40329,6 +40419,19 @@ function registerCollaborationCommands(program3) {
|
|
|
40329
40419
|
delete rest.number;
|
|
40330
40420
|
return rest;
|
|
40331
40421
|
}
|
|
40422
|
+
async function resolveTransferredRef(repo, number) {
|
|
40423
|
+
try {
|
|
40424
|
+
const { stdout } = await execFileP("gh", ["api", `repos/${repo}/issues/${number}`], { timeout: GIT_TIMEOUT_MS });
|
|
40425
|
+
const issue2 = JSON.parse(stdout);
|
|
40426
|
+
const match = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)$/.exec(issue2.html_url ?? "");
|
|
40427
|
+
if (!match || typeof issue2.number !== "number") return void 0;
|
|
40428
|
+
const [currentRepo, currentNumber] = [match[1], Number(match[2])];
|
|
40429
|
+
if (currentRepo.toLowerCase() === repo.toLowerCase() && currentNumber === number) return void 0;
|
|
40430
|
+
return { repo: currentRepo, number: currentNumber, url: issue2.html_url };
|
|
40431
|
+
} catch {
|
|
40432
|
+
return void 0;
|
|
40433
|
+
}
|
|
40434
|
+
}
|
|
40332
40435
|
issue.command("view <ref>").aliases(["show", "get", "read"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show`, `get` and `read` alias view (#5483/#5706/#6078)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). `parent` is synthetic: selectable here, composed by the wrapper via a REST parent read, never sent to gh (#5970). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
|
|
40333
40436
|
let parsed;
|
|
40334
40437
|
try {
|
|
@@ -40342,19 +40445,33 @@ function registerCollaborationCommands(program3) {
|
|
|
40342
40445
|
const fields = normalizeIssueViewJsonFields(o.json);
|
|
40343
40446
|
const gh = resolveIssueViewGhFields(fields);
|
|
40344
40447
|
const emit = (data, parent) => console.log(JSON.stringify({ ...gh.numberIsTransport ? withoutTransportNumber(data) : data, ...parent }));
|
|
40345
|
-
|
|
40448
|
+
const readIssue = async (targetRepo2, targetNumber) => {
|
|
40346
40449
|
if (o.comments || o.context) {
|
|
40347
40450
|
const boardProjectId = o.context ? await resolveBoardProjectId(o.repo) : void 0;
|
|
40348
|
-
const data2 = await runIssueViewContext(queryDeps(), { number:
|
|
40349
|
-
|
|
40350
|
-
return;
|
|
40451
|
+
const data2 = await runIssueViewContext(queryDeps(), { number: targetNumber, repo: targetRepo2, comments: Boolean(o.comments), context: Boolean(o.context), jsonFields: gh.ghFields, boardProjectId });
|
|
40452
|
+
return { data: data2, parent: await readParentField(targetNumber, targetRepo2) };
|
|
40351
40453
|
}
|
|
40352
|
-
const data = await ghJson(["issue", "view", String(
|
|
40353
|
-
|
|
40454
|
+
const data = await ghJson(["issue", "view", String(targetNumber), "--repo", targetRepo2, "--json", gh.ghFields]);
|
|
40455
|
+
return { data, parent: await readParentField(targetNumber, targetRepo2) };
|
|
40456
|
+
};
|
|
40457
|
+
try {
|
|
40458
|
+
const { data, parent } = await readIssue(repo, n);
|
|
40459
|
+
emit(data, parent);
|
|
40354
40460
|
} catch (e) {
|
|
40355
40461
|
const err = e;
|
|
40356
40462
|
const raw = (err.stderr || err.message || String(e)).trim();
|
|
40357
40463
|
const argvNote = o.json ? ` (received --json argv: ${JSON.stringify(o.json)}, normalized to "${fields}")` : "";
|
|
40464
|
+
const moved = /Could not resolve to an issue/i.test(raw) ? await resolveTransferredRef(repo, n) : void 0;
|
|
40465
|
+
if (moved) {
|
|
40466
|
+
try {
|
|
40467
|
+
const { data, parent } = await readIssue(moved.repo, moved.number);
|
|
40468
|
+
emit({ ...data, transferred: { from: `${repo}#${n}`, to: `${moved.repo}#${moved.number}`, url: moved.url } }, parent);
|
|
40469
|
+
return;
|
|
40470
|
+
} catch (reRead) {
|
|
40471
|
+
const reErr = reRead;
|
|
40472
|
+
return fail(`issue view: ${repo}#${n} was transferred to ${moved.repo}#${moved.number} (${moved.url}), but re-reading it there failed: ${(reErr.stderr || reErr.message || String(reRead)).trim()}`);
|
|
40473
|
+
}
|
|
40474
|
+
}
|
|
40358
40475
|
return fail(`issue view: ${raw}${argvNote}`);
|
|
40359
40476
|
}
|
|
40360
40477
|
});
|
|
@@ -42609,6 +42726,13 @@ function summaryFileLines(content, neutralize = true) {
|
|
|
42609
42726
|
return escapeMrkdwn(neutralize ? neutralizeHubProductBrands(bare) : bare);
|
|
42610
42727
|
}).filter(Boolean).map((l) => `\u2022 ${l}`);
|
|
42611
42728
|
}
|
|
42729
|
+
function announceSummaryFidelity(lines2, rangeLog) {
|
|
42730
|
+
const shipped = new Set(rangeLog.match(/#\d+/g) ?? []);
|
|
42731
|
+
if (shipped.size === 0) return null;
|
|
42732
|
+
const invented = [...new Set(lines2.flatMap((l) => l.match(/#\d+/g) ?? []))].filter((ref) => !shipped.has(ref));
|
|
42733
|
+
if (invented.length === 0) return null;
|
|
42734
|
+
return `--announce-summary-file cites ${invented.join(", ")}, which ship in no commit of this release range \u2014 a summary line must describe work that actually shipped. Rebuild the summary from \`git log origin/main..HEAD\` (read commit BODIES too: a bundled PR closes issues its subject never names), then rerun the same release command \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
42735
|
+
}
|
|
42612
42736
|
async function resolveAnnounceTarget(deps, repo) {
|
|
42613
42737
|
if (repo.toLowerCase() === ANNOUNCE_REPO.toLowerCase()) return { kind: "alerts" };
|
|
42614
42738
|
const target = deps.fetchReleaseTarget ? await deps.fetchReleaseTarget(slugOf(repo)) : { channel: null, language: "en" };
|
|
@@ -43332,6 +43456,14 @@ async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTr
|
|
|
43332
43456
|
if (lines2.length < 3 || lines2.length > 6) {
|
|
43333
43457
|
throw new Error(`--announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
43334
43458
|
}
|
|
43459
|
+
let rangeLog;
|
|
43460
|
+
try {
|
|
43461
|
+
rangeLog = await deps.run("git", ["log", "--format=%B", "origin/main..HEAD"]);
|
|
43462
|
+
} catch (e) {
|
|
43463
|
+
throw new Error(`could not read the hotfix commit range (git log origin/main..HEAD: ${e.message}) \u2014 the --announce-summary-file lines cannot be checked against what actually ships; fix the checkout, then rerun`);
|
|
43464
|
+
}
|
|
43465
|
+
const invented = announceSummaryFidelity(lines2, rangeLog);
|
|
43466
|
+
if (invented) throw new Error(invented);
|
|
43335
43467
|
}
|
|
43336
43468
|
const anchors = { repo: ctx.repo, tag, tagSha: mergedSha };
|
|
43337
43469
|
const start = await startReleaseLedger(ledger, anchors, { lane: "hotfix" });
|
|
@@ -44056,7 +44188,25 @@ async function announceSummaryRefusal(path2) {
|
|
|
44056
44188
|
if (lines2.length < 3 || lines2.length > 6) {
|
|
44057
44189
|
return `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
44058
44190
|
}
|
|
44059
|
-
|
|
44191
|
+
try {
|
|
44192
|
+
await execFileP("git", ["fetch", "origin"], { timeout: GIT_TIMEOUT_MS });
|
|
44193
|
+
} catch (e) {
|
|
44194
|
+
return `release: could not fetch origin to check the --announce-summary-file lines (git fetch origin: ${e.message}) \u2014 the lines cannot be checked against what actually ships; fix the checkout, then rerun the same release command`;
|
|
44195
|
+
}
|
|
44196
|
+
const revs = ["HEAD"];
|
|
44197
|
+
try {
|
|
44198
|
+
const upstream = await execFileP("git", ["rev-parse", "--verify", "--quiet", "@{upstream}"], { timeout: GIT_TIMEOUT_MS });
|
|
44199
|
+
if (upstream.stdout.trim()) revs.push("@{upstream}");
|
|
44200
|
+
} catch {
|
|
44201
|
+
}
|
|
44202
|
+
let rangeLog;
|
|
44203
|
+
try {
|
|
44204
|
+
rangeLog = (await execFileP("git", ["log", "--format=%B", ...revs, "--not", "origin/main"], { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
44205
|
+
} catch (e) {
|
|
44206
|
+
return `release: could not read the release commit range (git log ${revs.join(" ")} --not origin/main: ${e.message}) \u2014 the --announce-summary-file lines cannot be checked against what actually ships; fix the checkout, then rerun`;
|
|
44207
|
+
}
|
|
44208
|
+
const invented = announceSummaryFidelity(lines2, rangeLog);
|
|
44209
|
+
return invented ? `release: ${invented}` : null;
|
|
44060
44210
|
}
|
|
44061
44211
|
function trainApplyDeps() {
|
|
44062
44212
|
return {
|
|
@@ -44206,6 +44356,9 @@ function renderReleaseAbort(r) {
|
|
|
44206
44356
|
function renderReleaseSupersedeDeploy(r) {
|
|
44207
44357
|
return `mmi-cli devops release --supersede-deploy --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
44208
44358
|
}
|
|
44359
|
+
function renderReleaseSupersedePublish(r) {
|
|
44360
|
+
return `mmi-cli devops release --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
44361
|
+
}
|
|
44209
44362
|
function renderReleasePublishRetry(r) {
|
|
44210
44363
|
return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
|
|
44211
44364
|
}
|
|
@@ -44431,6 +44584,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44431
44584
|
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
|
|
44432
44585
|
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
|
|
44433
44586
|
{ flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553)" },
|
|
44587
|
+
{ flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 the next patch train then carries the fix (#6638)" },
|
|
44434
44588
|
{ flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
|
|
44435
44589
|
];
|
|
44436
44590
|
for (const f of RELEASE_ONLY_FLAGS) {
|
|
@@ -44461,6 +44615,9 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44461
44615
|
if (o.supersedeDeploy && commandName !== "release") {
|
|
44462
44616
|
return fail(`${commandName}: --supersede-deploy applies only to release \u2014 it closes a content-caused failed deploy leg on a published release's ledger. Run: mmi-cli devops release --supersede-deploy --apply`);
|
|
44463
44617
|
}
|
|
44618
|
+
if (o.supersedePublish && commandName !== "release") {
|
|
44619
|
+
return fail(`${commandName}: --supersede-publish applies only to release \u2014 it closes a content-caused failed publish leg on a promoted release's ledger. Run: mmi-cli devops release --supersede-publish --apply`);
|
|
44620
|
+
}
|
|
44464
44621
|
if (o.retryPublish && commandName !== "release") {
|
|
44465
44622
|
return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli devops release --retry-publish <run-id> --apply --watch`);
|
|
44466
44623
|
}
|
|
@@ -44508,6 +44665,9 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44508
44665
|
return failGraceful(`release --abort: ${e.message}`);
|
|
44509
44666
|
}
|
|
44510
44667
|
}
|
|
44668
|
+
if (o.supersedeDeploy && o.supersedePublish) {
|
|
44669
|
+
return fail("release: --supersede-deploy and --supersede-publish are mutually exclusive \u2014 close the one leg that failed content-caused; the other leg has its own resolution paths");
|
|
44670
|
+
}
|
|
44511
44671
|
if (o.supersedeDeploy) {
|
|
44512
44672
|
if (o.resume) return fail("release: --supersede-deploy and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-derives the very leg it closes");
|
|
44513
44673
|
if (o.abort) return fail("release: --supersede-deploy and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
|
|
@@ -44527,6 +44687,25 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
44527
44687
|
return failGraceful(`release --supersede-deploy: ${e.message}`);
|
|
44528
44688
|
}
|
|
44529
44689
|
}
|
|
44690
|
+
if (o.supersedePublish) {
|
|
44691
|
+
if (o.resume) return fail("release: --supersede-publish and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-reads the very failed run it closes");
|
|
44692
|
+
if (o.abort) return fail("release: --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PROMOTED release, abort only ever deletes a proven unpublished candidate");
|
|
44693
|
+
if (o.retryPublish) return fail("release: --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
|
|
44694
|
+
if (!o.apply) return fail("release: --supersede-publish requires --apply after explicit master approval; nothing was written");
|
|
44695
|
+
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
44696
|
+
return fail("release: --supersede-publish accepts only --apply, --repo, --json and --out; it closes a ledger leg and dispatches nothing");
|
|
44697
|
+
}
|
|
44698
|
+
if (o.repo) {
|
|
44699
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), "mmi-cli devops release --supersede-publish --apply");
|
|
44700
|
+
if (!guard.ok) return fail(`release: ${guard.message}`);
|
|
44701
|
+
}
|
|
44702
|
+
try {
|
|
44703
|
+
const result = await runReleaseSupersedePublish(trainApplyDeps(), { approved: true });
|
|
44704
|
+
return emitTrainResult("release --supersede-publish", o.json ? JSON.stringify(result, null, 2) : renderReleaseSupersedePublish(result), o.out);
|
|
44705
|
+
} catch (e) {
|
|
44706
|
+
return failGraceful(`release --supersede-publish: ${e.message}`);
|
|
44707
|
+
}
|
|
44708
|
+
}
|
|
44530
44709
|
if (o.resume) {
|
|
44531
44710
|
if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
|
|
44532
44711
|
try {
|
package/package.json
CHANGED