@mutmutco/codex-plugin 4.5.97 → 4.5.99
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/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) {
|
|
@@ -9247,7 +9474,7 @@ init_clean_exit();
|
|
|
9247
9474
|
var import_promises = require("node:fs/promises");
|
|
9248
9475
|
var import_node_os4 = require("node:os");
|
|
9249
9476
|
var import_node_path9 = require("node:path");
|
|
9250
|
-
var
|
|
9477
|
+
var import_node_crypto5 = require("node:crypto");
|
|
9251
9478
|
|
|
9252
9479
|
// src/board-priority.ts
|
|
9253
9480
|
var BOARD_PRIORITY_NAMES = ["Urgent", "High", "Medium", "Low"];
|
|
@@ -9334,7 +9561,7 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
9334
9561
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
9335
9562
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
9336
9563
|
const dir = deps.dir ?? (0, import_node_os4.tmpdir)();
|
|
9337
|
-
const file = (0, import_node_path9.join)(dir, `mmi-gh-body-${process.pid}-${(0,
|
|
9564
|
+
const file = (0, import_node_path9.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.md`);
|
|
9338
9565
|
await ensureDir((0, import_node_path9.dirname)(file), { recursive: true }).catch(() => {
|
|
9339
9566
|
});
|
|
9340
9567
|
await write(file, args[i + 1], "utf8");
|
|
@@ -9664,7 +9891,7 @@ async function defaultRestCreatePr(exec2, input, deps = {}) {
|
|
|
9664
9891
|
const remove2 = deps.remove ?? import_promises.unlink;
|
|
9665
9892
|
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
9666
9893
|
const dir = deps.dir ?? (0, import_node_os4.tmpdir)();
|
|
9667
|
-
const file = (0, import_node_path9.join)(dir, `mmi-rest-pr-${process.pid}-${(0,
|
|
9894
|
+
const file = (0, import_node_path9.join)(dir, `mmi-rest-pr-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.json`);
|
|
9668
9895
|
const payload = {
|
|
9669
9896
|
title: input.title,
|
|
9670
9897
|
head: input.head,
|
|
@@ -11064,7 +11291,7 @@ function findDuplicateLesson(source, openLessons) {
|
|
|
11064
11291
|
}
|
|
11065
11292
|
|
|
11066
11293
|
// src/session-identity.ts
|
|
11067
|
-
var
|
|
11294
|
+
var import_node_crypto6 = require("node:crypto");
|
|
11068
11295
|
var import_node_os8 = require("node:os");
|
|
11069
11296
|
|
|
11070
11297
|
// src/plugin-guard-io.ts
|
|
@@ -11948,7 +12175,7 @@ function readSessionIdSource(env) {
|
|
|
11948
12175
|
var synthesizedSessionId;
|
|
11949
12176
|
function fallbackSessionId(surface) {
|
|
11950
12177
|
if (!synthesizedSessionId) {
|
|
11951
|
-
synthesizedSessionId = `${SYNTH_SESSION_PREFIX}${surface}-${process.pid}-${(0,
|
|
12178
|
+
synthesizedSessionId = `${SYNTH_SESSION_PREFIX}${surface}-${process.pid}-${(0, import_node_crypto6.randomBytes)(3).toString("hex")}`;
|
|
11952
12179
|
}
|
|
11953
12180
|
return synthesizedSessionId;
|
|
11954
12181
|
}
|
|
@@ -14646,7 +14873,7 @@ async function propagateMayAutoMerge(repo, repoClass, rest) {
|
|
|
14646
14873
|
}
|
|
14647
14874
|
|
|
14648
14875
|
// src/bootstrap-apply.ts
|
|
14649
|
-
var
|
|
14876
|
+
var import_node_crypto7 = require("node:crypto");
|
|
14650
14877
|
|
|
14651
14878
|
// src/bootstrap-seeds.ts
|
|
14652
14879
|
var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
@@ -15877,7 +16104,7 @@ function planSeedAction(seed, exists) {
|
|
|
15877
16104
|
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
16105
|
}
|
|
15879
16106
|
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,
|
|
16107
|
+
var gateHash = (body) => (0, import_node_crypto7.createHash)("sha256").update(body, "utf8").digest("hex");
|
|
15881
16108
|
function parseGateRenderStamp(content) {
|
|
15882
16109
|
const match = /^# mmi-gate-render: sha256=([a-f0-9]{64})\n/.exec(content);
|
|
15883
16110
|
return match ? { hash: match[1], body: content.slice(match[0].length) } : null;
|
|
@@ -17345,10 +17572,10 @@ var rollout_plan_default = {
|
|
|
17345
17572
|
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
17573
|
},
|
|
17347
17574
|
baseline: {
|
|
17348
|
-
version: "4.5.
|
|
17349
|
-
tag: "v4.5.
|
|
17350
|
-
commit: "
|
|
17351
|
-
npm: "@mutmutco/cli@4.5.
|
|
17575
|
+
version: "4.5.99",
|
|
17576
|
+
tag: "v4.5.99",
|
|
17577
|
+
commit: "44a40b83250e",
|
|
17578
|
+
npm: "@mutmutco/cli@4.5.99"
|
|
17352
17579
|
},
|
|
17353
17580
|
exitCriterion: "fleet-n-of-n",
|
|
17354
17581
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17365,14 +17592,14 @@ var rollout_plan_default = {
|
|
|
17365
17592
|
repo: "mutmutco/mmi-hub",
|
|
17366
17593
|
role: "canary",
|
|
17367
17594
|
schedule: "train",
|
|
17368
|
-
v3Target: "v4.5.
|
|
17595
|
+
v3Target: "v4.5.99"
|
|
17369
17596
|
}
|
|
17370
17597
|
],
|
|
17371
17598
|
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
17599
|
rollback: {
|
|
17373
17600
|
independent: true,
|
|
17374
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17375
|
-
v3Target: "v4.5.
|
|
17601
|
+
mechanism: "npm dist-tag latest -> 4.5.99 and redeploy the Hub Lambda from tag v4.5.99 (44a40b83250e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17602
|
+
v3Target: "v4.5.99 (@mutmutco/cli@4.5.99, tag commit 44a40b83250e \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17376
17603
|
}
|
|
17377
17604
|
},
|
|
17378
17605
|
{
|
|
@@ -19730,7 +19957,9 @@ async function correlateRun(deps, args) {
|
|
|
19730
19957
|
return Number.isFinite(created) && created < threshold;
|
|
19731
19958
|
});
|
|
19732
19959
|
const tokens2 = args.mode === "dispatch" ? args.titleIncludes : void 0;
|
|
19733
|
-
const
|
|
19960
|
+
const fieldsFrom = args.mode === "dispatch" ? args.titleFieldsFrom : void 0;
|
|
19961
|
+
const titleHas = (title) => fieldsFrom === void 0 ? tokens2.every((t) => title.includes(t)) : tokens2.every((t, i) => title.split(" ")[fieldsFrom + i] === t);
|
|
19962
|
+
const titleMatch = tokens2 && tokens2.length ? candidates.find((c) => titleHas(c.row.displayTitle ?? "")) : void 0;
|
|
19734
19963
|
const match = titleMatch ?? (!("requireTitleMatch" in args && args.requireTitleMatch) && (args.mode === "workflow" || windowProvenWhole) ? candidates[0] : void 0);
|
|
19735
19964
|
if (match) return { runId: match.row.databaseId, runUrl: match.row.url, read: "found" };
|
|
19736
19965
|
if (!windowProvenWhole) truncatedWindow = true;
|
|
@@ -19756,7 +19985,7 @@ function correlateGetPublishRun(deps, since, product, mode) {
|
|
|
19756
19985
|
return correlateRun(deps, { workflow: "get-publish.yml", since, mode: "dispatch", titleIncludes: [product, ...mode ? [mode] : []], ...mode ? { requireTitleMatch: true } : {} });
|
|
19757
19986
|
}
|
|
19758
19987
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
19759
|
-
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
19988
|
+
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes, titleFieldsFrom: 2, ...titleIncludes?.length ? { requireTitleMatch: true } : {} });
|
|
19760
19989
|
}
|
|
19761
19990
|
function correlateReconcileRun(deps, since, titleIncludes) {
|
|
19762
19991
|
return correlateRun(deps, { workflow: "tenant-reconcile.yml", since, mode: "dispatch", titleIncludes });
|
|
@@ -21562,7 +21791,7 @@ async function runTenantControl(deps, options) {
|
|
|
21562
21791
|
const watch = options.watch ?? tenantControlWatches(action);
|
|
21563
21792
|
const base = { command: "tenant-control", repo, stage, action };
|
|
21564
21793
|
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 });
|
|
21794
|
+
const d = await deps.dispatchTenantControl({ repo, stage, action, lines: options.lines, since: options.logSince, until: options.logUntil, task: options.task, artifact: options.artifact, confirmDataLoss: options.confirmDataLoss });
|
|
21566
21795
|
if (!d.ok) {
|
|
21567
21796
|
const transport = d.category === "transport-failed";
|
|
21568
21797
|
return {
|
|
@@ -22778,7 +23007,7 @@ function checkLineEndings(probe) {
|
|
|
22778
23007
|
// src/release-ledger.ts
|
|
22779
23008
|
var import_node_fs22 = require("node:fs");
|
|
22780
23009
|
var import_node_path22 = require("node:path");
|
|
22781
|
-
var
|
|
23010
|
+
var import_node_crypto8 = require("node:crypto");
|
|
22782
23011
|
|
|
22783
23012
|
// src/file-lock.ts
|
|
22784
23013
|
var import_promises2 = require("node:fs/promises");
|
|
@@ -23140,7 +23369,7 @@ function releaseLedgerPath(cwd) {
|
|
|
23140
23369
|
}
|
|
23141
23370
|
function atomicWriteJson(path2, ledger) {
|
|
23142
23371
|
(0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(path2), { recursive: true });
|
|
23143
|
-
const tmp = `${path2}.tmp-${process.pid}-${(0,
|
|
23372
|
+
const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto8.randomBytes)(4).toString("hex")}`;
|
|
23144
23373
|
(0, import_node_fs22.writeFileSync)(tmp, `${JSON.stringify(ledger, null, 2)}
|
|
23145
23374
|
`, "utf8");
|
|
23146
23375
|
(0, import_node_fs22.renameSync)(tmp, path2);
|
|
@@ -29869,150 +30098,6 @@ function renderPluginCachePlan(plan, applied) {
|
|
|
29869
30098
|
return lines2.join("\n");
|
|
29870
30099
|
}
|
|
29871
30100
|
|
|
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
30101
|
// src/board-commands.ts
|
|
30017
30102
|
init_cli_shared();
|
|
30018
30103
|
|
|
@@ -30962,7 +31047,7 @@ init_hub_auth();
|
|
|
30962
31047
|
init_github_client();
|
|
30963
31048
|
|
|
30964
31049
|
// src/project-set.ts
|
|
30965
|
-
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "stageAllowlist", "suiteHold"];
|
|
31050
|
+
var UNSET_KEYS = ["projectAdmins", "installer", "oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "tenantTasks", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "releaseChannel", "releaseLanguage", "dsManifestPath", "consumesDesignSystem", "ci", "requiredChecks", "requiredCheckBranches", "ciExemptReason", "gate", "seedCanary", "notifyOnDeploy", "vercelVersionEnv", "stageAllowlist", "suiteHold", "internalPeers"];
|
|
30966
31051
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
30967
31052
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
30968
31053
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -31497,6 +31582,7 @@ var SETTABLE_VAR_KEYS = [
|
|
|
31497
31582
|
"requiredRuntimeSecrets",
|
|
31498
31583
|
"requiredBuildSecrets",
|
|
31499
31584
|
"tenantTasks",
|
|
31585
|
+
"internalPeers",
|
|
31500
31586
|
"edgeDomains",
|
|
31501
31587
|
"statusFieldId",
|
|
31502
31588
|
"statusOptions",
|
|
@@ -31535,6 +31621,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
31535
31621
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
31536
31622
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
31537
31623
|
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}} \u2014 merged per entry, at most 20 stored in total; retire ONE task with --unset-task NAME; use {artifact} in argv when required; a secretOutput is master-only',
|
|
31624
|
+
internalPeers: "JSON {owner/repo:[dev|rc|main]} \u2014 same-box suite peers via the box Caddy; clear with --unset internalPeers (#7886)",
|
|
31538
31625
|
secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
|
|
31539
31626
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
31540
31627
|
statusOptions: "JSON name\u2192id map",
|
|
@@ -31611,6 +31698,26 @@ function clearSecretsConfirmationError(unsets, existing, confirmed) {
|
|
|
31611
31698
|
if (confirmed) return null;
|
|
31612
31699
|
return `org project set: --unset secrets clears the WHOLE catalog for ${existing?.slug ?? "this project"} (${count} declared entr${count === 1 ? "y" : "ies"} would be removed). To drop ONE entry use --unset-secret <KEY>; re-run with --confirm-clear-secrets to wipe all ${count}.`;
|
|
31613
31700
|
}
|
|
31701
|
+
function parseInternalPeersVar(raw) {
|
|
31702
|
+
let parsed;
|
|
31703
|
+
try {
|
|
31704
|
+
parsed = JSON.parse(raw);
|
|
31705
|
+
} catch {
|
|
31706
|
+
throw new Error("org project set: internalPeers must be valid JSON");
|
|
31707
|
+
}
|
|
31708
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("org project set: internalPeers must be a JSON object");
|
|
31709
|
+
const entries = Object.entries(parsed);
|
|
31710
|
+
if (entries.length < 1 || entries.length > 20) throw new Error("org project set: internalPeers holds 1-20 peer declarations");
|
|
31711
|
+
const repoRe = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
31712
|
+
const validStages = /* @__PURE__ */ new Set(["dev", "rc", "main"]);
|
|
31713
|
+
const out = {};
|
|
31714
|
+
for (const [repo, rawStages] of entries) {
|
|
31715
|
+
if (!repoRe.test(repo)) throw new Error("org project set: internalPeers key must look like owner/name \u2014 got " + JSON.stringify(repo));
|
|
31716
|
+
if (!Array.isArray(rawStages) || rawStages.length < 1 || rawStages.length > 3 || rawStages.some((stg) => typeof stg !== "string" || !validStages.has(stg)) || new Set(rawStages).size !== rawStages.length) throw new Error("org project set: internalPeers stages must be a non-empty array of unique dev/rc/main values");
|
|
31717
|
+
out[repo] = rawStages;
|
|
31718
|
+
}
|
|
31719
|
+
return out;
|
|
31720
|
+
}
|
|
31614
31721
|
function parseSuiteHoldVar(raw) {
|
|
31615
31722
|
let parsed;
|
|
31616
31723
|
try {
|
|
@@ -31673,6 +31780,8 @@ function buildProjectSetPatch(input) {
|
|
|
31673
31780
|
patch[key] = parseBuildSecretsVar(raw);
|
|
31674
31781
|
} else if (key === "tenantTasks") {
|
|
31675
31782
|
patch[key] = parseTenantTasksVar(raw);
|
|
31783
|
+
} else if (key === "internalPeers") {
|
|
31784
|
+
patch[key] = parseInternalPeersVar(raw);
|
|
31676
31785
|
} else if (key === "secrets") {
|
|
31677
31786
|
patch[key] = parseSecretsCatalogVar(raw);
|
|
31678
31787
|
} else if (key === "edgeDomains") {
|
|
@@ -36547,7 +36656,7 @@ function readSuiteHotfix(path2) {
|
|
|
36547
36656
|
} catch {
|
|
36548
36657
|
throw new Error(`suite hotfix record unreadable at ${path2}; repair it, never restart`);
|
|
36549
36658
|
}
|
|
36550
|
-
if (r.schema !== "mmi-suite-hotfix/v1" || !r.repo || !r.member || !r.from || r.tag !== void 0 && !RELEASE_TAG.test(r.tag) || r.released && !r.tag || r.pinMerged && !r.released || r.leadTag !== void 0 && (!RELEASE_TAG.test(r.leadTag) || !r.pinMerged) || r.leadReleased && !r.leadTag || r.mapped && !r.leadReleased)
|
|
36659
|
+
if (r.schema !== "mmi-suite-hotfix/v1" || !r.repo || !r.member || !r.from || r.tag !== void 0 && !RELEASE_TAG.test(r.tag) || r.released && !r.tag || r.pinMerged && !r.released || r.mainPublished && !r.released || r.leadTag !== void 0 && (!RELEASE_TAG.test(r.leadTag) || !r.pinMerged) || r.leadReleased && !r.leadTag || r.mapped && !r.leadReleased)
|
|
36551
36660
|
throw new Error(`suite hotfix record has unknown or invalid state at ${path2}; repair it, never restart`);
|
|
36552
36661
|
return r;
|
|
36553
36662
|
}
|
|
@@ -36562,8 +36671,10 @@ async function suiteHotfix(deps, opts) {
|
|
|
36562
36671
|
if (found.repo === SUITE_REPO) throw new Error(`${SUITE_REPO} is the suite lead; hotfix a member, this command cuts the lead; nothing was written`);
|
|
36563
36672
|
const held = await deps.hold(found);
|
|
36564
36673
|
if (held) throw new Error(`${found.repo} is HELD (${held.reason}); clear it with suite hold clear before a suite hotfix; nothing was written`);
|
|
36674
|
+
const mainPath = mainBundlePath(found.bundles);
|
|
36565
36675
|
const plan = [
|
|
36566
36676
|
`${found.repo}: hotfix start --from ${opts.from} + release (suite-solo, in-process)`,
|
|
36677
|
+
`${found.repo}: publish main bundle (module-rc-publish channel=main) at ${mainPath}`,
|
|
36567
36678
|
`${SUITE_REPO}: main pin PR for ${found.id}, wait for merge`,
|
|
36568
36679
|
`${SUITE_REPO}: hotfix (patch) carrying only the merged pin PR onto main`,
|
|
36569
36680
|
`${SUITE_REPO}: record suite version -> member versions on its release`
|
|
@@ -36582,6 +36693,7 @@ async function suiteHotfix(deps, opts) {
|
|
|
36582
36693
|
const save = () => writeSuiteHotfix(path2, rec);
|
|
36583
36694
|
save();
|
|
36584
36695
|
const resume = `rerun suite hotfix ${found.id} --from ${rec.from} --owner-go to resume`;
|
|
36696
|
+
if (!rec.released) await deps.preflightMain(found.repo, mainPath);
|
|
36585
36697
|
if (!rec.tag) {
|
|
36586
36698
|
const started = await deps.hotfixStart(found.repo, { from: rec.from, ack: rec.ack });
|
|
36587
36699
|
if (!RELEASE_TAG.test(started.tag)) throw new Error(`${found.repo}: hotfix start returned no vX.Y.Z tag`);
|
|
@@ -36599,6 +36711,15 @@ async function suiteHotfix(deps, opts) {
|
|
|
36599
36711
|
rec.released = true;
|
|
36600
36712
|
save();
|
|
36601
36713
|
}
|
|
36714
|
+
if (!rec.mainPublished) {
|
|
36715
|
+
try {
|
|
36716
|
+
await deps.publishMain(found.repo, rec.tag, mainPath);
|
|
36717
|
+
} catch (e) {
|
|
36718
|
+
throw new Error(`${found.repo}: main bundle publish failed (${e.message}); ${rec.tag} is released: ${resume}`);
|
|
36719
|
+
}
|
|
36720
|
+
rec.mainPublished = true;
|
|
36721
|
+
save();
|
|
36722
|
+
}
|
|
36602
36723
|
if (!rec.pinMerged) {
|
|
36603
36724
|
const pin = await deps.soloPin(found.repo, rec.tag);
|
|
36604
36725
|
if (pin.status === "superseded") throw new Error(`${found.id} pin at ${rec.tag} is superseded by ${pin.pr}; a newer ${found.id} release owns the pin; stop`);
|
|
@@ -42631,6 +42752,13 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
42631
42752
|
const repo = await requireRepo(repoOption);
|
|
42632
42753
|
const { client, throttledPaths } = withRateLimitRetry(withAppClaimMarkerRead(deps.client ?? defaultGitHubClient(), deps.appClient), deps);
|
|
42633
42754
|
const actor = deps.actor ?? describeSessionIdentity();
|
|
42755
|
+
let viewerLogin = deps.viewerLogin;
|
|
42756
|
+
if (!viewerLogin) {
|
|
42757
|
+
try {
|
|
42758
|
+
viewerLogin = (await client.rest("GET", "user"))?.login;
|
|
42759
|
+
} catch {
|
|
42760
|
+
}
|
|
42761
|
+
}
|
|
42634
42762
|
const source = deps.actorSource ?? (deps.actor ? "the caller-supplied identity" : readSessionIdSource(process.env) ?? "a synthesized fallback");
|
|
42635
42763
|
const requester = `requesting lane: ${actor.surface}/${actor.session}@${actor.host} (session id from ${source})`;
|
|
42636
42764
|
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
@@ -42644,7 +42772,7 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
42644
42772
|
}
|
|
42645
42773
|
}
|
|
42646
42774
|
if (state?.toLowerCase() === "closed") continue;
|
|
42647
|
-
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
42775
|
+
const contest = await checkContest(client, { repository: repo, number }, actor, viewerLogin);
|
|
42648
42776
|
if (!contest.contested) continue;
|
|
42649
42777
|
const ref = `${repo}#${number}`;
|
|
42650
42778
|
const commentsPath = `repos/${repo}/issues/${number}/comments`;
|
|
@@ -44423,6 +44551,9 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
44423
44551
|
|
|
44424
44552
|
// src/worktree-residue-sweep.ts
|
|
44425
44553
|
var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
|
|
44554
|
+
function canonicalWorktreeRoot(primary) {
|
|
44555
|
+
return (0, import_node_path41.join)((0, import_node_path41.dirname)(primary), "mmi-worktrees", (0, import_node_path41.basename)(primary));
|
|
44556
|
+
}
|
|
44426
44557
|
function norm(p) {
|
|
44427
44558
|
const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
44428
44559
|
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
@@ -44491,16 +44622,14 @@ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
|
|
|
44491
44622
|
}
|
|
44492
44623
|
}
|
|
44493
44624
|
if (recorded.length) writePaths(file, remaining, deps);
|
|
44494
|
-
const roots = /* @__PURE__ */ new Set();
|
|
44495
|
-
for (const p of [...registered.slice(1), ...recorded]) roots.add((0, import_node_path41.dirname)(p));
|
|
44496
44625
|
const keptKeys = new Set(remaining.map(norm));
|
|
44497
|
-
|
|
44498
|
-
|
|
44499
|
-
|
|
44626
|
+
const root = registered[0] ? canonicalWorktreeRoot(registered[0]) : void 0;
|
|
44627
|
+
if (root && isDir(root) && !live.has(norm(root))) {
|
|
44628
|
+
report.root = root;
|
|
44629
|
+
let children = [];
|
|
44500
44630
|
try {
|
|
44501
44631
|
children = readDir(root);
|
|
44502
44632
|
} catch {
|
|
44503
|
-
continue;
|
|
44504
44633
|
}
|
|
44505
44634
|
for (const name of children) {
|
|
44506
44635
|
const child2 = (0, import_node_path41.join)(root, name);
|
|
@@ -44529,10 +44658,13 @@ function renderResidueSweepLines(report) {
|
|
|
44529
44658
|
if (!report) return [];
|
|
44530
44659
|
const lines2 = [];
|
|
44531
44660
|
const cleaned = report.swept.length + report.emptyRemoved.length;
|
|
44532
|
-
|
|
44661
|
+
const where = report.root ? ` (worktree root ${report.root})` : "";
|
|
44662
|
+
if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges${where}`);
|
|
44533
44663
|
for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
|
|
44534
44664
|
if (report.unrecorded.length) {
|
|
44535
|
-
|
|
44665
|
+
const shown = report.unrecorded.slice(0, 5).join(", ");
|
|
44666
|
+
const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
|
|
44667
|
+
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}`);
|
|
44536
44668
|
}
|
|
44537
44669
|
return lines2;
|
|
44538
44670
|
}
|
|
@@ -45762,7 +45894,7 @@ ${list}`);
|
|
|
45762
45894
|
console.error(line);
|
|
45763
45895
|
}
|
|
45764
45896
|
}
|
|
45765
|
-
const credential = "user";
|
|
45897
|
+
const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
|
|
45766
45898
|
if (o.json) printLine(JSON.stringify({ ...result, credential }));
|
|
45767
45899
|
else printLine(
|
|
45768
45900
|
`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
|
|
@@ -46349,8 +46481,8 @@ function trainApplyDeps() {
|
|
|
46349
46481
|
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
46350
46482
|
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
46351
46483
|
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
46352
|
-
dispatchTenantControl: async ({ repo, stage, action, lines: lines2, task, artifact, confirmDataLoss }) => {
|
|
46353
|
-
const res = await tenantControl({ repo, stage, action, ...lines2 != null ? { lines: lines2 } : {}, ...task ? { task } : {}, ...artifact ? { artifact } : {}, ...confirmDataLoss ? { confirmDataLoss } : {} }, registryClientDeps(await loadConfig()));
|
|
46484
|
+
dispatchTenantControl: async ({ repo, stage, action, lines: lines2, since, until, task, artifact, confirmDataLoss }) => {
|
|
46485
|
+
const res = await tenantControl({ repo, stage, action, ...lines2 != null ? { lines: lines2 } : {}, ...since ? { since } : {}, ...until ? { until } : {}, ...task ? { task } : {}, ...artifact ? { artifact } : {}, ...confirmDataLoss ? { confirmDataLoss } : {} }, registryClientDeps(await loadConfig()));
|
|
46354
46486
|
if (res.ok) return { ok: true };
|
|
46355
46487
|
const body = res.body;
|
|
46356
46488
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
@@ -51532,8 +51664,8 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
51532
51664
|
for (const wf of workflowsList) {
|
|
51533
51665
|
if (typeof wf?.path !== "string" || !wf.path) continue;
|
|
51534
51666
|
if (!wf.path.startsWith(".github/workflows/")) continue;
|
|
51535
|
-
const
|
|
51536
|
-
const name = `${repo}/${
|
|
51667
|
+
const basename4 = wf.path.split("/").pop() ?? wf.path;
|
|
51668
|
+
const name = `${repo}/${basename4.replace(/\.ya?ml$/, "")}`;
|
|
51537
51669
|
if (wf.state !== "active") {
|
|
51538
51670
|
disabled.push(name);
|
|
51539
51671
|
continue;
|
|
@@ -54074,7 +54206,7 @@ async function reportWrite(label, res) {
|
|
|
54074
54206
|
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
54075
54207
|
}
|
|
54076
54208
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
54077
|
-
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) => {
|
|
54209
|
+
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; dev/main retire needs master and exact data-loss confirmation").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--since <utc>", "logs only: read the journal from this UTC time, e.g. 2026-10-07T13:10Z (includes replaced containers)").option("--until <utc>", "logs only: read the journal up to this UTC time").option("--task <name>", "run-task only: registry-declared task name (never a command)").option("--artifact <id>", "run-task only: opaque id returned by tenant artifact put").option("--confirm-data-loss <owner/repo/stage>", "dev/main retire only: exact target to decommission; without it show the dry-run plan").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
54078
54210
|
try {
|
|
54079
54211
|
if (o.confirmDataLoss !== void 0 && (action !== "retire" || stage === "rc")) return fail("runtime tenant control: --confirm-data-loss is dev/main retire-only");
|
|
54080
54212
|
if (action === "retire" && (stage === "dev" || stage === "main")) {
|
|
@@ -54093,12 +54225,17 @@ Dry run: pass --confirm-data-loss ${repo}/${stage} to decommission.`);
|
|
|
54093
54225
|
lines2 = Number(o.lines);
|
|
54094
54226
|
if (!Number.isInteger(lines2) || lines2 < 1 || lines2 > 2e3) return fail("runtime tenant control: --lines must be an integer between 1 and 2000");
|
|
54095
54227
|
}
|
|
54228
|
+
for (const [flag, value] of [["--since", o.since], ["--until", o.until]]) {
|
|
54229
|
+
if (value === void 0) continue;
|
|
54230
|
+
if (action !== "logs") return fail(`runtime tenant control: ${flag} is valid only for the logs action`);
|
|
54231
|
+
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`);
|
|
54232
|
+
}
|
|
54096
54233
|
if (action === "run-task") {
|
|
54097
54234
|
if (!o.task) return fail("runtime tenant control: run-task requires --task <declared-name>");
|
|
54098
54235
|
} else if (o.task || o.artifact) {
|
|
54099
54236
|
return fail("runtime tenant control: --task/--artifact are valid only for run-task");
|
|
54100
54237
|
}
|
|
54101
|
-
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
|
|
54238
|
+
const result = await runTenantControl(trainApplyDeps(), { repo, stage, action, watch: o.watch, lines: lines2, logSince: o.since, logUntil: o.until, task: o.task, artifact: o.artifact, confirmDataLoss: o.confirmDataLoss });
|
|
54102
54239
|
if (!o.json && action === "verify-secrets" && result.secrets) {
|
|
54103
54240
|
const body = { ok: result.conclusion === "success", secrets: result.secrets, ssmStatus: result.conclusion === "success" ? "Success" : "Failed", raw: result.secretsRaw };
|
|
54104
54241
|
const { lines: lines3, failure } = renderVerifySecrets(body);
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -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.
|
|
@@ -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.
|