@mutmutco/codex-plugin 4.5.99 → 4.5.101
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 +169 -40
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/bootstrap/SKILL.md +3 -0
- package/skills/org-guide/references/domains-and-oauth.md +5 -1
- package/skills/org-guide/references/work-and-releases.md +1 -0
- package/skills/release/SKILL.md +3 -0
package/cli/dist/main.cjs
CHANGED
|
@@ -6043,7 +6043,7 @@ function isRetryableGitHubWaitReadError(e) {
|
|
|
6043
6043
|
const codes = /* @__PURE__ */ new Set();
|
|
6044
6044
|
if (typeof status === "number" && status > 0) codes.add(status);
|
|
6045
6045
|
for (const m of text.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) codes.add(Number(m[1]));
|
|
6046
|
-
const rateLimited = e?.rateLimited === true || /
|
|
6046
|
+
const rateLimited = e?.rateLimited === true || /rate limit (?:already )?exceeded|RATE_LIMITED|secondary rate|abuse detection/i.test(text);
|
|
6047
6047
|
if ([401, 404, 409, 422].some((c) => codes.has(c))) return false;
|
|
6048
6048
|
if (codes.has(403) && !rateLimited) return false;
|
|
6049
6049
|
if (codes.has(429) || rateLimited) return true;
|
|
@@ -8288,6 +8288,28 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
|
|
|
8288
8288
|
return { ok: false, status: 0, body: null, error: e.message, unreachable: "transport" };
|
|
8289
8289
|
}
|
|
8290
8290
|
}
|
|
8291
|
+
async function edgeDnsForStage(slug, stage, deps, ensure = false) {
|
|
8292
|
+
if (!deps.baseUrl) throw new Error("Hub API URL unavailable");
|
|
8293
|
+
const token = await deps.token();
|
|
8294
|
+
if (!token) throw new Error("Hub session token unavailable");
|
|
8295
|
+
const path2 = `/projects/${encodeURIComponent(slug)}/edge-dns/${stage}`;
|
|
8296
|
+
let body;
|
|
8297
|
+
if (ensure) {
|
|
8298
|
+
const write = await postJson(path2, {}, deps, "POST", { noRetry: true });
|
|
8299
|
+
if (!write.ok) throw new Error(`Hub edge dns ensure failed: ${write.error ?? `HTTP ${write.status}`}`);
|
|
8300
|
+
body = write.body;
|
|
8301
|
+
} else {
|
|
8302
|
+
const res = await retriedFetch(
|
|
8303
|
+
deps,
|
|
8304
|
+
`${deps.baseUrl.replace(/\/$/, "")}${path2}`,
|
|
8305
|
+
{ method: "GET", headers: { Authorization: `Bearer ${token}` } }
|
|
8306
|
+
);
|
|
8307
|
+
if (!res.ok) throw new Error(`Hub edge dns read failed: HTTP ${res.status}`);
|
|
8308
|
+
body = await res.json();
|
|
8309
|
+
}
|
|
8310
|
+
if (typeof body?.required !== "boolean") throw new Error("Hub edge dns response invalid");
|
|
8311
|
+
return body;
|
|
8312
|
+
}
|
|
8291
8313
|
async function edgeCertificateForStage(slug, stage, deps, ensure = false) {
|
|
8292
8314
|
if (!deps.baseUrl) throw new Error("Hub API URL unavailable");
|
|
8293
8315
|
const token = await deps.token();
|
|
@@ -15861,8 +15883,11 @@ function edgeDomainsByStage(meta) {
|
|
|
15861
15883
|
}
|
|
15862
15884
|
return out;
|
|
15863
15885
|
}
|
|
15864
|
-
function inferEdgeDomainsForWrite(slug) {
|
|
15886
|
+
function inferEdgeDomainsForWrite(slug, suite) {
|
|
15865
15887
|
const subdomain = defaultSubdomain(slug);
|
|
15888
|
+
if (suite === "mm-work" && slug !== suite) {
|
|
15889
|
+
return { dev: `dev.${subdomain}.mutmut.work`, main: `${subdomain}.mutmut.work` };
|
|
15890
|
+
}
|
|
15866
15891
|
return {
|
|
15867
15892
|
dev: `dev.${subdomain}.mutatismutandis.co`,
|
|
15868
15893
|
rc: `rc.${subdomain}.mutatismutandis.co`,
|
|
@@ -16236,10 +16261,11 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
16236
16261
|
}
|
|
16237
16262
|
const deployModel = inferDeployModelForWrite({ ...shape, projectType }, parsedRepo.fullName);
|
|
16238
16263
|
const releaseTrack = (isReleaseTrack(options.releaseTrack) ? options.releaseTrack : void 0) ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0);
|
|
16239
|
-
const
|
|
16240
|
-
const
|
|
16264
|
+
const suite = vars.SUITE || options.existing?.suite || void 0;
|
|
16265
|
+
const inferredDomains = inferEdgeDomainsForWrite(slug, suite);
|
|
16266
|
+
const derivedDomains = Object.fromEntries(branchesForTrack(releaseTrack).flatMap((branch) => {
|
|
16241
16267
|
const stage = branch === "development" ? "dev" : branch;
|
|
16242
|
-
return [stage, inferredDomains[stage]];
|
|
16268
|
+
return inferredDomains[stage] ? [[stage, inferredDomains[stage]]] : [];
|
|
16243
16269
|
}));
|
|
16244
16270
|
const num = (v) => {
|
|
16245
16271
|
if (v == null || v === "") return void 0;
|
|
@@ -16263,6 +16289,7 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
16263
16289
|
// Identity. name/division default off the repo name when the skill didn't pass them.
|
|
16264
16290
|
name: vars.NAME || parsedRepo.name,
|
|
16265
16291
|
division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
|
|
16292
|
+
suite: vars.SUITE || void 0,
|
|
16266
16293
|
repos: [`${parsedRepo.owner}/${slug}`],
|
|
16267
16294
|
branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
|
|
16268
16295
|
class: cls,
|
|
@@ -16362,6 +16389,11 @@ function decideSeedPrAction(openPrs) {
|
|
|
16362
16389
|
}
|
|
16363
16390
|
return { action: "create" };
|
|
16364
16391
|
}
|
|
16392
|
+
function decideFirstDevDeploy(input) {
|
|
16393
|
+
if (!input.devRowWritten) return "skip";
|
|
16394
|
+
if (input.seedMode === "direct") return "dispatch";
|
|
16395
|
+
return (input.seedPrs ?? []).some((pr) => typeof pr?.mergedAt === "string" && pr.mergedAt !== "") ? "dispatch" : "defer";
|
|
16396
|
+
}
|
|
16365
16397
|
function decideSeedDelivery(branchRules) {
|
|
16366
16398
|
const rules = Array.isArray(branchRules) ? branchRules : [];
|
|
16367
16399
|
const types = new Set(
|
|
@@ -17572,10 +17604,10 @@ var rollout_plan_default = {
|
|
|
17572
17604
|
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)."
|
|
17573
17605
|
},
|
|
17574
17606
|
baseline: {
|
|
17575
|
-
version: "4.5.
|
|
17576
|
-
tag: "v4.5.
|
|
17577
|
-
commit: "
|
|
17578
|
-
npm: "@mutmutco/cli@4.5.
|
|
17607
|
+
version: "4.5.101",
|
|
17608
|
+
tag: "v4.5.101",
|
|
17609
|
+
commit: "a3c82f0f7aa3",
|
|
17610
|
+
npm: "@mutmutco/cli@4.5.101"
|
|
17579
17611
|
},
|
|
17580
17612
|
exitCriterion: "fleet-n-of-n",
|
|
17581
17613
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17592,14 +17624,14 @@ var rollout_plan_default = {
|
|
|
17592
17624
|
repo: "mutmutco/mmi-hub",
|
|
17593
17625
|
role: "canary",
|
|
17594
17626
|
schedule: "train",
|
|
17595
|
-
v3Target: "v4.5.
|
|
17627
|
+
v3Target: "v4.5.101"
|
|
17596
17628
|
}
|
|
17597
17629
|
],
|
|
17598
17630
|
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.",
|
|
17599
17631
|
rollback: {
|
|
17600
17632
|
independent: true,
|
|
17601
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17602
|
-
v3Target: "v4.5.
|
|
17633
|
+
mechanism: "npm dist-tag latest -> 4.5.101 and redeploy the Hub Lambda from tag v4.5.101 (a3c82f0f7aa3); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17634
|
+
v3Target: "v4.5.101 (@mutmutco/cli@4.5.101, tag commit a3c82f0f7aa3 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17603
17635
|
}
|
|
17604
17636
|
},
|
|
17605
17637
|
{
|
|
@@ -21791,7 +21823,7 @@ async function runTenantControl(deps, options) {
|
|
|
21791
21823
|
const watch = options.watch ?? tenantControlWatches(action);
|
|
21792
21824
|
const base = { command: "tenant-control", repo, stage, action };
|
|
21793
21825
|
const since = (deps.now ?? Date.now)();
|
|
21794
|
-
const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, since: options.logSince, until: options.logUntil, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
|
|
21826
|
+
const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, edge: options.edge, since: options.logSince, until: options.logUntil, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
|
|
21795
21827
|
if (!d.ok) {
|
|
21796
21828
|
const transport = d.category === "transport-failed";
|
|
21797
21829
|
return {
|
|
@@ -25644,29 +25676,33 @@ Cut by ${formatSessionIdentity(cutBy)}
|
|
|
25644
25676
|
`;
|
|
25645
25677
|
}
|
|
25646
25678
|
var PLAIN_WORDS_HEADING = "## In plain words";
|
|
25679
|
+
var PLAIN_WORDS_HEADING_TR = "## Sade s\xF6zlerle";
|
|
25647
25680
|
var PLAIN_WORDS_MARKER = "<!-- mmi:big -->";
|
|
25648
|
-
function buildPlainWordsSection(lines2, big) {
|
|
25681
|
+
function buildPlainWordsSection(lines2, big, heading = PLAIN_WORDS_HEADING) {
|
|
25649
25682
|
const [title, ...notes] = lines2;
|
|
25650
|
-
const body = [
|
|
25683
|
+
const body = [heading, title ?? "", ...notes.slice(0, 5).map((n) => `- ${n}`)];
|
|
25651
25684
|
if (big) body.push(PLAIN_WORDS_MARKER);
|
|
25652
25685
|
return body.join("\n");
|
|
25653
25686
|
}
|
|
25654
25687
|
function noChangePlainWordsLine(suiteTag) {
|
|
25655
|
-
return `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}
|
|
25688
|
+
return suiteTag ? `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}.` : "No changes to this module in this release.";
|
|
25656
25689
|
}
|
|
25657
25690
|
function upsertPlainWordsSection(body, section) {
|
|
25658
|
-
const lines2 = body.split(/\r?\n/);
|
|
25659
|
-
const at = lines2.findIndex((l) => l.trim() === PLAIN_WORDS_HEADING);
|
|
25660
25691
|
let rest = body;
|
|
25661
|
-
|
|
25662
|
-
|
|
25663
|
-
|
|
25664
|
-
|
|
25665
|
-
|
|
25666
|
-
|
|
25692
|
+
for (const heading of [PLAIN_WORDS_HEADING, PLAIN_WORDS_HEADING_TR]) {
|
|
25693
|
+
const lines2 = rest.split(/\r?\n/);
|
|
25694
|
+
let stripping = false;
|
|
25695
|
+
let found = false;
|
|
25696
|
+
const kept = lines2.filter((line) => {
|
|
25697
|
+
if (line.trim() === heading) {
|
|
25698
|
+
stripping = true;
|
|
25699
|
+
found = true;
|
|
25700
|
+
return false;
|
|
25667
25701
|
}
|
|
25668
|
-
|
|
25669
|
-
|
|
25702
|
+
if (line.trim().startsWith("## ")) stripping = false;
|
|
25703
|
+
return !stripping;
|
|
25704
|
+
});
|
|
25705
|
+
if (found) rest = kept.join("\n");
|
|
25670
25706
|
}
|
|
25671
25707
|
const trimmedRest = rest.trim();
|
|
25672
25708
|
return trimmedRest ? `${section}
|
|
@@ -25679,8 +25715,11 @@ function plainWordsSummaryLines(content) {
|
|
|
25679
25715
|
async function writePlainWordsIfSuiteMember(deps, args) {
|
|
25680
25716
|
if (args.meta?.suite !== "mm-work") return;
|
|
25681
25717
|
try {
|
|
25682
|
-
const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag
|
|
25683
|
-
const section =
|
|
25718
|
+
const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag)];
|
|
25719
|
+
const section = [
|
|
25720
|
+
buildPlainWordsSection(lines2, args.big === true),
|
|
25721
|
+
...args.summaryLinesTr?.length ? [buildPlainWordsSection(args.summaryLinesTr, false, PLAIN_WORDS_HEADING_TR)] : []
|
|
25722
|
+
].join("\n\n");
|
|
25684
25723
|
const body = clean2(await deps.run("gh", ["release", "view", args.tag, "--repo", args.repo, "--json", "body", "-q", ".body"]));
|
|
25685
25724
|
await deps.run("gh", ["release", "edit", args.tag, "--repo", args.repo, "--notes", upsertPlainWordsSection(body, section)]);
|
|
25686
25725
|
} catch {
|
|
@@ -25689,7 +25728,8 @@ async function writePlainWordsIfSuiteMember(deps, args) {
|
|
|
25689
25728
|
async function writePlainWordsSection(deps, options, repo, meta, tag) {
|
|
25690
25729
|
try {
|
|
25691
25730
|
const summaryLines = options.announceSummaryFile ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFile)) : void 0;
|
|
25692
|
-
await
|
|
25731
|
+
const summaryLinesTr = options.announceSummaryFileTr ? plainWordsSummaryLines(await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(options.announceSummaryFileTr)) : void 0;
|
|
25732
|
+
await writePlainWordsIfSuiteMember(deps, { repo, tag, meta, summaryLines, summaryLinesTr, big: options.big, suiteTag: options.suiteTag });
|
|
25693
25733
|
} catch {
|
|
25694
25734
|
}
|
|
25695
25735
|
}
|
|
@@ -33890,6 +33930,28 @@ function edgeCertificateChecks(results) {
|
|
|
33890
33930
|
}];
|
|
33891
33931
|
});
|
|
33892
33932
|
}
|
|
33933
|
+
function edgeDnsChecks(results, repo) {
|
|
33934
|
+
return results.flatMap(({ stage, status, error }) => {
|
|
33935
|
+
if (!error && status?.required === false) return [];
|
|
33936
|
+
const bad = (status?.hosts ?? []).filter((h) => h.status === "missing" || h.status === "conflict");
|
|
33937
|
+
const ok = !error && status?.complete !== false && bad.length === 0;
|
|
33938
|
+
return [{
|
|
33939
|
+
ok,
|
|
33940
|
+
label: `Cloudflare edge DNS: ${stage}`,
|
|
33941
|
+
detail: ok ? void 0 : error ? `DNS records unreadable (${error})` : `${bad.map((h) => `${h.host} ${h.status}${h.hint ? ` (${h.hint})` : ""}`).join("; ")} \u2014 run: mmi-cli devops runtime edge dns ensure ${repo} ${stage}`
|
|
33942
|
+
}];
|
|
33943
|
+
});
|
|
33944
|
+
}
|
|
33945
|
+
function firstDevDeployCheck(repo, facts, factsRead) {
|
|
33946
|
+
const dev = facts?.stages?.dev;
|
|
33947
|
+
if (!factsRead || dev?.present !== true) return [];
|
|
33948
|
+
const ok = Boolean(dev.deployedAt || dev.deployedTag);
|
|
33949
|
+
return [{
|
|
33950
|
+
ok,
|
|
33951
|
+
label: "first dev deploy happened",
|
|
33952
|
+
detail: ok ? void 0 : `DEPLOY#dev has never deployed \u2014 run: mmi-cli devops runtime tenant redeploy ${repo} dev --watch`
|
|
33953
|
+
}];
|
|
33954
|
+
}
|
|
33893
33955
|
async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
33894
33956
|
const branchesWanted = expectedBranches(repoClass, releaseTrack);
|
|
33895
33957
|
const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
|
|
@@ -34012,6 +34074,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
34012
34074
|
if (check) checks.push(check);
|
|
34013
34075
|
}
|
|
34014
34076
|
checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? []));
|
|
34077
|
+
checks.push(...edgeDnsChecks(deps.edgeDns ?? [], repo));
|
|
34015
34078
|
const portRangeCheck = centralContainerPortRangeCheck(deps.deployModel, deps.projectMeta?.portRange, repo);
|
|
34016
34079
|
if (portRangeCheck) checks.push(portRangeCheck);
|
|
34017
34080
|
checks.push(...deployRowChecks(
|
|
@@ -34022,6 +34085,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
34022
34085
|
deps.deployFactsRead === true,
|
|
34023
34086
|
deps.projectMeta?.suite
|
|
34024
34087
|
));
|
|
34088
|
+
checks.push(...firstDevDeployCheck(repo, deps.deployFacts, deps.deployFactsRead === true));
|
|
34025
34089
|
const readme = await contentText(deps, repo, baseBranch, "README.md");
|
|
34026
34090
|
checks.push({
|
|
34027
34091
|
ok: readme !== null && readme.includes("## Agent context"),
|
|
@@ -34460,6 +34524,13 @@ function registerBootstrapCommands(program3) {
|
|
|
34460
34524
|
return { stage, error: e.message };
|
|
34461
34525
|
}
|
|
34462
34526
|
})),
|
|
34527
|
+
edgeDns: await Promise.all(["dev", "rc", "main"].filter((stage) => deployFacts?.stages?.[stage]?.present).map(async (stage) => {
|
|
34528
|
+
try {
|
|
34529
|
+
return { stage, status: await edgeDnsForStage(slug, stage, reg) };
|
|
34530
|
+
} catch (e) {
|
|
34531
|
+
return { stage, error: e.message };
|
|
34532
|
+
}
|
|
34533
|
+
})),
|
|
34463
34534
|
pushAllowlistOwners: verifyOwners,
|
|
34464
34535
|
pushAllowlistOwnersRead: verifyOwnersRead,
|
|
34465
34536
|
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null,
|
|
@@ -34857,7 +34928,7 @@ function registerBootstrapCommands(program3) {
|
|
|
34857
34928
|
github,
|
|
34858
34929
|
deployFacts: controlDeployFacts,
|
|
34859
34930
|
deployFactsRead: controlDeployFacts !== null,
|
|
34860
|
-
plannedMainDomain: registryMeta?.edgeDomains?.main ?? inferEdgeDomainsForWrite(slug).main
|
|
34931
|
+
plannedMainDomain: registryMeta?.edgeDomains?.main ?? inferEdgeDomainsForWrite(slug, suiteSlug || vars.SUITE).main
|
|
34861
34932
|
});
|
|
34862
34933
|
if (o.box) {
|
|
34863
34934
|
if (applyDeployModel !== "tenant-container" && applyDeployModel !== "solo-container")
|
|
@@ -34878,7 +34949,7 @@ function registerBootstrapCommands(program3) {
|
|
|
34878
34949
|
host: found.box.address,
|
|
34879
34950
|
box: found.box.name,
|
|
34880
34951
|
owners: scan.rows,
|
|
34881
|
-
domains: { ...inferEdgeDomainsForWrite(slug), ...domains, ...boxDomains },
|
|
34952
|
+
domains: { ...inferEdgeDomainsForWrite(slug, suiteSlug || vars.SUITE), ...domains, ...boxDomains },
|
|
34882
34953
|
fileless: applyDeployModel === "tenant-container"
|
|
34883
34954
|
});
|
|
34884
34955
|
controlRows = planned.rows;
|
|
@@ -35309,6 +35380,42 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
35309
35380
|
ddbWrites.push({ slug, action: `deploy-${coord.stage}`, record: payload });
|
|
35310
35381
|
applied.push(`DEPLOY#${coord.stage} ${coord.host}:${coord.port}${coord.domain ? ` ${coord.domain}` : ""}`);
|
|
35311
35382
|
}
|
|
35383
|
+
for (const coord of boxDeployPlan) {
|
|
35384
|
+
try {
|
|
35385
|
+
const dns = await edgeDnsForStage(slug, coord.stage, registryClientDeps(await loadConfig()), true);
|
|
35386
|
+
const bad = (dns.hosts ?? []).filter((h) => h.status === "conflict");
|
|
35387
|
+
applied.push(`edge dns ${coord.stage}: ${dns.required ? (dns.hosts ?? []).map((h) => `${h.host} ${h.status}`).join(", ") : "not required"}`);
|
|
35388
|
+
for (const h of bad) deferredFailures.push(`edge dns ${coord.stage} ${h.host}: ${h.hint}`);
|
|
35389
|
+
} catch (e) {
|
|
35390
|
+
applied.push(`edge dns ${coord.stage} (failed: ${e.message})`);
|
|
35391
|
+
deferredFailures.push(`edge dns ${coord.stage} not ensured: ${e.message}; re-run: mmi-cli devops runtime edge dns ensure ${repo} ${coord.stage}`);
|
|
35392
|
+
}
|
|
35393
|
+
}
|
|
35394
|
+
let seedPrs = [];
|
|
35395
|
+
if (seedPlan.mode === "pr" && seedPlan.branch) {
|
|
35396
|
+
try {
|
|
35397
|
+
seedPrs = JSON.parse((await gh2(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,mergedAt"])).stdout || "[]");
|
|
35398
|
+
} catch {
|
|
35399
|
+
seedPrs = [];
|
|
35400
|
+
}
|
|
35401
|
+
}
|
|
35402
|
+
const firstDeploy = decideFirstDevDeploy({ devRowWritten: boxDeployPlan.some((c) => c.stage === "dev"), seedMode: seedPlan.mode, seedPrs });
|
|
35403
|
+
if (firstDeploy === "defer") {
|
|
35404
|
+
applied.push("first dev deploy: deferred \u2014 the seed PR is not merged; the Hub dev auto-deploy runs on its merge (DEPLOY#dev now exists)");
|
|
35405
|
+
} else if (firstDeploy === "dispatch") {
|
|
35406
|
+
try {
|
|
35407
|
+
const out = await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(["runtime", "tenant", "redeploy", repo, "dev", "--json"])], { timeout: 12e4 });
|
|
35408
|
+
let run = {};
|
|
35409
|
+
try {
|
|
35410
|
+
run = JSON.parse(out.stdout);
|
|
35411
|
+
} catch {
|
|
35412
|
+
}
|
|
35413
|
+
applied.push(`first dev deploy dispatched at development: ${run.runUrl ?? run.dispatch ?? "run not yet correlated"}`);
|
|
35414
|
+
} catch (e) {
|
|
35415
|
+
applied.push(`first dev deploy (failed: ${e.message.split("\n")[0]})`);
|
|
35416
|
+
deferredFailures.push(`first dev deploy not dispatched; run: mmi-cli devops runtime tenant redeploy ${repo} dev --watch`);
|
|
35417
|
+
}
|
|
35418
|
+
}
|
|
35312
35419
|
controlRows = controlRows.map((row) => {
|
|
35313
35420
|
const coord = boxDeployPlan.find((p) => row.target === `DEPLOY#${p.stage}`);
|
|
35314
35421
|
return coord ? {
|
|
@@ -47419,7 +47526,7 @@ function memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles) {
|
|
|
47419
47526
|
return repo === SUITE_REPO ? summaryFile : memberSummaryFiles[repo];
|
|
47420
47527
|
}
|
|
47421
47528
|
var suiteStateDir = (root) => (0, import_node_path43.join)(resolvePrimaryGitDir(root), "mmi-runtime", "suite");
|
|
47422
|
-
function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
|
|
47529
|
+
function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr, memberSummaryFilesTr = {}) {
|
|
47423
47530
|
const recordPath = (0, import_node_path43.join)(suiteStateDir(root), "cut.json");
|
|
47424
47531
|
const base = (0, import_node_path43.join)(suiteStateDir(root), "checkouts");
|
|
47425
47532
|
const checkout = async (repo) => {
|
|
@@ -47530,7 +47637,12 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
|
|
|
47530
47637
|
});
|
|
47531
47638
|
const release = async (repo, expected, rcSha, opts) => inTrain(repo, async (deps) => {
|
|
47532
47639
|
await prepareRcCheckout((args) => deps.run("git", args), repo);
|
|
47533
|
-
const plainWords = {
|
|
47640
|
+
const plainWords = {
|
|
47641
|
+
announceSummaryFile: memberAnnounceSummaryFile(repo, summaryFile, memberSummaryFiles),
|
|
47642
|
+
announceSummaryFileTr: memberAnnounceSummaryFile(repo, summaryFileTr, memberSummaryFilesTr),
|
|
47643
|
+
big: opts?.big,
|
|
47644
|
+
suiteTag: opts?.suiteTag
|
|
47645
|
+
};
|
|
47534
47646
|
const current = await tagSha(repo, expected);
|
|
47535
47647
|
if (current) {
|
|
47536
47648
|
await deps.run("git", ["fetch", "origin", "--tags"]);
|
|
@@ -47771,6 +47883,12 @@ function parseMemberSummaryFiles(entries) {
|
|
|
47771
47883
|
}
|
|
47772
47884
|
return memberSummaryFiles;
|
|
47773
47885
|
}
|
|
47886
|
+
function validateSuiteReleaseSummary(path2) {
|
|
47887
|
+
if (!path2) throw new Error("suite release requires --announce-summary-file before promoting any member");
|
|
47888
|
+
const lines2 = (0, import_node_fs44.readFileSync)(path2, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
47889
|
+
if (lines2[0]?.startsWith("No changes to this module")) throw new Error("suite release requires a real MM-Work title, not the no-change placeholder");
|
|
47890
|
+
if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
|
|
47891
|
+
}
|
|
47774
47892
|
function suiteRcDataProdDeps() {
|
|
47775
47893
|
return {
|
|
47776
47894
|
fetch: async (url, init) => {
|
|
@@ -47865,18 +47983,17 @@ function registerSuiteCommands(program3) {
|
|
|
47865
47983
|
return fail(`suite abandon: ${e.message}`);
|
|
47866
47984
|
}
|
|
47867
47985
|
});
|
|
47868
|
-
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: a member's OWN plain-words summary (#7510) \u2014 never the suite lead's summary reused; a member with no entry still ships, with the coordinator-ruled no-change line", (val, prev = []) => [...prev, val], []).option("--big", "marks MM-Work's own release big for the plain-words section (#7510); a member is never marked big through this flag \u2014 it is big only via the read route's automatic minor/major detection").option("--json", "structured receipt").action(async (o) => {
|
|
47986
|
+
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: a member's OWN plain-words summary (#7510) \u2014 never the suite lead's summary reused; a member with no entry still ships, with the coordinator-ruled no-change line", (val, prev = []) => [...prev, val], []).option("--announce-summary-file-tr <path>", "Turkish plain-words summary for MM-Work itself").option("--member-summary-file-tr <owner/repo=path>", "repeatable: a member's own Turkish plain-words summary", (val, prev = []) => [...prev, val], []).option("--big", "marks MM-Work's own release big for the plain-words section (#7510); a member is never marked big through this flag \u2014 it is big only via the read route's automatic minor/major detection").option("--json", "structured receipt").action(async (o) => {
|
|
47869
47987
|
try {
|
|
47870
47988
|
if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
|
|
47871
|
-
|
|
47872
|
-
const lines2 = (0, import_node_fs44.readFileSync)(o.announceSummaryFile, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
47873
|
-
if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
|
|
47989
|
+
validateSuiteReleaseSummary(o.announceSummaryFile);
|
|
47874
47990
|
const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
|
|
47991
|
+
const memberSummaryFilesTr = parseMemberSummaryFiles(o.memberSummaryFileTr ?? []);
|
|
47875
47992
|
const root = await leadRoot();
|
|
47876
47993
|
const unlock = await acquireTrainWriterLock(root, "suite release");
|
|
47877
47994
|
let result;
|
|
47878
47995
|
try {
|
|
47879
|
-
result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles), true, o.big === true);
|
|
47996
|
+
result = await suiteRelease(suiteTrainIo(root, o.announceSummaryFile, memberSummaryFiles, o.announceSummaryFileTr, memberSummaryFilesTr), true, o.big === true);
|
|
47880
47997
|
} finally {
|
|
47881
47998
|
await unlock();
|
|
47882
47999
|
}
|
|
@@ -53143,6 +53260,17 @@ function registerEdgeCommands(program3) {
|
|
|
53143
53260
|
process.exitCode = 1;
|
|
53144
53261
|
}
|
|
53145
53262
|
});
|
|
53263
|
+
edge.command("dns <action> <owner/repo> <stage>").description("read (`status`) or create-when-missing (`ensure`, master-only) the proxied Cloudflare A records for a registered DEPLOY# row; existing records are never modified").action(async (action, repo, stage) => {
|
|
53264
|
+
if (action !== "status" && action !== "ensure") return fail("runtime edge dns: <action> must be status or ensure");
|
|
53265
|
+
if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime edge dns: <stage> must be dev, rc, or main");
|
|
53266
|
+
try {
|
|
53267
|
+
const result = await edgeDnsForStage(slugOf(repo), stage, registryClientDeps(await loadConfig()), action === "ensure");
|
|
53268
|
+
console.log(JSON.stringify(result, null, 2));
|
|
53269
|
+
if (result.required && result.complete === false) process.exitCode = 1;
|
|
53270
|
+
} catch (e) {
|
|
53271
|
+
fail(`runtime edge dns: ${e.message}`);
|
|
53272
|
+
}
|
|
53273
|
+
});
|
|
53146
53274
|
}
|
|
53147
53275
|
|
|
53148
53276
|
// src/command-register-developer.ts
|
|
@@ -54206,7 +54334,7 @@ async function reportWrite(label, res) {
|
|
|
54206
54334
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
54207
54335
|
}
|
|
54208
54336
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
54209
|
-
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
54337
|
+
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--edge", "logs only: Caddy journal for this stage\u2019s hosts (default window: last 15 minutes)").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
54210
54338
|
try {
|
|
54211
54339
|
if (o.confirmDataLoss !== void 0 && (action !== "retire" || stage === "rc")) return fail("runtime tenant control: --confirm-data-loss is dev/main retire-only");
|
|
54212
54340
|
if (action === "retire" && (stage === "dev" || stage === "main")) {
|
|
@@ -54219,6 +54347,7 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
|
|
|
54219
54347
|
return;
|
|
54220
54348
|
}
|
|
54221
54349
|
}
|
|
54350
|
+
if (o.edge && action !== "logs") return fail("runtime tenant control: --edge is valid only for the logs action");
|
|
54222
54351
|
let lines2;
|
|
54223
54352
|
if (o.lines !== void 0) {
|
|
54224
54353
|
if (action !== "logs") return fail("runtime tenant control: --lines is valid only for the logs action");
|
|
@@ -54235,7 +54364,7 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
|
|
|
54235
54364
|
} else if (o.task || o.artifact) {
|
|
54236
54365
|
return fail("runtime tenant control: --task/--artifact are valid only for run-task");
|
|
54237
54366
|
}
|
|
54238
|
-
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, logSince: o.since, logUntil: o.until, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
|
|
54367
|
+
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, edge: o.edge, logSince: o.since, logUntil: o.until, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
|
|
54239
54368
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
54240
54369
|
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
54241
54370
|
const { lines: lines3, failure } = renderVerifySecrets(body);
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -481,6 +481,9 @@ web tenants, `--project-type desktop-game --deploy-model none --clear-web-profil
|
|
|
481
481
|
`--project-type mobile-app --deploy-model none --clear-web-profile` for a phone app distributed through the
|
|
482
482
|
app stores, and
|
|
483
483
|
`--class content --project-type content --deploy-model content --clear-web-profile` for a content/KB repo.
|
|
484
|
+
An **MM-Work module** adds `--var SUITE=mm-work` (#7944): it registers `suite: mm-work` and derives its
|
|
485
|
+
own `dev.<sub>.mutmut.work` + `<sub>.mutmut.work` hosts, with no rc host (the suite serves its rc from the
|
|
486
|
+
live host). Without it, apply derives the `*.mutatismutandis.co` web-app hosts, which a module never uses.
|
|
484
487
|
Run the apply path with the board variables discovered above, or register the same values with
|
|
485
488
|
`mmi-cli oracle org project set` from the Hub or from the target project checkout:
|
|
486
489
|
|
|
@@ -4,9 +4,13 @@ MMI owns the central project registry and provisioning workflows. Read
|
|
|
4
4
|
`mmi-cli oracle org project get <owner/repo> --json` before changing hostnames or login settings.
|
|
5
5
|
Use the declared edge domains for each stage; do not guess hostnames from a slug or create a
|
|
6
6
|
second DNS/deployment control plane in a product repository. New project setup goes through
|
|
7
|
-
[bootstrap](../../bootstrap/SKILL.md).
|
|
7
|
+
[bootstrap](../../bootstrap/SKILL.md). An MM-Work suite module serves from its own
|
|
8
|
+
`dev.<name>.mutmut.work` and `<name>.mutmut.work` hosts, with no rc host; bootstrap declares them
|
|
9
|
+
with `--var SUITE=mm-work`.
|
|
8
10
|
|
|
9
11
|
Cloudflare owns public DNS and edge TLS. Preserve unrelated records during an authorized change.
|
|
12
|
+
The Hub creates each registered host's proxied record during bootstrap and never edits an existing
|
|
13
|
+
one; check or re-run it with `mmi-cli devops runtime edge dns status|ensure <owner/repo> <stage>`.
|
|
10
14
|
A dev stage is private only after its independent edge-block check succeeds. A configured record
|
|
11
15
|
or successful deployment alone does not prove access control. Use [stage](../../stage/SKILL.md)
|
|
12
16
|
for the supported private live workflow.
|
|
@@ -109,6 +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
|
+
MM-Work suite release also requires a real `--announce-summary-file` title (never `No changes to this module`); optional `--announce-summary-file-tr` and repeatable `--member-summary-file-tr owner/repo=path` add Turkish plain words alongside English member files supplied with `--member-summary-file`.
|
|
112
113
|
|
|
113
114
|
Announcement delivery uses the developer's GitHub-authenticated Hub session. The Hub checks project
|
|
114
115
|
authority, resolves the registered channel, and reads the MMI Future Slack key inside its vault boundary.
|
package/skills/release/SKILL.md
CHANGED
|
@@ -15,6 +15,9 @@ 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
|
+
Suite release requires `--announce-summary-file <path>` with 3–6 curated lines and refuses before promotion when it is missing or its title starts with `No changes to this module`.
|
|
19
|
+
Use `--member-summary-file <owner/repo=path>` for English member notes, `--announce-summary-file-tr <path>` for MM-Work's Turkish notes and repeatable `--member-summary-file-tr <owner/repo=path>` for Turkish member notes; members without English files keep the no-change line.
|
|
20
|
+
|
|
18
21
|
Read META and name the lane before describing or executing anything:
|
|
19
22
|
|
|
20
23
|
```bash
|