@mutmutco/codex-plugin 4.5.98 → 4.5.100
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 +421 -210
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/bootstrap/SKILL.md +3 -0
- package/skills/bootstrap/seeds/gate.template.yml +3 -0
- package/skills/org-guide/references/domains-and-oauth.md +5 -1
- package/skills/org-guide/references/work-and-releases.md +6 -0
package/cli/dist/main.cjs
CHANGED
|
@@ -5731,6 +5731,182 @@ init_github_quota();
|
|
|
5731
5731
|
// src/pr-checks-rest.ts
|
|
5732
5732
|
init_cli_shared();
|
|
5733
5733
|
|
|
5734
|
+
// ../shared/github-quota-scope.ts
|
|
5735
|
+
var import_node_crypto3 = require("node:crypto");
|
|
5736
|
+
function appQuotaScope(appId, installationId, origin = "https://api.github.com", resource = "graphql") {
|
|
5737
|
+
return `github-app-v1:${(0, import_node_crypto3.createHash)("sha256").update(JSON.stringify([new URL(origin).origin, appId.trim(), installationId.trim(), resource])).digest("hex")}`;
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5740
|
+
// src/app-actor.ts
|
|
5741
|
+
var import_node_crypto4 = require("node:crypto");
|
|
5742
|
+
var APP_ACTOR_ENV = "MMI_ACTOR";
|
|
5743
|
+
var APP_VAULT_REPO = "mutmutco/MMI-Hub";
|
|
5744
|
+
var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
|
|
5745
|
+
var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
|
|
5746
|
+
"pr checks-wait",
|
|
5747
|
+
"pr view",
|
|
5748
|
+
"issue list",
|
|
5749
|
+
"issue view",
|
|
5750
|
+
"api get"
|
|
5751
|
+
]);
|
|
5752
|
+
var APP_POLL_PERMISSIONS = {
|
|
5753
|
+
metadata: "read",
|
|
5754
|
+
contents: "read",
|
|
5755
|
+
pull_requests: "read",
|
|
5756
|
+
// #7185: without issues:read GraphQL silently drops pr view's closingIssuesReferences.
|
|
5757
|
+
issues: "read",
|
|
5758
|
+
checks: "read",
|
|
5759
|
+
statuses: "read",
|
|
5760
|
+
actions: "read"
|
|
5761
|
+
};
|
|
5762
|
+
var APP_JWT_BACKDATE_S = 60;
|
|
5763
|
+
var APP_JWT_TTL_S = 9 * 60;
|
|
5764
|
+
var AppActorError = class extends Error {
|
|
5765
|
+
constructor(message2, code = "mint-failed") {
|
|
5766
|
+
super(message2);
|
|
5767
|
+
this.code = code;
|
|
5768
|
+
}
|
|
5769
|
+
code;
|
|
5770
|
+
};
|
|
5771
|
+
function commandPathOf(cmd) {
|
|
5772
|
+
const parts = [];
|
|
5773
|
+
let cursor = cmd;
|
|
5774
|
+
while (cursor && cursor.parent) {
|
|
5775
|
+
parts.unshift(cursor.name());
|
|
5776
|
+
cursor = cursor.parent;
|
|
5777
|
+
}
|
|
5778
|
+
return parts.join(" ");
|
|
5779
|
+
}
|
|
5780
|
+
function b64url(input) {
|
|
5781
|
+
return Buffer.from(input).toString("base64url");
|
|
5782
|
+
}
|
|
5783
|
+
function mintAppJwt(appId, privateKeyPem, nowSec) {
|
|
5784
|
+
if (!Number.isFinite(nowSec)) throw new AppActorError("app actor: invalid clock reading for JWT mint");
|
|
5785
|
+
const now = Math.floor(nowSec);
|
|
5786
|
+
const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
5787
|
+
const payload = b64url(JSON.stringify({
|
|
5788
|
+
iat: now - APP_JWT_BACKDATE_S,
|
|
5789
|
+
exp: now + APP_JWT_TTL_S,
|
|
5790
|
+
iss: appId
|
|
5791
|
+
}));
|
|
5792
|
+
const signer = (0, import_node_crypto4.createSign)("RSA-SHA256");
|
|
5793
|
+
signer.update(`${header}.${payload}`);
|
|
5794
|
+
return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
|
|
5795
|
+
}
|
|
5796
|
+
var activeQuotaScope;
|
|
5797
|
+
function activeAppQuotaScope() {
|
|
5798
|
+
return activeQuotaScope;
|
|
5799
|
+
}
|
|
5800
|
+
async function mintInstallationToken(deps) {
|
|
5801
|
+
const [appId, installationId, privateKey] = await Promise.all(APP_VAULT_KEYS.map((k) => deps.fetchSecret(k)));
|
|
5802
|
+
if (!appId || !installationId || !privateKey) {
|
|
5803
|
+
const missing = APP_VAULT_KEYS.filter((_, i) => ![appId, installationId, privateKey][i]);
|
|
5804
|
+
if (missing.length === APP_VAULT_KEYS.length) {
|
|
5805
|
+
throw new AppActorError(
|
|
5806
|
+
`app actor: vault denied all App credentials (${APP_VAULT_REPO} tier) \u2014 not an automation host, or no vault access`,
|
|
5807
|
+
"vault-denied"
|
|
5808
|
+
);
|
|
5809
|
+
}
|
|
5810
|
+
throw new AppActorError(
|
|
5811
|
+
`app actor: vault carries a PARTIAL App credential set \u2014 missing ${missing.join(", ")} (${APP_VAULT_REPO} tier); fix the vault. Automation fails closed, no personal-token fallback`,
|
|
5812
|
+
"mint-failed"
|
|
5813
|
+
);
|
|
5814
|
+
}
|
|
5815
|
+
const nowSec = deps.nowSec ?? (() => Math.floor(Date.now() / 1e3));
|
|
5816
|
+
let res;
|
|
5817
|
+
try {
|
|
5818
|
+
const jwt = mintAppJwt(appId.trim(), privateKey, nowSec());
|
|
5819
|
+
res = await deps.fetch(`https://api.github.com/app/installations/${installationId.trim()}/access_tokens`, {
|
|
5820
|
+
method: "POST",
|
|
5821
|
+
headers: {
|
|
5822
|
+
authorization: `Bearer ${jwt}`,
|
|
5823
|
+
accept: "application/vnd.github+json",
|
|
5824
|
+
"content-type": "application/json",
|
|
5825
|
+
"user-agent": "mmi-cli-app-actor"
|
|
5826
|
+
},
|
|
5827
|
+
body: JSON.stringify({ permissions: APP_POLL_PERMISSIONS })
|
|
5828
|
+
});
|
|
5829
|
+
} catch (e) {
|
|
5830
|
+
if (e instanceof AppActorError) throw e;
|
|
5831
|
+
throw new AppActorError(`app actor: token mint transport failed (${e?.constructor?.name ?? "Error"}) \u2014 automation fails closed, no personal-token fallback`);
|
|
5832
|
+
}
|
|
5833
|
+
if (!res.ok) {
|
|
5834
|
+
throw new AppActorError(`app actor: installation token mint failed (HTTP ${res.status}) \u2014 automation fails closed, no personal-token fallback`);
|
|
5835
|
+
}
|
|
5836
|
+
let body;
|
|
5837
|
+
try {
|
|
5838
|
+
body = await res.json();
|
|
5839
|
+
} catch {
|
|
5840
|
+
throw new AppActorError("app actor: mint response unparsable \u2014 automation fails closed");
|
|
5841
|
+
}
|
|
5842
|
+
if (!body.token || !body.expires_at) {
|
|
5843
|
+
throw new AppActorError("app actor: mint response carried no token \u2014 automation fails closed");
|
|
5844
|
+
}
|
|
5845
|
+
return { token: body.token, expiresAt: body.expires_at, quotaScope: appQuotaScope(appId, installationId, process.env.GITHUB_API_URL) };
|
|
5846
|
+
}
|
|
5847
|
+
async function pollTokenOrUndefined(deps) {
|
|
5848
|
+
try {
|
|
5849
|
+
return (await mintInstallationToken(deps)).token;
|
|
5850
|
+
} catch {
|
|
5851
|
+
return void 0;
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
var APP_POLL_HEADROOM_FRACTION = 0.4;
|
|
5855
|
+
var APP_POLL_RESET_DEFAULT_MS = 36e5;
|
|
5856
|
+
var appPollGuardState = {};
|
|
5857
|
+
function appPollGuardSnapshot() {
|
|
5858
|
+
return { ...appPollGuardState };
|
|
5859
|
+
}
|
|
5860
|
+
function appPollAllowsApp(nowMs) {
|
|
5861
|
+
if (appPollGuardState.fallbackUntilMs !== void 0) {
|
|
5862
|
+
if (nowMs < appPollGuardState.fallbackUntilMs) return false;
|
|
5863
|
+
appPollGuardState = {};
|
|
5864
|
+
}
|
|
5865
|
+
const { remaining, limit } = appPollGuardState;
|
|
5866
|
+
if (remaining === void 0 || limit === void 0 || limit <= 0) return true;
|
|
5867
|
+
return remaining / limit >= APP_POLL_HEADROOM_FRACTION;
|
|
5868
|
+
}
|
|
5869
|
+
function latchAppPollFallback(nowMs) {
|
|
5870
|
+
appPollGuardState.fallbackUntilMs = appPollGuardState.resetAtMs ?? nowMs + APP_POLL_RESET_DEFAULT_MS;
|
|
5871
|
+
}
|
|
5872
|
+
function noteAppPollResponse(headers, rateLimited, nowMs) {
|
|
5873
|
+
if (headers.remaining !== void 0) appPollGuardState.remaining = headers.remaining;
|
|
5874
|
+
if (headers.limit !== void 0) appPollGuardState.limit = headers.limit;
|
|
5875
|
+
if (headers.resetSec !== void 0 && headers.resetSec > 0) appPollGuardState.resetAtMs = headers.resetSec * 1e3;
|
|
5876
|
+
const { remaining, limit } = appPollGuardState;
|
|
5877
|
+
const belowFloor = !rateLimited && remaining !== void 0 && limit !== void 0 && limit > 0 && remaining / limit < APP_POLL_HEADROOM_FRACTION;
|
|
5878
|
+
const headroomUnverifiable = !rateLimited && (headers.remaining === void 0 || headers.limit === void 0);
|
|
5879
|
+
if (rateLimited || belowFloor || headroomUnverifiable && appPollGuardState.resetAtMs !== void 0) {
|
|
5880
|
+
latchAppPollFallback(nowMs);
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
function resetAppPollGuard() {
|
|
5884
|
+
appPollGuardState = {};
|
|
5885
|
+
}
|
|
5886
|
+
var notedIneligibleCommand = false;
|
|
5887
|
+
async function activateAppActor(commandPath3, env, mint, note = (line) => process.stderr.write(`${line}
|
|
5888
|
+
`)) {
|
|
5889
|
+
activeQuotaScope = void 0;
|
|
5890
|
+
const requested = (env[APP_ACTOR_ENV] ?? "").trim();
|
|
5891
|
+
if (!requested) return "personal";
|
|
5892
|
+
if (requested !== "app") {
|
|
5893
|
+
throw new AppActorError(`app actor: unknown ${APP_ACTOR_ENV} value \u2014 only 'app' is supported; refusing to guess an identity`);
|
|
5894
|
+
}
|
|
5895
|
+
if (!APP_ELIGIBLE_COMMANDS.has(commandPath3)) {
|
|
5896
|
+
if (!notedIneligibleCommand) {
|
|
5897
|
+
notedIneligibleCommand = true;
|
|
5898
|
+
note(`app actor: '${commandPath3}' is personal-only (viewer/@me/authorship) \u2014 running on the personal token despite ${APP_ACTOR_ENV}=app`);
|
|
5899
|
+
}
|
|
5900
|
+
return "personal";
|
|
5901
|
+
}
|
|
5902
|
+
delete env.GH_TOKEN;
|
|
5903
|
+
delete env.GITHUB_TOKEN;
|
|
5904
|
+
const minted = await mint();
|
|
5905
|
+
env.GH_TOKEN = minted.token;
|
|
5906
|
+
activeQuotaScope = minted.quotaScope;
|
|
5907
|
+
return "app";
|
|
5908
|
+
}
|
|
5909
|
+
|
|
5734
5910
|
// src/merge-ci-policy.ts
|
|
5735
5911
|
function resolveMergeCiPolicy(input) {
|
|
5736
5912
|
if (input.registryCi === "none") {
|
|
@@ -6111,12 +6287,19 @@ function isReleaseShaDeployEnumerationCandidate(row, nonDeployEvents) {
|
|
|
6111
6287
|
}
|
|
6112
6288
|
|
|
6113
6289
|
// src/pr-checks-rest.ts
|
|
6290
|
+
init_github_quota();
|
|
6114
6291
|
var REST_GH_TIMEOUT_MS = 2e4;
|
|
6115
6292
|
var pollTokenProvider;
|
|
6116
6293
|
var pollTokenOnce;
|
|
6294
|
+
var lastPollSource;
|
|
6295
|
+
function lastRestPollSource() {
|
|
6296
|
+
return lastPollSource;
|
|
6297
|
+
}
|
|
6117
6298
|
function setPollTokenProvider(provider) {
|
|
6118
6299
|
pollTokenProvider = provider;
|
|
6119
6300
|
pollTokenOnce = void 0;
|
|
6301
|
+
resetAppPollGuard();
|
|
6302
|
+
lastPollSource = void 0;
|
|
6120
6303
|
}
|
|
6121
6304
|
async function pollToken() {
|
|
6122
6305
|
if (!pollTokenProvider) return void 0;
|
|
@@ -6137,24 +6320,60 @@ function parseIncludedOutput(out) {
|
|
|
6137
6320
|
});
|
|
6138
6321
|
return { body, status, etag, pages };
|
|
6139
6322
|
}
|
|
6323
|
+
function parseRateLimitHeaders(out) {
|
|
6324
|
+
const first = (name) => {
|
|
6325
|
+
const match = new RegExp(`^x-ratelimit-${name}:\\s*(\\d+)\\s*$`, "im").exec(out);
|
|
6326
|
+
return match ? Number(match[1]) : void 0;
|
|
6327
|
+
};
|
|
6328
|
+
return { remaining: first("remaining"), limit: first("limit"), resetSec: first("reset") };
|
|
6329
|
+
}
|
|
6140
6330
|
var etagCache = /* @__PURE__ */ new Map();
|
|
6331
|
+
function appPoolDetail() {
|
|
6332
|
+
const guard = appPollGuardSnapshot();
|
|
6333
|
+
const counts = guard.remaining !== void 0 && guard.limit !== void 0 ? `App pool at ${guard.remaining}/${guard.limit}` : "App pool headroom unverified";
|
|
6334
|
+
return guard.resetAtMs !== void 0 ? `${counts}, resets ${new Date(guard.resetAtMs).toISOString()}` : counts;
|
|
6335
|
+
}
|
|
6141
6336
|
async function defaultGhApi(args) {
|
|
6142
|
-
const
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
})
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6337
|
+
const appToken = await pollToken();
|
|
6338
|
+
let source = appToken && appPollAllowsApp(Date.now()) ? "app" : "user";
|
|
6339
|
+
if (lastPollSource !== void 0 && lastPollSource !== source) {
|
|
6340
|
+
console.warn(source === "user" ? `poll credential: ${appPoolDetail()} \u2014 below the ${APP_POLL_HEADROOM_FRACTION * 100}% App-poll headroom floor (#7843); REST poll reads switch to the ambient user token until the App pool resets` : "poll credential: App pool reset passed \u2014 REST poll reads return to the App installation token (#7843)");
|
|
6341
|
+
}
|
|
6342
|
+
for (; ; ) {
|
|
6343
|
+
const key = `${source}\0${args.join("\0")}`;
|
|
6344
|
+
const cached = etagCache.get(key);
|
|
6345
|
+
let stdout;
|
|
6346
|
+
try {
|
|
6347
|
+
({ stdout } = await execFileP("gh", ["api", "--include", ...cached ? ["-H", `If-None-Match: ${cached.etag}`] : [], ...args], {
|
|
6348
|
+
timeout: REST_GH_TIMEOUT_MS,
|
|
6349
|
+
// Only on the App source: passing `env` at all replaces the inherited environment wholesale,
|
|
6350
|
+
// and `gh` needs the rest of it (PATH, GH_HOST, config home). Ambient rides the inherited env.
|
|
6351
|
+
...source === "app" && appToken ? { env: { ...process.env, GH_TOKEN: appToken } } : {}
|
|
6352
|
+
}));
|
|
6353
|
+
} catch (e) {
|
|
6354
|
+
const out = e.stdout;
|
|
6355
|
+
const errOut = typeof out === "string" ? out : void 0;
|
|
6356
|
+
const status = errOut !== void 0 ? parseIncludedOutput(errOut).status : void 0;
|
|
6357
|
+
if (cached && status === 304) {
|
|
6358
|
+
lastPollSource = source;
|
|
6359
|
+
return cached.body;
|
|
6360
|
+
}
|
|
6361
|
+
if (source === "app" && appToken && (status === 403 || status === 429)) {
|
|
6362
|
+
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)`);
|
|
6364
|
+
lastPollSource = "user";
|
|
6365
|
+
source = "user";
|
|
6366
|
+
continue;
|
|
6367
|
+
}
|
|
6368
|
+
throw e;
|
|
6369
|
+
}
|
|
6370
|
+
const { body, etag, pages } = parseIncludedOutput(stdout);
|
|
6371
|
+
if (source === "app") noteAppPollResponse(parseRateLimitHeaders(stdout), false, Date.now());
|
|
6372
|
+
lastPollSource = source;
|
|
6373
|
+
if (etag && pages === 1) etagCache.set(key, { etag, body });
|
|
6374
|
+
else etagCache.delete(key);
|
|
6375
|
+
return body;
|
|
6153
6376
|
}
|
|
6154
|
-
const { body, etag, pages } = parseIncludedOutput(stdout);
|
|
6155
|
-
if (etag && pages === 1) etagCache.set(key, { etag, body });
|
|
6156
|
-
else etagCache.delete(key);
|
|
6157
|
-
return body;
|
|
6158
6377
|
}
|
|
6159
6378
|
function classifyCheckRun(run) {
|
|
6160
6379
|
if (run.status !== "completed") return "pending";
|
|
@@ -6300,12 +6519,20 @@ async function ambientGhApi(args) {
|
|
|
6300
6519
|
const { stdout } = await execFileP("gh", ["api", ...args], { timeout: REST_GH_TIMEOUT_MS });
|
|
6301
6520
|
return stdout;
|
|
6302
6521
|
}
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
const
|
|
6307
|
-
|
|
6308
|
-
|
|
6522
|
+
var REPO_SETTINGS_RATE_LIMIT_DELAYS_MS = [15e3, 45e3];
|
|
6523
|
+
async function fetchRestRepoMergeSettings(repo, gh2 = defaultGhApi, ambientGh = ambientGhApi, options = {}) {
|
|
6524
|
+
const delays = options.delaysMs ?? REPO_SETTINGS_RATE_LIMIT_DELAYS_MS;
|
|
6525
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6526
|
+
for (let attempt = 0; ; attempt++) {
|
|
6527
|
+
const primary = await readRepoMergeSettingsOnce(repo, gh2);
|
|
6528
|
+
if (primary.proven) return primary;
|
|
6529
|
+
const fallback = await readRepoMergeSettingsOnce(repo, ambientGh);
|
|
6530
|
+
if (fallback.proven) return fallback;
|
|
6531
|
+
const reason = `${primary.reason}; ambient fallback: ${fallback.reason}`;
|
|
6532
|
+
if (attempt >= delays.length || !isRateLimitText(reason)) return { proven: false, reason };
|
|
6533
|
+
console.warn(`pr merge: repository settings read was rate-limited; retrying in ${Math.round(delays[attempt] / 1e3)}s`);
|
|
6534
|
+
await sleep2(delays[attempt]);
|
|
6535
|
+
}
|
|
6309
6536
|
}
|
|
6310
6537
|
var PR_SNAPSHOT_READ_RETRIES = 3;
|
|
6311
6538
|
var PR_SNAPSHOT_READ_DELAY_MS = 2e3;
|
|
@@ -7496,9 +7723,9 @@ function extractWorkflowCrons(yamlText) {
|
|
|
7496
7723
|
function workflowEntry(repo, workflowPath, yamlText) {
|
|
7497
7724
|
const crons = extractWorkflowCrons(yamlText);
|
|
7498
7725
|
if (!crons.length) return null;
|
|
7499
|
-
const
|
|
7726
|
+
const basename4 = workflowPath.split("/").pop() ?? workflowPath;
|
|
7500
7727
|
return {
|
|
7501
|
-
name: `${repo}/${
|
|
7728
|
+
name: `${repo}/${basename4.replace(/\.ya?ml$/, "")}`,
|
|
7502
7729
|
cadence: crons.join(" + "),
|
|
7503
7730
|
executor: "github-actions",
|
|
7504
7731
|
llm: llmFromHeader(yamlText),
|
|
@@ -7827,8 +8054,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
7827
8054
|
const crons = extractWorkflowCrons(yamlText);
|
|
7828
8055
|
const header = parseScheduleHeader(yamlText);
|
|
7829
8056
|
if (!crons.length && !header.schedule) return null;
|
|
7830
|
-
const
|
|
7831
|
-
const expectedId = `${repo}/${
|
|
8057
|
+
const basename4 = workflowPath.split("/").pop() ?? workflowPath;
|
|
8058
|
+
const expectedId = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
|
|
7832
8059
|
if (isHarbourUnmanaged(yamlText)) return null;
|
|
7833
8060
|
const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
|
|
7834
8061
|
if (missing.length) {
|
|
@@ -8061,6 +8288,28 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
|
|
|
8061
8288
|
return { ok: false, status: 0, body: null, error: e.message, unreachable: "transport" };
|
|
8062
8289
|
}
|
|
8063
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
|
+
}
|
|
8064
8313
|
async function edgeCertificateForStage(slug, stage, deps, ensure = false) {
|
|
8065
8314
|
if (!deps.baseUrl) throw new Error("Hub API URL unavailable");
|
|
8066
8315
|
const token = await deps.token();
|
|
@@ -9247,7 +9496,7 @@ init_clean_exit();
|
|
|
9247
9496
|
var import_promises = require("node:fs/promises");
|
|
9248
9497
|
var import_node_os4 = require("node:os");
|
|
9249
9498
|
var import_node_path9 = require("node:path");
|
|
9250
|
-
var
|
|
9499
|
+
var import_node_crypto5 = require("node:crypto");
|
|
9251
9500
|
|
|
9252
9501
|
// src/board-priority.ts
|
|
9253
9502
|
var BOARD_PRIORITY_NAMES = ["Urgent", "High", "Medium", "Low"];
|
|
@@ -9334,7 +9583,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
9334
9583
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
9335
9584
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
9336
9585
|
const dir = deps.dir ?? (0, import_node_os4.tmpdir)();
|
|
9337
|
-
const file = (0, import_node_path9.join)(dir, `mmi-gh-body-${process.pid}-${(0,
|
|
9586
|
+
const file = (0, import_node_path9.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.md`);
|
|
9338
9587
|
await ensureDir((0, import_node_path9.dirname)(file), { recursive: true }).catch(() => {
|
|
9339
9588
|
});
|
|
9340
9589
|
await write(file, args[i + 1], "utf8");
|
|
@@ -9664,7 +9913,7 @@ async function defaultRestCreatePr(exec2, input, deps = {}) {
|
|
|
9664
9913
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
9665
9914
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
9666
9915
|
const dir = deps.dir ?? (0, import_node_os4.tmpdir)();
|
|
9667
|
-
const file = (0, import_node_path9.join)(dir, `mmi-rest-pr-${process.pid}-${(0,
|
|
9916
|
+
const file = (0, import_node_path9.join)(dir, `mmi-rest-pr-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.json`);
|
|
9668
9917
|
const payload = {
|
|
9669
9918
|
title: input.title,
|
|
9670
9919
|
head: input.head,
|
|
@@ -11064,7 +11313,7 @@ function findDuplicateLesson(source, openLessons) {
|
|
|
11064
11313
|
}
|
|
11065
11314
|
|
|
11066
11315
|
// src/session-identity.ts
|
|
11067
|
-
var
|
|
11316
|
+
var import_node_crypto6 = require("node:crypto");
|
|
11068
11317
|
var import_node_os8 = require("node:os");
|
|
11069
11318
|
|
|
11070
11319
|
// src/plugin-guard-io.ts
|
|
@@ -11948,7 +12197,7 @@ function readSessionIdSource(env) {
|
|
|
11948
12197
|
var synthesizedSessionId;
|
|
11949
12198
|
function fallbackSessionId(surface) {
|
|
11950
12199
|
if (!synthesizedSessionId) {
|
|
11951
|
-
synthesizedSessionId = `${SYNTH_SESSION_PREFIX}${surface}-${process.pid}-${(0,
|
|
12200
|
+
synthesizedSessionId = `${SYNTH_SESSION_PREFIX}${surface}-${process.pid}-${(0, import_node_crypto6.randomBytes)(3).toString("hex")}`;
|
|
11952
12201
|
}
|
|
11953
12202
|
return synthesizedSessionId;
|
|
11954
12203
|
}
|
|
@@ -14646,7 +14895,7 @@ async function propagateMayAutoMerge(repo, repoClass, rest) {
|
|
|
14646
14895
|
}
|
|
14647
14896
|
|
|
14648
14897
|
// src/bootstrap-apply.ts
|
|
14649
|
-
var
|
|
14898
|
+
var import_node_crypto7 = require("node:crypto");
|
|
14650
14899
|
|
|
14651
14900
|
// src/bootstrap-seeds.ts
|
|
14652
14901
|
var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
@@ -15634,8 +15883,11 @@ function edgeDomainsByStage(meta) {
|
|
|
15634
15883
|
}
|
|
15635
15884
|
return out;
|
|
15636
15885
|
}
|
|
15637
|
-
function inferEdgeDomainsForWrite(slug) {
|
|
15886
|
+
function inferEdgeDomainsForWrite(slug, suite) {
|
|
15638
15887
|
const subdomain = defaultSubdomain(slug);
|
|
15888
|
+
if (suite === "mm-work" && slug !== suite) {
|
|
15889
|
+
return { dev: `dev.${subdomain}.mutmut.work`, main: `${subdomain}.mutmut.work` };
|
|
15890
|
+
}
|
|
15639
15891
|
return {
|
|
15640
15892
|
dev: `dev.${subdomain}.mutatismutandis.co`,
|
|
15641
15893
|
rc: `rc.${subdomain}.mutatismutandis.co`,
|
|
@@ -15877,7 +16129,7 @@ function planSeedAction(seed, exists) {
|
|
|
15877
16129
|
return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
|
|
15878
16130
|
}
|
|
15879
16131
|
var GATE_RENDER_NOTE = "# Rendered from the registry gate config by mmi-cli devops bootstrap apply <repo> --only gate.yml --from-config; edit the config, not this file.\n";
|
|
15880
|
-
var gateHash = (body) => (0,
|
|
16132
|
+
var gateHash = (body) => (0, import_node_crypto7.createHash)("sha256").update(body, "utf8").digest("hex");
|
|
15881
16133
|
function parseGateRenderStamp(content) {
|
|
15882
16134
|
const match = /^# mmi-gate-render: sha256=([a-f0-9]{64})\n/.exec(content);
|
|
15883
16135
|
return match ? { hash: match[1], body: content.slice(match[0].length) } : null;
|
|
@@ -16009,10 +16261,11 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
16009
16261
|
}
|
|
16010
16262
|
const deployModel = inferDeployModelForWrite({ ...shape, projectType }, parsedRepo.fullName);
|
|
16011
16263
|
const releaseTrack = (isReleaseTrack(options.releaseTrack) ? options.releaseTrack : void 0) ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0);
|
|
16012
|
-
const
|
|
16013
|
-
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) => {
|
|
16014
16267
|
const stage = branch === "development" ? "dev" : branch;
|
|
16015
|
-
return [stage, inferredDomains[stage]];
|
|
16268
|
+
return inferredDomains[stage] ? [[stage, inferredDomains[stage]]] : [];
|
|
16016
16269
|
}));
|
|
16017
16270
|
const num = (v) => {
|
|
16018
16271
|
if (v == null || v === "") return void 0;
|
|
@@ -16036,6 +16289,7 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
16036
16289
|
// Identity. name/division default off the repo name when the skill didn't pass them.
|
|
16037
16290
|
name: vars.NAME || parsedRepo.name,
|
|
16038
16291
|
division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
|
|
16292
|
+
suite: vars.SUITE || void 0,
|
|
16039
16293
|
repos: [`${parsedRepo.owner}/${slug}`],
|
|
16040
16294
|
branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
|
|
16041
16295
|
class: cls,
|
|
@@ -16135,6 +16389,11 @@ function decideSeedPrAction(openPrs) {
|
|
|
16135
16389
|
}
|
|
16136
16390
|
return { action: "create" };
|
|
16137
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
|
+
}
|
|
16138
16397
|
function decideSeedDelivery(branchRules) {
|
|
16139
16398
|
const rules = Array.isArray(branchRules) ? branchRules : [];
|
|
16140
16399
|
const types = new Set(
|
|
@@ -17345,10 +17604,10 @@ var rollout_plan_default = {
|
|
|
17345
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)."
|
|
17346
17605
|
},
|
|
17347
17606
|
baseline: {
|
|
17348
|
-
version: "4.5.
|
|
17349
|
-
tag: "v4.5.
|
|
17350
|
-
commit: "
|
|
17351
|
-
npm: "@mutmutco/cli@4.5.
|
|
17607
|
+
version: "4.5.100",
|
|
17608
|
+
tag: "v4.5.100",
|
|
17609
|
+
commit: "ba367dd0d814",
|
|
17610
|
+
npm: "@mutmutco/cli@4.5.100"
|
|
17352
17611
|
},
|
|
17353
17612
|
exitCriterion: "fleet-n-of-n",
|
|
17354
17613
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17365,14 +17624,14 @@ var rollout_plan_default = {
|
|
|
17365
17624
|
repo: "mutmutco/mmi-hub",
|
|
17366
17625
|
role: "canary",
|
|
17367
17626
|
schedule: "train",
|
|
17368
|
-
v3Target: "v4.5.
|
|
17627
|
+
v3Target: "v4.5.100"
|
|
17369
17628
|
}
|
|
17370
17629
|
],
|
|
17371
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.",
|
|
17372
17631
|
rollback: {
|
|
17373
17632
|
independent: true,
|
|
17374
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17375
|
-
v3Target: "v4.5.
|
|
17633
|
+
mechanism: "npm dist-tag latest -> 4.5.100 and redeploy the Hub Lambda from tag v4.5.100 (ba367dd0d814); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17634
|
+
v3Target: "v4.5.100 (@mutmutco/cli@4.5.100, tag commit ba367dd0d814 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17376
17635
|
}
|
|
17377
17636
|
},
|
|
17378
17637
|
{
|
|
@@ -19730,7 +19989,9 @@ async function correlateRun(deps, args) {
|
|
|
19730
19989
|
return Number.isFinite(created) && created < threshold;
|
|
19731
19990
|
});
|
|
19732
19991
|
const tokens2 = args.mode === "dispatch" ? args.titleIncludes : void 0;
|
|
19733
|
-
const
|
|
19992
|
+
const fieldsFrom = args.mode === "dispatch" ? args.titleFieldsFrom : void 0;
|
|
19993
|
+
const titleHas = (title) => fieldsFrom === void 0 ? tokens2.every((t) => title.includes(t)) : tokens2.every((t, i) => title.split(" ")[fieldsFrom + i] === t);
|
|
19994
|
+
const titleMatch = tokens2 && tokens2.length ? candidates.find((c) => titleHas(c.row.displayTitle ?? "")) : void 0;
|
|
19734
19995
|
const match = titleMatch ?? (!("requireTitleMatch" in args && args.requireTitleMatch) && (args.mode === "workflow" || windowProvenWhole) ? candidates[0] : void 0);
|
|
19735
19996
|
if (match) return { runId: match.row.databaseId, runUrl: match.row.url, read: "found" };
|
|
19736
19997
|
if (!windowProvenWhole) truncatedWindow = true;
|
|
@@ -19756,7 +20017,7 @@ function correlateGetPublishRun(deps, since, product, mode) {
|
|
|
19756
20017
|
return correlateRun(deps, { workflow: "get-publish.yml", since, mode: "dispatch", titleIncludes: [product, ...mode ? [mode] : []], ...mode ? { requireTitleMatch: true } : {} });
|
|
19757
20018
|
}
|
|
19758
20019
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
19759
|
-
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
20020
|
+
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes, titleFieldsFrom: 2, ...titleIncludes?.length ? { requireTitleMatch: true } : {} });
|
|
19760
20021
|
}
|
|
19761
20022
|
function correlateReconcileRun(deps, since, titleIncludes) {
|
|
19762
20023
|
return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
|
|
@@ -21562,7 +21823,7 @@ async function runTenantControl(deps, options) {
|
|
|
21562
21823
|
const watch = options.watch ?? tenantControlWatches(action);
|
|
21563
21824
|
const base = { command: "tenant-control", repo, stage, action };
|
|
21564
21825
|
const since = (deps.now ?? Date.now)();
|
|
21565
|
-
const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
|
|
21826
|
+
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 });
|
|
21566
21827
|
if (!d.ok) {
|
|
21567
21828
|
const transport = d.category === "transport-failed";
|
|
21568
21829
|
return {
|
|
@@ -22778,7 +23039,7 @@ function checkLineEndings(probe) {
|
|
|
22778
23039
|
// src/release-ledger.ts
|
|
22779
23040
|
var import_node_fs22 = require("node:fs");
|
|
22780
23041
|
var import_node_path22 = require("node:path");
|
|
22781
|
-
var
|
|
23042
|
+
var import_node_crypto8 = require("node:crypto");
|
|
22782
23043
|
|
|
22783
23044
|
// src/file-lock.ts
|
|
22784
23045
|
var import_promises2 = require("node:fs/promises");
|
|
@@ -23140,7 +23401,7 @@ function releaseLedgerPath(cwd) {
|
|
|
23140
23401
|
}
|
|
23141
23402
|
function atomicWriteJson(path2, ledger) {
|
|
23142
23403
|
(0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(path2), { recursive: true });
|
|
23143
|
-
const tmp = `${path2}.tmp-${process.pid}-${(0,
|
|
23404
|
+
const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto8.randomBytes)(4).toString("hex")}`;
|
|
23144
23405
|
(0, import_node_fs22.writeFileSync)(tmp, `${JSON.stringify(ledger, null, 2)}
|
|
23145
23406
|
`, "utf8");
|
|
23146
23407
|
(0, import_node_fs22.renameSync)(tmp, path2);
|
|
@@ -29869,150 +30130,6 @@ function renderPluginCachePlan(plan, applied) {
|
|
|
29869
30130
|
return lines2.join("\n");
|
|
29870
30131
|
}
|
|
29871
30132
|
|
|
29872
|
-
// ../shared/github-quota-scope.ts
|
|
29873
|
-
var import_node_crypto7 = require("node:crypto");
|
|
29874
|
-
function appQuotaScope(appId, installationId, origin = "https://api.github.com", resource = "graphql") {
|
|
29875
|
-
return `github-app-v1:${(0, import_node_crypto7.createHash)("sha256").update(JSON.stringify([new URL(origin).origin, appId.trim(), installationId.trim(), resource])).digest("hex")}`;
|
|
29876
|
-
}
|
|
29877
|
-
|
|
29878
|
-
// src/app-actor.ts
|
|
29879
|
-
var import_node_crypto8 = require("node:crypto");
|
|
29880
|
-
var APP_ACTOR_ENV = "MMI_ACTOR";
|
|
29881
|
-
var APP_VAULT_REPO = "mutmutco/MMI-Hub";
|
|
29882
|
-
var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
|
|
29883
|
-
var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
|
|
29884
|
-
"pr checks-wait",
|
|
29885
|
-
"pr view",
|
|
29886
|
-
"issue list",
|
|
29887
|
-
"issue view",
|
|
29888
|
-
"api get"
|
|
29889
|
-
]);
|
|
29890
|
-
var APP_POLL_PERMISSIONS = {
|
|
29891
|
-
metadata: "read",
|
|
29892
|
-
contents: "read",
|
|
29893
|
-
pull_requests: "read",
|
|
29894
|
-
// #7185: without issues:read GraphQL silently drops pr view's closingIssuesReferences.
|
|
29895
|
-
issues: "read",
|
|
29896
|
-
checks: "read",
|
|
29897
|
-
statuses: "read",
|
|
29898
|
-
actions: "read"
|
|
29899
|
-
};
|
|
29900
|
-
var APP_JWT_BACKDATE_S = 60;
|
|
29901
|
-
var APP_JWT_TTL_S = 9 * 60;
|
|
29902
|
-
var AppActorError = class extends Error {
|
|
29903
|
-
constructor(message2, code = "mint-failed") {
|
|
29904
|
-
super(message2);
|
|
29905
|
-
this.code = code;
|
|
29906
|
-
}
|
|
29907
|
-
code;
|
|
29908
|
-
};
|
|
29909
|
-
function commandPathOf(cmd) {
|
|
29910
|
-
const parts = [];
|
|
29911
|
-
let cursor = cmd;
|
|
29912
|
-
while (cursor && cursor.parent) {
|
|
29913
|
-
parts.unshift(cursor.name());
|
|
29914
|
-
cursor = cursor.parent;
|
|
29915
|
-
}
|
|
29916
|
-
return parts.join(" ");
|
|
29917
|
-
}
|
|
29918
|
-
function b64url(input) {
|
|
29919
|
-
return Buffer.from(input).toString("base64url");
|
|
29920
|
-
}
|
|
29921
|
-
function mintAppJwt(appId, privateKeyPem, nowSec) {
|
|
29922
|
-
if (!Number.isFinite(nowSec)) throw new AppActorError("app actor: invalid clock reading for JWT mint");
|
|
29923
|
-
const now = Math.floor(nowSec);
|
|
29924
|
-
const header = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
29925
|
-
const payload = b64url(JSON.stringify({
|
|
29926
|
-
iat: now - APP_JWT_BACKDATE_S,
|
|
29927
|
-
exp: now + APP_JWT_TTL_S,
|
|
29928
|
-
iss: appId
|
|
29929
|
-
}));
|
|
29930
|
-
const signer = (0, import_node_crypto8.createSign)("RSA-SHA256");
|
|
29931
|
-
signer.update(`${header}.${payload}`);
|
|
29932
|
-
return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
|
|
29933
|
-
}
|
|
29934
|
-
var activeQuotaScope;
|
|
29935
|
-
function activeAppQuotaScope() {
|
|
29936
|
-
return activeQuotaScope;
|
|
29937
|
-
}
|
|
29938
|
-
async function mintInstallationToken(deps) {
|
|
29939
|
-
const [appId, installationId, privateKey] = await Promise.all(APP_VAULT_KEYS.map((k) => deps.fetchSecret(k)));
|
|
29940
|
-
if (!appId || !installationId || !privateKey) {
|
|
29941
|
-
const missing = APP_VAULT_KEYS.filter((_, i) => ![appId, installationId, privateKey][i]);
|
|
29942
|
-
if (missing.length === APP_VAULT_KEYS.length) {
|
|
29943
|
-
throw new AppActorError(
|
|
29944
|
-
`app actor: vault denied all App credentials (${APP_VAULT_REPO} tier) \u2014 not an automation host, or no vault access`,
|
|
29945
|
-
"vault-denied"
|
|
29946
|
-
);
|
|
29947
|
-
}
|
|
29948
|
-
throw new AppActorError(
|
|
29949
|
-
`app actor: vault carries a PARTIAL App credential set \u2014 missing ${missing.join(", ")} (${APP_VAULT_REPO} tier); fix the vault. Automation fails closed, no personal-token fallback`,
|
|
29950
|
-
"mint-failed"
|
|
29951
|
-
);
|
|
29952
|
-
}
|
|
29953
|
-
const nowSec = deps.nowSec ?? (() => Math.floor(Date.now() / 1e3));
|
|
29954
|
-
let res;
|
|
29955
|
-
try {
|
|
29956
|
-
const jwt = mintAppJwt(appId.trim(), privateKey, nowSec());
|
|
29957
|
-
res = await deps.fetch(`https://api.github.com/app/installations/${installationId.trim()}/access_tokens`, {
|
|
29958
|
-
method: "POST",
|
|
29959
|
-
headers: {
|
|
29960
|
-
authorization: `Bearer ${jwt}`,
|
|
29961
|
-
accept: "application/vnd.github+json",
|
|
29962
|
-
"content-type": "application/json",
|
|
29963
|
-
"user-agent": "mmi-cli-app-actor"
|
|
29964
|
-
},
|
|
29965
|
-
body: JSON.stringify({ permissions: APP_POLL_PERMISSIONS })
|
|
29966
|
-
});
|
|
29967
|
-
} catch (e) {
|
|
29968
|
-
if (e instanceof AppActorError) throw e;
|
|
29969
|
-
throw new AppActorError(`app actor: token mint transport failed (${e?.constructor?.name ?? "Error"}) \u2014 automation fails closed, no personal-token fallback`);
|
|
29970
|
-
}
|
|
29971
|
-
if (!res.ok) {
|
|
29972
|
-
throw new AppActorError(`app actor: installation token mint failed (HTTP ${res.status}) \u2014 automation fails closed, no personal-token fallback`);
|
|
29973
|
-
}
|
|
29974
|
-
let body;
|
|
29975
|
-
try {
|
|
29976
|
-
body = await res.json();
|
|
29977
|
-
} catch {
|
|
29978
|
-
throw new AppActorError("app actor: mint response unparsable \u2014 automation fails closed");
|
|
29979
|
-
}
|
|
29980
|
-
if (!body.token || !body.expires_at) {
|
|
29981
|
-
throw new AppActorError("app actor: mint response carried no token \u2014 automation fails closed");
|
|
29982
|
-
}
|
|
29983
|
-
return { token: body.token, expiresAt: body.expires_at, quotaScope: appQuotaScope(appId, installationId, process.env.GITHUB_API_URL) };
|
|
29984
|
-
}
|
|
29985
|
-
async function pollTokenOrUndefined(deps) {
|
|
29986
|
-
try {
|
|
29987
|
-
return (await mintInstallationToken(deps)).token;
|
|
29988
|
-
} catch {
|
|
29989
|
-
return void 0;
|
|
29990
|
-
}
|
|
29991
|
-
}
|
|
29992
|
-
var notedIneligibleCommand = false;
|
|
29993
|
-
async function activateAppActor(commandPath3, env, mint, note = (line) => process.stderr.write(`${line}
|
|
29994
|
-
`)) {
|
|
29995
|
-
activeQuotaScope = void 0;
|
|
29996
|
-
const requested = (env[APP_ACTOR_ENV] ?? "").trim();
|
|
29997
|
-
if (!requested) return "personal";
|
|
29998
|
-
if (requested !== "app") {
|
|
29999
|
-
throw new AppActorError(`app actor: unknown ${APP_ACTOR_ENV} value \u2014 only 'app' is supported; refusing to guess an identity`);
|
|
30000
|
-
}
|
|
30001
|
-
if (!APP_ELIGIBLE_COMMANDS.has(commandPath3)) {
|
|
30002
|
-
if (!notedIneligibleCommand) {
|
|
30003
|
-
notedIneligibleCommand = true;
|
|
30004
|
-
note(`app actor: '${commandPath3}' is personal-only (viewer/@me/authorship) \u2014 running on the personal token despite ${APP_ACTOR_ENV}=app`);
|
|
30005
|
-
}
|
|
30006
|
-
return "personal";
|
|
30007
|
-
}
|
|
30008
|
-
delete env.GH_TOKEN;
|
|
30009
|
-
delete env.GITHUB_TOKEN;
|
|
30010
|
-
const minted = await mint();
|
|
30011
|
-
env.GH_TOKEN = minted.token;
|
|
30012
|
-
activeQuotaScope = minted.quotaScope;
|
|
30013
|
-
return "app";
|
|
30014
|
-
}
|
|
30015
|
-
|
|
30016
30133
|
// src/board-commands.ts
|
|
30017
30134
|
init_cli_shared();
|
|
30018
30135
|
|
|
@@ -33805,6 +33922,28 @@ function edgeCertificateChecks(results) {
|
|
|
33805
33922
|
}];
|
|
33806
33923
|
});
|
|
33807
33924
|
}
|
|
33925
|
+
function edgeDnsChecks(results, repo) {
|
|
33926
|
+
return results.flatMap(({ stage, status, error }) => {
|
|
33927
|
+
if (!error && status?.required === false) return [];
|
|
33928
|
+
const bad = (status?.hosts ?? []).filter((h) => h.status === "missing" || h.status === "conflict");
|
|
33929
|
+
const ok = !error && status?.complete !== false && bad.length === 0;
|
|
33930
|
+
return [{
|
|
33931
|
+
ok,
|
|
33932
|
+
label: `Cloudflare edge DNS: ${stage}`,
|
|
33933
|
+
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}`
|
|
33934
|
+
}];
|
|
33935
|
+
});
|
|
33936
|
+
}
|
|
33937
|
+
function firstDevDeployCheck(repo, facts, factsRead) {
|
|
33938
|
+
const dev = facts?.stages?.dev;
|
|
33939
|
+
if (!factsRead || dev?.present !== true) return [];
|
|
33940
|
+
const ok = Boolean(dev.deployedAt || dev.deployedTag);
|
|
33941
|
+
return [{
|
|
33942
|
+
ok,
|
|
33943
|
+
label: "first dev deploy happened",
|
|
33944
|
+
detail: ok ? void 0 : `DEPLOY#dev has never deployed \u2014 run: mmi-cli devops runtime tenant redeploy ${repo} dev --watch`
|
|
33945
|
+
}];
|
|
33946
|
+
}
|
|
33808
33947
|
async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
33809
33948
|
const branchesWanted = expectedBranches(repoClass, releaseTrack);
|
|
33810
33949
|
const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
|
|
@@ -33927,6 +34066,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
33927
34066
|
if (check) checks.push(check);
|
|
33928
34067
|
}
|
|
33929
34068
|
checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? []));
|
|
34069
|
+
checks.push(...edgeDnsChecks(deps.edgeDns ?? [], repo));
|
|
33930
34070
|
const portRangeCheck = centralContainerPortRangeCheck(deps.deployModel, deps.projectMeta?.portRange, repo);
|
|
33931
34071
|
if (portRangeCheck) checks.push(portRangeCheck);
|
|
33932
34072
|
checks.push(...deployRowChecks(
|
|
@@ -33937,6 +34077,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
33937
34077
|
deps.deployFactsRead === true,
|
|
33938
34078
|
deps.projectMeta?.suite
|
|
33939
34079
|
));
|
|
34080
|
+
checks.push(...firstDevDeployCheck(repo, deps.deployFacts, deps.deployFactsRead === true));
|
|
33940
34081
|
const readme = await contentText(deps, repo, baseBranch, "README.md");
|
|
33941
34082
|
checks.push({
|
|
33942
34083
|
ok: readme !== null && readme.includes("## Agent context"),
|
|
@@ -34375,6 +34516,13 @@ function registerBootstrapCommands(program3) {
|
|
|
34375
34516
|
return { stage, error: e.message };
|
|
34376
34517
|
}
|
|
34377
34518
|
})),
|
|
34519
|
+
edgeDns: await Promise.all(["dev", "rc", "main"].filter((stage) => deployFacts?.stages?.[stage]?.present).map(async (stage) => {
|
|
34520
|
+
try {
|
|
34521
|
+
return { stage, status: await edgeDnsForStage(slug, stage, reg) };
|
|
34522
|
+
} catch (e) {
|
|
34523
|
+
return { stage, error: e.message };
|
|
34524
|
+
}
|
|
34525
|
+
})),
|
|
34378
34526
|
pushAllowlistOwners: verifyOwners,
|
|
34379
34527
|
pushAllowlistOwnersRead: verifyOwnersRead,
|
|
34380
34528
|
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null,
|
|
@@ -34772,7 +34920,7 @@ function registerBootstrapCommands(program3) {
|
|
|
34772
34920
|
github,
|
|
34773
34921
|
deployFacts: controlDeployFacts,
|
|
34774
34922
|
deployFactsRead: controlDeployFacts !== null,
|
|
34775
|
-
plannedMainDomain: registryMeta?.edgeDomains?.main ?? inferEdgeDomainsForWrite(slug).main
|
|
34923
|
+
plannedMainDomain: registryMeta?.edgeDomains?.main ?? inferEdgeDomainsForWrite(slug, suiteSlug || vars.SUITE).main
|
|
34776
34924
|
});
|
|
34777
34925
|
if (o.box) {
|
|
34778
34926
|
if (applyDeployModel !== "tenant-container" && applyDeployModel !== "solo-container")
|
|
@@ -34793,7 +34941,7 @@ function registerBootstrapCommands(program3) {
|
|
|
34793
34941
|
host: found.box.address,
|
|
34794
34942
|
box: found.box.name,
|
|
34795
34943
|
owners: scan.rows,
|
|
34796
|
-
domains: { ...inferEdgeDomainsForWrite(slug), ...domains, ...boxDomains },
|
|
34944
|
+
domains: { ...inferEdgeDomainsForWrite(slug, suiteSlug || vars.SUITE), ...domains, ...boxDomains },
|
|
34797
34945
|
fileless: applyDeployModel === "tenant-container"
|
|
34798
34946
|
});
|
|
34799
34947
|
controlRows = planned.rows;
|
|
@@ -35224,6 +35372,42 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
35224
35372
|
ddbWrites.push({ slug, action: `deploy-${coord.stage}`, record: payload });
|
|
35225
35373
|
applied.push(`DEPLOY#${coord.stage} ${coord.host}:${coord.port}${coord.domain ? ` ${coord.domain}` : ""}`);
|
|
35226
35374
|
}
|
|
35375
|
+
for (const coord of boxDeployPlan) {
|
|
35376
|
+
try {
|
|
35377
|
+
const dns = await edgeDnsForStage(slug, coord.stage, registryClientDeps(await loadConfig()), true);
|
|
35378
|
+
const bad = (dns.hosts ?? []).filter((h) => h.status === "conflict");
|
|
35379
|
+
applied.push(`edge dns ${coord.stage}: ${dns.required ? (dns.hosts ?? []).map((h) => `${h.host} ${h.status}`).join(", ") : "not required"}`);
|
|
35380
|
+
for (const h of bad) deferredFailures.push(`edge dns ${coord.stage} ${h.host}: ${h.hint}`);
|
|
35381
|
+
} catch (e) {
|
|
35382
|
+
applied.push(`edge dns ${coord.stage} (failed: ${e.message})`);
|
|
35383
|
+
deferredFailures.push(`edge dns ${coord.stage} not ensured: ${e.message}; re-run: mmi-cli devops runtime edge dns ensure ${repo} ${coord.stage}`);
|
|
35384
|
+
}
|
|
35385
|
+
}
|
|
35386
|
+
let seedPrs = [];
|
|
35387
|
+
if (seedPlan.mode === "pr" && seedPlan.branch) {
|
|
35388
|
+
try {
|
|
35389
|
+
seedPrs = JSON.parse((await gh2(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,mergedAt"])).stdout || "[]");
|
|
35390
|
+
} catch {
|
|
35391
|
+
seedPrs = [];
|
|
35392
|
+
}
|
|
35393
|
+
}
|
|
35394
|
+
const firstDeploy = decideFirstDevDeploy({ devRowWritten: boxDeployPlan.some((c) => c.stage === "dev"), seedMode: seedPlan.mode, seedPrs });
|
|
35395
|
+
if (firstDeploy === "defer") {
|
|
35396
|
+
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)");
|
|
35397
|
+
} else if (firstDeploy === "dispatch") {
|
|
35398
|
+
try {
|
|
35399
|
+
const out = await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(["runtime", "tenant", "redeploy", repo, "dev", "--json"])], { timeout: 12e4 });
|
|
35400
|
+
let run = {};
|
|
35401
|
+
try {
|
|
35402
|
+
run = JSON.parse(out.stdout);
|
|
35403
|
+
} catch {
|
|
35404
|
+
}
|
|
35405
|
+
applied.push(`first dev deploy dispatched at development: ${run.runUrl ?? run.dispatch ?? "run not yet correlated"}`);
|
|
35406
|
+
} catch (e) {
|
|
35407
|
+
applied.push(`first dev deploy (failed: ${e.message.split("\n")[0]})`);
|
|
35408
|
+
deferredFailures.push(`first dev deploy not dispatched; run: mmi-cli devops runtime tenant redeploy ${repo} dev --watch`);
|
|
35409
|
+
}
|
|
35410
|
+
}
|
|
35227
35411
|
controlRows = controlRows.map((row) => {
|
|
35228
35412
|
const coord = boxDeployPlan.find((p) => row.target === `DEPLOY#${p.stage}`);
|
|
35229
35413
|
return coord ? {
|
|
@@ -42667,6 +42851,13 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
42667
42851
|
const repo = await requireRepo(repoOption);
|
|
42668
42852
|
const { client, throttledPaths } = withRateLimitRetry(withAppClaimMarkerRead(deps.client ?? defaultGitHubClient(), deps.appClient), deps);
|
|
42669
42853
|
const actor = deps.actor ?? describeSessionIdentity();
|
|
42854
|
+
let viewerLogin = deps.viewerLogin;
|
|
42855
|
+
if (!viewerLogin) {
|
|
42856
|
+
try {
|
|
42857
|
+
viewerLogin = (await client.rest("GET", "user"))?.login;
|
|
42858
|
+
} catch {
|
|
42859
|
+
}
|
|
42860
|
+
}
|
|
42670
42861
|
const source = deps.actorSource ?? (deps.actor ? "the caller-supplied identity" : readSessionIdSource(process.env) ?? "a synthesized fallback");
|
|
42671
42862
|
const requester = `requesting lane: ${actor.surface}/${actor.session}@${actor.host} (session id from ${source})`;
|
|
42672
42863
|
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
@@ -42680,7 +42871,7 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
42680
42871
|
}
|
|
42681
42872
|
}
|
|
42682
42873
|
if (state?.toLowerCase() === "closed") continue;
|
|
42683
|
-
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
42874
|
+
const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin);
|
|
42684
42875
|
if (!contest.contested) continue;
|
|
42685
42876
|
const ref = `${repo}#${number}`;
|
|
42686
42877
|
const commentsPath = `repos/${repo}/issues/${number}/comments`;
|
|
@@ -44459,6 +44650,9 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
44459
44650
|
|
|
44460
44651
|
// src/worktree-residue-sweep.ts
|
|
44461
44652
|
var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
|
|
44653
|
+
function canonicalWorktreeRoot(primary) {
|
|
44654
|
+
return (0, import_node_path41.join)((0, import_node_path41.dirname)(primary), "mmi-worktrees", (0, import_node_path41.basename)(primary));
|
|
44655
|
+
}
|
|
44462
44656
|
function norm(p) {
|
|
44463
44657
|
const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
44464
44658
|
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
@@ -44527,16 +44721,14 @@ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
|
|
|
44527
44721
|
}
|
|
44528
44722
|
}
|
|
44529
44723
|
if (recorded.length) writePaths(file, remaining, deps);
|
|
44530
|
-
const roots = /* @__PURE__ */ new Set();
|
|
44531
|
-
for (const p of [...registered.slice(1), ...recorded]) roots.add((0, import_node_path41.dirname)(p));
|
|
44532
44724
|
const keptKeys = new Set(remaining.map(norm));
|
|
44533
|
-
|
|
44534
|
-
|
|
44535
|
-
|
|
44725
|
+
const root = registered[0] ? canonicalWorktreeRoot(registered[0]) : void 0;
|
|
44726
|
+
if (root && isDir(root) && !live.has(norm(root))) {
|
|
44727
|
+
report.root = root;
|
|
44728
|
+
let children = [];
|
|
44536
44729
|
try {
|
|
44537
44730
|
children = readDir(root);
|
|
44538
44731
|
} catch {
|
|
44539
|
-
continue;
|
|
44540
44732
|
}
|
|
44541
44733
|
for (const name of children) {
|
|
44542
44734
|
const child2 = (0, import_node_path41.join)(root, name);
|
|
@@ -44565,10 +44757,13 @@ function renderResidueSweepLines(report) {
|
|
|
44565
44757
|
if (!report) return [];
|
|
44566
44758
|
const lines2 = [];
|
|
44567
44759
|
const cleaned = report.swept.length + report.emptyRemoved.length;
|
|
44568
|
-
|
|
44760
|
+
const where = report.root ? ` (worktree root ${report.root})` : "";
|
|
44761
|
+
if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges${where}`);
|
|
44569
44762
|
for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
|
|
44570
44763
|
if (report.unrecorded.length) {
|
|
44571
|
-
|
|
44764
|
+
const shown = report.unrecorded.slice(0, 5).join(", ");
|
|
44765
|
+
const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
|
|
44766
|
+
lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${shown}${more}`);
|
|
44572
44767
|
}
|
|
44573
44768
|
return lines2;
|
|
44574
44769
|
}
|
|
@@ -45798,7 +45993,7 @@ ${list}`);
|
|
|
45798
45993
|
console.error(line);
|
|
45799
45994
|
}
|
|
45800
45995
|
}
|
|
45801
|
-
const credential = "user";
|
|
45996
|
+
const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
|
|
45802
45997
|
if (o.json) printLine(JSON.stringify({ ...result, credential }));
|
|
45803
45998
|
else printLine(
|
|
45804
45999
|
`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
|
|
@@ -46385,8 +46580,8 @@ function trainApplyDeps() {
|
|
|
46385
46580
|
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
46386
46581
|
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
46387
46582
|
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
46388
|
-
dispatchTenantControl: async ({ repo, stage, action, lines: lines2, task, artifact, confirmDataLoss }) => {
|
|
46389
|
-
const res = await tenantControl({ repo, stage, action, ...lines2 != null ? { lines: lines2 } : {}, ...task ? { task } : {}, ...artifact ? { artifact } : {}, ...confirmDataLoss ? { confirmDataLoss } : {} }, registryClientDeps(await loadConfig()));
|
|
46583
|
+
dispatchTenantControl: async ({ repo, stage, action, lines: lines2, since, until, task, artifact, confirmDataLoss }) => {
|
|
46584
|
+
const res = await tenantControl({ repo, stage, action, ...lines2 != null ? { lines: lines2 } : {}, ...since ? { since } : {}, ...until ? { until } : {}, ...task ? { task } : {}, ...artifact ? { artifact } : {}, ...confirmDataLoss ? { confirmDataLoss } : {} }, registryClientDeps(await loadConfig()));
|
|
46390
46585
|
if (res.ok) return { ok: true };
|
|
46391
46586
|
const body = res.body;
|
|
46392
46587
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
@@ -51568,8 +51763,8 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
51568
51763
|
for (const wf of workflowsList) {
|
|
51569
51764
|
if (typeof wf?.path !== "string" || !wf.path) continue;
|
|
51570
51765
|
if (!wf.path.startsWith(".github/workflows/")) continue;
|
|
51571
|
-
const
|
|
51572
|
-
const name = `${repo}/${
|
|
51766
|
+
const basename4 = wf.path.split("/").pop() ?? wf.path;
|
|
51767
|
+
const name = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
|
|
51573
51768
|
if (wf.state !== "active") {
|
|
51574
51769
|
disabled.push(name);
|
|
51575
51770
|
continue;
|
|
@@ -53047,6 +53242,17 @@ function registerEdgeCommands(program3) {
|
|
|
53047
53242
|
process.exitCode = 1;
|
|
53048
53243
|
}
|
|
53049
53244
|
});
|
|
53245
|
+
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) => {
|
|
53246
|
+
if (action !== "status" && action !== "ensure") return fail("runtime edge dns: <action> must be status or ensure");
|
|
53247
|
+
if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime edge dns: <stage> must be dev, rc, or main");
|
|
53248
|
+
try {
|
|
53249
|
+
const result = await edgeDnsForStage(slugOf(repo), stage, registryClientDeps(await loadConfig()), action === "ensure");
|
|
53250
|
+
console.log(JSON.stringify(result, null, 2));
|
|
53251
|
+
if (result.required && result.complete === false) process.exitCode = 1;
|
|
53252
|
+
} catch (e) {
|
|
53253
|
+
fail(`runtime edge dns: ${e.message}`);
|
|
53254
|
+
}
|
|
53255
|
+
});
|
|
53050
53256
|
}
|
|
53051
53257
|
|
|
53052
53258
|
// src/command-register-developer.ts
|
|
@@ -54110,7 +54316,7 @@ async function reportWrite(label, res) {
|
|
|
54110
54316
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
54111
54317
|
}
|
|
54112
54318
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
54113
|
-
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("--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) => {
|
|
54319
|
+
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) => {
|
|
54114
54320
|
try {
|
|
54115
54321
|
if (o.confirmDataLoss !== void 0 && (action !== "retire" || stage === "rc")) return fail("runtime tenant control: --confirm-data-loss is dev/main retire-only");
|
|
54116
54322
|
if (action === "retire" && (stage === "dev" || stage === "main")) {
|
|
@@ -54129,12 +54335,17 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
|
|
|
54129
54335
|
lines2 = Number(o.lines);
|
|
54130
54336
|
if (!Number.isInteger(lines2) || lines2 < 1 || lines2 > 2e3) return fail("runtime tenant control: --lines must be an integer between 1 and 2000");
|
|
54131
54337
|
}
|
|
54338
|
+
for (const [flag, value] of [["--since", o.since], ["--until", o.until]]) {
|
|
54339
|
+
if (value === void 0) continue;
|
|
54340
|
+
if (action !== "logs") return fail(`runtime tenant control: ${flag} is valid only for the logs action`);
|
|
54341
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?Z$/.test(value)) return fail(`runtime tenant control: ${flag} must be a UTC time like 2026-10-07T13:10Z`);
|
|
54342
|
+
}
|
|
54132
54343
|
if (action === "run-task") {
|
|
54133
54344
|
if (!o.task) return fail("runtime tenant control: run-task requires --task <declared-name>");
|
|
54134
54345
|
} else if (o.task || o.artifact) {
|
|
54135
54346
|
return fail("runtime tenant control: --task/--artifact are valid only for run-task");
|
|
54136
54347
|
}
|
|
54137
|
-
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
|
|
54348
|
+
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 });
|
|
54138
54349
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
54139
54350
|
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
54140
54351
|
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
|
|
|
@@ -3,6 +3,9 @@ name: gate
|
|
|
3
3
|
# and v* tags so /release and /hotfix can discover required contexts on the tagged SHA.
|
|
4
4
|
#
|
|
5
5
|
# Runner: self-hosted mmi-runner (mmi-live) — see docs/Guides/gh-runner-runbook.md in MMI-Hub.
|
|
6
|
+
# #7914 (owner, 2026-10-06): this gate holds only fast, deterministic checks. Browser and long suites
|
|
7
|
+
# (screens, geometry/motion, synthetic, Chrome renders and tests) never go here; they run in their own
|
|
8
|
+
# workflow, nightly through the Hub schedule plane and on demand (docs/decisions/2026-10-06-browser-checks-nightly.md).
|
|
6
9
|
# #4118: if GATE_CMD runs tsgo or other memory-heavy typecheck / browser work, pin this job to
|
|
7
10
|
# runs-on: [self-hosted, linux, x64, mmi-heavy]
|
|
8
11
|
# instead — standard lanes are MemoryMax=1200M and will cgroup-OOM those workloads.
|
|
@@ -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.
|
|
@@ -26,6 +26,12 @@ Open a PR through `mmi-cli devops pr create` with a closing issue link, and land
|
|
|
26
26
|
`mmi-cli devops pr land`. Verify required checks, merge, issue closure, and cleanup before calling
|
|
27
27
|
the implementation complete. A merged implementation is not a production release.
|
|
28
28
|
|
|
29
|
+
Browser checks and long CI run nightly, never as PR checks (owner, 2026-10-06;
|
|
30
|
+
`docs/decisions/2026-10-06-browser-checks-nightly.md`). The required `gate` holds only fast,
|
|
31
|
+
deterministic checks. Screen, geometry, motion, synthetic and Chrome suites run in a separate
|
|
32
|
+
workflow armed nightly through the Hub schedule plane and on demand; it is never required, and a red
|
|
33
|
+
run updates one `ci-heavy-red` issue. Never add such a suite to a gate or a local test policy.
|
|
34
|
+
|
|
29
35
|
## Report MMI tooling bugs
|
|
30
36
|
|
|
31
37
|
This workflow covers bugs in MMI tooling, including its CLI, plugin, and shared workflows.
|