@mutmutco/codex-plugin 4.5.101 → 4.5.103
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 +152 -25
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/bootstrap/SKILL.md +5 -1
- package/skills/bootstrap/seeds/suite-Dockerfile.template +32 -0
- package/skills/bootstrap/seeds/suite-compose.template.yml +26 -0
- package/skills/bootstrap/seeds/suite-files.template.json +1 -0
- package/skills/bootstrap/seeds/suite-gate.template.yml +35 -0
- package/skills/bootstrap/seeds/suite-guide.template.md +11 -0
- package/skills/bootstrap/seeds/suite-module-package-lock.template.json +887 -0
- package/skills/bootstrap/seeds/suite-module-package.template.json +23 -0
- package/skills/bootstrap/seeds/suite-module.template.jsx +105 -0
- package/skills/bootstrap/seeds/suite-npmrc.template +2 -0
- package/skills/bootstrap/seeds/suite-package-lock.template.json +50 -0
- package/skills/bootstrap/seeds/suite-package.template.json +19 -0
- package/skills/bootstrap/seeds/suite-screens.template.json +170 -0
- package/skills/bootstrap/seeds/suite-screens.template.yml +61 -0
- package/skills/bootstrap/seeds/suite-server.template.ts +22 -0
- package/skills/bootstrap/seeds/suite-tsconfig.template.json +14 -0
- package/skills/bootstrap/seeds/suite-vite.template.mjs +12 -0
- package/skills/org-guide/references/domains-and-oauth.md +3 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -6053,6 +6053,16 @@ function isRetryableGitHubWaitReadError(e) {
|
|
|
6053
6053
|
}
|
|
6054
6054
|
return /Something went wrong while executing your query/.test(text) || /^\s*unexpected end of JSON input\s*$/m.test(text);
|
|
6055
6055
|
}
|
|
6056
|
+
function refusalWaitMs(e, nowMs) {
|
|
6057
|
+
const err = e;
|
|
6058
|
+
const text = [err?.stdout, err?.stderr, e instanceof Error ? e.message : ""].filter((t) => typeof t === "string").join("\n");
|
|
6059
|
+
const header = (name) => new RegExp(`^${name}:\\s*(\\d+)\\s*$`, "im").exec(text)?.[1];
|
|
6060
|
+
const retryAfter = header("retry-after");
|
|
6061
|
+
if (retryAfter !== void 0) return Number(retryAfter) * 1e3;
|
|
6062
|
+
const reset = header("x-ratelimit-remaining") === "0" ? Number(header("x-ratelimit-reset")) : typeof err?.rateLimitReset === "number" ? err.rateLimitReset : NaN;
|
|
6063
|
+
if (reset * 1e3 > nowMs) return reset * 1e3 - nowMs;
|
|
6064
|
+
return /secondary rate|abuse detection/i.test(text) ? 6e4 : void 0;
|
|
6065
|
+
}
|
|
6056
6066
|
function waitReadFailureMessage(e) {
|
|
6057
6067
|
const err = e;
|
|
6058
6068
|
const stderr = typeof err?.stderr === "string" ? err.stderr.trim() : "";
|
|
@@ -6180,9 +6190,21 @@ async function waitForPrChecks(deps) {
|
|
|
6180
6190
|
return { policy, status: "failure", reason: waitReadFailureMessage(e), detail: "github-read-failed", waitedMs: now() - started };
|
|
6181
6191
|
}
|
|
6182
6192
|
lastDetail = `github-read-transient (${waitReadFailureMessage(e)})`;
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6193
|
+
const askedMs = refusalWaitMs(e, now());
|
|
6194
|
+
const jitter = deps.jitterMs ?? (() => Math.floor(Math.random() * PR_CHECKS_RATELIMIT_JITTER_MAX_MS));
|
|
6195
|
+
const waitMs = askedMs !== void 0 && askedMs > PR_CHECKS_POLL_MS ? askedMs + jitter() : PR_CHECKS_POLL_MS;
|
|
6196
|
+
if (askedMs !== void 0 && now() + waitMs >= deadline) {
|
|
6197
|
+
return {
|
|
6198
|
+
policy,
|
|
6199
|
+
status: "rate-limited",
|
|
6200
|
+
reason: `GitHub refused the read for rate limit and asked for ${Math.round(askedMs / 1e3)}s, past the wait deadline (${lastDetail})`,
|
|
6201
|
+
detail: "rate-limited",
|
|
6202
|
+
waitedMs: now() - started
|
|
6203
|
+
};
|
|
6204
|
+
}
|
|
6205
|
+
deps.log?.(`transient GitHub read failure \u2014 retrying in ${Math.round(waitMs / 1e3)}s within the wait budget: ${lastDetail}`);
|
|
6206
|
+
report(askedMs !== void 0 ? "rate-limited" : "pending");
|
|
6207
|
+
await deps.sleep(waitMs);
|
|
6186
6208
|
continue;
|
|
6187
6209
|
}
|
|
6188
6210
|
report(state);
|
|
@@ -6300,6 +6322,7 @@ function setPollTokenProvider(provider) {
|
|
|
6300
6322
|
pollTokenOnce = void 0;
|
|
6301
6323
|
resetAppPollGuard();
|
|
6302
6324
|
lastPollSource = void 0;
|
|
6325
|
+
observedPool.clear();
|
|
6303
6326
|
}
|
|
6304
6327
|
async function pollToken() {
|
|
6305
6328
|
if (!pollTokenProvider) return void 0;
|
|
@@ -6327,12 +6350,43 @@ function parseRateLimitHeaders(out) {
|
|
|
6327
6350
|
};
|
|
6328
6351
|
return { remaining: first("remaining"), limit: first("limit"), resetSec: first("reset") };
|
|
6329
6352
|
}
|
|
6353
|
+
function describeRefusal(out) {
|
|
6354
|
+
const header = (name) => new RegExp(`^${name}:\\s*(.+?)\\s*$`, "im").exec(out)?.[1];
|
|
6355
|
+
let message2 = "";
|
|
6356
|
+
const body = out.slice(out.search(/\r?\n\r?\n/) + 1).trim();
|
|
6357
|
+
try {
|
|
6358
|
+
message2 = String(JSON.parse(body).message ?? "");
|
|
6359
|
+
} catch {
|
|
6360
|
+
message2 = body;
|
|
6361
|
+
}
|
|
6362
|
+
const parts = [
|
|
6363
|
+
["resource", header("x-ratelimit-resource")],
|
|
6364
|
+
["remaining", header("x-ratelimit-remaining")],
|
|
6365
|
+
["reset", header("x-ratelimit-reset")],
|
|
6366
|
+
["retry-after", header("retry-after")]
|
|
6367
|
+
].filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${v}`);
|
|
6368
|
+
const text = message2.replace(/\s+/g, " ").slice(0, 160);
|
|
6369
|
+
return [parts.join(" "), text ? `"${text}"` : ""].filter(Boolean).join(" ") || "no rate-limit headers or message";
|
|
6370
|
+
}
|
|
6330
6371
|
var etagCache = /* @__PURE__ */ new Map();
|
|
6331
6372
|
function appPoolDetail() {
|
|
6332
6373
|
const guard = appPollGuardSnapshot();
|
|
6333
6374
|
const counts = guard.remaining !== void 0 && guard.limit !== void 0 ? `App pool at ${guard.remaining}/${guard.limit}` : "App pool headroom unverified";
|
|
6334
6375
|
return guard.resetAtMs !== void 0 ? `${counts}, resets ${new Date(guard.resetAtMs).toISOString()}` : counts;
|
|
6335
6376
|
}
|
|
6377
|
+
var observedPool = /* @__PURE__ */ new Map();
|
|
6378
|
+
var OBSERVED_POOL_MAX_AGE_MS = 3 * 6e4;
|
|
6379
|
+
function notePoolHeaders(source, out, nowMs) {
|
|
6380
|
+
const { remaining, resetSec } = parseRateLimitHeaders(out);
|
|
6381
|
+
const resource = /^x-ratelimit-resource:\s*(\S+)\s*$/im.exec(out)?.[1];
|
|
6382
|
+
if (remaining !== void 0 && resetSec !== void 0 && (resource ?? "core") === "core") {
|
|
6383
|
+
observedPool.set(source, { remaining, reset: resetSec, atMs: nowMs });
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
function observedCorePool(source, nowMs) {
|
|
6387
|
+
const seen = source ? observedPool.get(source) : void 0;
|
|
6388
|
+
return seen && nowMs - seen.atMs <= OBSERVED_POOL_MAX_AGE_MS && seen.reset * 1e3 > nowMs ? { remaining: seen.remaining, reset: seen.reset } : void 0;
|
|
6389
|
+
}
|
|
6336
6390
|
async function defaultGhApi(args) {
|
|
6337
6391
|
const appToken = await pollToken();
|
|
6338
6392
|
let source = appToken && appPollAllowsApp(Date.now()) ? "app" : "user";
|
|
@@ -6354,13 +6408,14 @@ async function defaultGhApi(args) {
|
|
|
6354
6408
|
const out = e.stdout;
|
|
6355
6409
|
const errOut = typeof out === "string" ? out : void 0;
|
|
6356
6410
|
const status = errOut !== void 0 ? parseIncludedOutput(errOut).status : void 0;
|
|
6411
|
+
if (errOut !== void 0) notePoolHeaders(source, errOut, Date.now());
|
|
6357
6412
|
if (cached && status === 304) {
|
|
6358
6413
|
lastPollSource = source;
|
|
6359
6414
|
return cached.body;
|
|
6360
6415
|
}
|
|
6361
6416
|
if (source === "app" && appToken && (status === 403 || status === 429)) {
|
|
6362
6417
|
noteAppPollResponse(parseRateLimitHeaders(errOut), true, Date.now());
|
|
6363
|
-
console.warn(`poll credential: App pool refused the read (HTTP ${status}, ${appPoolDetail()}) \u2014 REST poll reads switch to the ambient user token until the App pool resets (#7843)`);
|
|
6418
|
+
console.warn(`poll credential: App pool refused the read (HTTP ${status}, ${appPoolDetail()}; response: ${describeRefusal(errOut)}) \u2014 REST poll reads switch to the ambient user token until the App pool resets (#7843)`);
|
|
6364
6419
|
lastPollSource = "user";
|
|
6365
6420
|
source = "user";
|
|
6366
6421
|
continue;
|
|
@@ -6368,6 +6423,7 @@ async function defaultGhApi(args) {
|
|
|
6368
6423
|
throw e;
|
|
6369
6424
|
}
|
|
6370
6425
|
const { body, etag, pages } = parseIncludedOutput(stdout);
|
|
6426
|
+
notePoolHeaders(source, stdout, Date.now());
|
|
6371
6427
|
if (source === "app") noteAppPollResponse(parseRateLimitHeaders(stdout), false, Date.now());
|
|
6372
6428
|
lastPollSource = source;
|
|
6373
6429
|
if (etag && pages === 1) etagCache.set(key, { etag, body });
|
|
@@ -6840,7 +6896,9 @@ async function fetchRequiredCheckContexts(repo, branch, gh2 = defaultGhApi) {
|
|
|
6840
6896
|
}
|
|
6841
6897
|
return contexts;
|
|
6842
6898
|
}
|
|
6843
|
-
async function fetchRestCorePool(gh2 = defaultGhApi) {
|
|
6899
|
+
async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
|
|
6900
|
+
const seen = gh2 === defaultGhApi ? observedCorePool(await pollToken() && appPollAllowsApp(nowMs) ? "app" : "user", nowMs) : void 0;
|
|
6901
|
+
if (seen) return { state: "ok", ...seen };
|
|
6844
6902
|
let core;
|
|
6845
6903
|
try {
|
|
6846
6904
|
core = JSON.parse(await gh2(["rate_limit"])).resources?.core;
|
|
@@ -16108,6 +16166,39 @@ function gateConfigToVars(gate) {
|
|
|
16108
16166
|
if (extras.steps?.length) out.GATE_EXTRA_STEPS_YAML = gateExtraStepsYaml(extras.steps);
|
|
16109
16167
|
return out;
|
|
16110
16168
|
}
|
|
16169
|
+
function suiteModuleSeeds(seeds, suite, deployModel) {
|
|
16170
|
+
if (suite !== "mm-work" || deployModel !== "tenant-container") return seeds;
|
|
16171
|
+
const replacements = {
|
|
16172
|
+
Dockerfile: "suite-Dockerfile.template",
|
|
16173
|
+
"docker-compose.yml": "suite-compose.template.yml",
|
|
16174
|
+
".github/workflows/gate.yml": "suite-gate.template.yml"
|
|
16175
|
+
};
|
|
16176
|
+
const additions = {
|
|
16177
|
+
"src/index.ts": "suite-server.template.ts",
|
|
16178
|
+
"package.json": "suite-package.template.json",
|
|
16179
|
+
"package-lock.json": "suite-package-lock.template.json",
|
|
16180
|
+
"tsconfig.json": "suite-tsconfig.template.json",
|
|
16181
|
+
"module/package.json": "suite-module-package.template.json",
|
|
16182
|
+
"module/package-lock.json": "suite-module-package-lock.template.json",
|
|
16183
|
+
"module/.npmrc": "suite-npmrc.template",
|
|
16184
|
+
"module/src/module.jsx": "suite-module.template.jsx",
|
|
16185
|
+
"module/vite.config.mjs": "suite-vite.template.mjs",
|
|
16186
|
+
"module/screens/screens.json": "suite-screens.template.json",
|
|
16187
|
+
"module/screens/files.json": "suite-files.template.json",
|
|
16188
|
+
".github/workflows/screens.yml": "suite-screens.template.yml",
|
|
16189
|
+
"docs/Guides/mm-work-module.md": "suite-guide.template.md"
|
|
16190
|
+
};
|
|
16191
|
+
return [
|
|
16192
|
+
...seeds.filter((s) => s.target !== "docs/Guides/google-login.md").map((s) => replacements[s.target] ? { ...s, source: `seed:${replacements[s.target]}` } : s),
|
|
16193
|
+
...Object.entries(additions).map(([target, source]) => ({
|
|
16194
|
+
target,
|
|
16195
|
+
source: `seed:${source}`,
|
|
16196
|
+
ownership: "repo",
|
|
16197
|
+
classes: ["deployable"],
|
|
16198
|
+
deployModels: ["tenant-container"]
|
|
16199
|
+
}))
|
|
16200
|
+
];
|
|
16201
|
+
}
|
|
16111
16202
|
function seedMatchesDeployModel(seed, deployModel) {
|
|
16112
16203
|
if (!seed.deployModels?.length) return true;
|
|
16113
16204
|
return deployModel != null && seed.deployModels.includes(deployModel);
|
|
@@ -16289,7 +16380,11 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
16289
16380
|
// Identity. name/division default off the repo name when the skill didn't pass them.
|
|
16290
16381
|
name: vars.NAME || parsedRepo.name,
|
|
16291
16382
|
division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
|
|
16292
|
-
suite
|
|
16383
|
+
suite,
|
|
16384
|
+
...suite === "mm-work" && deployModel === "tenant-container" ? {
|
|
16385
|
+
requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"],
|
|
16386
|
+
requiredChecks: ["gate", "screens / screens"]
|
|
16387
|
+
} : {},
|
|
16293
16388
|
repos: [`${parsedRepo.owner}/${slug}`],
|
|
16294
16389
|
branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
|
|
16295
16390
|
class: cls,
|
|
@@ -17604,10 +17699,10 @@ var rollout_plan_default = {
|
|
|
17604
17699
|
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)."
|
|
17605
17700
|
},
|
|
17606
17701
|
baseline: {
|
|
17607
|
-
version: "4.5.
|
|
17608
|
-
tag: "v4.5.
|
|
17609
|
-
commit: "
|
|
17610
|
-
npm: "@mutmutco/cli@4.5.
|
|
17702
|
+
version: "4.5.103",
|
|
17703
|
+
tag: "v4.5.103",
|
|
17704
|
+
commit: "c2dd99cf0d72",
|
|
17705
|
+
npm: "@mutmutco/cli@4.5.103"
|
|
17611
17706
|
},
|
|
17612
17707
|
exitCriterion: "fleet-n-of-n",
|
|
17613
17708
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17624,14 +17719,14 @@ var rollout_plan_default = {
|
|
|
17624
17719
|
repo: "mutmutco/mmi-hub",
|
|
17625
17720
|
role: "canary",
|
|
17626
17721
|
schedule: "train",
|
|
17627
|
-
v3Target: "v4.5.
|
|
17722
|
+
v3Target: "v4.5.103"
|
|
17628
17723
|
}
|
|
17629
17724
|
],
|
|
17630
17725
|
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.",
|
|
17631
17726
|
rollback: {
|
|
17632
17727
|
independent: true,
|
|
17633
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17634
|
-
v3Target: "v4.5.
|
|
17728
|
+
mechanism: "npm dist-tag latest -> 4.5.103 and redeploy the Hub Lambda from tag v4.5.103 (c2dd99cf0d72); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17729
|
+
v3Target: "v4.5.103 (@mutmutco/cli@4.5.103, tag commit c2dd99cf0d72 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17635
17730
|
}
|
|
17636
17731
|
},
|
|
17637
17732
|
{
|
|
@@ -33919,14 +34014,15 @@ function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead, suit
|
|
|
33919
34014
|
};
|
|
33920
34015
|
});
|
|
33921
34016
|
}
|
|
33922
|
-
function edgeCertificateChecks(results) {
|
|
34017
|
+
function edgeCertificateChecks(results, repo) {
|
|
33923
34018
|
return results.flatMap((result) => {
|
|
33924
34019
|
if (!result) return [];
|
|
33925
34020
|
const { stage, status, error } = result;
|
|
34021
|
+
const fix = status?.missing && !status.pending ? ` \u2014 run: mmi-cli devops runtime edge cert ensure ${repo} ${stage}` : "";
|
|
33926
34022
|
return [{
|
|
33927
34023
|
ok: !error && (status?.required === false || status?.required === true && status.missing === false),
|
|
33928
34024
|
label: `Cloudflare edge certificate: ${stage}`,
|
|
33929
|
-
detail: error ? `certificate coverage unreadable (${error}); cannot verify TLS` : status?.hint
|
|
34025
|
+
detail: error ? `certificate coverage unreadable (${error}); cannot verify TLS` : status?.hint && `${status.hint}${fix}`
|
|
33930
34026
|
}];
|
|
33931
34027
|
});
|
|
33932
34028
|
}
|
|
@@ -33990,6 +34086,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
33990
34086
|
detail: exactAllowlist ? void 0 : ownersUnverified ? "org owner baseline is unreadable or empty; the exact restriction is unverified" : deps.pushAllowlistOwnersRead === true ? "restriction must contain exactly the org owners + mmi-github-app and no teams" : "restrictions.users is empty or unset"
|
|
33991
34087
|
});
|
|
33992
34088
|
}
|
|
34089
|
+
const packageAccess = suiteModulePackageAccessCheck(deps.projectMeta?.suite, deps.deployModel);
|
|
34090
|
+
if (packageAccess) checks.push(packageAccess);
|
|
33993
34091
|
let environmentDrift2 = [];
|
|
33994
34092
|
try {
|
|
33995
34093
|
environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client, deps.projectMeta?.suite);
|
|
@@ -34073,7 +34171,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
34073
34171
|
const check = npmrcGitHubPackagesAuthCheck(path2, await contentText(deps, repo, baseBranch, path2));
|
|
34074
34172
|
if (check) checks.push(check);
|
|
34075
34173
|
}
|
|
34076
|
-
checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? []));
|
|
34174
|
+
checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? [], repo));
|
|
34077
34175
|
checks.push(...edgeDnsChecks(deps.edgeDns ?? [], repo));
|
|
34078
34176
|
const portRangeCheck = centralContainerPortRangeCheck(deps.deployModel, deps.projectMeta?.portRange, repo);
|
|
34079
34177
|
if (portRangeCheck) checks.push(portRangeCheck);
|
|
@@ -34331,6 +34429,14 @@ function laneNotEvaluatedFromThisBranch(doctor) {
|
|
|
34331
34429
|
const only = reasons.length === 1 ? reasons[0] : void 0;
|
|
34332
34430
|
return only?.code === "branch-mismatch" ? only.title : void 0;
|
|
34333
34431
|
}
|
|
34432
|
+
function suiteModulePackageAccessCheck(suite, deployModel) {
|
|
34433
|
+
if (suite !== "mm-work" || deployModel !== "tenant-container") return void 0;
|
|
34434
|
+
return {
|
|
34435
|
+
ok: true,
|
|
34436
|
+
label: "mmwork-ds package access (manual org-admin step; unverified)",
|
|
34437
|
+
detail: "An org admin must add this repository with Read under @mutmutco/mmwork-ds \u2192 Package settings \u2192 Manage Actions access. No GitHub REST API can verify or grant this; without it CI fails with 403 read_package."
|
|
34438
|
+
};
|
|
34439
|
+
}
|
|
34334
34440
|
function attachTrainDoctorVerdicts(report, trainDoctors) {
|
|
34335
34441
|
const marked = trainDoctors.map((doctor) => {
|
|
34336
34442
|
const notEvaluatedFromThisBranch = laneNotEvaluatedFromThisBranch(doctor);
|
|
@@ -34819,13 +34925,6 @@ function registerBootstrapCommands(program3) {
|
|
|
34819
34925
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
34820
34926
|
const slug = parsedRepo.slug;
|
|
34821
34927
|
const onlyTarget = o.fromConfig && o.only.trim() === "gate.yml" ? ".github/workflows/gate.yml" : o.only.trim();
|
|
34822
|
-
const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
|
|
34823
|
-
if (onlyTarget && !seedsToApply.length) {
|
|
34824
|
-
const known = manifest.seeds.map((s) => s.target).join("\n ");
|
|
34825
|
-
return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
|
|
34826
|
-
${known}`);
|
|
34827
|
-
}
|
|
34828
|
-
const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
|
|
34829
34928
|
const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
|
|
34830
34929
|
const readFile10 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
|
|
34831
34930
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
@@ -34895,6 +34994,20 @@ function registerBootstrapCommands(program3) {
|
|
|
34895
34994
|
applyProjectType = o.projectType || registryMeta?.projectType || payload.projectType;
|
|
34896
34995
|
} catch {
|
|
34897
34996
|
}
|
|
34997
|
+
const availableSeeds = suiteModuleSeeds(manifest.seeds, vars.SUITE || registryMeta?.suite, applyDeployModel);
|
|
34998
|
+
const seedsToApply = onlyTarget ? availableSeeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : availableSeeds;
|
|
34999
|
+
if (onlyTarget && !seedsToApply.length) {
|
|
35000
|
+
return fail(`bootstrap apply: --only '${onlyTarget}' names no seed for this project. Declared targets:
|
|
35001
|
+
${availableSeeds.map((s) => s.target).join("\n ")}`);
|
|
35002
|
+
}
|
|
35003
|
+
const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
|
|
35004
|
+
if (vars.SUITE === "mm-work" || registryMeta?.suite === "mm-work") {
|
|
35005
|
+
if (applyDeployModel === "tenant-container") {
|
|
35006
|
+
const moduleGate = withDerivedRepoVars({}, parsedRepo, o.class, effectiveTrack, registryRequiredCheckBranches, ["gate", "screens / screens"]);
|
|
35007
|
+
vars.GATE_RULESET_CONTEXTS_JSON = moduleGate.GATE_RULESET_CONTEXTS_JSON;
|
|
35008
|
+
if (registryMeta) registryMeta = { ...registryMeta, requiredChecks: ["gate", "screens / screens"] };
|
|
35009
|
+
}
|
|
35010
|
+
}
|
|
34898
35011
|
const controlClient = defaultGitHubClient();
|
|
34899
35012
|
let controlOwners = [];
|
|
34900
35013
|
let controlDeployFacts = null;
|
|
@@ -46588,8 +46701,11 @@ function trainApplyDeps() {
|
|
|
46588
46701
|
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
46589
46702
|
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
46590
46703
|
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
46591
|
-
|
|
46592
|
-
|
|
46704
|
+
// #7961: forward every SET field rather than re-listing them, so a new option (e.g. `edge`) cannot be
|
|
46705
|
+
// dropped silently. Unset/false fields are omitted: the route rejects e.g. `edge` on a non-logs action.
|
|
46706
|
+
dispatchTenantControl: async (input) => {
|
|
46707
|
+
const payload = Object.fromEntries(Object.entries(input).filter(([, v]) => v != null && v !== "" && v !== false));
|
|
46708
|
+
const res = await tenantControl(payload, registryClientDeps(await loadConfig()));
|
|
46593
46709
|
if (res.ok) return { ok: true };
|
|
46594
46710
|
const body = res.body;
|
|
46595
46711
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
@@ -53271,6 +53387,17 @@ function registerEdgeCommands(program3) {
|
|
|
53271
53387
|
fail(`runtime edge dns: ${e.message}`);
|
|
53272
53388
|
}
|
|
53273
53389
|
});
|
|
53390
|
+
edge.command("cert <action> <owner/repo> <stage>").description("read (`status`) or order-when-missing (`ensure`, master-only) the Cloudflare advanced edge certificate pack (<x> + *.<x>) a dev/rc host needs").action(async (action, repo, stage) => {
|
|
53391
|
+
if (action !== "status" && action !== "ensure") return fail("runtime edge cert: <action> must be status or ensure");
|
|
53392
|
+
if (stage !== "dev" && stage !== "rc") return fail("runtime edge cert: <stage> must be dev or rc (main is covered by the zone certificate)");
|
|
53393
|
+
try {
|
|
53394
|
+
const result = await edgeCertificateForStage(slugOf(repo), stage, registryClientDeps(await loadConfig()), action === "ensure");
|
|
53395
|
+
console.log(JSON.stringify(result, null, 2));
|
|
53396
|
+
if (result.required && result.missing) process.exitCode = 1;
|
|
53397
|
+
} catch (e) {
|
|
53398
|
+
fail(`runtime edge cert: ${e.message}`);
|
|
53399
|
+
}
|
|
53400
|
+
});
|
|
53274
53401
|
}
|
|
53275
53402
|
|
|
53276
53403
|
// src/command-register-developer.ts
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -507,7 +507,11 @@ the bootstrap seed Dockerfile and compose templates). The seeded compose is file
|
|
|
507
507
|
`labels: { mmi.runtime-env: "true" }`, no `env_file`, and a loopback-only port; the deploy injects the stage's
|
|
508
508
|
vault values at runtime (`DEPLOY#<stage>.noEnvFile: true`) and the app reads plain env vars — it must **not** self-load SSM and must **not** ship a committed `.env`.
|
|
509
509
|
|
|
510
|
-
For a `
|
|
510
|
+
For a `suite: mm-work` tenant-container module, bootstrap apply selects the MM-Work module seed instead: a live `/health` server and `/module.js`, the example module scaffold, secret-backed image build, and `gate` plus `screens / screens`. It sets `requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"]` and those required checks. Before the first CI/deploy, an org admin must grant the repo Read on `@mutmutco/mmwork-ds` under Package settings → Manage Actions access (no GitHub REST API; verify names the manual step).
|
|
511
|
+
|
|
512
|
+
**Never provision a Google OAuth client for an MM-Work suite module, even if old META declares `oauth`.** Modules sign in through the MM-Work pass and reach Google via `ctx.act`; read the package's GUIDE. The suite seed omits the Google-login guide.
|
|
513
|
+
|
|
514
|
+
For a non-suite `web-app` that declares `oauth` META, print the canonical OAuth surface and provision the client once:
|
|
511
515
|
|
|
512
516
|
```bash
|
|
513
517
|
mmi-cli vault org oauth plan --repo "$OWNER/$REPO" # the exact JS origins + redirect URIs + canonical SSM keys
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# MM-Work module tenant container: the TypeScript server plus the module bundle it serves.
|
|
2
|
+
|
|
3
|
+
# The module bundle. The GitHub Packages token (for @mutmutco/mmwork-ds) arrives as a BuildKit secret
|
|
4
|
+
# and is never written into a layer; compose passes it from the registry's requiredBuildSecrets.
|
|
5
|
+
FROM node:24-bookworm-slim AS module-build
|
|
6
|
+
WORKDIR /module
|
|
7
|
+
COPY module/package.json module/package-lock.json module/.npmrc ./
|
|
8
|
+
RUN --mount=type=secret,id=NODE_AUTH_TOKEN,env=NODE_AUTH_TOKEN npm ci --no-audit --no-fund
|
|
9
|
+
COPY module/src ./src
|
|
10
|
+
COPY module/vite.config.mjs ./
|
|
11
|
+
RUN npm run build
|
|
12
|
+
|
|
13
|
+
# The server.
|
|
14
|
+
FROM node:24-bookworm-slim AS server-build
|
|
15
|
+
WORKDIR /app
|
|
16
|
+
COPY package.json package-lock.json ./
|
|
17
|
+
RUN npm ci --no-audit --no-fund
|
|
18
|
+
COPY tsconfig.json ./
|
|
19
|
+
COPY src ./src
|
|
20
|
+
RUN npm run build && npm prune --omit=dev
|
|
21
|
+
|
|
22
|
+
FROM node:24-bookworm-slim AS runtime
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
ENV NODE_ENV=production
|
|
25
|
+
COPY --from=server-build /app/package.json ./package.json
|
|
26
|
+
COPY --from=server-build /app/node_modules ./node_modules
|
|
27
|
+
COPY --from=server-build /app/dist ./dist
|
|
28
|
+
COPY --from=module-build /module/dist/module.js ./module/dist/module.js
|
|
29
|
+
USER node
|
|
30
|
+
EXPOSE 3000
|
|
31
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
32
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: Dockerfile
|
|
6
|
+
secrets:
|
|
7
|
+
- NODE_AUTH_TOKEN
|
|
8
|
+
restart: unless-stopped
|
|
9
|
+
# Fileless deploy (DEPLOY#<stage>.noEnvFile: true): no .env exists. tenant-deploy.yml streams the
|
|
10
|
+
# stage-scoped vault values over SSH into a tmpfs (/dev/shm) runtime env, and the box injects them
|
|
11
|
+
# into every service labelled mmi.runtime-env. Never add env_file.
|
|
12
|
+
labels:
|
|
13
|
+
mmi.runtime-env: "true"
|
|
14
|
+
environment:
|
|
15
|
+
NODE_ENV: production
|
|
16
|
+
MMI_STAGE: ${MMI_STAGE:-dev}
|
|
17
|
+
MMI_PORT: ${MMI_PORT:-3000}
|
|
18
|
+
PORT: ${PORT:-3000}
|
|
19
|
+
MMI_EDGE_DOMAIN: ${MMI_EDGE_DOMAIN:-}
|
|
20
|
+
ports:
|
|
21
|
+
# Loopback only: the box Caddy is the one public door (TLS), never the bare port.
|
|
22
|
+
- "127.0.0.1:${MMI_PORT:-3000}:${PORT:-3000}"
|
|
23
|
+
|
|
24
|
+
secrets:
|
|
25
|
+
NODE_AUTH_TOKEN:
|
|
26
|
+
environment: NODE_AUTH_TOKEN
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "result": { "items": [{ "id": "sample", "name": "Sample brief" }] } }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: gate
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: {{GATE_PUSH_BRANCHES_YAML}}
|
|
6
|
+
tags: ['v*']
|
|
7
|
+
concurrency:
|
|
8
|
+
group: gate-${{ github.event.pull_request.number || github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
packages: read
|
|
13
|
+
jobs:
|
|
14
|
+
gate:
|
|
15
|
+
runs-on: [self-hosted, linux, x64, mmi-live]
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
steps:
|
|
18
|
+
- uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
|
|
19
|
+
with:
|
|
20
|
+
lockfile-path: package-lock.json
|
|
21
|
+
- name: Isolate npm cache
|
|
22
|
+
run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
|
|
23
|
+
- uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
|
|
24
|
+
with:
|
|
25
|
+
node-version: 24
|
|
26
|
+
npm-version: '12'
|
|
27
|
+
- run: npm ci --no-audit --no-fund
|
|
28
|
+
- run: npm ci --prefix module --no-audit --no-fund
|
|
29
|
+
env:
|
|
30
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
31
|
+
- uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}
|
|
32
|
+
with:
|
|
33
|
+
command: npm run typecheck && npm run build && npm --prefix module run check
|
|
34
|
+
max-seconds: {{GATE_MAX_SECONDS}}
|
|
35
|
+
working-directory: .
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# MM-Work module
|
|
2
|
+
|
|
3
|
+
Read the matching `module/node_modules/@mutmutco/mmwork-ds/GUIDE.md` before changing this module. The starter entry and Vite setting come from MM-Work's `examples/example-module`; replace the example screens with your product's screens using only the shared design system.
|
|
4
|
+
|
|
5
|
+
Sign-in belongs to MM-Work: verify its pass before adding protected backend routes. Do not provision a Google OAuth client or implement a separate login. Google and other shared services are reached through `ctx.act`, not module-owned credentials.
|
|
6
|
+
|
|
7
|
+
Before the first CI/deploy, an org admin must grant this repo **Read** on `@mutmutco/mmwork-ds` under **Package settings → Manage Actions access**. This has no GitHub REST API. The Actions token installs the module in CI; the tenant image build uses the registry's `NODE_AUTH_TOKEN=@github-packages-token` BuildKit secret. Never commit a token.
|
|
8
|
+
|
|
9
|
+
The server exposes `/health` and `/module.js`. Register the module's bundle URL, id, name and capabilities with MM-Work following the GUIDE. The seed deliberately exposes no authenticated product API yet.
|
|
10
|
+
|
|
11
|
+
`gate` installs and checks the backend and module. `screens / screens` follows MM-PM's non-blocking PR/push/tag shape, using MM-Work's reusable runner with `run: false` (browser audits are not merge gates). Configure scheduled/on-demand screen audits separately when the product has screens to audit.
|