@mutmutco/codex-plugin 4.5.134 → 4.5.136
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/.codex-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +260 -69
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/bootstrap/seeds/gate.template.yml +4 -2
- package/skills/bootstrap/seeds/suite-gate.template.yml +4 -2
- package/skills/bootstrap/seeds/suite-screens.template.yml +2 -0
- package/skills/org-guide/references/work-and-releases.md +1 -1
- package/skills/release/SKILL.md +3 -3
package/cli/dist/main.cjs
CHANGED
|
@@ -17509,6 +17509,7 @@ function assertGitBashOnWindowsForJervHubRelease(repo, platform2 = process.platf
|
|
|
17509
17509
|
|
|
17510
17510
|
// src/train-apply-correlation.ts
|
|
17511
17511
|
var import_node_fs19 = require("node:fs");
|
|
17512
|
+
init_github_quota();
|
|
17512
17513
|
var import_node_path17 = require("node:path");
|
|
17513
17514
|
|
|
17514
17515
|
// src/fleet-lockstep-enforce.ts
|
|
@@ -18274,10 +18275,10 @@ var rollout_plan_default = {
|
|
|
18274
18275
|
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)."
|
|
18275
18276
|
},
|
|
18276
18277
|
baseline: {
|
|
18277
|
-
version: "4.5.
|
|
18278
|
-
tag: "v4.5.
|
|
18279
|
-
commit: "
|
|
18280
|
-
npm: "@mutmutco/cli@4.5.
|
|
18278
|
+
version: "4.5.136",
|
|
18279
|
+
tag: "v4.5.136",
|
|
18280
|
+
commit: "642526387569",
|
|
18281
|
+
npm: "@mutmutco/cli@4.5.136"
|
|
18281
18282
|
},
|
|
18282
18283
|
exitCriterion: "fleet-n-of-n",
|
|
18283
18284
|
hubOnlyShortcut: "forbidden",
|
|
@@ -18294,14 +18295,14 @@ var rollout_plan_default = {
|
|
|
18294
18295
|
repo: "mutmutco/mmi-hub",
|
|
18295
18296
|
role: "canary",
|
|
18296
18297
|
schedule: "train",
|
|
18297
|
-
v3Target: "v4.5.
|
|
18298
|
+
v3Target: "v4.5.136"
|
|
18298
18299
|
}
|
|
18299
18300
|
],
|
|
18300
18301
|
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.",
|
|
18301
18302
|
rollback: {
|
|
18302
18303
|
independent: true,
|
|
18303
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
18304
|
-
v3Target: "v4.5.
|
|
18304
|
+
mechanism: "npm dist-tag latest -> 4.5.136 and redeploy the Hub Lambda from tag v4.5.136 (642526387569); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
18305
|
+
v3Target: "v4.5.136 (@mutmutco/cli@4.5.136, tag commit 642526387569 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
18305
18306
|
}
|
|
18306
18307
|
},
|
|
18307
18308
|
{
|
|
@@ -20419,11 +20420,22 @@ async function loadReleaseTrackBranchHints(deps) {
|
|
|
20419
20420
|
]);
|
|
20420
20421
|
return { hasDevelopmentBranch, hasMainBranch, hasRcBranch };
|
|
20421
20422
|
}
|
|
20423
|
+
async function readTrainLogin(deps, waitMs = 18e4, attempts = 5) {
|
|
20424
|
+
for (let i = 1; ; i++) {
|
|
20425
|
+
try {
|
|
20426
|
+
return requireValue(clean2(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
20427
|
+
} catch (e) {
|
|
20428
|
+
if (i >= attempts || !isRateLimitText(e instanceof Error ? e.message : String(e))) throw e;
|
|
20429
|
+
deps.warn?.(`GitHub login read rate-limited; retrying in ${waitMs / 6e4} min (${i}/${attempts - 1})`);
|
|
20430
|
+
await resolveSleep(deps)(waitMs);
|
|
20431
|
+
}
|
|
20432
|
+
}
|
|
20433
|
+
}
|
|
20422
20434
|
async function buildTrainApplyContext(deps) {
|
|
20423
20435
|
const repo = requireValue(clean2(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
20424
20436
|
const [owner, name] = repo.split("/");
|
|
20425
20437
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
20426
|
-
const login =
|
|
20438
|
+
const login = await readTrainLogin(deps);
|
|
20427
20439
|
const verdict = await deps.trainAuthority(repo);
|
|
20428
20440
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
20429
20441
|
if (!verdict.train) {
|
|
@@ -22443,7 +22455,7 @@ async function runTenantRedeploy(deps, options) {
|
|
|
22443
22455
|
const repo = options.repo;
|
|
22444
22456
|
const [owner, name] = repo.split("/");
|
|
22445
22457
|
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
22446
|
-
const login =
|
|
22458
|
+
const login = await readTrainLogin(deps);
|
|
22447
22459
|
const verdict = await deps.trainAuthority(repo);
|
|
22448
22460
|
if (!verdict.ok) throw new Error(`${commandAuthorityLabel(owner)}: train authority could not be verified (${verdict.error})`);
|
|
22449
22461
|
if (!verdict.train) throw new Error(`${commandAuthorityLabel(owner)}: @${login} is ${verdict.role} \u2014 no train authority on ${repo}`);
|
|
@@ -26417,36 +26429,75 @@ async function collectReleaseCredits(deps, repo, body) {
|
|
|
26417
26429
|
return [];
|
|
26418
26430
|
}
|
|
26419
26431
|
}
|
|
26420
|
-
|
|
26421
|
-
|
|
26422
|
-
|
|
26432
|
+
var UPDATE_SECTIONS = {
|
|
26433
|
+
en: { fixed: "Fixed", new: "New" },
|
|
26434
|
+
tr: { fixed: "D\xFCzeldi", new: "Yeni" }
|
|
26435
|
+
};
|
|
26436
|
+
var UPDATE_LINE_MAX = 120;
|
|
26437
|
+
var UPDATE_SECTION_MAX = 8;
|
|
26438
|
+
var UPDATE_LINE = /^[^:\s][^:]{0,39}: \S.*\.$/u;
|
|
26439
|
+
var UPDATE_FORBIDDEN = /#\d|\bv?\d+\.\d+\.\d+|^(?:feat|fix|chore|refactor|docs|test|ci|build|perf|style)\b/;
|
|
26440
|
+
function parseUpdateNotes(content, lang = "en") {
|
|
26441
|
+
const words = UPDATE_SECTIONS[lang];
|
|
26442
|
+
const lines2 = content.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
26443
|
+
const notes = { fixed: [], new: [] };
|
|
26444
|
+
let section;
|
|
26445
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26446
|
+
for (const line of lines2) {
|
|
26447
|
+
const key = line === words.fixed ? "fixed" : line === words.new ? "new" : void 0;
|
|
26448
|
+
if (key) {
|
|
26449
|
+
if (seen.has(key) || key === "fixed" && seen.has("new")) throw new Error(`${lang} summary: "${words.fixed}" then "${words.new}", each at most once \u2014 see ${SUMMARY_GUIDE}`);
|
|
26450
|
+
seen.add(key);
|
|
26451
|
+
section = key;
|
|
26452
|
+
continue;
|
|
26453
|
+
}
|
|
26454
|
+
if (!section) throw new Error(`${lang} summary: start with "${words.fixed}" or "${words.new}" on its own line \u2014 see ${SUMMARY_GUIDE}`);
|
|
26455
|
+
if (line.length > UPDATE_LINE_MAX || !UPDATE_LINE.test(line) || UPDATE_FORBIDDEN.test(line)) {
|
|
26456
|
+
throw new Error(`${lang} summary line must read "<Area>: <sentence>." in at most ${UPDATE_LINE_MAX} characters, with no issue numbers, versions or commit prefixes: "${line}" \u2014 see ${SUMMARY_GUIDE}`);
|
|
26457
|
+
}
|
|
26458
|
+
notes[section].push(line);
|
|
26459
|
+
}
|
|
26460
|
+
for (const key of seen) {
|
|
26461
|
+
const n = notes[key].length;
|
|
26462
|
+
if (n === 0 || n > UPDATE_SECTION_MAX) throw new Error(`${lang} summary: each section needs 1\u2013${UPDATE_SECTION_MAX} lines (found ${n}) \u2014 see ${SUMMARY_GUIDE}`);
|
|
26463
|
+
}
|
|
26464
|
+
if (!seen.size) throw new Error(`${lang} summary needs a "${words.fixed}" or "${words.new}" section \u2014 see ${SUMMARY_GUIDE}`);
|
|
26465
|
+
return notes;
|
|
26466
|
+
}
|
|
26467
|
+
function buildPlainWordsSection(notes, big, lang = "en", credits = []) {
|
|
26468
|
+
const words = UPDATE_SECTIONS[lang];
|
|
26469
|
+
const body = [lang === "en" ? PLAIN_WORDS_HEADING : PLAIN_WORDS_HEADING_TR];
|
|
26470
|
+
for (const key of ["fixed", "new"]) {
|
|
26471
|
+
if (notes[key].length) body.push(`### ${words[key]}`, ...notes[key].map((n) => `- ${n}`));
|
|
26472
|
+
}
|
|
26423
26473
|
if (big) body.push(PLAIN_WORDS_MARKER);
|
|
26424
26474
|
const ids = [...new Set(credits.filter((id) => REPORT_ID.test(id)))].sort().slice(0, 20);
|
|
26425
26475
|
if (ids.length) body.push(`<!-- mmi:credits ${ids.join(",")} -->`);
|
|
26426
26476
|
return body.join("\n");
|
|
26427
26477
|
}
|
|
26428
26478
|
var SUMMARY_GUIDE = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing";
|
|
26429
|
-
|
|
26430
|
-
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
}
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
26437
|
-
|
|
26438
|
-
|
|
26439
|
-
|
|
26440
|
-
|
|
26479
|
+
var SUMMARY_FLAG = { en: "--announce-summary-file", tr: "--announce-summary-file-tr" };
|
|
26480
|
+
function validateSuiteReleaseSummary(path2, pathTr) {
|
|
26481
|
+
const files = [["en", path2], ["tr", pathTr]];
|
|
26482
|
+
for (const [lang, file] of files) {
|
|
26483
|
+
if (!file) throw new Error(`requires ${SUMMARY_FLAG[lang]} <path> before promoting any member \u2014 see ${SUMMARY_GUIDE}`);
|
|
26484
|
+
}
|
|
26485
|
+
for (const [lang, file] of files) {
|
|
26486
|
+
let content;
|
|
26487
|
+
try {
|
|
26488
|
+
content = (0, import_node_fs25.readFileSync)(file, "utf8");
|
|
26489
|
+
} catch (e) {
|
|
26490
|
+
throw new Error(`could not read summary file ${file}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
|
|
26491
|
+
}
|
|
26492
|
+
parseUpdateNotes(content, lang);
|
|
26441
26493
|
}
|
|
26442
|
-
validatePlainWordsSummary(content);
|
|
26443
26494
|
}
|
|
26444
|
-
async function validateReleaseSummaries(releases, files, releaseExists) {
|
|
26495
|
+
async function validateReleaseSummaries(releases, files, filesTr, releaseExists) {
|
|
26445
26496
|
const refusals = [];
|
|
26446
26497
|
for (const { repo, tag } of releases) {
|
|
26447
26498
|
if (await releaseExists(repo, tag)) continue;
|
|
26448
26499
|
try {
|
|
26449
|
-
validateSuiteReleaseSummary(files[repo]);
|
|
26500
|
+
validateSuiteReleaseSummary(files[repo], filesTr[repo]);
|
|
26450
26501
|
} catch (e) {
|
|
26451
26502
|
refusals.push(`${repo}: ${e.message}`);
|
|
26452
26503
|
}
|
|
@@ -26454,15 +26505,20 @@ async function validateReleaseSummaries(releases, files, releaseExists) {
|
|
|
26454
26505
|
if (refusals.length) throw new Error(`suite release summaries refused; no member was released:
|
|
26455
26506
|
${refusals.join("\n")}`);
|
|
26456
26507
|
}
|
|
26457
|
-
async function requireSuiteReleaseSummary(deps, path2) {
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26508
|
+
async function requireSuiteReleaseSummary(deps, path2, pathTr) {
|
|
26509
|
+
const files = [["en", path2], ["tr", pathTr]];
|
|
26510
|
+
for (const [lang, file] of files) {
|
|
26511
|
+
if (!file) throw new Error(`a new mm-work suite Release requires ${SUMMARY_FLAG[lang]} <path> in the update-notes template \u2014 see ${SUMMARY_GUIDE}`);
|
|
26512
|
+
}
|
|
26513
|
+
for (const [lang, file] of files) {
|
|
26514
|
+
let content;
|
|
26515
|
+
try {
|
|
26516
|
+
content = await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(file);
|
|
26517
|
+
} catch (e) {
|
|
26518
|
+
throw new Error(`could not read ${SUMMARY_FLAG[lang]} ${file}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
|
|
26519
|
+
}
|
|
26520
|
+
parseUpdateNotes(content, lang);
|
|
26464
26521
|
}
|
|
26465
|
-
validatePlainWordsSummary(content);
|
|
26466
26522
|
}
|
|
26467
26523
|
function upsertPlainWordsSection(body, section) {
|
|
26468
26524
|
let rest = body;
|
|
@@ -26486,19 +26542,15 @@ function upsertPlainWordsSection(body, section) {
|
|
|
26486
26542
|
|
|
26487
26543
|
${trimmedRest}` : section;
|
|
26488
26544
|
}
|
|
26489
|
-
function plainWordsSummaryLines(content) {
|
|
26490
|
-
return content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, 6);
|
|
26491
|
-
}
|
|
26492
26545
|
async function writePlainWordsIfSuiteMember(deps, args) {
|
|
26493
26546
|
if (args.meta?.suite !== "mm-work") return;
|
|
26494
26547
|
try {
|
|
26495
|
-
if (!args.
|
|
26496
|
-
const lines2 = args.summaryLines;
|
|
26548
|
+
if (!args.summary) return;
|
|
26497
26549
|
const body = clean2(await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body", "-q", ".body"]));
|
|
26498
26550
|
const credits = await collectReleaseCredits(deps, args.repo, body);
|
|
26499
26551
|
const section = [
|
|
26500
|
-
buildPlainWordsSection(
|
|
26501
|
-
...args.
|
|
26552
|
+
buildPlainWordsSection(args.summary, args.big === true, "en", credits),
|
|
26553
|
+
...args.summaryTr ? [buildPlainWordsSection(args.summaryTr, false, "tr")] : []
|
|
26502
26554
|
].join("\n\n");
|
|
26503
26555
|
await deps.run("gh", ["release", "edit", args.tag, "--repo", args.repo, "--notes", upsertPlainWordsSection(body, section)]);
|
|
26504
26556
|
} catch (e) {
|
|
@@ -26507,9 +26559,10 @@ async function writePlainWordsIfSuiteMember(deps, args) {
|
|
|
26507
26559
|
}
|
|
26508
26560
|
async function writePlainWordsSection(deps, options, repo, meta, tag) {
|
|
26509
26561
|
try {
|
|
26510
|
-
const
|
|
26511
|
-
const
|
|
26512
|
-
|
|
26562
|
+
const read = deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8"));
|
|
26563
|
+
const summary = options.announceSummaryFile ? parseUpdateNotes(await read(options.announceSummaryFile), "en") : void 0;
|
|
26564
|
+
const summaryTr = options.announceSummaryFileTr ? parseUpdateNotes(await read(options.announceSummaryFileTr), "tr") : void 0;
|
|
26565
|
+
await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summary, summaryTr, big: options.big, suiteTag: options.suiteTag });
|
|
26513
26566
|
} catch {
|
|
26514
26567
|
}
|
|
26515
26568
|
}
|
|
@@ -27844,7 +27897,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
27844
27897
|
const preMainPartial = await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo);
|
|
27845
27898
|
const promotedPartial = !preMainPartial && await isPromotedUnreleasedTag(deps, tag, tagSha, ctx.repo);
|
|
27846
27899
|
const partialRelease = preMainPartial || promotedPartial;
|
|
27847
|
-
if (partialRelease && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
|
|
27900
|
+
if (partialRelease && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile, options.announceSummaryFileTr);
|
|
27848
27901
|
if (!partialRelease) {
|
|
27849
27902
|
try {
|
|
27850
27903
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
|
|
@@ -28720,7 +28773,7 @@ async function runTrainApplyLocked(command, deps, options = {}) {
|
|
|
28720
28773
|
const branchHints = await loadReleaseTrackBranchHints(deps);
|
|
28721
28774
|
const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
|
|
28722
28775
|
assertSuiteAutoVersion(ctx.repo, meta, process.env, command === "release" && !directTrack);
|
|
28723
|
-
if (command === "release" && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
|
|
28776
|
+
if (command === "release" && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile, options.announceSummaryFileTr);
|
|
28724
28777
|
const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options, bumpIntent };
|
|
28725
28778
|
if (command === "rcand") {
|
|
28726
28779
|
if (directTrack) {
|
|
@@ -39055,7 +39108,7 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
|
|
|
39055
39108
|
await assertConsumerBomLineage(deps, mergedSha, tag);
|
|
39056
39109
|
}
|
|
39057
39110
|
const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
|
|
39058
|
-
if (!releaseExists && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
|
|
39111
|
+
if (!releaseExists && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile, options.announceSummaryFileTr);
|
|
39059
39112
|
if (!releaseExists && isHubControlRepo(ctx.repo)) {
|
|
39060
39113
|
if (!options.announceSummaryFile) {
|
|
39061
39114
|
throw new Error("a new MMI-Hub hotfix Release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same hotfix release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing");
|
|
@@ -39804,6 +39857,7 @@ function boardAdvanceFailureMessage(result) {
|
|
|
39804
39857
|
}
|
|
39805
39858
|
|
|
39806
39859
|
// src/pr-land.ts
|
|
39860
|
+
var PR_LAND_QUEUE_TIMEOUT_MS = 45 * 6e4;
|
|
39807
39861
|
function prHeadBehindBase(input) {
|
|
39808
39862
|
if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
|
|
39809
39863
|
return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
|
|
@@ -39927,6 +39981,17 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
39927
39981
|
}
|
|
39928
39982
|
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
39929
39983
|
base.ciPolicy = ciPolicy;
|
|
39984
|
+
const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
|
|
39985
|
+
let queueSnapshot;
|
|
39986
|
+
if (hasQueue) {
|
|
39987
|
+
base.mergeQueue = { base: resolved.base, state: "queued" };
|
|
39988
|
+
if (!deps.readMergeQueue) throw new Error("merge queue reader unavailable");
|
|
39989
|
+
queueSnapshot = await deps.readMergeQueue(prNumber, repo);
|
|
39990
|
+
if (queueSnapshot.isInMergeQueue || queueSnapshot.state === "MERGED") {
|
|
39991
|
+
base.mergeStatus = "auto-merge-enqueued";
|
|
39992
|
+
return pollPrMergeQueue(prNumber, options, deps, base, queueSnapshot);
|
|
39993
|
+
}
|
|
39994
|
+
}
|
|
39930
39995
|
const checksWaitError = (checksWait) => {
|
|
39931
39996
|
if (checksWait.status === "conflicting") {
|
|
39932
39997
|
return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
|
|
@@ -39962,6 +40027,14 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
39962
40027
|
if (merge.mergeStatus === "failed") {
|
|
39963
40028
|
return { ...base, error: merge.error ?? "merge failed" };
|
|
39964
40029
|
}
|
|
40030
|
+
if (hasQueue) {
|
|
40031
|
+
if (merge.mergeStatus === "merged") {
|
|
40032
|
+
return queueMerged(base, deps);
|
|
40033
|
+
}
|
|
40034
|
+
const deadline = (deps.now ?? (() => Date.now()))() + PR_LAND_QUEUE_TIMEOUT_MS;
|
|
40035
|
+
await (deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))))(PR_LAND_POLL_MS);
|
|
40036
|
+
return pollPrMergeQueue(prNumber, options, deps, base, void 0, deadline);
|
|
40037
|
+
}
|
|
39965
40038
|
if (merge.mergeStatus === "merged") {
|
|
39966
40039
|
return {
|
|
39967
40040
|
...base,
|
|
@@ -39984,6 +40057,53 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
39984
40057
|
error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
|
|
39985
40058
|
};
|
|
39986
40059
|
}
|
|
40060
|
+
function queueMerged(base, deps) {
|
|
40061
|
+
const result = {
|
|
40062
|
+
...base,
|
|
40063
|
+
status: "merged-via-queue",
|
|
40064
|
+
mergeStatus: "merged",
|
|
40065
|
+
mergeQueue: { ...base.mergeQueue, state: "merged" },
|
|
40066
|
+
branchFinality: prLandedBranchFinality(base.pr, base.base)
|
|
40067
|
+
};
|
|
40068
|
+
deps.queueProgress?.(result);
|
|
40069
|
+
return result;
|
|
40070
|
+
}
|
|
40071
|
+
async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
|
|
40072
|
+
const now = deps.now ?? (() => Date.now());
|
|
40073
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
40074
|
+
const deadline = deadlineMs ?? now() + PR_LAND_QUEUE_TIMEOUT_MS;
|
|
40075
|
+
while (now() < deadline) {
|
|
40076
|
+
const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
|
|
40077
|
+
snapshot = void 0;
|
|
40078
|
+
if (read.state === "MERGED") return queueMerged(base, deps);
|
|
40079
|
+
if (read.isInMergeQueue) {
|
|
40080
|
+
base.mergeQueue = {
|
|
40081
|
+
base: base.base,
|
|
40082
|
+
state: "queued",
|
|
40083
|
+
...read.mergeQueueEntry ? { position: read.mergeQueueEntry.position, entryState: read.mergeQueueEntry.state } : {}
|
|
40084
|
+
};
|
|
40085
|
+
deps.queueProgress?.(base);
|
|
40086
|
+
if (options.enqueueOnly) return { ...base, status: "queued" };
|
|
40087
|
+
} else if (read.state === "OPEN" && !read.autoMergeRequest) {
|
|
40088
|
+
const run = await deps.findMergeGroupRun(pr, base.repo, base.base);
|
|
40089
|
+
const result2 = {
|
|
40090
|
+
...base,
|
|
40091
|
+
mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
|
|
40092
|
+
error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}`
|
|
40093
|
+
};
|
|
40094
|
+
deps.queueProgress?.(result2);
|
|
40095
|
+
return result2;
|
|
40096
|
+
}
|
|
40097
|
+
await sleep2(PR_LAND_POLL_MS);
|
|
40098
|
+
}
|
|
40099
|
+
const result = {
|
|
40100
|
+
...base,
|
|
40101
|
+
mergeQueue: { ...base.mergeQueue, state: "timeout" },
|
|
40102
|
+
error: "merge queue did not reach MERGED within 45 minutes \u2014 rerun pr land to resume polling"
|
|
40103
|
+
};
|
|
40104
|
+
deps.queueProgress?.(result);
|
|
40105
|
+
return result;
|
|
40106
|
+
}
|
|
39987
40107
|
|
|
39988
40108
|
// src/test-policy-core.ts
|
|
39989
40109
|
var import_node_child_process13 = require("node:child_process");
|
|
@@ -41044,18 +41164,19 @@ async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
|
|
|
41044
41164
|
bodyText: method === "--squash" ? extra?.bodyText : void 0
|
|
41045
41165
|
});
|
|
41046
41166
|
try {
|
|
41047
|
-
return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path);
|
|
41167
|
+
return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue);
|
|
41048
41168
|
} finally {
|
|
41049
41169
|
overrideBody?.cleanup();
|
|
41050
41170
|
}
|
|
41051
41171
|
}
|
|
41052
|
-
async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
|
|
41172
|
+
async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, mergeQueue = false) {
|
|
41053
41173
|
const headRef = (await io.gh(["pr", "view", prNumber, ...args, "--json", "headRefName", "--jq", ".headRefName"], GC_GH_TIMEOUT_MS).catch(() => "")).trim();
|
|
41054
41174
|
const deleteBranch = !isProtectedBranch(headRef);
|
|
41055
41175
|
const readMergeState = () => readGhPrStateWithRetry(
|
|
41056
41176
|
() => io.gh(["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], GC_GH_TIMEOUT_MS)
|
|
41057
41177
|
);
|
|
41058
41178
|
const confirmEnqueueOutcome = async () => {
|
|
41179
|
+
if (mergeQueue) return { mergeStatus: "auto-merge-enqueued" };
|
|
41059
41180
|
const stateRead = await readMergeState();
|
|
41060
41181
|
if (!stateRead.ok) {
|
|
41061
41182
|
return { mergeStatus: "failed", error: `could not read PR state after merge: ${stateRead.error}` };
|
|
@@ -41831,6 +41952,57 @@ function applyChecklistCheck(body, query, checked) {
|
|
|
41831
41952
|
return { ok: true, edit: setChecklistMarker(body, sel.item, checked), item: sel.item };
|
|
41832
41953
|
}
|
|
41833
41954
|
|
|
41955
|
+
// src/pr-merge-queue.ts
|
|
41956
|
+
init_cli_shared();
|
|
41957
|
+
var readApi = async (args) => {
|
|
41958
|
+
const env = await ghReadEnv();
|
|
41959
|
+
return (await execFileP("gh", ["api", ...args], {
|
|
41960
|
+
timeout: 2e4,
|
|
41961
|
+
...env ? { env } : {}
|
|
41962
|
+
})).stdout;
|
|
41963
|
+
};
|
|
41964
|
+
async function queueGraphql(repo, fields, gh2) {
|
|
41965
|
+
const [owner, name] = repo.split("/");
|
|
41966
|
+
const response = JSON.parse(await gh2(["graphql", "-f", `query=query { repository(owner:${JSON.stringify(owner)},name:${JSON.stringify(name)}) { ${fields} } }`]));
|
|
41967
|
+
if (response.errors?.length) throw new Error(response.errors.map((e) => e.message).join("; "));
|
|
41968
|
+
if (!response.data?.repository) throw new Error("merge queue repository read failed");
|
|
41969
|
+
return response.data.repository;
|
|
41970
|
+
}
|
|
41971
|
+
async function hasPrMergeQueue(repo, base, gh2 = readApi) {
|
|
41972
|
+
const result = await queueGraphql(repo, `mergeQueue(branch:${JSON.stringify(base)}) { id }`, gh2);
|
|
41973
|
+
return result.mergeQueue !== null;
|
|
41974
|
+
}
|
|
41975
|
+
async function readPrMergeQueue(pr, repo, gh2 = readApi) {
|
|
41976
|
+
if (!/^\d+$/.test(pr)) throw new Error("merge queue read requires a PR number");
|
|
41977
|
+
const result = await queueGraphql(
|
|
41978
|
+
repo,
|
|
41979
|
+
`pullRequest(number:${pr}) { state isInMergeQueue mergeQueueEntry { state position } autoMergeRequest { enabledAt } }`,
|
|
41980
|
+
gh2
|
|
41981
|
+
);
|
|
41982
|
+
if (!result.pullRequest) throw new Error(`merge queue PR #${pr} not found`);
|
|
41983
|
+
return result.pullRequest;
|
|
41984
|
+
}
|
|
41985
|
+
async function findPrMergeGroupRun(pr, repo, base, gh2 = readApi) {
|
|
41986
|
+
const pages = JSON.parse(await gh2([`repos/${repo}/actions/runs?event=merge_group&per_page=100`, "--paginate", "--slurp"]));
|
|
41987
|
+
const prefix = `gh-readonly-queue/${base}/pr-${pr}-`;
|
|
41988
|
+
const run = pages.flatMap((page) => page.workflow_runs).filter((run2) => run2.head_branch?.startsWith(prefix)).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id - a.id)[0];
|
|
41989
|
+
return run ? { id: run.id, url: run.html_url, conclusion: run.conclusion } : void 0;
|
|
41990
|
+
}
|
|
41991
|
+
function prMergeQueueLine(result) {
|
|
41992
|
+
const queue = result.mergeQueue;
|
|
41993
|
+
if (!queue) return void 0;
|
|
41994
|
+
switch (queue.state) {
|
|
41995
|
+
case "queued":
|
|
41996
|
+
return `pr land: PR #${result.pr} queued on ${queue.base}${queue.position === void 0 ? "" : ` (position ${queue.position})`}`;
|
|
41997
|
+
case "merged":
|
|
41998
|
+
return `pr land: PR #${result.pr} merged via merge queue`;
|
|
41999
|
+
case "ejected":
|
|
42000
|
+
return `pr land: PR #${result.pr} ejected from merge queue \u2014 ${queue.ejectedRun ? `merge_group run ${queue.ejectedRun.id} ${queue.ejectedRun.conclusion} ${queue.ejectedRun.url}` : "merge_group run not found"}`;
|
|
42001
|
+
case "timeout":
|
|
42002
|
+
return `pr land: PR #${result.pr} merge queue timeout on ${queue.base}`;
|
|
42003
|
+
}
|
|
42004
|
+
}
|
|
42005
|
+
|
|
41834
42006
|
// src/command-register-collaboration.ts
|
|
41835
42007
|
init_github_client();
|
|
41836
42008
|
init_github_quota();
|
|
@@ -46778,7 +46950,7 @@ ${list}`);
|
|
|
46778
46950
|
}
|
|
46779
46951
|
class PrHeadBehindBaseError extends Error {
|
|
46780
46952
|
}
|
|
46781
|
-
pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
|
|
46953
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
|
|
46782
46954
|
if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
|
|
46783
46955
|
try {
|
|
46784
46956
|
const parsed = parseIssueRef(number, o.repo);
|
|
@@ -46813,7 +46985,14 @@ ${list}`);
|
|
|
46813
46985
|
return;
|
|
46814
46986
|
}
|
|
46815
46987
|
if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
|
|
46816
|
-
|
|
46988
|
+
let mergeQueueEnabled = false;
|
|
46989
|
+
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly }, {
|
|
46990
|
+
hasMergeQueue: async (repo, base) => mergeQueueEnabled = await hasPrMergeQueue(repo, base),
|
|
46991
|
+
readMergeQueue: readPrMergeQueue,
|
|
46992
|
+
findMergeGroupRun: findPrMergeGroupRun,
|
|
46993
|
+
queueProgress: (receipt) => {
|
|
46994
|
+
if (!o.json) printLine(prMergeQueueLine(receipt));
|
|
46995
|
+
},
|
|
46817
46996
|
resolveRepo: async (prNumber, repoOpt) => {
|
|
46818
46997
|
const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
|
|
46819
46998
|
let repoFromGh = "";
|
|
@@ -46903,7 +47082,8 @@ ${list}`);
|
|
|
46903
47082
|
},
|
|
46904
47083
|
mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
|
|
46905
47084
|
allowedClosing: landClosingGuardInput?.closing,
|
|
46906
|
-
bodyText: landSquashBody
|
|
47085
|
+
bodyText: landSquashBody,
|
|
47086
|
+
mergeQueue: mergeQueueEnabled
|
|
46907
47087
|
}),
|
|
46908
47088
|
updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
|
|
46909
47089
|
pollMerged: async (prNumber, repo, deadlineMs) => {
|
|
@@ -46927,7 +47107,7 @@ ${list}`);
|
|
|
46927
47107
|
return false;
|
|
46928
47108
|
}
|
|
46929
47109
|
});
|
|
46930
|
-
if (result.status !== "failed") {
|
|
47110
|
+
if (result.status !== "failed" && result.status !== "queued") {
|
|
46931
47111
|
const boardAdvance = await advanceClosedIssuesToDone2(number, result.repo);
|
|
46932
47112
|
const crossRepoFilingIssue = result.repo ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), result.repo, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
|
|
46933
47113
|
const recovery = result.repo ? buildPostMergeReconRecovery({
|
|
@@ -47470,16 +47650,18 @@ async function announceSummaryLineCountRefusal(path2) {
|
|
|
47470
47650
|
return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
47471
47651
|
}
|
|
47472
47652
|
const lines2 = summaryFileLines(content);
|
|
47653
|
+
const first = content.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
|
|
47654
|
+
if (first && Object.values(UPDATE_SECTIONS).some((w) => first === w.fixed || first === w.new)) return null;
|
|
47473
47655
|
return lines2.length < 3 || lines2.length > 6 ? `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing` : null;
|
|
47474
47656
|
}
|
|
47475
|
-
async function announceSummaryRefusal(path2, isHub = false) {
|
|
47657
|
+
async function announceSummaryRefusal(path2, isHub = false, isWorkSuite = false) {
|
|
47476
47658
|
let lines2;
|
|
47477
47659
|
try {
|
|
47478
47660
|
lines2 = summaryFileLines(await (0, import_promises9.readFile)(path2, "utf8"));
|
|
47479
47661
|
} catch (e) {
|
|
47480
47662
|
return `release: could not read --announce-summary-file ${path2}: ${e.message} \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
47481
47663
|
}
|
|
47482
|
-
if (lines2.length < 3 || lines2.length > 6) {
|
|
47664
|
+
if (!isWorkSuite && (lines2.length < 3 || lines2.length > 6)) {
|
|
47483
47665
|
return `release: --announce-summary-file must contain 3-6 non-empty LLM-curated lines (found ${lines2.length}) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`;
|
|
47484
47666
|
}
|
|
47485
47667
|
if (isHub) {
|
|
@@ -47577,7 +47759,13 @@ function trainApplyDeps() {
|
|
|
47577
47759
|
// Slack release announcement (#883/#6521/#6523): the org alerts channel for the Hub, the project's own
|
|
47578
47760
|
// registry releaseChannel META for a product repo — best-effort inside announceRelease itself.
|
|
47579
47761
|
announce: (args) => announceRelease({
|
|
47580
|
-
|
|
47762
|
+
// #8236: the announcement reads an already-PUBLISHED Release, so its `release view` rides the App
|
|
47763
|
+
// read pool like the train's other reads (#8030), never the owner's personal REST quota. Scoped
|
|
47764
|
+
// here, not in isAppRoutableGhReadArgs: other train paths may view a draft the App cannot see.
|
|
47765
|
+
run: async (file, cmdArgs) => {
|
|
47766
|
+
const env = file === "gh" && (cmdArgs[0] === "release" && cmdArgs[1] === "view" || isAppRoutableGhReadArgs(cmdArgs)) ? await ghReadEnv() : void 0;
|
|
47767
|
+
return (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS, ...env ? { env } : {} })).stdout;
|
|
47768
|
+
},
|
|
47581
47769
|
postAnnouncement: async (payload) => {
|
|
47582
47770
|
const res = await releaseAnnouncement(payload, registryClientDeps(await loadConfig()));
|
|
47583
47771
|
const body = res.body;
|
|
@@ -47951,6 +48139,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
47951
48139
|
const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone, or cut the suite lead alone (requires --owner-go; a member rcand is always refused)").option("--owner-go", "explicit owner approval for a suite-solo backend release").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
|
|
47952
48140
|
const RELEASE_ONLY_FLAGS = [
|
|
47953
48141
|
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901) and for a product repo whose registry META sets `releaseChannel` (#6523), in its `releaseLanguage`; nothing is posted to a project channel without it" },
|
|
48142
|
+
{ flags: "--announce-summary-file-tr <path>", description: "Turkish update-notes summary; required next to --announce-summary-file for a new mm-work suite Release (#8230)" },
|
|
47954
48143
|
{ flags: "--ack <shas>", description: "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)" },
|
|
47955
48144
|
{ 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)" },
|
|
47956
48145
|
{ 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)" },
|
|
@@ -48144,7 +48333,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
48144
48333
|
const raw = await withReleaseProgress(
|
|
48145
48334
|
trainApplyDeps(),
|
|
48146
48335
|
"Checking the existing release",
|
|
48147
|
-
(deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, suiteSolo: o.suiteSolo, big: o.big })
|
|
48336
|
+
(deps) => runReleaseResume(deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, announceSummaryFileTr: o.announceSummaryFileTr, suiteSolo: o.suiteSolo, big: o.big })
|
|
48148
48337
|
);
|
|
48149
48338
|
const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
|
|
48150
48339
|
const projectInfoSync = await withReleaseProgress(
|
|
@@ -48200,7 +48389,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
48200
48389
|
}
|
|
48201
48390
|
if (isWorkSuite) {
|
|
48202
48391
|
try {
|
|
48203
|
-
await requireSuiteReleaseSummary({}, o.announceSummaryFile);
|
|
48392
|
+
await requireSuiteReleaseSummary({}, o.announceSummaryFile, o.announceSummaryFileTr);
|
|
48204
48393
|
} catch (e) {
|
|
48205
48394
|
return fail(`release: ${e.message}`);
|
|
48206
48395
|
}
|
|
@@ -48211,14 +48400,14 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
48211
48400
|
isHub ? "release: a new MMI-Hub release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing" : `release: ${slug} announces to its project release channel, so a new release requires --announce-summary-file <path> with 3-6 short plain-language lines in the project's releaseLanguage (${releaseLanguage2}); create the summary file, then rerun the same release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing`
|
|
48212
48401
|
);
|
|
48213
48402
|
}
|
|
48214
|
-
const refusal = await announceSummaryRefusal(o.announceSummaryFile, isHub);
|
|
48403
|
+
const refusal = await announceSummaryRefusal(o.announceSummaryFile, isHub, isWorkSuite);
|
|
48215
48404
|
if (refusal) return fail(refusal);
|
|
48216
48405
|
}
|
|
48217
48406
|
}
|
|
48218
48407
|
if (o.apply) {
|
|
48219
48408
|
try {
|
|
48220
48409
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
48221
|
-
const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev, suiteSolo: o.suiteSolo, big: o.big });
|
|
48410
|
+
const apply = (deps) => runTrainApply(commandName, deps, { watch: o.watch, announceSummaryFile: o.announceSummaryFile, announceSummaryFileTr: o.announceSummaryFileTr, ack, dev: o.dev, suiteSolo: o.suiteSolo, big: o.big });
|
|
48222
48411
|
const raw = await withReleaseProgress(trainApplyDeps(), commandName === "release" ? "Preparing release" : "Preparing release candidate", apply);
|
|
48223
48412
|
const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
|
|
48224
48413
|
let projectInfoSync;
|
|
@@ -48403,16 +48592,16 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
48403
48592
|
if (o.suiteSolo !== o.ownerGo) return fail("hotfix start: solo suite hotfix requires BOTH --suite-solo --owner-go");
|
|
48404
48593
|
return runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from, suiteSolo: o.suiteSolo, ack: o.ack ? o.ack.split(",").map((s) => s.trim()).filter(Boolean) : void 0 }), o, renderHotfixStart);
|
|
48405
48594
|
});
|
|
48406
|
-
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--ack <shas>", "accepted for flag symmetry with hotfix start; hotfix release proves every carry by ancestry, so it is ignored here, never an error (#7338)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--suite-solo", "release one suite module alone (requires --owner-go)").option("--owner-go", "explicit owner approval for a solo backend hotfix").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
|
|
48595
|
+
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line release Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068), optional for a product repo announcing to its project release channel (#6517)").option("--announce-summary-file-tr <path>", "Turkish update-notes summary; required next to --announce-summary-file for a new mm-work suite hotfix Release (#8230)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--ack <shas>", "accepted for flag symmetry with hotfix start; hotfix release proves every carry by ancestry, so it is ignored here, never an error (#7338)").option("--retry-deploy", "re-run only the failed deploy and/or fold-port alignment legs of a published hotfix of this same tag (#6985)").option("--suite-solo", "release one suite module alone (requires --owner-go)").option("--owner-go", "explicit owner approval for a solo backend hotfix").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => {
|
|
48407
48596
|
if (o.suiteSolo !== o.ownerGo) return fail("hotfix release: solo suite release requires BOTH --suite-solo --owner-go");
|
|
48408
48597
|
if (o.retryDeploy) {
|
|
48409
|
-
if (o.announceSummaryFile || o.carries) {
|
|
48598
|
+
if (o.announceSummaryFile || o.announceSummaryFileTr || o.carries) {
|
|
48410
48599
|
return fail("hotfix release: --retry-deploy accepts only --json and --out; it re-runs failed deploy/alignment legs of the published tag and does not recut");
|
|
48411
48600
|
}
|
|
48412
48601
|
return runHotfixSub("retry-deploy", () => runHotfixRetryDeploy(trainApplyDeps(), version), o, renderHotfixRetryDeploy);
|
|
48413
48602
|
}
|
|
48414
48603
|
return runHotfixSub("release", async () => {
|
|
48415
|
-
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
|
|
48604
|
+
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, announceSummaryFileTr: o.announceSummaryFileTr, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
|
|
48416
48605
|
const suitePin = o.suiteSolo ? await pinCompletedMember(result) : void 0;
|
|
48417
48606
|
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
48418
48607
|
return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
|
|
@@ -48745,10 +48934,11 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
|
|
|
48745
48934
|
};
|
|
48746
48935
|
return {
|
|
48747
48936
|
recordPath,
|
|
48748
|
-
validateHotfixSummary: () => requireSuiteReleaseSummary({}, summaryFile),
|
|
48937
|
+
validateHotfixSummary: () => requireSuiteReleaseSummary({}, summaryFile, summaryFileTr),
|
|
48749
48938
|
validateReleaseSummaries: (releases) => validateReleaseSummaries(
|
|
48750
48939
|
releases,
|
|
48751
48940
|
{ ...memberSummaryFiles, [SUITE_REPO]: summaryFile },
|
|
48941
|
+
{ ...memberSummaryFilesTr, [SUITE_REPO]: summaryFileTr },
|
|
48752
48942
|
async (repo, tag) => {
|
|
48753
48943
|
try {
|
|
48754
48944
|
await gh(["release", "view", tag, "--repo", repo, "--json", "tagName"]);
|
|
@@ -48839,7 +49029,7 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
|
|
|
48839
49029
|
},
|
|
48840
49030
|
// #7738: a member hotfixes as suite-solo; the lead hotfixes under its suite-lead capability (inTrain sets it).
|
|
48841
49031
|
hotfixStart: (repo, o) => inTrain(repo, async (deps) => {
|
|
48842
|
-
await requireSuiteReleaseSummary(deps, summaryFile);
|
|
49032
|
+
await requireSuiteReleaseSummary(deps, summaryFile, summaryFileTr);
|
|
48843
49033
|
const r = await runHotfixStart(deps, { from: o.from, ack: o.ack, suiteSolo: repo !== SUITE_REPO });
|
|
48844
49034
|
return { tag: r.tag };
|
|
48845
49035
|
}, repo !== SUITE_REPO),
|
|
@@ -49023,7 +49213,7 @@ function registerSuiteCommands(program3) {
|
|
|
49023
49213
|
return fail(`suite abandon: ${e.message}`);
|
|
49024
49214
|
}
|
|
49025
49215
|
});
|
|
49026
|
-
suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train; also this cut's plain-words summary for MM-Work itself").option("--member-summary-file <owner/repo=path>", "repeatable: required
|
|
49216
|
+
suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train; also this cut's plain-words summary for MM-Work itself").option("--member-summary-file <owner/repo=path>", "repeatable: required update-notes summary (Fixed/New) for each member being released; carried members need none (#8230)", (val, prev = []) => [...prev, val], []).option("--announce-summary-file-tr <path>", "required Turkish update-notes summary (D\xFCzeldi/Yeni) for MM-Work itself (#8230)").option("--member-summary-file-tr <owner/repo=path>", "repeatable: required Turkish update-notes summary for each member being released (#8230)", (val, prev = []) => [...prev, val], []).option("--revamp <owner/repo>", `repeatable: marks that repo's release an owner-approved revamp in the plain-words section \u2014 the only "big" signal; versions carry no meaning (#8224)`, (val, prev = []) => [...prev, val], []).option("--big", "alias for --revamp mutmutco/MM-Work (#7510)").option("--json", "structured receipt").action(async (o) => {
|
|
49027
49217
|
try {
|
|
49028
49218
|
if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
|
|
49029
49219
|
const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
|
|
@@ -49046,7 +49236,7 @@ function registerSuiteCommands(program3) {
|
|
|
49046
49236
|
return fail(`suite release: ${e.message}`);
|
|
49047
49237
|
}
|
|
49048
49238
|
});
|
|
49049
|
-
suite.command("hotfix <member>").description("member hotfix, main pin PR, MM-Work patch release, and the recorded suite version -> member versions mapping (#7738)").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR(s) or SHA(s) the member hotfix carries").option("--ack <shas>", "dev shas verified as not needed on main (see hotfix start --ack)").option("--announce-summary-file <path>", "required
|
|
49239
|
+
suite.command("hotfix <member>").description("member hotfix, main pin PR, MM-Work patch release, and the recorded suite version -> member versions mapping (#7738)").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR(s) or SHA(s) the member hotfix carries").option("--ack <shas>", "dev shas verified as not needed on main (see hotfix start --ack)").option("--announce-summary-file <path>", "required update-notes summary (Fixed/New) for the member and lead hotfix Releases (#8230)").option("--announce-summary-file-tr <path>", "required Turkish update-notes summary (D\xFCzeldi/Yeni) (#8230)").option("--owner-go", "explicit owner approval: ships the member and MM-Work to production (not needed for --dry-run)").option("--dry-run", "read-only: print the ordered plan; writes nothing").option("--json", "structured receipt").action(async (member, o) => {
|
|
49050
49240
|
try {
|
|
49051
49241
|
if (!o.ownerGo && !o.dryRun) throw new Error("suite hotfix requires --owner-go (it ships to production); nothing was written. --dry-run prints the plan");
|
|
49052
49242
|
const root = await leadRoot();
|
|
@@ -50874,7 +51064,8 @@ var surfaces_default = {
|
|
|
50874
51064
|
],
|
|
50875
51065
|
outputs: [
|
|
50876
51066
|
"cli/dist/index.cjs",
|
|
50877
|
-
"cli/dist/main.cjs"
|
|
51067
|
+
"cli/dist/main.cjs",
|
|
51068
|
+
"cli/dist/runbook-sweep.cjs"
|
|
50878
51069
|
]
|
|
50879
51070
|
}
|
|
50880
51071
|
],
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -34,6 +34,8 @@ name: gate
|
|
|
34
34
|
# (seed touch: keep BOM digests in lockstep after classifier land.)
|
|
35
35
|
on:
|
|
36
36
|
pull_request:
|
|
37
|
+
merge_group:
|
|
38
|
+
types: [checks_requested]
|
|
37
39
|
push:
|
|
38
40
|
branches: {{GATE_PUSH_BRANCHES_YAML}}
|
|
39
41
|
tags: ['v*']
|
|
@@ -99,10 +101,10 @@ jobs:
|
|
|
99
101
|
lockfile-path: {{GATE_CACHE_DEP_PATH}}
|
|
100
102
|
fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
|
|
101
103
|
- name: Runbook guard
|
|
102
|
-
if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
|
|
104
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}
|
|
103
105
|
uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
|
|
104
106
|
with:
|
|
105
|
-
base-ref:
|
|
107
|
+
base-ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || format('origin/{0}', github.base_ref) }}
|
|
106
108
|
# #5427: per-job npm cache under $RUNNER_TEMP — never the shared ~/.npm. Twelve mmi-live lanes
|
|
107
109
|
# share the gha-runner home; npm hardlinks from that cache into node_modules, so one job's
|
|
108
110
|
# `npm ci` can make another job's vitest/dist/workers/forks.js vanish mid-suite (Cannot find
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
name: gate
|
|
2
2
|
on:
|
|
3
3
|
pull_request:
|
|
4
|
+
merge_group:
|
|
5
|
+
types: [checks_requested]
|
|
4
6
|
push:
|
|
5
7
|
branches: {{GATE_PUSH_BRANCHES_YAML}}
|
|
6
8
|
tags: ['v*']
|
|
@@ -49,10 +51,10 @@ jobs:
|
|
|
49
51
|
lockfile-path: package-lock.json
|
|
50
52
|
fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
|
|
51
53
|
- name: Runbook guard
|
|
52
|
-
if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
|
|
54
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}
|
|
53
55
|
uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
|
|
54
56
|
with:
|
|
55
|
-
base-ref:
|
|
57
|
+
base-ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || format('origin/{0}', github.base_ref) }}
|
|
56
58
|
- name: Isolate npm cache
|
|
57
59
|
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
58
60
|
run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
|
|
@@ -109,7 +109,7 @@ commit, so build and verify the lines from one pass over that range —
|
|
|
109
109
|
subject, body and changed files — never from subjects, PR titles or memory; a line whose claim is not
|
|
110
110
|
in that output does not go in the file. A Hub summary never mentions a Jerv-side project; the train
|
|
111
111
|
refuses such a line.
|
|
112
|
-
Every new MM-Work suite Release, solo and hotfix included, requires
|
|
112
|
+
Every new MM-Work suite Release, solo and hotfix included, requires `--announce-summary-file` and `--announce-summary-file-tr` in the bilingual update-notes template of the [MM-Work GUIDE, "Update notes"](https://github.com/mutmutco/MM-Work/blob/development/packages/mmwork-ds/GUIDE.md) (`Fixed`/`Düzeldi`, then `New`/`Yeni`, with `<Area>: <sentence>.` lines). A suite release requires `--member-summary-file owner/repo=path` and `--member-summary-file-tr owner/repo=path` for every member it releases. Details: the release skill.
|
|
113
113
|
|
|
114
114
|
Announcement delivery uses the developer's GitHub-authenticated Hub session. The Hub checks project
|
|
115
115
|
authority, resolves the registered channel, and reads the MMI Future Slack key inside its vault boundary.
|
package/skills/release/SKILL.md
CHANGED
|
@@ -15,8 +15,8 @@ Full-track repos ship exactly what is on `rc` (never `development`): merge `rc
|
|
|
15
15
|
|
|
16
16
|
In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: run `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` instead (a lead-only cut needs `--suite-solo --owner-go`). A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync.
|
|
17
17
|
|
|
18
|
-
Every new mm-work suite Release (including solo and hotfix releases) requires `--announce-summary-file <path>`
|
|
19
|
-
|
|
18
|
+
Every new mm-work suite Release (including solo and hotfix releases) requires both `--announce-summary-file <path>` (English) and `--announce-summary-file-tr <path>` (Turkish, "sen" form) in the update-notes template. The canonical wording lives in the [MM-Work GUIDE, "Update notes"](https://github.com/mutmutco/MM-Work/blob/development/packages/mmwork-ds/GUIDE.md); this repo links it and never copies it. Each file has the section word `Fixed` / `Düzeldi` (the problem the person saw, present tense) and/or `New` / `Yeni` (what you can now do) on its own line, Fixed first, each only when it has lines. Under them go 1–8 lines of `<Area>: <sentence>.`, each at most 120 characters, with no issue numbers, versions or commit prefixes. Off-template or missing files refuse before tagging. Do not write the head (app · date · time) or the thanks line: MM-Work renders both from the Release date and credits. Suite release also requires a valid `--member-summary-file <owner/repo=path>` and `--member-summary-file-tr <owner/repo=path>` for every member being released, before promoting any member. Carried, held and already-released members need none, and existing Releases can resume without new files.
|
|
19
|
+
The `/user-releases` feed serves template notes as `updateNotes: {en?, tr?: {fixed[], new[]}}`. Older title-and-notes bodies keep `plainWords` and get `updateNotes: null`.
|
|
20
20
|
|
|
21
21
|
Read META and name the lane before describing or executing anything:
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ The JSON is on stdout alone; a stale CLI first prints its self-update notice on
|
|
|
40
40
|
|
|
41
41
|
Run from the primary checkout on the lane's start branch (`rc`; `development` for direct-track and `--dev`). A Jerv-Hub release on Windows must run under Git Bash (`"C:/Program Files/Git/bin/bash.exe" -lc "…"`) — the train refuses `--apply` before tagging when it is not. Export the bump intent once from the bare argument — `MMI_BUMP_INTENT=minor|major|patch`, unset → `patch`; `MMI_RELEASE_VERSION=X.Y.Z` only for an exact target the tag math cannot derive.
|
|
42
42
|
|
|
43
|
-
Two cases need a summary file. (1) **MMI-Hub** — always: write a fresh 3–6 line neutral summary to `f=$(mkdir -p .jerv/tmp && mktemp .jerv/tmp/release-summary.XXXXXX)` — sourced from the range `origin/main..origin/development`, rewritten in Hub-subsystem terms, never a product or brand name, and never a Jerv-side project (the train refuses such a line). Hub scope is only `mutmutco/MMI-Hub`: never a product's board, `ds-propagate.yml`, or a product's deploy state. **Build and verify every line from ONE command's output**, never from commit subjects or PR titles alone: `git log --stat --format='%h %s%n%b' origin/main..origin/development` gives each commit's subject, its full body (where a bundled PR records `Closes #N`) and the files it changed in a single pass. A line whose claim you cannot point to in that output does not go in the file, and every `#N` a Hub line cites must appear there: the train refuses `--apply` on a citation that ships in no commit of the cut. (2) **Any product repo whose META carries `releaseChannel`** (`mmi-cli oracle org project get {owner}/{repo} --json`) — required too: read `releaseLanguage` (default `en`) and write 3–6 lines IN THAT LANGUAGE, in plain words for the project's audience: what changed and why it matters to a user; no commit prefixes, no issue or PR numbers, no file names, no product-internal jargon. The train refuses `--apply` without the file, and raw release notes are never posted to a project channel. A repo with no `releaseChannel` announces nothing, but an mm-work suite repo still requires the summary for its GitHub Release.
|
|
43
|
+
Two cases need a summary file. (1) **MMI-Hub** — always: write a fresh 3–6 line neutral summary to `f=$(mkdir -p .jerv/tmp && mktemp .jerv/tmp/release-summary.XXXXXX)` — sourced from the range `origin/main..origin/development`, rewritten in Hub-subsystem terms, never a product or brand name, and never a Jerv-side project (the train refuses such a line). Hub scope is only `mutmutco/MMI-Hub`: never a product's board, `ds-propagate.yml`, or a product's deploy state. **Build and verify every line from ONE command's output**, never from commit subjects or PR titles alone: `git log --stat --format='%h %s%n%b' origin/main..origin/development` gives each commit's subject, its full body (where a bundled PR records `Closes #N`) and the files it changed in a single pass. A line whose claim you cannot point to in that output does not go in the file, and every `#N` a Hub line cites must appear there: the train refuses `--apply` on a citation that ships in no commit of the cut. (2) **Any product repo whose META carries `releaseChannel`** (`mmi-cli oracle org project get {owner}/{repo} --json`) — required too: read `releaseLanguage` (default `en`) and write 3–6 lines IN THAT LANGUAGE, in plain words for the project's audience: what changed and why it matters to a user; no commit prefixes, no issue or PR numbers, no file names, no product-internal jargon. The train refuses `--apply` without the file, and raw release notes are never posted to a project channel. A repo with no `releaseChannel` announces nothing, but an mm-work suite repo still requires the summary for its GitHub Release. An mm-work suite repo's summary follows the update-notes template below instead of the 3–6 line rule.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
mkdir -p .jerv/tmp
|