@mutmutco/codex-plugin 4.5.96 → 4.5.98
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 +294 -38
- package/cli/package.json +1 -1
- package/package.json +1 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -5927,7 +5927,7 @@ async function waitForPrChecks(deps) {
|
|
|
5927
5927
|
reason = NO_CI_LIVE_CHECKS_CONTRADICTION_REASON;
|
|
5928
5928
|
deps.log?.(`merge CI policy contradiction: ${reason}; waiting for live PR checks`);
|
|
5929
5929
|
policy = "wait-for-checks";
|
|
5930
|
-
queuedStates.push(firstState);
|
|
5930
|
+
if (!deps.pollChecksWithEvidence) queuedStates.push(firstState);
|
|
5931
5931
|
} catch (e) {
|
|
5932
5932
|
if (!isRetryableGitHubWaitReadError(e)) {
|
|
5933
5933
|
return { policy, status: "failure", reason: waitReadFailureMessage(e), detail: "github-read-failed" };
|
|
@@ -5955,6 +5955,8 @@ async function waitForPrChecks(deps) {
|
|
|
5955
5955
|
let successStreak = 0;
|
|
5956
5956
|
let failureStreak = 0;
|
|
5957
5957
|
let zeroRunsStreak = 0;
|
|
5958
|
+
let evidenceHeadSha;
|
|
5959
|
+
let lastEvidence;
|
|
5958
5960
|
report("starting");
|
|
5959
5961
|
while (now() < deadline) {
|
|
5960
5962
|
if (deps.pollRateLimit) {
|
|
@@ -5986,7 +5988,17 @@ async function waitForPrChecks(deps) {
|
|
|
5986
5988
|
}
|
|
5987
5989
|
let state;
|
|
5988
5990
|
try {
|
|
5989
|
-
|
|
5991
|
+
if (queuedStates.length) state = queuedStates.shift();
|
|
5992
|
+
else if (deps.pollChecksWithEvidence) {
|
|
5993
|
+
lastEvidence = await deps.pollChecksWithEvidence();
|
|
5994
|
+
if (evidenceHeadSha !== void 0 && evidenceHeadSha !== lastEvidence.headSha) {
|
|
5995
|
+
successStreak = 0;
|
|
5996
|
+
failureStreak = 0;
|
|
5997
|
+
zeroRunsStreak = 0;
|
|
5998
|
+
}
|
|
5999
|
+
evidenceHeadSha = lastEvidence.headSha;
|
|
6000
|
+
state = lastEvidence.state;
|
|
6001
|
+
} else state = await deps.pollChecks();
|
|
5990
6002
|
} catch (e) {
|
|
5991
6003
|
if (!isRetryableGitHubWaitReadError(e)) {
|
|
5992
6004
|
return { policy, status: "failure", reason: waitReadFailureMessage(e), detail: "github-read-failed", waitedMs: now() - started };
|
|
@@ -6032,7 +6044,13 @@ async function waitForPrChecks(deps) {
|
|
|
6032
6044
|
if (state === "success") {
|
|
6033
6045
|
successStreak += 1;
|
|
6034
6046
|
if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
|
|
6035
|
-
return {
|
|
6047
|
+
return {
|
|
6048
|
+
policy,
|
|
6049
|
+
status: "success",
|
|
6050
|
+
reason,
|
|
6051
|
+
detail: "checks-success",
|
|
6052
|
+
...lastEvidence ? { headSha: lastEvidence.headSha, checks: lastEvidence.checks, workflowRuns: lastEvidence.workflows } : {}
|
|
6053
|
+
};
|
|
6036
6054
|
}
|
|
6037
6055
|
lastDetail = "confirming-success";
|
|
6038
6056
|
await deps.sleep(PR_CHECKS_CONFIRM_MS);
|
|
@@ -6347,6 +6365,36 @@ async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredCont
|
|
|
6347
6365
|
const { relevant } = partitionByRequired(entries, requiredContexts);
|
|
6348
6366
|
return derivePollState(relevant);
|
|
6349
6367
|
}
|
|
6368
|
+
async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
|
|
6369
|
+
const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
|
|
6370
|
+
const [entries, rawRuns] = await Promise.all([
|
|
6371
|
+
fetchHeadCheckEntries(snapshot.headSha, repo, gh2),
|
|
6372
|
+
gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(snapshot.headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
|
|
6373
|
+
]);
|
|
6374
|
+
const checks = entries.filter((entry) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(entry.name)).map((entry) => ({ name: entry.name, state: entry.bucket }));
|
|
6375
|
+
const workflows = parseNdjsonLines(rawRuns).flatMap((run) => {
|
|
6376
|
+
if (run.head_sha !== snapshot.headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
|
|
6377
|
+
if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name)) return [];
|
|
6378
|
+
return [{
|
|
6379
|
+
id: run.id,
|
|
6380
|
+
name: run.name,
|
|
6381
|
+
headSha: snapshot.headSha,
|
|
6382
|
+
status: run.status,
|
|
6383
|
+
conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
|
|
6384
|
+
...typeof run.html_url === "string" ? { url: run.html_url } : {}
|
|
6385
|
+
}];
|
|
6386
|
+
});
|
|
6387
|
+
const workflowBuckets = workflows.map((run) => {
|
|
6388
|
+
if (run.status !== "completed") return "pending";
|
|
6389
|
+
return run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped" ? "pass" : "fail";
|
|
6390
|
+
});
|
|
6391
|
+
return {
|
|
6392
|
+
state: derivePollState([...checks.map((check) => check.state), ...workflowBuckets]),
|
|
6393
|
+
headSha: snapshot.headSha,
|
|
6394
|
+
checks,
|
|
6395
|
+
workflows
|
|
6396
|
+
};
|
|
6397
|
+
}
|
|
6350
6398
|
async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
|
|
6351
6399
|
return derivePollState(partitionByRequired(await fetchHeadCheckEntries(sha, repo, gh2), null).relevant);
|
|
6352
6400
|
}
|
|
@@ -13143,6 +13191,7 @@ function deriveLoopStaleness(item, loopState, now = Date.now()) {
|
|
|
13143
13191
|
};
|
|
13144
13192
|
}
|
|
13145
13193
|
var CLAIM_MARKER_PATTERN = /<!--\s*mmi-claim:\s*(\{[\s\S]*?\})\s*-->/g;
|
|
13194
|
+
var CLAIM_RELEASE_PATTERN = /<!--\s*mmi-claim-release:\s*(\{[\s\S]*?\})\s*-->/g;
|
|
13146
13195
|
var CLAIM_MARKER_LIVE_MS = 24 * 60 * 6e4;
|
|
13147
13196
|
var CLAIM_STAMP_IDLE_MS = 4 * 60 * 6e4;
|
|
13148
13197
|
function isValidClaimMarker(parsed) {
|
|
@@ -13161,10 +13210,36 @@ function parseClaimMarkers(body) {
|
|
|
13161
13210
|
}
|
|
13162
13211
|
return out;
|
|
13163
13212
|
}
|
|
13213
|
+
function isValidClaimRelease(parsed) {
|
|
13214
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
13215
|
+
const release = parsed;
|
|
13216
|
+
return typeof release.v === "number" && typeof release.ts === "string" && Number.isFinite(Date.parse(release.ts)) && (release.session === void 0 || typeof release.session === "string") && (release.surface === void 0 || typeof release.surface === "string") && (release.host === void 0 || typeof release.host === "string");
|
|
13217
|
+
}
|
|
13218
|
+
function parseClaimReleases(body) {
|
|
13219
|
+
const out = [];
|
|
13220
|
+
for (const match of body.matchAll(CLAIM_RELEASE_PATTERN)) {
|
|
13221
|
+
try {
|
|
13222
|
+
const parsed = JSON.parse(match[1]);
|
|
13223
|
+
if (isValidClaimRelease(parsed)) out.push(parsed);
|
|
13224
|
+
} catch {
|
|
13225
|
+
}
|
|
13226
|
+
}
|
|
13227
|
+
return out;
|
|
13228
|
+
}
|
|
13229
|
+
function renderClaimReleaseBody(release) {
|
|
13230
|
+
return `Released by \`${formatSessionIdentity(release)}\` at ${release.ts}.
|
|
13231
|
+
|
|
13232
|
+
<!-- mmi-claim-release: ${JSON.stringify(release)} -->`;
|
|
13233
|
+
}
|
|
13164
13234
|
function latestClaimMarker(comments) {
|
|
13165
13235
|
for (let i = comments.length - 1; i >= 0; i -= 1) {
|
|
13166
|
-
const
|
|
13167
|
-
|
|
13236
|
+
const body = comments[i].body;
|
|
13237
|
+
const markers = parseClaimMarkers(body);
|
|
13238
|
+
const releases = parseClaimReleases(body);
|
|
13239
|
+
if (!markers.length && !releases.length) continue;
|
|
13240
|
+
if (!releases.length) return markers[markers.length - 1];
|
|
13241
|
+
if (!markers.length) return void 0;
|
|
13242
|
+
return body.lastIndexOf("mmi-claim-release:") > body.lastIndexOf("mmi-claim:") ? void 0 : markers[markers.length - 1];
|
|
13168
13243
|
}
|
|
13169
13244
|
return void 0;
|
|
13170
13245
|
}
|
|
@@ -13198,7 +13273,7 @@ function claimFromMarker(item, marker, linked = {}, now = Date.now()) {
|
|
|
13198
13273
|
holder: describeClaimMarker(marker),
|
|
13199
13274
|
ageMs: now - Date.parse(marker.ts),
|
|
13200
13275
|
currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress",
|
|
13201
|
-
...claimStampStaleness(marker, { activityAt: [
|
|
13276
|
+
...claimStampStaleness(marker, { activityAt: [...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
|
|
13202
13277
|
};
|
|
13203
13278
|
}
|
|
13204
13279
|
function classifyClaimableMarker(comments, actor, now = Date.now(), activityAt = []) {
|
|
@@ -13280,7 +13355,7 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
|
|
|
13280
13355
|
warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
|
|
13281
13356
|
continue;
|
|
13282
13357
|
}
|
|
13283
|
-
const verdict = classifyClaimableMarker(comments, actor, deps.now
|
|
13358
|
+
const verdict = classifyClaimableMarker(comments, actor, deps.now);
|
|
13284
13359
|
if (verdict.verdict === "claimable") {
|
|
13285
13360
|
stillClaimable.push(item);
|
|
13286
13361
|
} else if (verdict.verdict === "owned") {
|
|
@@ -14201,6 +14276,20 @@ async function unclaimBoardIssue(options, deps = {}) {
|
|
|
14201
14276
|
if (!options.allowPartial) throw new Error(warning);
|
|
14202
14277
|
return { item, viewer, repo: currentRepo, status: item.status, partial: true, warning };
|
|
14203
14278
|
}
|
|
14279
|
+
try {
|
|
14280
|
+
const actor = deps.session ?? describeSessionIdentity();
|
|
14281
|
+
const release = {
|
|
14282
|
+
v: 1,
|
|
14283
|
+
session: actor.session,
|
|
14284
|
+
surface: actor.surface,
|
|
14285
|
+
host: actor.host,
|
|
14286
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
14287
|
+
};
|
|
14288
|
+
await postIssueComment(client, { ref: item.ref, body: renderClaimReleaseBody(release) });
|
|
14289
|
+
} catch (e) {
|
|
14290
|
+
process.stderr.write(`warning: ${item.ref} was unclaimed, but recording the release marker comment failed: ${ghError(e)}
|
|
14291
|
+
`);
|
|
14292
|
+
}
|
|
14204
14293
|
const optionId = cfg.statusOptions[toStatus];
|
|
14205
14294
|
let unclaimCredential = "user";
|
|
14206
14295
|
try {
|
|
@@ -15224,7 +15313,7 @@ function createDocsIndexDeps(repoRoot2) {
|
|
|
15224
15313
|
|
|
15225
15314
|
// src/gate-budget.ts
|
|
15226
15315
|
var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
|
|
15227
|
-
var BLESSED_RUNNER_GATE_SHA = "
|
|
15316
|
+
var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
|
|
15228
15317
|
var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
|
|
15229
15318
|
var LOCAL_USE = "./.github/actions/run-with-budget";
|
|
15230
15319
|
var FULL_SHA = /^[0-9a-f]{40}$/;
|
|
@@ -17256,10 +17345,10 @@ var rollout_plan_default = {
|
|
|
17256
17345
|
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)."
|
|
17257
17346
|
},
|
|
17258
17347
|
baseline: {
|
|
17259
|
-
version: "4.5.
|
|
17260
|
-
tag: "v4.5.
|
|
17261
|
-
commit: "
|
|
17262
|
-
npm: "@mutmutco/cli@4.5.
|
|
17348
|
+
version: "4.5.98",
|
|
17349
|
+
tag: "v4.5.98",
|
|
17350
|
+
commit: "125c59187d10",
|
|
17351
|
+
npm: "@mutmutco/cli@4.5.98"
|
|
17263
17352
|
},
|
|
17264
17353
|
exitCriterion: "fleet-n-of-n",
|
|
17265
17354
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17276,14 +17365,14 @@ var rollout_plan_default = {
|
|
|
17276
17365
|
repo: "mutmutco/mmi-hub",
|
|
17277
17366
|
role: "canary",
|
|
17278
17367
|
schedule: "train",
|
|
17279
|
-
v3Target: "v4.5.
|
|
17368
|
+
v3Target: "v4.5.98"
|
|
17280
17369
|
}
|
|
17281
17370
|
],
|
|
17282
17371
|
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.",
|
|
17283
17372
|
rollback: {
|
|
17284
17373
|
independent: true,
|
|
17285
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17286
|
-
v3Target: "v4.5.
|
|
17374
|
+
mechanism: "npm dist-tag latest -> 4.5.98 and redeploy the Hub Lambda from tag v4.5.98 (125c59187d10); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17375
|
+
v3Target: "v4.5.98 (@mutmutco/cli@4.5.98, tag commit 125c59187d10 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17287
17376
|
}
|
|
17288
17377
|
},
|
|
17289
17378
|
{
|
|
@@ -30465,8 +30554,10 @@ function trainPlanTargetsFromTags(tags, explicitRaw) {
|
|
|
30465
30554
|
}
|
|
30466
30555
|
function derivedStagePlan(derived, shell2, stops = true) {
|
|
30467
30556
|
const { port } = derived;
|
|
30468
|
-
const
|
|
30469
|
-
const
|
|
30557
|
+
const baseOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
|
|
30558
|
+
const projectName = derived.config.env?.COMPOSE_PROJECT_NAME?.trim();
|
|
30559
|
+
const envOrder = projectName ? [...baseOrder, "COMPOSE_PROJECT_NAME"] : baseOrder;
|
|
30560
|
+
const resolved = { MMI_STAGE: "dev", MMI_PORT: String(port), COMPOSE_PROFILES: "local", ...projectName ? { COMPOSE_PROJECT_NAME: projectName } : {} };
|
|
30470
30561
|
const ensureEnv = shell2 === "powershell" ? `if (-not (Test-Path -LiteralPath '.env')) { Copy-Item -LiteralPath '.env.example' -Destination '.env' }` : `[ -f .env ] || cp .env.example .env`;
|
|
30471
30562
|
const envPrefix = shell2 === "powershell" ? envOrder.map((k) => `$env:${k}='${resolved[k]}'`).join("; ") : envOrder.map((k) => `${k}=${resolved[k]}`).join(" ");
|
|
30472
30563
|
const build = shell2 === "powershell" ? `${envPrefix}; ${derived.config.build ?? "docker compose build --no-cache"}` : `${envPrefix} ${derived.config.build ?? "docker compose build --no-cache"}`;
|
|
@@ -30871,7 +30962,7 @@ init_hub_auth();
|
|
|
30871
30962
|
init_github_client();
|
|
30872
30963
|
|
|
30873
30964
|
// src/project-set.ts
|
|
30874
|
-
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"];
|
|
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", "internalPeers"];
|
|
30875
30966
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
30876
30967
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
30877
30968
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -31406,6 +31497,7 @@ var SETTABLE_VAR_KEYS = [
|
|
|
31406
31497
|
"requiredRuntimeSecrets",
|
|
31407
31498
|
"requiredBuildSecrets",
|
|
31408
31499
|
"tenantTasks",
|
|
31500
|
+
"internalPeers",
|
|
31409
31501
|
"edgeDomains",
|
|
31410
31502
|
"statusFieldId",
|
|
31411
31503
|
"statusOptions",
|
|
@@ -31444,6 +31536,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
31444
31536
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
31445
31537
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
31446
31538
|
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',
|
|
31539
|
+
internalPeers: "JSON {owner/repo:[dev|rc|main]} \u2014 same-box suite peers via the box Caddy; clear with --unset internalPeers (#7886)",
|
|
31447
31540
|
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",
|
|
31448
31541
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
31449
31542
|
statusOptions: "JSON name\u2192id map",
|
|
@@ -31520,6 +31613,26 @@ function clearSecretsConfirmationError(unsets, existing, confirmed) {
|
|
|
31520
31613
|
if (confirmed) return null;
|
|
31521
31614
|
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}.`;
|
|
31522
31615
|
}
|
|
31616
|
+
function parseInternalPeersVar(raw) {
|
|
31617
|
+
let parsed;
|
|
31618
|
+
try {
|
|
31619
|
+
parsed = JSON.parse(raw);
|
|
31620
|
+
} catch {
|
|
31621
|
+
throw new Error("org project set: internalPeers must be valid JSON");
|
|
31622
|
+
}
|
|
31623
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("org project set: internalPeers must be a JSON object");
|
|
31624
|
+
const entries = Object.entries(parsed);
|
|
31625
|
+
if (entries.length < 1 || entries.length > 20) throw new Error("org project set: internalPeers holds 1-20 peer declarations");
|
|
31626
|
+
const repoRe = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
31627
|
+
const validStages = /* @__PURE__ */ new Set(["dev", "rc", "main"]);
|
|
31628
|
+
const out = {};
|
|
31629
|
+
for (const [repo, rawStages] of entries) {
|
|
31630
|
+
if (!repoRe.test(repo)) throw new Error("org project set: internalPeers key must look like owner/name \u2014 got " + JSON.stringify(repo));
|
|
31631
|
+
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");
|
|
31632
|
+
out[repo] = rawStages;
|
|
31633
|
+
}
|
|
31634
|
+
return out;
|
|
31635
|
+
}
|
|
31523
31636
|
function parseSuiteHoldVar(raw) {
|
|
31524
31637
|
let parsed;
|
|
31525
31638
|
try {
|
|
@@ -31582,6 +31695,8 @@ function buildProjectSetPatch(input) {
|
|
|
31582
31695
|
patch[key] = parseBuildSecretsVar(raw);
|
|
31583
31696
|
} else if (key === "tenantTasks") {
|
|
31584
31697
|
patch[key] = parseTenantTasksVar(raw);
|
|
31698
|
+
} else if (key === "internalPeers") {
|
|
31699
|
+
patch[key] = parseInternalPeersVar(raw);
|
|
31585
31700
|
} else if (key === "secrets") {
|
|
31586
31701
|
patch[key] = parseSecretsCatalogVar(raw);
|
|
31587
31702
|
} else if (key === "edgeDomains") {
|
|
@@ -36456,7 +36571,7 @@ function readSuiteHotfix(path2) {
|
|
|
36456
36571
|
} catch {
|
|
36457
36572
|
throw new Error(`suite hotfix record unreadable at ${path2}; repair it, never restart`);
|
|
36458
36573
|
}
|
|
36459
|
-
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)
|
|
36574
|
+
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)
|
|
36460
36575
|
throw new Error(`suite hotfix record has unknown or invalid state at ${path2}; repair it, never restart`);
|
|
36461
36576
|
return r;
|
|
36462
36577
|
}
|
|
@@ -36471,8 +36586,10 @@ async function suiteHotfix(deps, opts) {
|
|
|
36471
36586
|
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`);
|
|
36472
36587
|
const held = await deps.hold(found);
|
|
36473
36588
|
if (held) throw new Error(`${found.repo} is HELD (${held.reason}); clear it with suite hold clear before a suite hotfix; nothing was written`);
|
|
36589
|
+
const mainPath = mainBundlePath(found.bundles);
|
|
36474
36590
|
const plan = [
|
|
36475
36591
|
`${found.repo}: hotfix start --from ${opts.from} + release (suite-solo, in-process)`,
|
|
36592
|
+
`${found.repo}: publish main bundle (module-rc-publish channel=main) at ${mainPath}`,
|
|
36476
36593
|
`${SUITE_REPO}: main pin PR for ${found.id}, wait for merge`,
|
|
36477
36594
|
`${SUITE_REPO}: hotfix (patch) carrying only the merged pin PR onto main`,
|
|
36478
36595
|
`${SUITE_REPO}: record suite version -> member versions on its release`
|
|
@@ -36491,6 +36608,7 @@ async function suiteHotfix(deps, opts) {
|
|
|
36491
36608
|
const save = () => writeSuiteHotfix(path2, rec);
|
|
36492
36609
|
save();
|
|
36493
36610
|
const resume = `rerun suite hotfix ${found.id} --from ${rec.from} --owner-go to resume`;
|
|
36611
|
+
if (!rec.released) await deps.preflightMain(found.repo, mainPath);
|
|
36494
36612
|
if (!rec.tag) {
|
|
36495
36613
|
const started = await deps.hotfixStart(found.repo, { from: rec.from, ack: rec.ack });
|
|
36496
36614
|
if (!RELEASE_TAG.test(started.tag)) throw new Error(`${found.repo}: hotfix start returned no vX.Y.Z tag`);
|
|
@@ -36508,6 +36626,15 @@ async function suiteHotfix(deps, opts) {
|
|
|
36508
36626
|
rec.released = true;
|
|
36509
36627
|
save();
|
|
36510
36628
|
}
|
|
36629
|
+
if (!rec.mainPublished) {
|
|
36630
|
+
try {
|
|
36631
|
+
await deps.publishMain(found.repo, rec.tag, mainPath);
|
|
36632
|
+
} catch (e) {
|
|
36633
|
+
throw new Error(`${found.repo}: main bundle publish failed (${e.message}); ${rec.tag} is released: ${resume}`);
|
|
36634
|
+
}
|
|
36635
|
+
rec.mainPublished = true;
|
|
36636
|
+
save();
|
|
36637
|
+
}
|
|
36511
36638
|
if (!rec.pinMerged) {
|
|
36512
36639
|
const pin = await deps.soloPin(found.repo, rec.tag);
|
|
36513
36640
|
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`);
|
|
@@ -39572,11 +39699,6 @@ var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["development", "main", "master
|
|
|
39572
39699
|
function isProtectedBranch(branch) {
|
|
39573
39700
|
return PROTECTED_BRANCHES.has(branch.trim());
|
|
39574
39701
|
}
|
|
39575
|
-
function deriveComposeProjectName(worktreePath) {
|
|
39576
|
-
const base = worktreePath.replace(/\\/g, "/").replace(/\/+$/, "").split("/").pop()?.toLowerCase() ?? "";
|
|
39577
|
-
const name = base.replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
|
|
39578
|
-
return name || void 0;
|
|
39579
|
-
}
|
|
39580
39702
|
|
|
39581
39703
|
// src/pr-merge-helpers.ts
|
|
39582
39704
|
function buildPrMergeArgs(input) {
|
|
@@ -41789,6 +41911,53 @@ function parsePrChecksTable(stdout) {
|
|
|
41789
41911
|
}
|
|
41790
41912
|
return rows;
|
|
41791
41913
|
}
|
|
41914
|
+
function includeMissingRequiredChecks(rows, required) {
|
|
41915
|
+
const reported = new Set(rows.map((row) => row.name));
|
|
41916
|
+
const missing = [...new Set(required)].filter((name) => !reported.has(name)).map((name) => ({
|
|
41917
|
+
name,
|
|
41918
|
+
status: "missing",
|
|
41919
|
+
details: "expected required check has not reported"
|
|
41920
|
+
}));
|
|
41921
|
+
return [...rows, ...missing];
|
|
41922
|
+
}
|
|
41923
|
+
async function requiredCheckContexts(repo, prNumber) {
|
|
41924
|
+
const { stdout: prOutput } = await execFileP("gh", ["pr", "view", String(prNumber), "--repo", repo, "--json", "baseRefName"], { timeout: GC_GH_TIMEOUT_MS2 });
|
|
41925
|
+
const baseRef = JSON.parse(prOutput).baseRefName;
|
|
41926
|
+
if (typeof baseRef !== "string" || !baseRef) throw new Error("pull request response had no base branch");
|
|
41927
|
+
const read = async (path2) => {
|
|
41928
|
+
try {
|
|
41929
|
+
const { stdout } = await execFileP("gh", ["api", path2], { timeout: GC_GH_TIMEOUT_MS2 });
|
|
41930
|
+
return JSON.parse(stdout);
|
|
41931
|
+
} catch (e) {
|
|
41932
|
+
const err = e;
|
|
41933
|
+
if (/\b404\b|Not Found/i.test(`${err.stderr ?? ""} ${err.message ?? ""}`)) return null;
|
|
41934
|
+
throw e;
|
|
41935
|
+
}
|
|
41936
|
+
};
|
|
41937
|
+
const [classic, rules] = await Promise.all([
|
|
41938
|
+
read(`repos/${repo}/branches/${encodeURIComponent(baseRef)}/protection/required_status_checks`),
|
|
41939
|
+
read(`repos/${repo}/rules/branches/${encodeURIComponent(baseRef)}`)
|
|
41940
|
+
]);
|
|
41941
|
+
const contexts = /* @__PURE__ */ new Set();
|
|
41942
|
+
const oldContexts = classic?.contexts;
|
|
41943
|
+
if (Array.isArray(oldContexts)) {
|
|
41944
|
+
for (const name of oldContexts) if (typeof name === "string") contexts.add(name);
|
|
41945
|
+
}
|
|
41946
|
+
if (Array.isArray(rules)) {
|
|
41947
|
+
for (const rule of rules) {
|
|
41948
|
+
const required = rule.type === "required_status_checks" ? rule.parameters?.required_status_checks : void 0;
|
|
41949
|
+
if (Array.isArray(required)) for (const check of required) {
|
|
41950
|
+
const name = check?.context;
|
|
41951
|
+
if (typeof name === "string") contexts.add(name);
|
|
41952
|
+
}
|
|
41953
|
+
}
|
|
41954
|
+
}
|
|
41955
|
+
return [...contexts];
|
|
41956
|
+
}
|
|
41957
|
+
function checksStateWithExpected(state, rows, required) {
|
|
41958
|
+
const checks = includeMissingRequiredChecks(rows, required);
|
|
41959
|
+
return checks.some((row) => row.status === "missing") && (state === "success" || state === "no-checks-reported") ? { state: "incomplete", rows: checks } : { state, rows: checks };
|
|
41960
|
+
}
|
|
41792
41961
|
function parseLinkedIssues(body) {
|
|
41793
41962
|
const refs = /* @__PURE__ */ new Set();
|
|
41794
41963
|
const re = /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved|ref|refs|re|references)\s+#(\d+)\b/gi;
|
|
@@ -42049,6 +42218,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
42049
42218
|
return stdout;
|
|
42050
42219
|
};
|
|
42051
42220
|
const withLogs = async (rows) => o.logs ? attachFailureLogs(rows, fetchLog) : rows;
|
|
42221
|
+
let expected = [];
|
|
42052
42222
|
const snap = async () => {
|
|
42053
42223
|
let stdout = "";
|
|
42054
42224
|
let stderr = "";
|
|
@@ -42063,14 +42233,15 @@ function registerPrLifecycleCommands(program3) {
|
|
|
42063
42233
|
const parsed = parseGhPrChecksResult(stdout, stderr);
|
|
42064
42234
|
const state = pollStateFromParsed(parsed);
|
|
42065
42235
|
const rows = parsePrChecksTable(stdout);
|
|
42066
|
-
return
|
|
42236
|
+
return checksStateWithExpected(state, rows, expected);
|
|
42067
42237
|
};
|
|
42068
42238
|
try {
|
|
42239
|
+
expected = await requiredCheckContexts(repo, n);
|
|
42069
42240
|
if (!o.watch) {
|
|
42070
42241
|
const { state, rows } = await snap();
|
|
42071
42242
|
const output = { state, checks: await withLogs(rows) };
|
|
42072
42243
|
console.log(JSON.stringify(output));
|
|
42073
|
-
if (state === "failure" || state === "failing") process.exitCode = 1;
|
|
42244
|
+
if (state === "failure" || state === "failing" || state === "incomplete") process.exitCode = 1;
|
|
42074
42245
|
return;
|
|
42075
42246
|
}
|
|
42076
42247
|
const deadline = Date.now() + CHECKS_WATCH_TIMEOUT_MS;
|
|
@@ -42801,11 +42972,18 @@ function parseDockerContainerLs(stdout) {
|
|
|
42801
42972
|
function containersPublishingPort(containers, port) {
|
|
42802
42973
|
return containers.filter((c) => c.publishedHostPorts.includes(port));
|
|
42803
42974
|
}
|
|
42975
|
+
function stageComposeProjectName(cwd) {
|
|
42976
|
+
const parts = cwd.replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
42977
|
+
const base = parts.pop() ?? "";
|
|
42978
|
+
const parent = parts.pop();
|
|
42979
|
+
const raw = parent ? `${parent}-${base}` : base;
|
|
42980
|
+
const name = raw.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
|
|
42981
|
+
return name || "mmi-stage";
|
|
42982
|
+
}
|
|
42804
42983
|
function isOwnComposeContainer(container, cwd) {
|
|
42805
42984
|
if (container.composeWorkingDir && normPath(container.composeWorkingDir) === normPath(cwd)) return true;
|
|
42806
42985
|
if (container.composeWorkingDir && pathUnder(container.composeWorkingDir, cwd)) return true;
|
|
42807
|
-
|
|
42808
|
-
return Boolean(derived && container.composeProject === derived);
|
|
42986
|
+
return container.composeProject === stageComposeProjectName(cwd);
|
|
42809
42987
|
}
|
|
42810
42988
|
function planStagePortTeardown(containers, port, cwd) {
|
|
42811
42989
|
const ownProjects = [];
|
|
@@ -43047,11 +43225,13 @@ function composeResolvesPort(cwd) {
|
|
|
43047
43225
|
return (0, import_node_fs38.existsSync)(envFile) && envFileKeys((0, import_node_fs38.readFileSync)(envFile, "utf8")).has("PORT");
|
|
43048
43226
|
}
|
|
43049
43227
|
function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
|
|
43228
|
+
const composeProject = config.env?.COMPOSE_PROJECT_NAME ?? vaultEnvMerge?.COMPOSE_PROJECT_NAME ?? stageComposeProjectName(cwd);
|
|
43050
43229
|
return {
|
|
43051
43230
|
// Vault secrets first so the stage-selection contract (MMI_STAGE/MMI_PORT/…) always wins on any collision.
|
|
43052
43231
|
...vaultEnvMerge ?? {},
|
|
43053
43232
|
...stageProcessEnv(stagePort, stageExtraEnv(config, stagePort)),
|
|
43054
|
-
...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {}
|
|
43233
|
+
...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {},
|
|
43234
|
+
COMPOSE_PROJECT_NAME: composeProject
|
|
43055
43235
|
};
|
|
43056
43236
|
}
|
|
43057
43237
|
function stageComposeFiles(cwd) {
|
|
@@ -43201,6 +43381,58 @@ async function killTree(pid) {
|
|
|
43201
43381
|
}
|
|
43202
43382
|
}
|
|
43203
43383
|
}
|
|
43384
|
+
async function describeExitedComposeServices(cwd, env) {
|
|
43385
|
+
const dockerEnv = { ...process.env, ...env };
|
|
43386
|
+
const run = (args) => execFileP3("docker", args, {
|
|
43387
|
+
cwd,
|
|
43388
|
+
windowsHide: true,
|
|
43389
|
+
timeout: DOCKER_TIMEOUT_MS,
|
|
43390
|
+
maxBuffer: 1024 * 1024 * 4,
|
|
43391
|
+
env: dockerEnv
|
|
43392
|
+
}).then(({ stdout }) => stdout);
|
|
43393
|
+
let rows;
|
|
43394
|
+
try {
|
|
43395
|
+
const text = (await run(["compose", "ps", "--all", "--format", "json"])).trim();
|
|
43396
|
+
if (!text) return void 0;
|
|
43397
|
+
try {
|
|
43398
|
+
const parsed = JSON.parse(text);
|
|
43399
|
+
rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
43400
|
+
} catch {
|
|
43401
|
+
rows = text.split(/\r?\n/).flatMap((line) => {
|
|
43402
|
+
try {
|
|
43403
|
+
return [JSON.parse(line)];
|
|
43404
|
+
} catch {
|
|
43405
|
+
return [];
|
|
43406
|
+
}
|
|
43407
|
+
});
|
|
43408
|
+
}
|
|
43409
|
+
} catch {
|
|
43410
|
+
return void 0;
|
|
43411
|
+
}
|
|
43412
|
+
const exited = rows.filter((row) => typeof row.State === "string" && /^(exited|dead)$/i.test(row.State));
|
|
43413
|
+
if (!exited.length) return void 0;
|
|
43414
|
+
const parts = [];
|
|
43415
|
+
for (const row of exited.slice(0, 3)) {
|
|
43416
|
+
const service = String(row.Service ?? row.Name ?? "unknown");
|
|
43417
|
+
let exit = row.ExitCode ?? row.exitCode;
|
|
43418
|
+
if (exit == null && row.ID) {
|
|
43419
|
+
try {
|
|
43420
|
+
exit = (await run(["inspect", "--format", "{{.State.ExitCode}}", String(row.ID)])).trim() || void 0;
|
|
43421
|
+
} catch {
|
|
43422
|
+
}
|
|
43423
|
+
}
|
|
43424
|
+
let tail = "";
|
|
43425
|
+
try {
|
|
43426
|
+
tail = (await run(["compose", "logs", "--no-color", "--tail", "50", service])).trim();
|
|
43427
|
+
} catch {
|
|
43428
|
+
}
|
|
43429
|
+
parts.push(`service '${service}' exited before health (exit ${exit ?? "unknown"})${tail ? ` \u2014 log tail:
|
|
43430
|
+
${tail}` : ""}`);
|
|
43431
|
+
}
|
|
43432
|
+
return `
|
|
43433
|
+
compose services exited before health:
|
|
43434
|
+
${parts.join("\n")}`;
|
|
43435
|
+
}
|
|
43204
43436
|
async function waitForHealth(url, timeoutMs, anyStatus = false) {
|
|
43205
43437
|
const deadline = Date.now() + timeoutMs;
|
|
43206
43438
|
let last = "";
|
|
@@ -43301,7 +43533,15 @@ async function startStage(config = {}, opts = {}) {
|
|
|
43301
43533
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
43302
43534
|
else await waitForProcessStability(child2);
|
|
43303
43535
|
} catch (e) {
|
|
43536
|
+
let detail;
|
|
43537
|
+
if (state.healthUrl) {
|
|
43538
|
+
try {
|
|
43539
|
+
detail = await describeExitedComposeServices(cwd, upEnv);
|
|
43540
|
+
} catch {
|
|
43541
|
+
}
|
|
43542
|
+
}
|
|
43304
43543
|
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd, upEnv);
|
|
43544
|
+
if (detail) throw new Error(`${e.message}${detail}`, { cause: e });
|
|
43305
43545
|
throw e;
|
|
43306
43546
|
}
|
|
43307
43547
|
const result = {
|
|
@@ -45242,6 +45482,11 @@ ${list}`);
|
|
|
45242
45482
|
} else {
|
|
45243
45483
|
printLine(`${label}: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
45244
45484
|
for (const line of failedChecksReceiptLines(label, result)) printLine(line);
|
|
45485
|
+
if (result.status === "success" && result.headSha) {
|
|
45486
|
+
const checks = result.checks?.map((check) => `${check.name}=${check.state}`).join(", ") || "none";
|
|
45487
|
+
const workflows = result.workflowRuns?.map((run) => `${run.name}#${run.id}=${run.status}/${run.conclusion ?? "pending"}`).join(", ") || "none";
|
|
45488
|
+
printLine(`${label}: verified head ${result.headSha}; checks: ${checks}; workflows: ${workflows}`);
|
|
45489
|
+
}
|
|
45245
45490
|
}
|
|
45246
45491
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
45247
45492
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
@@ -45294,6 +45539,7 @@ ${list}`);
|
|
|
45294
45539
|
// pending must hold the wait open, and a non-required red must fail it; #4396's scoping is the
|
|
45295
45540
|
// merge paths' contract (`pr land`, `pr merge --wait`), which mirror the GitHub merge button.
|
|
45296
45541
|
pollChecks: () => pollRestPrChecks(number, repo),
|
|
45542
|
+
pollChecksWithEvidence: () => pollRestPrChecksWithWorkflowEvidence(number, repo),
|
|
45297
45543
|
pollMergeable: () => pollRestPrMergeable(number, repo),
|
|
45298
45544
|
pollRateLimit: () => waitLoopCorePool("pr checks-wait"),
|
|
45299
45545
|
// #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
|
|
@@ -51812,10 +52058,13 @@ function deriveStage(inputs) {
|
|
|
51812
52058
|
// #2655: only bootstrap a `.env` when the repo ships `.env.example` (legacy). Without it, the stage is
|
|
51813
52059
|
// vault-native — the runner injects the declared dev secrets into the compose process env (no disk file).
|
|
51814
52060
|
...inputs.hasEnvExample ? { ensureEnv: { example: ".env.example", target: ".env" } } : {},
|
|
51815
|
-
//
|
|
51816
|
-
//
|
|
52061
|
+
// MMI_STAGE is the local dev stage name the estate compose contract selects on (`${MMI_STAGE:-dev}`
|
|
52062
|
+
// in the seeded compose template): `development` never matched it, so a derived stage booted with
|
|
52063
|
+
// the wrong stage identity (#7870). MMI_PORT is the host-published port (the free STAGE_PORT); PORT
|
|
52064
|
+
// is intentionally absent so compose reads the container's bind port from .env rather than this
|
|
52065
|
+
// overriding it to the publish port (#1505).
|
|
51817
52066
|
env: {
|
|
51818
|
-
MMI_STAGE: "
|
|
52067
|
+
MMI_STAGE: "dev",
|
|
51819
52068
|
MMI_PORT: "$STAGE_PORT",
|
|
51820
52069
|
COMPOSE_PROFILES: "local"
|
|
51821
52070
|
}
|
|
@@ -52014,13 +52263,20 @@ function registerStageCommands(program3) {
|
|
|
52014
52263
|
const portRangeMeta = project2?.portRange ?? void 0;
|
|
52015
52264
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
52016
52265
|
const repo = Array.isArray(project2?.repos) && typeof project2.repos[0] === "string" ? project2.repos[0] : slug;
|
|
52266
|
+
const resolution = decideStage({
|
|
52267
|
+
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
52268
|
+
hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
|
|
52269
|
+
hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
|
|
52270
|
+
repo
|
|
52271
|
+
});
|
|
52272
|
+
if (resolution.source === "derived" && resolution.derived) {
|
|
52273
|
+
resolution.derived.config.env = {
|
|
52274
|
+
...resolution.derived.config.env,
|
|
52275
|
+
COMPOSE_PROJECT_NAME: stageComposeProjectName(process.cwd())
|
|
52276
|
+
};
|
|
52277
|
+
}
|
|
52017
52278
|
return {
|
|
52018
|
-
resolution
|
|
52019
|
-
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
52020
|
-
hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
|
|
52021
|
-
hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
|
|
52022
|
-
repo
|
|
52023
|
-
}),
|
|
52279
|
+
resolution,
|
|
52024
52280
|
project: project2,
|
|
52025
52281
|
cfg
|
|
52026
52282
|
};
|
package/cli/package.json
CHANGED