@mutmutco/cli 4.3.7 → 4.3.9
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/dist/main.cjs +564 -223
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5226,7 +5226,7 @@ function unknownCommandDomainGuide(parentPath, token) {
|
|
|
5226
5226
|
}
|
|
5227
5227
|
|
|
5228
5228
|
// src/command-composition.ts
|
|
5229
|
-
var
|
|
5229
|
+
var import_node_os21 = require("node:os");
|
|
5230
5230
|
var import_node_path46 = require("node:path");
|
|
5231
5231
|
|
|
5232
5232
|
// src/board-read.ts
|
|
@@ -6998,6 +6998,19 @@ function isPromotionBase(base, track) {
|
|
|
6998
6998
|
}
|
|
6999
6999
|
|
|
7000
7000
|
// src/readiness-audit.ts
|
|
7001
|
+
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
7002
|
+
try {
|
|
7003
|
+
const res = await fetch(url, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
|
|
7004
|
+
return { ok: res.ok, status: res.status, url };
|
|
7005
|
+
} catch (e) {
|
|
7006
|
+
return { ok: false, error: e.message, url };
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
async function probePublicHealth(publicUrl) {
|
|
7010
|
+
const root = await probeHttpBounded(publicUrl);
|
|
7011
|
+
if (root.status !== 401 && root.status !== 403) return root;
|
|
7012
|
+
return probeHttpBounded(`${publicUrl.replace(/\/$/, "")}/health`);
|
|
7013
|
+
}
|
|
7001
7014
|
var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
|
|
7002
7015
|
function pickTenantDeployRun(rows, slug, stage) {
|
|
7003
7016
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
|
@@ -7016,7 +7029,8 @@ function tenantRuntimeHints(stage, fact, probe) {
|
|
|
7016
7029
|
if (!fact.sshHostPresent && fact.substrate === "hetzner-ssh") hints.push(`DEPLOY#${stage} has hetzner-ssh substrate but no sshHost presence; tenant-deploy cannot reach the box.`);
|
|
7017
7030
|
if (!fact.domain) hints.push(`DEPLOY#${stage} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
|
|
7018
7031
|
if (typeof fact.port !== "number") hints.push(`DEPLOY#${stage} has no edgeVhost.port; Caddy upstream/healthUrl hints cannot be checked.`);
|
|
7019
|
-
if (probe?.ok === false
|
|
7032
|
+
if (probe?.url?.endsWith("/health") && probe.ok === false) hints.push(`Root is auth-walled (401/403) and ${probe.url} answered ${probe.status ?? probe.error}; expose a 2xx /health route or fix the app \u2014 the edge is fine.`);
|
|
7033
|
+
else if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
|
|
7020
7034
|
if (stage === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
|
|
7021
7035
|
return hints;
|
|
7022
7036
|
}
|
|
@@ -9435,7 +9449,7 @@ function invalidReference(ref) {
|
|
|
9435
9449
|
return new Error(`invalid reference "${ref}" \u2014 expected ${ISSUE_REF_SHAPES}`);
|
|
9436
9450
|
}
|
|
9437
9451
|
function parseIssueRef(ref, expectedRepo) {
|
|
9438
|
-
const trimmed = ref.trim();
|
|
9452
|
+
const trimmed = String(ref).trim();
|
|
9439
9453
|
const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)$/i);
|
|
9440
9454
|
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
9441
9455
|
const bare = trimmed.match(/^#?(\d+)$/);
|
|
@@ -11061,14 +11075,7 @@ function writableOrUnknown(writable) {
|
|
|
11061
11075
|
function renderBoardSource() {
|
|
11062
11076
|
return "source: live";
|
|
11063
11077
|
}
|
|
11064
|
-
async function
|
|
11065
|
-
const cfg = resolveBoardConfig(options.config);
|
|
11066
|
-
const client = deps.client ?? defaultGitHubClient();
|
|
11067
|
-
let collected;
|
|
11068
|
-
let writable;
|
|
11069
|
-
let pullRequests;
|
|
11070
|
-
let github;
|
|
11071
|
-
let snapshotFallback;
|
|
11078
|
+
async function collectBoardPreferringSnapshot(cfg, options, deps) {
|
|
11072
11079
|
const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
|
|
11073
11080
|
{
|
|
11074
11081
|
owner: cfg.projectOwner,
|
|
@@ -11094,19 +11101,37 @@ async function readBoard(options, deps = {}) {
|
|
|
11094
11101
|
warnings.push(message2);
|
|
11095
11102
|
partial = true;
|
|
11096
11103
|
}
|
|
11104
|
+
return {
|
|
11105
|
+
collected: {
|
|
11106
|
+
items: read.items,
|
|
11107
|
+
viewer: snapshot.viewer,
|
|
11108
|
+
repo: currentRepo,
|
|
11109
|
+
projectId: snapshot.project.id,
|
|
11110
|
+
projectTitle: snapshot.project.title,
|
|
11111
|
+
warnings,
|
|
11112
|
+
partial
|
|
11113
|
+
},
|
|
11114
|
+
snapshot
|
|
11115
|
+
};
|
|
11116
|
+
}
|
|
11117
|
+
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
|
|
11118
|
+
if (attempt?.state === "unavailable") {
|
|
11119
|
+
collected.warnings.push(`Hub board snapshot unavailable (${attempt.reason}) \u2014 served by the direct user-auth read (emergency fallback)`);
|
|
11120
|
+
}
|
|
11121
|
+
return { collected };
|
|
11122
|
+
}
|
|
11123
|
+
async function readBoard(options, deps = {}) {
|
|
11124
|
+
const cfg = resolveBoardConfig(options.config);
|
|
11125
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
11126
|
+
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
11127
|
+
let writable;
|
|
11128
|
+
let pullRequests;
|
|
11129
|
+
let github;
|
|
11130
|
+
if (snapshot) {
|
|
11097
11131
|
for (const unreadable of snapshot.unreadableRepos) {
|
|
11098
|
-
warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
|
|
11099
|
-
partial = true;
|
|
11132
|
+
collected.warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
|
|
11133
|
+
collected.partial = true;
|
|
11100
11134
|
}
|
|
11101
|
-
collected = {
|
|
11102
|
-
items: read.items,
|
|
11103
|
-
viewer: snapshot.viewer,
|
|
11104
|
-
repo: currentRepo,
|
|
11105
|
-
projectId: snapshot.project.id,
|
|
11106
|
-
projectTitle: snapshot.project.title,
|
|
11107
|
-
warnings,
|
|
11108
|
-
partial
|
|
11109
|
-
};
|
|
11110
11135
|
writable = {
|
|
11111
11136
|
repos: new Set(snapshot.writableRepos.map((repo) => repo.toLowerCase())),
|
|
11112
11137
|
unknown: new Set(snapshot.unreadableRepos.map((entry) => entry.repo.toLowerCase()))
|
|
@@ -11114,11 +11139,6 @@ async function readBoard(options, deps = {}) {
|
|
|
11114
11139
|
pullRequests = snapshot.pullRequests;
|
|
11115
11140
|
github = snapshot.github;
|
|
11116
11141
|
} else {
|
|
11117
|
-
if (attempt?.state === "unavailable") snapshotFallback = attempt.reason;
|
|
11118
|
-
collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
|
|
11119
|
-
if (snapshotFallback) {
|
|
11120
|
-
collected.warnings.push(`Hub board snapshot unavailable (${snapshotFallback}) \u2014 served by the direct user-auth read (emergency fallback)`);
|
|
11121
|
-
}
|
|
11122
11142
|
const probed = await resolveWritableReposForClaimables(collected.items, client);
|
|
11123
11143
|
collected.warnings.push(...probed.warnings);
|
|
11124
11144
|
collected.partial = collected.partial || probed.partial;
|
|
@@ -12057,21 +12077,45 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
12057
12077
|
partial: false
|
|
12058
12078
|
};
|
|
12059
12079
|
}
|
|
12060
|
-
async function
|
|
12080
|
+
async function resolveClaimWritable(collected, client, snapshot, unscanned) {
|
|
12081
|
+
const probe = async (items) => {
|
|
12082
|
+
const probed2 = await resolveWritableReposForClaimables(items, client);
|
|
12083
|
+
collected.warnings.push(...probed2.warnings);
|
|
12084
|
+
collected.partial = collected.partial || probed2.partial;
|
|
12085
|
+
return probed2;
|
|
12086
|
+
};
|
|
12087
|
+
if (!snapshot) {
|
|
12088
|
+
const probed2 = await probe(collected.items);
|
|
12089
|
+
return { repos: probed2.repos, unknown: probed2.unknown };
|
|
12090
|
+
}
|
|
12091
|
+
for (const unreadable of snapshot.unreadableRepos) {
|
|
12092
|
+
collected.warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
|
|
12093
|
+
collected.partial = true;
|
|
12094
|
+
}
|
|
12095
|
+
const repos = new Set(snapshot.writableRepos.map((repo) => repo.toLowerCase()));
|
|
12096
|
+
const unknown = new Set(snapshot.unreadableRepos.map((entry) => entry.repo.toLowerCase()));
|
|
12097
|
+
const missed = unscanned.filter((item) => !repos.has(item.repository.toLowerCase()) && !unknown.has(item.repository.toLowerCase()));
|
|
12098
|
+
if (!missed.length) return { repos, unknown };
|
|
12099
|
+
const probed = await probe(missed);
|
|
12100
|
+
return { repos: /* @__PURE__ */ new Set([...repos, ...probed.repos]), unknown: /* @__PURE__ */ new Set([...unknown, ...probed.unknown]) };
|
|
12101
|
+
}
|
|
12102
|
+
async function prepareClaimContext(options, selectors, deps, collected, snapshot) {
|
|
12061
12103
|
const cfg = resolveBoardConfig(options.config);
|
|
12062
12104
|
const client = deps.client ?? defaultGitHubClient();
|
|
12063
12105
|
const board = { owner: cfg.projectOwner, number: cfg.projectNumber };
|
|
12106
|
+
const unscanned = [];
|
|
12064
12107
|
for (const selector of selectors) {
|
|
12065
12108
|
try {
|
|
12066
12109
|
findBoardItem(collected.items, selector, board);
|
|
12067
12110
|
} catch {
|
|
12068
12111
|
const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
|
|
12069
|
-
if (fallback)
|
|
12112
|
+
if (fallback) {
|
|
12113
|
+
collected.items.push(fallback);
|
|
12114
|
+
unscanned.push(fallback);
|
|
12115
|
+
}
|
|
12070
12116
|
}
|
|
12071
12117
|
}
|
|
12072
|
-
const writable = await
|
|
12073
|
-
collected.warnings.push(...writable.warnings);
|
|
12074
|
-
collected.partial = collected.partial || writable.partial;
|
|
12118
|
+
const writable = await resolveClaimWritable(collected, client, snapshot, unscanned);
|
|
12075
12119
|
const report = {
|
|
12076
12120
|
project: { owner: cfg.projectOwner, number: cfg.projectNumber, id: collected.projectId, title: collected.projectTitle || String(cfg.projectNumber) },
|
|
12077
12121
|
viewer: collected.viewer,
|
|
@@ -12209,14 +12253,15 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12209
12253
|
}
|
|
12210
12254
|
async function claimBoardIssue(options, deps = {}) {
|
|
12211
12255
|
const cfg = resolveBoardConfig(options.config);
|
|
12212
|
-
const collected = await
|
|
12256
|
+
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
12213
12257
|
const selector = parseIssueSelector(options.selector, collected.repo, options.repo);
|
|
12214
|
-
const ctx = await prepareClaimContext(options, [selector], deps, collected);
|
|
12215
|
-
|
|
12258
|
+
const ctx = await prepareClaimContext(options, [selector], deps, collected, snapshot);
|
|
12259
|
+
const result = await claimOneBoardItem(ctx, selector, options);
|
|
12260
|
+
return ctx.report.warnings.length ? { ...result, warnings: [...ctx.report.warnings] } : result;
|
|
12216
12261
|
}
|
|
12217
12262
|
async function claimBoardIssues(options, deps = {}) {
|
|
12218
12263
|
const cfg = resolveBoardConfig(options.config);
|
|
12219
|
-
const collected = await
|
|
12264
|
+
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
12220
12265
|
const selectors = [];
|
|
12221
12266
|
const seen = /* @__PURE__ */ new Set();
|
|
12222
12267
|
for (const raw of options.selectors) {
|
|
@@ -12226,7 +12271,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12226
12271
|
seen.add(key);
|
|
12227
12272
|
selectors.push(selector);
|
|
12228
12273
|
}
|
|
12229
|
-
const ctx = await prepareClaimContext(options, selectors, deps, collected);
|
|
12274
|
+
const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
|
|
12230
12275
|
const results = new Array(selectors.length);
|
|
12231
12276
|
let next = 0;
|
|
12232
12277
|
const worker = async () => {
|
|
@@ -12247,7 +12292,9 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12247
12292
|
viewer: ctx.report.viewer,
|
|
12248
12293
|
repo: ctx.report.repo,
|
|
12249
12294
|
results,
|
|
12250
|
-
failed: results.filter((result) => !result.claimed).length
|
|
12295
|
+
failed: results.filter((result) => !result.claimed).length,
|
|
12296
|
+
// #6162: as in claimBoardIssue — the emergency user-auth fallback is named with the verdict.
|
|
12297
|
+
...ctx.report.warnings.length ? { warnings: [...ctx.report.warnings] } : {}
|
|
12251
12298
|
};
|
|
12252
12299
|
}
|
|
12253
12300
|
async function moveBoardIssues(options, deps = {}) {
|
|
@@ -15403,10 +15450,10 @@ var rollout_plan_default = {
|
|
|
15403
15450
|
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)."
|
|
15404
15451
|
},
|
|
15405
15452
|
baseline: {
|
|
15406
|
-
version: "4.3.
|
|
15407
|
-
tag: "v4.3.
|
|
15408
|
-
commit: "
|
|
15409
|
-
npm: "@mutmutco/cli@4.3.
|
|
15453
|
+
version: "4.3.9",
|
|
15454
|
+
tag: "v4.3.9",
|
|
15455
|
+
commit: "64fc1be95e37",
|
|
15456
|
+
npm: "@mutmutco/cli@4.3.9"
|
|
15410
15457
|
},
|
|
15411
15458
|
exitCriterion: "fleet-n-of-n",
|
|
15412
15459
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15423,14 +15470,14 @@ var rollout_plan_default = {
|
|
|
15423
15470
|
repo: "mutmutco/mmi-hub",
|
|
15424
15471
|
role: "canary",
|
|
15425
15472
|
schedule: "train",
|
|
15426
|
-
v3Target: "v4.3.
|
|
15473
|
+
v3Target: "v4.3.9"
|
|
15427
15474
|
}
|
|
15428
15475
|
],
|
|
15429
15476
|
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.",
|
|
15430
15477
|
rollback: {
|
|
15431
15478
|
independent: true,
|
|
15432
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15433
|
-
v3Target: "v4.3.
|
|
15479
|
+
mechanism: "npm dist-tag latest -> 4.3.9 and redeploy the Hub Lambda from tag v4.3.9 (64fc1be95e37); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15480
|
+
v3Target: "v4.3.9 (@mutmutco/cli@4.3.9, tag commit 64fc1be95e37 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15434
15481
|
}
|
|
15435
15482
|
},
|
|
15436
15483
|
{
|
|
@@ -18286,7 +18333,8 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
18286
18333
|
if (dispatch.deployStatus !== "success") {
|
|
18287
18334
|
return {
|
|
18288
18335
|
...dispatch,
|
|
18289
|
-
note: `${dispatch.note}; Jerv Gateway deploy deferred until package publication is proven green
|
|
18336
|
+
note: `${dispatch.note}; Jerv Gateway deploy deferred until package publication is proven green`,
|
|
18337
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "pending" }]
|
|
18290
18338
|
};
|
|
18291
18339
|
}
|
|
18292
18340
|
try {
|
|
@@ -18964,6 +19012,7 @@ Resume it: ${resumeCommand}
|
|
|
18964
19012
|
|
|
18965
19013
|
// src/train-doctor.ts
|
|
18966
19014
|
var import_node_fs23 = require("node:fs");
|
|
19015
|
+
var import_node_os13 = require("node:os");
|
|
18967
19016
|
var import_node_path22 = require("node:path");
|
|
18968
19017
|
|
|
18969
19018
|
// src/doctor-io.ts
|
|
@@ -20599,8 +20648,11 @@ function deployPhaseInput(model, dispatch, opts = {}) {
|
|
|
20599
20648
|
note: deployRun ? `${deployRun.workflow} release run` : dispatch.note
|
|
20600
20649
|
};
|
|
20601
20650
|
}
|
|
20602
|
-
case "registry-publish":
|
|
20651
|
+
case "registry-publish": {
|
|
20652
|
+
const hostRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "jerv-gateway");
|
|
20653
|
+
if (hostRun) return { state: runRowState(hostRun, dispatch.deployStatus), ...sha ? { sha } : {}, workflow: hostRun.workflow, note: dispatch.note };
|
|
20603
20654
|
return { state: "skipped", ...sha ? { sha } : {}, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" };
|
|
20655
|
+
}
|
|
20604
20656
|
case "tenant-container":
|
|
20605
20657
|
case "solo-container":
|
|
20606
20658
|
case "static-cdn":
|
|
@@ -20661,6 +20713,7 @@ function releasePhaseInputsFromDispatch(model, dispatch, publishDispatch, publis
|
|
|
20661
20713
|
function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
|
|
20662
20714
|
const deployRun = rows.find((r) => r.workflow === "deploy.yml");
|
|
20663
20715
|
const publishRun = rows.find((r) => r.workflow === "publish.yml");
|
|
20716
|
+
const hostRun = rows.find((r) => r.workflow === "jerv-gateway");
|
|
20664
20717
|
switch (model) {
|
|
20665
20718
|
case "hub-serverless":
|
|
20666
20719
|
return {
|
|
@@ -20679,7 +20732,9 @@ function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
|
|
|
20679
20732
|
};
|
|
20680
20733
|
case "registry-publish":
|
|
20681
20734
|
return {
|
|
20682
|
-
|
|
20735
|
+
// #884/#6145: an operator-host deploy leg (Jerv-Hub's Gateway) outranks 'skipped' here exactly as
|
|
20736
|
+
// in deployPhaseInput — it carries no runId, so no run metadata and no resume re-verification.
|
|
20737
|
+
deploy: hostRun ? { state: runRowState(hostRun, "pending"), sha: tagSha, workflow: hostRun.workflow, note: "jerv-gateway operator-host deploy on the release SHA" } : { state: "skipped", sha: tagSha, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" },
|
|
20683
20738
|
publish: publishRun ? {
|
|
20684
20739
|
state: runRowState(publishRun, "pending"),
|
|
20685
20740
|
sha: tagSha,
|
|
@@ -21046,12 +21101,14 @@ async function selfConvergeTrainCli(input) {
|
|
|
21046
21101
|
var TRAIN_LANES = ["release", "rcand", "hotfix"];
|
|
21047
21102
|
var TROUBLESHOOTING_GUIDE = "docs/Guides/train-troubleshooting.md";
|
|
21048
21103
|
var SCRATCH_BRANCH_GLOBS = ["train/check/*", "hotfix-fold/*--port-*"];
|
|
21104
|
+
var COMPOSE_GUARD_HARD_FAIL = /^secrets preflight: .*noEnvFile is (not )?true.*$/m;
|
|
21049
21105
|
function readLocalWorkflowsDefault(root) {
|
|
21050
21106
|
const dir = (0, import_node_path22.join)(root, ".github", "workflows");
|
|
21051
21107
|
let names;
|
|
21052
21108
|
try {
|
|
21053
21109
|
names = (0, import_node_fs23.readdirSync)(dir);
|
|
21054
|
-
} catch {
|
|
21110
|
+
} catch (e) {
|
|
21111
|
+
if (e.code === "ENOENT") return [];
|
|
21055
21112
|
return null;
|
|
21056
21113
|
}
|
|
21057
21114
|
const files = [];
|
|
@@ -21063,6 +21120,25 @@ function readLocalWorkflowsDefault(root) {
|
|
|
21063
21120
|
}
|
|
21064
21121
|
return files;
|
|
21065
21122
|
}
|
|
21123
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
21124
|
+
async function probeRegistryWhoami(train) {
|
|
21125
|
+
const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path22.join)((0, import_node_os13.tmpdir)(), "mmi-npmrc-"));
|
|
21126
|
+
const rc = (0, import_node_path22.join)(dir, "npmrc");
|
|
21127
|
+
try {
|
|
21128
|
+
(0, import_node_fs23.writeFileSync)(rc, "//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n");
|
|
21129
|
+
await train.runSelf(["secrets", "use", "npm/NPM_TOKEN", "--slug", "_org", "--", "npm", "whoami", "--registry", NPM_REGISTRY, "--userconfig", rc]);
|
|
21130
|
+
return { kind: "ok", detail: "the registry answered a login" };
|
|
21131
|
+
} catch (e) {
|
|
21132
|
+
const err = e;
|
|
21133
|
+
const stderr = typeof err.stderr === "string" ? err.stderr : "";
|
|
21134
|
+
const first = (clean2(stderr) || message(e)).split("\n")[0] ?? "";
|
|
21135
|
+
if (err.code === SECRETS_USE_WRAPPER_EXIT_CODE) return { kind: "unprobed", detail: first };
|
|
21136
|
+
if (/\bE401\b|401 Unauthorized/.test(stderr)) return { kind: "rejected", detail: "npm whoami answered 401 Unauthorized" };
|
|
21137
|
+
return { kind: "unverified", detail: first };
|
|
21138
|
+
} finally {
|
|
21139
|
+
(0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
|
|
21140
|
+
}
|
|
21141
|
+
}
|
|
21066
21142
|
var defaultDeps2 = {
|
|
21067
21143
|
cliVersion: resolveClientVersion,
|
|
21068
21144
|
fetchReleasedVersion: fetchNpmReleasedVersion,
|
|
@@ -21080,6 +21156,14 @@ var defaultDeps2 = {
|
|
|
21080
21156
|
}
|
|
21081
21157
|
},
|
|
21082
21158
|
healAlignmentLeg: healStaleAlignmentLeg,
|
|
21159
|
+
readSurfacesRaw: (cwd) => {
|
|
21160
|
+
try {
|
|
21161
|
+
return (0, import_node_fs23.readFileSync)((0, import_node_path22.join)(cwd, "surfaces.json"), "utf8");
|
|
21162
|
+
} catch {
|
|
21163
|
+
return void 0;
|
|
21164
|
+
}
|
|
21165
|
+
},
|
|
21166
|
+
registryWhoami: probeRegistryWhoami,
|
|
21083
21167
|
env: process.env
|
|
21084
21168
|
};
|
|
21085
21169
|
function message(e) {
|
|
@@ -21281,12 +21365,12 @@ async function runTrainDoctor(input) {
|
|
|
21281
21365
|
...!hints.hasMainBranch ? ["main"] : [],
|
|
21282
21366
|
...!hints.hasRcBranch && track === "full" ? ["rc"] : []
|
|
21283
21367
|
];
|
|
21284
|
-
if (missing.length) add({ code: "bootstrap-gap", severity: "blocker", source: "origin", title: `train branch(es) missing on origin: ${missing.join(", ")}`, remedy: `bootstrap the repo train: \`mmi-cli bootstrap apply ${repo} --execute\` (from the MMI-Hub root), then rerun` });
|
|
21368
|
+
if (missing.length) add({ code: "bootstrap-gap", severity: "blocker", source: "origin", title: `train branch(es) missing on origin: ${missing.join(", ")}`, remedy: `bootstrap the repo train: \`mmi-cli devops bootstrap apply ${repo} --execute\` (from the MMI-Hub root), then rerun` });
|
|
21285
21369
|
}
|
|
21286
21370
|
try {
|
|
21287
21371
|
const required = await discoverRequiredCheckContexts(train, ctx, stage);
|
|
21288
21372
|
if (required.length === 0) {
|
|
21289
|
-
add({ code: "bootstrap-gap", severity: "
|
|
21373
|
+
add({ code: "bootstrap-gap", severity: "warning", source: "origin", title: `no ruleset requires a status check on ${stage} \u2014 the train tags without a check wait (the GitHub push gate is the backstop)`, remedy: `activate the product ruleset: \`mmi-cli devops bootstrap apply ${repo} --execute\` (from the MMI-Hub root), or \`mmi-cli devops ci audit --repo ${repo}\`` });
|
|
21290
21374
|
} else {
|
|
21291
21375
|
try {
|
|
21292
21376
|
assertTagAddressableRequiredContexts({ readWorkflows: () => workflows }, required, repo);
|
|
@@ -21357,13 +21441,25 @@ async function runTrainDoctor(input) {
|
|
|
21357
21441
|
add({ code: "npm-major-mismatch", severity: "blocker", source: "local", title: "local npm major differs from the npm CI declares (#5666)", remedy: message(e) });
|
|
21358
21442
|
}
|
|
21359
21443
|
}
|
|
21444
|
+
if (/"publishVisibility"\s*:\s*"private"/.test(deps.readSurfacesRaw(cwd) ?? "")) {
|
|
21445
|
+
const probe = await deps.registryWhoami(train);
|
|
21446
|
+
if (probe.kind === "rejected") {
|
|
21447
|
+
add({ code: "npm-token-rejected", severity: "warning", source: "origin", title: `the vault npm token (_org npm/NPM_TOKEN) is REJECTED by registry.npmjs.org \u2014 ${probe.detail}; every authenticated \`npm view\` of a restricted package will 404 exactly like an absent one`, remedy: 'rotate the org token: mint a fresh npm automation token, store it with `mmi-cli vault secrets set npm/NPM_TOKEN --slug _org` (master or an exact grant), and rerun. Until whoami answers a login, a restricted-package 404 proves nothing \u2014 never read it as "not published" (#6184)' });
|
|
21448
|
+
} else if (probe.kind === "unprobed") {
|
|
21449
|
+
add({ code: "npm-token-rejected", severity: "info", source: "origin", title: `the vault npm token was not probed (no _org npm/NPM_TOKEN grant, or a vault error): ${probe.detail}`, remedy: "nothing here \u2014 the release does not depend on it (CI publishes through OIDC Trusted Publishing). Before verifying a private publish by hand, run the whoami probe from an account holding the `_org` npm/NPM_TOKEN grant" });
|
|
21450
|
+
} else if (probe.kind === "unverified") {
|
|
21451
|
+
unverified("the vault npm token (`npm whoami` through the `_org` npm/NPM_TOKEN hop)", probe.detail);
|
|
21452
|
+
}
|
|
21453
|
+
}
|
|
21360
21454
|
try {
|
|
21361
|
-
await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo]);
|
|
21455
|
+
await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
|
|
21362
21456
|
} catch (e) {
|
|
21363
21457
|
const err = e;
|
|
21364
21458
|
const text = [typeof err.stdout === "string" ? err.stdout : "", typeof err.stderr === "string" ? err.stderr : "", message(e)].join("\n");
|
|
21365
21459
|
if (/^missing /m.test(text)) {
|
|
21366
|
-
add({ code: "secrets-missing", severity: "blocker", source: "origin", title: `required ${stage} secret name(s) are absent: ${clean2(text.match(/^missing .*$/m)?.[0] ?? "")}`, remedy: `provision them in the vault (\`mmi-cli vault secrets request <KEY> --repo ${repo}\`), then rerun \`mmi-cli vault secrets preflight --stage ${stage} --repo ${repo}\`` });
|
|
21460
|
+
add({ code: "secrets-missing", severity: "blocker", source: "origin", title: `required ${stage} secret name(s) are absent: ${clean2(text.match(/^missing .*$/m)?.[0] ?? "")}`, remedy: `provision them in the vault (\`mmi-cli vault secrets request <KEY> --repo ${repo}\`), then rerun \`mmi-cli vault secrets preflight --stage ${stage} --repo ${repo}${lane === "hotfix" ? " --lane hotfix" : ""}\`` });
|
|
21461
|
+
} else if (COMPOSE_GUARD_HARD_FAIL.test(text)) {
|
|
21462
|
+
add({ code: "compose-guard-mismatch", severity: "blocker", source: "origin", title: clean2(text.match(COMPOSE_GUARD_HARD_FAIL)?.[0] ?? text), remedy: `fix the compose/DEPLOY#${stage}.noEnvFile pair per the fileless-transition guide the preflight printed, then rerun \`mmi-cli vault secrets preflight --stage ${stage} --repo ${repo}${lane === "hotfix" ? " --lane hotfix" : ""}\`; --skip-compose-guard only after independent verification (#2813)` });
|
|
21367
21463
|
} else {
|
|
21368
21464
|
unverified(`the ${stage} secrets preflight`, clean2(text) || e);
|
|
21369
21465
|
}
|
|
@@ -21553,7 +21649,7 @@ function enforceGateBudget(deps, repo) {
|
|
|
21553
21649
|
);
|
|
21554
21650
|
}
|
|
21555
21651
|
}
|
|
21556
|
-
async function preflight(deps, ctx, stage, meta) {
|
|
21652
|
+
async function preflight(deps, ctx, stage, meta, lane) {
|
|
21557
21653
|
const model = requireDeployModel(meta, ctx.repo);
|
|
21558
21654
|
if (model === "content") {
|
|
21559
21655
|
throw new Error(`${ctx.repo} is a content repo (deployModel=content) \u2014 the release train does not apply (trunk-based; PR to main)`);
|
|
@@ -21561,7 +21657,7 @@ async function preflight(deps, ctx, stage, meta) {
|
|
|
21561
21657
|
if (model === "none") {
|
|
21562
21658
|
throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
|
|
21563
21659
|
}
|
|
21564
|
-
await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo]);
|
|
21660
|
+
await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
|
|
21565
21661
|
await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
|
|
21566
21662
|
await assertActionsJobsCanStart(deps, ctx.repo);
|
|
21567
21663
|
enforceGateBudget(deps, ctx.repo);
|
|
@@ -22207,7 +22303,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22207
22303
|
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
22208
22304
|
const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
|
|
22209
22305
|
const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
|
|
22210
|
-
const
|
|
22306
|
+
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
22211
22307
|
deps,
|
|
22212
22308
|
ledger,
|
|
22213
22309
|
ledgerAnchors,
|
|
@@ -22220,6 +22316,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22220
22316
|
workflow: failedReleaseRun?.workflow ?? (isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow")
|
|
22221
22317
|
}
|
|
22222
22318
|
);
|
|
22319
|
+
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch);
|
|
22223
22320
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha })), {
|
|
22224
22321
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
22225
22322
|
});
|
|
@@ -22244,7 +22341,6 @@ ${recovery.note}`), recoveryInput);
|
|
|
22244
22341
|
landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
22245
22342
|
});
|
|
22246
22343
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
22247
|
-
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
|
|
22248
22344
|
if (publishSkipNote) dispatch = { ...dispatch, note: `${dispatch.note}; tenant-publish.yml skipped (${publishSkipNote})` };
|
|
22249
22345
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors };
|
|
22250
22346
|
}
|
|
@@ -22520,17 +22616,18 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
22520
22616
|
historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : recovered.deployStatus;
|
|
22521
22617
|
historicalNote = recovered.note;
|
|
22522
22618
|
}
|
|
22619
|
+
const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
|
|
22620
|
+
note: historicalNote,
|
|
22621
|
+
deployStatus: historicalDeploy,
|
|
22622
|
+
workflowRuns: historicalRows
|
|
22623
|
+
});
|
|
22523
22624
|
if (persisted) {
|
|
22524
|
-
const phaseInputs = phaseInputsFromRunRows(deployModel, historicalRows, tagSha, { repo: ctx.repo });
|
|
22625
|
+
const phaseInputs = phaseInputsFromRunRows(deployModel, dispatch2.workflowRuns ?? historicalRows, tagSha, { repo: ctx.repo });
|
|
22525
22626
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(phaseInputs.deploy), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
22526
22627
|
await recordPhase(ledger, deps, anchors, "publish", phaseEntry(phaseInputs.publish), { strict: true, landed: "the verified GitHub Release and origin/main at the immutable tag SHA" });
|
|
22527
22628
|
await recordPhase(ledger, deps, anchors, "githubRelease", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: the GitHub Release exists at the immutable tag SHA" }), { strict: true, landed: "the promotion (verified live)" });
|
|
22528
22629
|
await recordPhase(ledger, deps, anchors, "promotion", phaseEntry({ state: "complete", sha: tagSha, note: "re-verified live: origin/main contains the immutable tag SHA" }), { strict: true, landed: "nothing beyond the already-public release" });
|
|
22529
22630
|
}
|
|
22530
|
-
const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
|
|
22531
|
-
note: historicalNote,
|
|
22532
|
-
deployStatus: historicalDeploy
|
|
22533
|
-
});
|
|
22534
22631
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
22535
22632
|
if (persisted) {
|
|
22536
22633
|
await recordPhase(ledger, deps, anchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2, rcAlignment2)), { strict: true, landed: "the released, verified, deploy-resolved release (alignment PRs reused/landed)" });
|
|
@@ -22612,7 +22709,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22612
22709
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
22613
22710
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
22614
22711
|
const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
|
|
22615
|
-
const
|
|
22712
|
+
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
22616
22713
|
deps,
|
|
22617
22714
|
ledger,
|
|
22618
22715
|
anchors,
|
|
@@ -22625,6 +22722,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22625
22722
|
workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow"
|
|
22626
22723
|
}
|
|
22627
22724
|
);
|
|
22725
|
+
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
22628
22726
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
22629
22727
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha })), {
|
|
22630
22728
|
strict: ledgerMode === "strict",
|
|
@@ -22643,8 +22741,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22643
22741
|
strict: ledgerMode === "strict",
|
|
22644
22742
|
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
22645
22743
|
});
|
|
22646
|
-
|
|
22647
|
-
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, dispatch);
|
|
22744
|
+
const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
22648
22745
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
22649
22746
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
22650
22747
|
const rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
@@ -25269,6 +25366,13 @@ function registerBoardCommands(program3) {
|
|
|
25269
25366
|
const lane = holder.surface && holder.session && holder.host ? ` (${holder.surface}/${holder.session}@${holder.host})` : "";
|
|
25270
25367
|
return `@${holder.login}${lane}`;
|
|
25271
25368
|
}
|
|
25369
|
+
function printClaimWarnings(warnings, toStderr = false) {
|
|
25370
|
+
for (const warning of warnings ?? []) {
|
|
25371
|
+
if (toStderr) process.stderr.write(`Warning: ${warning}
|
|
25372
|
+
`);
|
|
25373
|
+
else console.log(`Warning: ${warning}`);
|
|
25374
|
+
}
|
|
25375
|
+
}
|
|
25272
25376
|
function claimVerdict(ref, result) {
|
|
25273
25377
|
const holder = formatClaimHolder(result.holder);
|
|
25274
25378
|
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
@@ -25291,24 +25395,26 @@ function registerBoardCommands(program3) {
|
|
|
25291
25395
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
25292
25396
|
board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
|
|
25293
25397
|
withExamples(mutating(
|
|
25294
|
-
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
25398
|
+
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
25295
25399
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
25296
25400
|
).action(async (issueRefs, o) => {
|
|
25297
25401
|
if (issueRefs.length === 1) {
|
|
25298
25402
|
const issueRef = issueRefs[0];
|
|
25299
25403
|
try {
|
|
25404
|
+
const config = await loadConfigForBoardSelector2(issueRef, o.repo);
|
|
25300
25405
|
const result = await claimBoardIssue({
|
|
25301
|
-
config
|
|
25406
|
+
config,
|
|
25302
25407
|
selector: issueRef,
|
|
25303
25408
|
repo: o.repo,
|
|
25304
25409
|
assignee: o.for,
|
|
25305
25410
|
force: o.force,
|
|
25306
25411
|
check: o.check,
|
|
25307
25412
|
allowPartial: o.allowPartial
|
|
25308
|
-
});
|
|
25413
|
+
}, { snapshot: registryClientDeps(config) });
|
|
25309
25414
|
if (!result.checked) invalidateStatuslineBoardCache();
|
|
25310
25415
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25311
25416
|
console.log(claimVerdict(result.item.ref, result));
|
|
25417
|
+
printClaimWarnings(result.warnings);
|
|
25312
25418
|
} catch (e) {
|
|
25313
25419
|
if (refuseRateLimited(e, o.json)) return;
|
|
25314
25420
|
return failGraceful(`board claim failed: ${e.message}`);
|
|
@@ -25316,22 +25422,25 @@ function registerBoardCommands(program3) {
|
|
|
25316
25422
|
return;
|
|
25317
25423
|
}
|
|
25318
25424
|
try {
|
|
25425
|
+
const config = await loadConfigForBoardSelector2(issueRefs[0], o.repo);
|
|
25319
25426
|
const bulk = await claimBoardIssues({
|
|
25320
|
-
config
|
|
25427
|
+
config,
|
|
25321
25428
|
selectors: issueRefs,
|
|
25322
25429
|
repo: o.repo,
|
|
25323
25430
|
assignee: o.for,
|
|
25324
25431
|
force: o.force,
|
|
25325
25432
|
check: o.check,
|
|
25326
25433
|
allowPartial: o.allowPartial
|
|
25327
|
-
});
|
|
25434
|
+
}, { snapshot: registryClientDeps(config) });
|
|
25328
25435
|
if (bulk.results.some((r) => r.claimed && !r.checked)) invalidateStatuslineBoardCache();
|
|
25329
25436
|
if (o.json) {
|
|
25330
25437
|
console.log(JSON.stringify(bulk.results));
|
|
25438
|
+
printClaimWarnings(bulk.warnings, true);
|
|
25331
25439
|
} else {
|
|
25332
25440
|
for (const result of bulk.results) {
|
|
25333
25441
|
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
25334
25442
|
}
|
|
25443
|
+
printClaimWarnings(bulk.warnings);
|
|
25335
25444
|
}
|
|
25336
25445
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
25337
25446
|
} catch (e) {
|
|
@@ -25505,7 +25614,7 @@ function registerBoardCommands(program3) {
|
|
|
25505
25614
|
|
|
25506
25615
|
// src/bootstrap-commands.ts
|
|
25507
25616
|
var import_node_fs26 = require("node:fs");
|
|
25508
|
-
var
|
|
25617
|
+
var import_node_os14 = require("node:os");
|
|
25509
25618
|
var import_node_path25 = require("node:path");
|
|
25510
25619
|
|
|
25511
25620
|
// src/command-plans.ts
|
|
@@ -25644,7 +25753,7 @@ function trainPlan(command, options = {}) {
|
|
|
25644
25753
|
{ label: "verify the fix is merged on development (the only hotfix origin)", gated: true },
|
|
25645
25754
|
// #6068: hotfix start/release run the SAME shared preflight as release/rcand, before any git mutation.
|
|
25646
25755
|
{ label: "verify registry META for this project", command: "mmi-cli oracle org project get <owner/repo>", gated: true },
|
|
25647
|
-
{ label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
25756
|
+
{ label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo> --lane hotfix", gated: true },
|
|
25648
25757
|
{ label: "preflight local npm major vs the CI-declared npm, GitHub Actions hosted job start (billing/spending), and the gate wall-clock budget", command: "shared train preflight (#5666/#5604/#3178) \u2014 runs inside hotfix start and hotfix release", gated: true },
|
|
25649
25758
|
{ label: "refuse on a prior pending/failed train ledger leg (release, rcand or hotfix) before mutating anything", command: "shared release ledger (#5987/#6068) \u2014 finish that run first", gated: true },
|
|
25650
25759
|
{ label: "branch hotfix from main and cherry-pick the dev commits", command: "git cherry-pick -x <dev-sha>", gated: true },
|
|
@@ -27357,7 +27466,7 @@ function registerBootstrapCommands(program3) {
|
|
|
27357
27466
|
const readFile9 = (p) => (0, import_node_fs26.existsSync)(p) ? (0, import_node_fs26.readFileSync)(p, "utf8") : null;
|
|
27358
27467
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
27359
27468
|
const putSeed = async (target, content, ref, sha) => {
|
|
27360
|
-
const tmp = (0, import_node_path25.join)((0,
|
|
27469
|
+
const tmp = (0, import_node_path25.join)((0, import_node_os14.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
27361
27470
|
(0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
|
|
27362
27471
|
try {
|
|
27363
27472
|
await gh(contentPutInputArgs(repo, target, tmp));
|
|
@@ -27956,7 +28065,7 @@ LIVE apply to ${repo}:
|
|
|
27956
28065
|
} catch {
|
|
27957
28066
|
existingSha = void 0;
|
|
27958
28067
|
}
|
|
27959
|
-
const tmp = (0, import_node_path25.join)((0,
|
|
28068
|
+
const tmp = (0, import_node_path25.join)((0, import_node_os14.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
27960
28069
|
const desiredContent = desiredByRepo.get(rec.repo);
|
|
27961
28070
|
if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
|
|
27962
28071
|
(0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
|
|
@@ -28131,7 +28240,7 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
|
|
|
28131
28240
|
} catch {
|
|
28132
28241
|
existingSha = void 0;
|
|
28133
28242
|
}
|
|
28134
|
-
const tmp = (0, import_node_path25.join)((0,
|
|
28243
|
+
const tmp = (0, import_node_path25.join)((0, import_node_os14.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
28135
28244
|
(0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
|
|
28136
28245
|
try {
|
|
28137
28246
|
await gh(contentPutInputArgs(repo, seed.target, tmp));
|
|
@@ -28252,7 +28361,7 @@ function formatDeployStatus(r) {
|
|
|
28252
28361
|
`running version: ${r.runningVersion ?? "none stamped"}`,
|
|
28253
28362
|
`last deploy run: ${formatLastRun(r)}`,
|
|
28254
28363
|
`public URL: ${r.publicUrl ?? "none"}`,
|
|
28255
|
-
`health probe: ${r.health ? `${r.health.ok ? "ok" : "failed"}${r.health.status ? ` (HTTP ${r.health.status})` : ""}${r.health.error ? ` \u2014 ${r.health.error}` : ""}` : "not probed"}`,
|
|
28364
|
+
`health probe: ${r.health ? `${r.health.ok ? "ok" : "failed"}${r.health.status ? ` (HTTP ${r.health.status})` : ""}${r.health.error ? ` \u2014 ${r.health.error}` : ""}${r.health.url && r.health.url !== r.publicUrl ? ` at ${r.health.url}` : ""}` : "not probed"}`,
|
|
28256
28365
|
`deploy state: ${r.deployOk === void 0 ? "not stamped" : r.deployOk ? "ok" : "failed"}`
|
|
28257
28366
|
];
|
|
28258
28367
|
if (r.hints.length) {
|
|
@@ -28263,22 +28372,9 @@ function formatDeployStatus(r) {
|
|
|
28263
28372
|
|
|
28264
28373
|
// src/deploy-commands.ts
|
|
28265
28374
|
var STAGES2 = ["dev", "rc", "main"];
|
|
28266
|
-
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
28267
|
-
const controller = new AbortController();
|
|
28268
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
28269
|
-
timeout.unref?.();
|
|
28270
|
-
try {
|
|
28271
|
-
const res = await fetch(url, { method: "GET", signal: controller.signal });
|
|
28272
|
-
return { ok: res.ok, status: res.status };
|
|
28273
|
-
} catch (e) {
|
|
28274
|
-
return { ok: false, error: e.message };
|
|
28275
|
-
} finally {
|
|
28276
|
-
clearTimeout(timeout);
|
|
28277
|
-
}
|
|
28278
|
-
}
|
|
28279
28375
|
function registerDeployCommands(program3) {
|
|
28280
28376
|
const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
|
|
28281
|
-
deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
|
|
28377
|
+
deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo); an auth-walled root (401/403) is re-probed at /health and the `url` field names the endpoint that answered (#6137)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
|
|
28282
28378
|
if (!STAGES2.includes(stage)) {
|
|
28283
28379
|
return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
|
|
28284
28380
|
}
|
|
@@ -28290,7 +28386,7 @@ function registerDeployCommands(program3) {
|
|
|
28290
28386
|
const deployFacts = await fetchDeployFactsBySlug(slug, reg);
|
|
28291
28387
|
const publicUrl = publicUrlFromDeployFact(deployFacts?.stages[stage] ?? null);
|
|
28292
28388
|
const [health, lastRun] = await Promise.all([
|
|
28293
|
-
publicUrl ?
|
|
28389
|
+
publicUrl ? probePublicHealth(publicUrl) : Promise.resolve(void 0),
|
|
28294
28390
|
fetchLastTenantDeployRun(slug, stage)
|
|
28295
28391
|
]);
|
|
28296
28392
|
const report = buildDeployStatusReport({
|
|
@@ -29021,7 +29117,8 @@ function composeMarksRuntimeEnvTrue(composeText) {
|
|
|
29021
29117
|
return false;
|
|
29022
29118
|
});
|
|
29023
29119
|
}
|
|
29024
|
-
function promotionSourceBranch(stage, releaseTrack) {
|
|
29120
|
+
function promotionSourceBranch(stage, releaseTrack, lane) {
|
|
29121
|
+
if (lane === "hotfix") return "main";
|
|
29025
29122
|
if (stage === "main") return releaseTrack === "direct" ? "development" : "rc";
|
|
29026
29123
|
return "development";
|
|
29027
29124
|
}
|
|
@@ -29887,7 +29984,7 @@ async function collectWaveStatus(deps) {
|
|
|
29887
29984
|
|
|
29888
29985
|
// src/discovery-commands.ts
|
|
29889
29986
|
var import_node_fs30 = require("node:fs");
|
|
29890
|
-
var
|
|
29987
|
+
var import_node_os15 = require("node:os");
|
|
29891
29988
|
var import_node_path28 = require("node:path");
|
|
29892
29989
|
var GC_GH_TIMEOUT_MS = 2e4;
|
|
29893
29990
|
async function collectStatus() {
|
|
@@ -29922,7 +30019,7 @@ async function collectStatus() {
|
|
|
29922
30019
|
try {
|
|
29923
30020
|
const cfg = await loadConfigOrDiscover();
|
|
29924
30021
|
if (cfg.sagaApiUrl) {
|
|
29925
|
-
const report = await readBoard({ config: cfg });
|
|
30022
|
+
const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
|
|
29926
30023
|
claimedItems = report.primary.userOwned.map((item) => ({
|
|
29927
30024
|
number: item.number,
|
|
29928
30025
|
title: item.title,
|
|
@@ -29969,8 +30066,8 @@ var PRIORITY_RANK = {
|
|
|
29969
30066
|
};
|
|
29970
30067
|
async function recommendNext(repo, deps) {
|
|
29971
30068
|
const load = deps?.loadConfig ?? loadConfigForRepo;
|
|
29972
|
-
const reader = deps?.readBoard ?? readBoard;
|
|
29973
30069
|
const cfg = await load(repo);
|
|
30070
|
+
const reader = deps?.readBoard ?? ((opts) => readBoard(opts, { snapshot: registryClientDeps(cfg) }));
|
|
29974
30071
|
if (!cfg.sagaApiUrl) throw new Error("Hub API URL not configured \u2014 the board was NOT read (run `mmi-cli doctor`)");
|
|
29975
30072
|
let report;
|
|
29976
30073
|
try {
|
|
@@ -30016,7 +30113,7 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
30016
30113
|
let board = { ok: false, detail: "no config" };
|
|
30017
30114
|
try {
|
|
30018
30115
|
if (cfg.sagaApiUrl) {
|
|
30019
|
-
const report = await readBoard({ config: cfg });
|
|
30116
|
+
const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
|
|
30020
30117
|
const total = report.primary.claimable.length + report.primary.userOwned.length + report.primary.taken.length;
|
|
30021
30118
|
board = { ok: true, detail: `board has ${total} active items (${report.primary.claimable.length} claimable, ${report.primary.userOwned.length} yours)` };
|
|
30022
30119
|
} else {
|
|
@@ -30076,7 +30173,7 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
30076
30173
|
else if (top) nextCommand = `mmi-cli oracle board claim ${top.number} # ${top.title}`;
|
|
30077
30174
|
else nextCommand = "mmi-cli oracle board read \u2014 no claimable items found";
|
|
30078
30175
|
}
|
|
30079
|
-
const home = (0,
|
|
30176
|
+
const home = (0, import_node_os15.homedir)();
|
|
30080
30177
|
const plugin = onboardPluginGate({
|
|
30081
30178
|
readKnown: () => readFileSyncSafe((0, import_node_path28.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs30.readFileSync),
|
|
30082
30179
|
readSettings: () => readFileSyncSafe((0, import_node_path28.join)(home, ".claude", "settings.json"), import_node_fs30.readFileSync)
|
|
@@ -32025,7 +32122,7 @@ var import_node_fs31 = require("node:fs");
|
|
|
32025
32122
|
var import_node_crypto9 = require("node:crypto");
|
|
32026
32123
|
|
|
32027
32124
|
// src/issue-body.ts
|
|
32028
|
-
var
|
|
32125
|
+
var import_node_os16 = require("node:os");
|
|
32029
32126
|
var TextArgError = class extends Error {
|
|
32030
32127
|
constructor(message2, code, offendingFlag) {
|
|
32031
32128
|
super(message2);
|
|
@@ -32037,7 +32134,7 @@ var TextArgError = class extends Error {
|
|
|
32037
32134
|
offendingFlag;
|
|
32038
32135
|
};
|
|
32039
32136
|
function emptyStdinMessage(fileFlag) {
|
|
32040
|
-
if ((0,
|
|
32137
|
+
if ((0, import_node_os16.platform)() === "win32") {
|
|
32041
32138
|
return `${fileFlag} - read empty stdin (on Windows, ${fileFlag} - is unreliable through the npm .cmd shim \u2014 use ${fileFlag} <path>, or pipe to \`node cli/dist/index.cjs\` directly)`;
|
|
32042
32139
|
}
|
|
32043
32140
|
return `${fileFlag} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${fileFlag} <path>)`;
|
|
@@ -32577,6 +32674,10 @@ function validateBatchSpecs(specs) {
|
|
|
32577
32674
|
spec.labels = [...spec.labels ?? [], ...alias];
|
|
32578
32675
|
delete spec.label;
|
|
32579
32676
|
}
|
|
32677
|
+
if (spec.labels !== void 0 && (!Array.isArray(spec.labels) || spec.labels.some((l) => typeof l !== "string" || !l.trim()))) {
|
|
32678
|
+
errors.push({ row, error: "labels must be an array of non-empty strings" });
|
|
32679
|
+
continue;
|
|
32680
|
+
}
|
|
32580
32681
|
if (spec.repo !== void 0 && !/^[\w.-]+\/[\w.-]+$/.test(spec.repo)) {
|
|
32581
32682
|
errors.push({ row, error: `bad repo "${spec.repo}" \u2014 expected owner/repo` });
|
|
32582
32683
|
continue;
|
|
@@ -32585,13 +32686,17 @@ function validateBatchSpecs(specs) {
|
|
|
32585
32686
|
errors.push({ row, error: `unknown type "${spec.type}" \u2014 expected one of: ${validTypes.join(", ")}` });
|
|
32586
32687
|
continue;
|
|
32587
32688
|
}
|
|
32588
|
-
if (
|
|
32689
|
+
if (typeof spec.title !== "string" || !spec.title.trim()) {
|
|
32589
32690
|
errors.push({ row, error: "missing or empty title" });
|
|
32590
32691
|
continue;
|
|
32591
32692
|
}
|
|
32693
|
+
if (spec.body !== void 0 && typeof spec.body !== "string") {
|
|
32694
|
+
errors.push({ row, error: "body must be a string" });
|
|
32695
|
+
continue;
|
|
32696
|
+
}
|
|
32592
32697
|
let priority;
|
|
32593
32698
|
try {
|
|
32594
|
-
priority = spec.priority ? normalizePriority(spec.priority) : "medium";
|
|
32699
|
+
priority = spec.priority ? normalizePriority(String(spec.priority)) : "medium";
|
|
32595
32700
|
} catch (e) {
|
|
32596
32701
|
errors.push({ row, error: e.message });
|
|
32597
32702
|
continue;
|
|
@@ -32604,6 +32709,14 @@ function validateBatchSpecs(specs) {
|
|
|
32604
32709
|
if (!labelsCarrySurface(spec.labels)) spec.labels = [...spec.labels ?? [], surfaceLabel(spec.surface)];
|
|
32605
32710
|
delete spec.surface;
|
|
32606
32711
|
}
|
|
32712
|
+
if (spec.parent !== void 0) {
|
|
32713
|
+
try {
|
|
32714
|
+
parseIssueRef(spec.parent);
|
|
32715
|
+
} catch (e) {
|
|
32716
|
+
errors.push({ row, error: e.message });
|
|
32717
|
+
continue;
|
|
32718
|
+
}
|
|
32719
|
+
}
|
|
32607
32720
|
validated.push({ row, spec, priority, type: spec.type });
|
|
32608
32721
|
}
|
|
32609
32722
|
return { ok: errors.length === 0, errors, validated };
|
|
@@ -34030,6 +34143,8 @@ var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!,
|
|
|
34030
34143
|
projectV2 { id }
|
|
34031
34144
|
}
|
|
34032
34145
|
}`;
|
|
34146
|
+
var ProjectInfoReadUnavailableError = class extends Error {
|
|
34147
|
+
};
|
|
34033
34148
|
function shortDescriptionFromReadme(markdown) {
|
|
34034
34149
|
const lines2 = markdown.replace(/\r/g, "").split("\n");
|
|
34035
34150
|
const h1 = lines2.findIndex((line) => /^#\s+\S/.test(line.trim()));
|
|
@@ -34516,7 +34631,7 @@ function registerSchedulesCommands(program3) {
|
|
|
34516
34631
|
// src/secrets-commands.ts
|
|
34517
34632
|
var import_node_fs33 = require("node:fs");
|
|
34518
34633
|
var import_node_path30 = require("node:path");
|
|
34519
|
-
var
|
|
34634
|
+
var import_node_os17 = require("node:os");
|
|
34520
34635
|
|
|
34521
34636
|
// src/secrets-diff.ts
|
|
34522
34637
|
var TIMEOUT_MS2 = 8e3;
|
|
@@ -34639,7 +34754,7 @@ async function decryptRailsCredentials(input) {
|
|
|
34639
34754
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
34640
34755
|
"puts JSON.generate(config.config)"
|
|
34641
34756
|
].join("\n");
|
|
34642
|
-
const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path30.join)((0,
|
|
34757
|
+
const scriptDir = (0, import_node_fs33.mkdtempSync)((0, import_node_path30.join)((0, import_node_os17.tmpdir)(), "mmi-rails-decrypt-"));
|
|
34643
34758
|
const scriptPath = (0, import_node_path30.join)(scriptDir, "decrypt.rb");
|
|
34644
34759
|
(0, import_node_fs33.writeFileSync)(scriptPath, script, "utf8");
|
|
34645
34760
|
try {
|
|
@@ -34751,10 +34866,13 @@ function registerSecretsCommands(program3) {
|
|
|
34751
34866
|
const ok = body !== void 0 ? await secretsOrgCatalogSet(d, body, { replace: o.replace, remove: o.remove }) : await secretsOrgCatalogRemove(d, o.remove ?? []);
|
|
34752
34867
|
if (!ok) process.exitCode = 1;
|
|
34753
34868
|
}));
|
|
34754
|
-
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--skip-compose-guard", "skip the #2813 fileless-compose \u2194 DEPLOY#.noEnvFile promotion guard").option("--json", "machine-readable output").action(async (o) => {
|
|
34869
|
+
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--skip-compose-guard", "skip the #2813 fileless-compose \u2194 DEPLOY#.noEnvFile promotion guard").option("--lane <hotfix>", "only hotfix is accepted: the hotfix lane deploys main, so the #2813 guard reads main, not rc (#6136); release/rcand need no flag").option("--json", "machine-readable output").action(async (o) => {
|
|
34755
34870
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
34756
34871
|
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
34757
34872
|
}
|
|
34873
|
+
if (o.lane !== void 0 && o.lane !== "hotfix") {
|
|
34874
|
+
return fail("secrets preflight: --lane must be hotfix (the only lane that changes the compose source)");
|
|
34875
|
+
}
|
|
34758
34876
|
const cfg = await loadConfig();
|
|
34759
34877
|
if (!cfg.sagaApiUrl) {
|
|
34760
34878
|
fail("secrets: Hub API URL not configured");
|
|
@@ -34775,7 +34893,7 @@ function registerSecretsCommands(program3) {
|
|
|
34775
34893
|
let filelessOk = true;
|
|
34776
34894
|
if (meta && centralContainer && !o.skipComposeGuard) {
|
|
34777
34895
|
const stage = o.stage;
|
|
34778
|
-
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
|
|
34896
|
+
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo), o.lane);
|
|
34779
34897
|
const cwdRepo = repoFromRemoteUrl((await execFileP("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
|
|
34780
34898
|
const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
|
|
34781
34899
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
@@ -34981,10 +35099,11 @@ async function resolveBoardConfig2(repoOption) {
|
|
|
34981
35099
|
const floor = await loadConfig();
|
|
34982
35100
|
if (!floor.sagaApiUrl) return null;
|
|
34983
35101
|
const slug = repoOption ? (repoOption.replace(/\.git$/, "").split("/").pop() ?? repoOption).toLowerCase() : await repoSlug();
|
|
34984
|
-
const
|
|
35102
|
+
const registry2 = registryClientDeps(floor);
|
|
35103
|
+
const read = await fetchProjectBySlugChecked(slug, registry2);
|
|
34985
35104
|
if (!read.ok || !read.project) return null;
|
|
34986
35105
|
const cfg = boardConfigFromProject(read.project, floor);
|
|
34987
|
-
return readBoard({ config: cfg, repo: repoOption, allowPartial: true });
|
|
35106
|
+
return readBoard({ config: cfg, repo: repoOption, allowPartial: true }, { snapshot: registry2 });
|
|
34988
35107
|
}
|
|
34989
35108
|
function registerSessionReport(program3) {
|
|
34990
35109
|
const report = program3.commands.find((c) => c.name() === "report");
|
|
@@ -35926,7 +36045,7 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
35926
36045
|
// src/merge-cleanup.ts
|
|
35927
36046
|
var import_node_fs37 = require("node:fs");
|
|
35928
36047
|
var import_node_path34 = require("node:path");
|
|
35929
|
-
var
|
|
36048
|
+
var import_node_os18 = require("node:os");
|
|
35930
36049
|
|
|
35931
36050
|
// src/board-advance.ts
|
|
35932
36051
|
function repoOf2(ref) {
|
|
@@ -36753,24 +36872,31 @@ function runTestPolicy(root, deps = {}) {
|
|
|
36753
36872
|
` + (deps.base === HOTFIX_DIFF_BASE ? ` Hotfix lane (--base ${HOTFIX_DIFF_BASE}): pass --policy-ref <exact-${HOTFIX_POLICY_REF}-commit> so the current policy is read from ${HOTFIX_POLICY_REF}.` : ` Restore ${POLICY_FILE} on this branch; the policy file belongs on every non-hotfix lane.`)
|
|
36754
36873
|
}] : [];
|
|
36755
36874
|
const present = (path2) => exists((0, import_node_path33.join)(root, path2));
|
|
36875
|
+
const policyTree = policySource.sha ? new Set(git2(["ls-tree", "-r", "-z", "--name-only", policySource.sha], root).split("\0").filter(Boolean).map((p) => (0, import_node_path33.join)(root, p))) : null;
|
|
36876
|
+
const policyTreeExists = policyTree ? (abs) => policyTree.has(abs) : exists;
|
|
36877
|
+
const where = policySource.sha ? `${policySource.ref}@${policySource.sha}` : "this worktree";
|
|
36756
36878
|
const removedByThisDiff = removedPaths(changed);
|
|
36757
36879
|
const staleFindings = [];
|
|
36758
|
-
const unresolved = unresolvedProtectedEntries(policy, root,
|
|
36880
|
+
const unresolved = unresolvedProtectedEntries(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
|
|
36759
36881
|
if (unresolved.length > 0) {
|
|
36760
36882
|
staleFindings.push({
|
|
36761
36883
|
kind: "stale-protected-entry",
|
|
36762
36884
|
paths: unresolved,
|
|
36763
|
-
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
|
|
36764
|
-
` + unresolved.map((p) => ` ${p}`).join("\n") +
|
|
36885
|
+
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist on ${where}:
|
|
36886
|
+
` + unresolved.map((p) => ` ${p}`).join("\n") + `
|
|
36887
|
+
An entry naming a missing file reads as protection while protecting nothing.
|
|
36888
|
+
Restore the file on ${where}, or remove its entry.`
|
|
36765
36889
|
});
|
|
36766
36890
|
}
|
|
36767
|
-
const staleSatisfiers = unresolvedSatisfiers(policy, root,
|
|
36891
|
+
const staleSatisfiers = unresolvedSatisfiers(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
|
|
36768
36892
|
if (staleSatisfiers.length > 0) {
|
|
36769
36893
|
staleFindings.push({
|
|
36770
36894
|
kind: "stale-satisfied-by",
|
|
36771
36895
|
paths: staleSatisfiers,
|
|
36772
|
-
detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist:
|
|
36773
|
-
` + staleSatisfiers.map((p) => ` ${p}`).join("\n") +
|
|
36896
|
+
detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist on ${where}:
|
|
36897
|
+
` + staleSatisfiers.map((p) => ` ${p}`).join("\n") + `
|
|
36898
|
+
A glob discharged by coverage that is not there is a glob enforcing nothing, quietly.
|
|
36899
|
+
Restore the file on ${where}, or drop it from satisfiedBy so the glob asks for a test again.`
|
|
36774
36900
|
});
|
|
36775
36901
|
}
|
|
36776
36902
|
const override = lookup.override;
|
|
@@ -36957,7 +37083,7 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
|
|
|
36957
37083
|
return rewritten === base ? null : rewritten;
|
|
36958
37084
|
}
|
|
36959
37085
|
function writeSquashBodyFile(body) {
|
|
36960
|
-
const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path34.join)((0,
|
|
37086
|
+
const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path34.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
|
|
36961
37087
|
const path2 = (0, import_node_path34.join)(dir, "body.txt");
|
|
36962
37088
|
(0, import_node_fs37.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
|
|
36963
37089
|
`, "utf8");
|
|
@@ -37085,15 +37211,17 @@ function cleanupGitArgs(cwd, args) {
|
|
|
37085
37211
|
}
|
|
37086
37212
|
async function remoteBranchExists2(branch, options = {}) {
|
|
37087
37213
|
if (!branch) return void 0;
|
|
37214
|
+
const remote = options.remote ?? "origin";
|
|
37088
37215
|
try {
|
|
37089
|
-
if (options.prune) await execFileP("git", cleanupGitArgs(options.cwd, ["fetch",
|
|
37090
|
-
return (await execFileP("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads",
|
|
37216
|
+
if (options.prune) await execFileP("git", cleanupGitArgs(options.cwd, ["fetch", remote, "--prune"]), { timeout: GIT_TIMEOUT_MS });
|
|
37217
|
+
return (await execFileP("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads", remote, branch]), { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
|
|
37091
37218
|
} catch {
|
|
37092
37219
|
return void 0;
|
|
37093
37220
|
}
|
|
37094
37221
|
}
|
|
37095
37222
|
async function deleteMergedRemoteBranch(options) {
|
|
37096
|
-
const
|
|
37223
|
+
const remote = options.remote ?? "origin";
|
|
37224
|
+
const remediation = `git push ${remote} --delete ${options.branch}`;
|
|
37097
37225
|
if (!options.branch) {
|
|
37098
37226
|
return {
|
|
37099
37227
|
branch: options.branch,
|
|
@@ -37122,13 +37250,13 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37122
37250
|
existedBefore: false,
|
|
37123
37251
|
attempted: false,
|
|
37124
37252
|
status: "failed",
|
|
37125
|
-
error: `could not verify absence of
|
|
37253
|
+
error: `could not verify absence of ${remote}/${options.branch}`,
|
|
37126
37254
|
remediation
|
|
37127
37255
|
};
|
|
37128
37256
|
}
|
|
37129
37257
|
}
|
|
37130
37258
|
try {
|
|
37131
|
-
await options.execGit(["push",
|
|
37259
|
+
await options.execGit(["push", remote, "--delete", options.branch]);
|
|
37132
37260
|
} catch (e) {
|
|
37133
37261
|
const exists2 = await options.branchExists(options.branch);
|
|
37134
37262
|
if (exists2 === false) {
|
|
@@ -37162,7 +37290,7 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37162
37290
|
existedBefore: options.existedBefore,
|
|
37163
37291
|
attempted: true,
|
|
37164
37292
|
status: "failed",
|
|
37165
|
-
error: exists ?
|
|
37293
|
+
error: exists ? `${remote} still reports ${options.branch} after deletion` : `could not verify deletion of ${remote}/${options.branch}`,
|
|
37166
37294
|
remediation
|
|
37167
37295
|
};
|
|
37168
37296
|
}
|
|
@@ -37504,11 +37632,13 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
37504
37632
|
const actor = deps.actor ?? describeSessionIdentity();
|
|
37505
37633
|
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
37506
37634
|
for (const number of issues) {
|
|
37635
|
+
const state = await client.rest("GET", `repos/${repo}/issues/${number}`).then((issue) => issue?.state, () => void 0);
|
|
37636
|
+
if (state?.toLowerCase() === "closed") continue;
|
|
37507
37637
|
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
37508
37638
|
if (!contest.contested) continue;
|
|
37509
37639
|
const ref = `${repo}#${number}`;
|
|
37510
37640
|
const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : "another lane";
|
|
37511
|
-
return `pr create: REFUSED \u2014 ${ref} is held by ${holder} with live or unreadable work evidence; run \`mmi-cli oracle board claim ${ref} --force\` before creating this PR`;
|
|
37641
|
+
return `pr create: REFUSED \u2014 ${ref} is held by ${holder} with live or unreadable work evidence; run \`mmi-cli oracle board claim ${ref} --force\` (move it to In Progress first if it sits In Review) or drop the closing line before creating this PR`;
|
|
37512
37642
|
}
|
|
37513
37643
|
return void 0;
|
|
37514
37644
|
}
|
|
@@ -37519,13 +37649,13 @@ var import_node_path38 = require("node:path");
|
|
|
37519
37649
|
|
|
37520
37650
|
// src/jervcode-node-modules-cleanup.ts
|
|
37521
37651
|
var import_node_fs39 = require("node:fs");
|
|
37522
|
-
var
|
|
37652
|
+
var import_node_os19 = require("node:os");
|
|
37523
37653
|
var import_node_path36 = require("node:path");
|
|
37524
37654
|
var JERVCODE_PACKAGE_ENTRY = (0, import_node_path36.join)("node_modules", "@jervaise", "jervcode", "dist", "launcher-entry.js");
|
|
37525
37655
|
var WIN_NAMES2 = ["jervcode.cmd", "jervcode"];
|
|
37526
37656
|
var POSIX_NAMES2 = ["jervcode"];
|
|
37527
37657
|
var NODE_MODULES_CLEANUP_TIMEOUT_MS = 3e5;
|
|
37528
|
-
function jervcodeCandidatePaths(env = process.env, home = (0,
|
|
37658
|
+
function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os19.homedir)(), platform2 = process.platform) {
|
|
37529
37659
|
const names = platform2 === "win32" ? WIN_NAMES2 : POSIX_NAMES2;
|
|
37530
37660
|
const out = [];
|
|
37531
37661
|
for (const dir of jervCliCandidateDirs(env, home, platform2)) {
|
|
@@ -37533,7 +37663,7 @@ function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os18.h
|
|
|
37533
37663
|
}
|
|
37534
37664
|
return out;
|
|
37535
37665
|
}
|
|
37536
|
-
function resolveJervcodePath(env = process.env, home = (0,
|
|
37666
|
+
function resolveJervcodePath(env = process.env, home = (0, import_node_os19.homedir)(), platform2 = process.platform, exists = import_node_fs39.existsSync) {
|
|
37537
37667
|
for (const candidate of jervcodeCandidatePaths(env, home, platform2)) {
|
|
37538
37668
|
if (exists(candidate)) return candidate;
|
|
37539
37669
|
}
|
|
@@ -37542,7 +37672,7 @@ function resolveJervcodePath(env = process.env, home = (0, import_node_os18.home
|
|
|
37542
37672
|
function jervcodeExecFileArgs(args, opts = {}) {
|
|
37543
37673
|
const platform2 = opts.platform ?? process.platform;
|
|
37544
37674
|
const exists = opts.exists ?? import_node_fs39.existsSync;
|
|
37545
|
-
const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0,
|
|
37675
|
+
const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0, import_node_os19.homedir)(), platform2, exists);
|
|
37546
37676
|
if (resolved) {
|
|
37547
37677
|
const entry = (0, import_node_path36.join)((0, import_node_path36.join)(resolved, ".."), JERVCODE_PACKAGE_ENTRY);
|
|
37548
37678
|
if (exists(entry)) {
|
|
@@ -37908,6 +38038,69 @@ function safeRemoveTree(path2) {
|
|
|
37908
38038
|
}
|
|
37909
38039
|
(0, import_node_fs41.unlinkSync)(path2);
|
|
37910
38040
|
}
|
|
38041
|
+
function errorMessage(e) {
|
|
38042
|
+
return e instanceof Error ? e.message : String(e);
|
|
38043
|
+
}
|
|
38044
|
+
function resolvedOrRaw(path2) {
|
|
38045
|
+
try {
|
|
38046
|
+
return normPath2((0, import_node_fs41.realpathSync)(path2));
|
|
38047
|
+
} catch {
|
|
38048
|
+
return normPath2(path2);
|
|
38049
|
+
}
|
|
38050
|
+
}
|
|
38051
|
+
function unlinkEscapingReparsePoints(root, primaryRoot) {
|
|
38052
|
+
let realRoot;
|
|
38053
|
+
try {
|
|
38054
|
+
if ((0, import_node_fs41.lstatSync)(root).isSymbolicLink()) {
|
|
38055
|
+
return { ok: false, error: `delete root ${normPath2(root)} is a reparse point resolving to ${resolvedOrRaw(root)}` };
|
|
38056
|
+
}
|
|
38057
|
+
realRoot = normPath2((0, import_node_fs41.realpathSync)(root));
|
|
38058
|
+
} catch (e) {
|
|
38059
|
+
if (e.code === "ENOENT") return { ok: true, unlinked: [] };
|
|
38060
|
+
return { ok: false, error: `cannot resolve delete root ${normPath2(root)}: ${errorMessage(e)}` };
|
|
38061
|
+
}
|
|
38062
|
+
const realPrimary = resolvedOrRaw(primaryRoot);
|
|
38063
|
+
const forbidden = isPathAtOrWithin(realPrimary, realRoot) ? realPrimary : [`${realPrimary}/node_modules`, `${realPrimary}/packages`].find((p) => isPathAtOrWithin(realRoot, p));
|
|
38064
|
+
if (forbidden) {
|
|
38065
|
+
return { ok: false, error: `delete root ${normPath2(root)} (resolved ${realRoot}) is or contains the primary checkout path ${forbidden}` };
|
|
38066
|
+
}
|
|
38067
|
+
const unlinked = [];
|
|
38068
|
+
const stack = [realRoot];
|
|
38069
|
+
while (stack.length) {
|
|
38070
|
+
const dir = stack.pop();
|
|
38071
|
+
let entries;
|
|
38072
|
+
try {
|
|
38073
|
+
entries = (0, import_node_fs41.readdirSync)(dir, { withFileTypes: true });
|
|
38074
|
+
} catch (e) {
|
|
38075
|
+
return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
|
|
38076
|
+
}
|
|
38077
|
+
for (const entry of entries) {
|
|
38078
|
+
const child2 = (0, import_node_path38.join)(dir, entry.name);
|
|
38079
|
+
if (entry.isSymbolicLink()) {
|
|
38080
|
+
let target = "";
|
|
38081
|
+
try {
|
|
38082
|
+
target = normPath2((0, import_node_fs41.realpathSync)(child2));
|
|
38083
|
+
} catch {
|
|
38084
|
+
target = "";
|
|
38085
|
+
}
|
|
38086
|
+
if (target && isPathAtOrWithin(target, realRoot)) continue;
|
|
38087
|
+
try {
|
|
38088
|
+
safeRemoveTree(child2);
|
|
38089
|
+
} catch (e) {
|
|
38090
|
+
return { ok: false, error: `cannot unlink reparse point ${normPath2(child2)} -> ${target || "unresolvable"}: ${errorMessage(e)}` };
|
|
38091
|
+
}
|
|
38092
|
+
unlinked.push(normPath2(child2));
|
|
38093
|
+
continue;
|
|
38094
|
+
}
|
|
38095
|
+
if (entry.isDirectory()) stack.push(child2);
|
|
38096
|
+
}
|
|
38097
|
+
}
|
|
38098
|
+
return { ok: true, unlinked };
|
|
38099
|
+
}
|
|
38100
|
+
function reparseEscapeRemediation(wtPath) {
|
|
38101
|
+
const quote = (value) => value.replace(/'/g, "''");
|
|
38102
|
+
return `inspect '${quote(wtPath)}' manually \u2014 a reparse point resolves outside the worktree; never remove it recursively`;
|
|
38103
|
+
}
|
|
37911
38104
|
async function describePreCleanFailure(wtPath, execGit, error) {
|
|
37912
38105
|
const dryRun = await execGit(["-C", wtPath, "clean", "-ndX"]).catch(() => "");
|
|
37913
38106
|
const remaining = dryRun.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("Would remove ")).map((line) => line.slice("Would remove ".length));
|
|
@@ -37942,6 +38135,25 @@ async function verifyBranchHead(git3, branch, expectedHeadOid) {
|
|
|
37942
38135
|
}
|
|
37943
38136
|
return { ok: true };
|
|
37944
38137
|
}
|
|
38138
|
+
async function switchPrimaryCheckoutOffMergedBranch(wtPath, branch, baseRef, execGit, expectedHeadOid) {
|
|
38139
|
+
const git3 = (args) => execGit(["-C", wtPath, ...args]);
|
|
38140
|
+
const headCheck = await verifyBranchHead(git3, branch, expectedHeadOid);
|
|
38141
|
+
if (!headCheck.ok) return { ok: false, error: headCheck.error ? `${headCheck.reason}: ${headCheck.error}` : headCheck.reason };
|
|
38142
|
+
const porcelain = await git3(["status", "--porcelain"]).catch(() => void 0);
|
|
38143
|
+
if (porcelain === void 0) return { ok: false, error: "could not read the primary checkout status" };
|
|
38144
|
+
if (porcelainHasBlockingChanges(porcelain)) return { ok: false, error: "dirty-worktree" };
|
|
38145
|
+
try {
|
|
38146
|
+
await git3(["switch", baseRef]);
|
|
38147
|
+
} catch (e) {
|
|
38148
|
+
return { ok: false, error: `switch to ${baseRef} failed: ${formatGitCommandError(e)}` };
|
|
38149
|
+
}
|
|
38150
|
+
try {
|
|
38151
|
+
await git3(["branch", "-D", branch]);
|
|
38152
|
+
} catch (e) {
|
|
38153
|
+
return { ok: false, switchedTo: baseRef, error: `branch -D ${branch} failed after switching to ${baseRef}: ${formatGitCommandError(e)}` };
|
|
38154
|
+
}
|
|
38155
|
+
return { ok: true, switchedTo: baseRef };
|
|
38156
|
+
}
|
|
37945
38157
|
async function teardownWorktreeStage(worktreePath) {
|
|
37946
38158
|
try {
|
|
37947
38159
|
const result = await stopStage({ cwd: worktreePath, requiredIdentityCwd: worktreePath, globalStatePath: false });
|
|
@@ -38035,7 +38247,7 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
38035
38247
|
}
|
|
38036
38248
|
function isPrMergeWorktreePartial(cleanup) {
|
|
38037
38249
|
const worktree = cleanup?.worktree;
|
|
38038
|
-
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked");
|
|
38250
|
+
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked" && worktree.status !== "switched-primary");
|
|
38039
38251
|
}
|
|
38040
38252
|
function prMergeLocalCleanupExitCode(cleanup) {
|
|
38041
38253
|
return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
|
|
@@ -38075,10 +38287,29 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38075
38287
|
const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
|
|
38076
38288
|
if (!wtPath || mainWorktreeTarget) {
|
|
38077
38289
|
if (wtPath && mainWorktreeTarget) {
|
|
38290
|
+
const switched = await switchPrimaryCheckoutOffMergedBranch(wtPath, branch, options.baseRef, execGit, options.expectedHeadOid);
|
|
38291
|
+
if (switched.ok) {
|
|
38292
|
+
report.worktree = { path: wtPath, status: "switched-primary", reason: "main-worktree", switchedTo: switched.switchedTo };
|
|
38293
|
+
report.localBranch = { name: branch, status: "deleted" };
|
|
38294
|
+
return report;
|
|
38295
|
+
}
|
|
38296
|
+
if (switched.switchedTo) {
|
|
38297
|
+
report.worktree = {
|
|
38298
|
+
path: wtPath,
|
|
38299
|
+
status: "switched-primary",
|
|
38300
|
+
reason: "main-worktree",
|
|
38301
|
+
switchedTo: switched.switchedTo,
|
|
38302
|
+
error: switched.error,
|
|
38303
|
+
remediation: primaryCheckoutBranchRemediation(options.primaryRoot, options.baseRef, branch)
|
|
38304
|
+
};
|
|
38305
|
+
report.localBranch = { name: branch, status: "failed", error: switched.error };
|
|
38306
|
+
return report;
|
|
38307
|
+
}
|
|
38078
38308
|
report.worktree = {
|
|
38079
38309
|
path: wtPath,
|
|
38080
38310
|
status: "not-attempted",
|
|
38081
38311
|
reason: "main-worktree",
|
|
38312
|
+
error: switched.error,
|
|
38082
38313
|
remediation: primaryCheckoutBranchRemediation(options.primaryRoot, options.baseRef, branch)
|
|
38083
38314
|
};
|
|
38084
38315
|
}
|
|
@@ -38098,7 +38329,12 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38098
38329
|
return report;
|
|
38099
38330
|
}
|
|
38100
38331
|
const porcelain = await execGit(["-C", wtPath, "status", "--porcelain"]).catch(() => void 0);
|
|
38101
|
-
if (porcelain
|
|
38332
|
+
if (porcelain === void 0) {
|
|
38333
|
+
report.worktree = { path: wtPath, status: "refused", reason: "status-unreadable", error: "could not read the worktree status" };
|
|
38334
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "status-unreadable" };
|
|
38335
|
+
return report;
|
|
38336
|
+
}
|
|
38337
|
+
if (porcelain.trim()) {
|
|
38102
38338
|
report.worktree = { path: wtPath, status: "refused", reason: "dirty-worktree" };
|
|
38103
38339
|
report.localBranch = { name: branch, status: "not-attempted", reason: "dirty-worktree" };
|
|
38104
38340
|
return report;
|
|
@@ -38148,6 +38384,25 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38148
38384
|
report.localBranch = { name: branch, status: "not-attempted", reason: "archive-failed" };
|
|
38149
38385
|
return report;
|
|
38150
38386
|
}
|
|
38387
|
+
const unlinkedReparsePoints = [];
|
|
38388
|
+
const refuseReparseEscape = (error) => {
|
|
38389
|
+
report.worktree = {
|
|
38390
|
+
path: wtPath,
|
|
38391
|
+
status: "refused",
|
|
38392
|
+
reason: "reparse-escape",
|
|
38393
|
+
error,
|
|
38394
|
+
artifactsArchive,
|
|
38395
|
+
stageTeardown,
|
|
38396
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38397
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38398
|
+
remediation: reparseEscapeRemediation(wtPath)
|
|
38399
|
+
};
|
|
38400
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "reparse-escape" };
|
|
38401
|
+
return report;
|
|
38402
|
+
};
|
|
38403
|
+
const preHelperGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
|
|
38404
|
+
if (!preHelperGuard.ok) return refuseReparseEscape(preHelperGuard.error);
|
|
38405
|
+
unlinkedReparsePoints.push(...preHelperGuard.unlinked);
|
|
38151
38406
|
if (pathExists((0, import_node_path38.join)(wtPath, "node_modules"))) {
|
|
38152
38407
|
const nmRemoved = await (options.removeRealNodeModules ?? ((p) => removeWorktreeNodeModulesViaHelper(p, { cwd: mainWorktreePath })))(wtPath);
|
|
38153
38408
|
if (!nmRemoved.ok) {
|
|
@@ -38159,6 +38414,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38159
38414
|
artifactsArchive,
|
|
38160
38415
|
stageTeardown,
|
|
38161
38416
|
tmpEvidenceCount: tmpEvidence.length,
|
|
38417
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38162
38418
|
remediation: `jervcode worktree-node-modules-cleanup --worktree '${wtPath.replace(/'/g, "''")}'`
|
|
38163
38419
|
};
|
|
38164
38420
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
@@ -38175,12 +38431,16 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38175
38431
|
artifactsArchive,
|
|
38176
38432
|
stageTeardown,
|
|
38177
38433
|
tmpEvidenceCount: tmpEvidence.length,
|
|
38434
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38178
38435
|
// #6076: name the surviving node_modules remover when the dry run found one, else the clean.
|
|
38179
38436
|
remediation: preClean.remediation ?? `git -C '${wtPath.replace(/'/g, "''")}' clean -ffdX`
|
|
38180
38437
|
};
|
|
38181
38438
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
38182
38439
|
return report;
|
|
38183
38440
|
}
|
|
38441
|
+
const preRemoveGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
|
|
38442
|
+
if (!preRemoveGuard.ok) return refuseReparseEscape(preRemoveGuard.error);
|
|
38443
|
+
unlinkedReparsePoints.push(...preRemoveGuard.unlinked);
|
|
38184
38444
|
moveCwdToSafeWorktree(wtPath, safeCwd);
|
|
38185
38445
|
const removal = await removeWorktreeWithReconcile(
|
|
38186
38446
|
wtPath,
|
|
@@ -38195,7 +38455,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38195
38455
|
error: removal.error,
|
|
38196
38456
|
artifactsArchive,
|
|
38197
38457
|
stageTeardown,
|
|
38198
|
-
tmpEvidenceCount: tmpEvidence.length
|
|
38458
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38459
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
|
|
38199
38460
|
};
|
|
38200
38461
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-failed" };
|
|
38201
38462
|
return report;
|
|
@@ -38206,7 +38467,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38206
38467
|
...removal.reason ? { reason: removal.reason } : {},
|
|
38207
38468
|
artifactsArchive,
|
|
38208
38469
|
stageTeardown,
|
|
38209
|
-
tmpEvidenceCount: tmpEvidence.length
|
|
38470
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38471
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
|
|
38210
38472
|
};
|
|
38211
38473
|
if (pathExists(wtPath)) {
|
|
38212
38474
|
const residue = await (options.removeResidueDir?.(wtPath) ?? removeResidueDirectory(wtPath));
|
|
@@ -38254,12 +38516,18 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
38254
38516
|
lines2.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
|
|
38255
38517
|
} else if (wt.status === "retained-locked") {
|
|
38256
38518
|
lines2.push(`pr merge: worktree ${wt.path} cleanup deferred \u2014 its Windows parent shell still holds the cwd; ${wt.remediation ?? "exit the shell and remove the residue from the primary checkout"}`);
|
|
38519
|
+
} else if (wt.status === "switched-primary") {
|
|
38520
|
+
const now = wt.switchedTo ? `to ${wt.switchedTo}` : "to the base branch";
|
|
38521
|
+
lines2.push(wt.error ? `pr merge: primary checkout ${wt.path} switched ${now} but deleting the merged branch failed \u2014 ${wt.error}; remediate: ${wt.remediation ?? "delete the merged branch manually"}` : `pr merge: primary checkout ${wt.path} held the merged branch \u2014 switched it back ${now} and deleted the branch`);
|
|
38257
38522
|
} else if (wt.status === "not-attempted" && wt.reason === "main-worktree") {
|
|
38258
|
-
lines2.push(`pr merge: merged branch is still checked out in the primary checkout ${wt.path} \u2014 not removed; remediate: ${wt.remediation ?? "switch it back to the base branch and delete the merged branch"}`);
|
|
38523
|
+
lines2.push(`pr merge: merged branch is still checked out in the primary checkout ${wt.path} \u2014 not removed${wt.error ? ` (${wt.error})` : ""}; remediate: ${wt.remediation ?? "switch it back to the base branch and delete the merged branch"}`);
|
|
38259
38524
|
}
|
|
38260
38525
|
if (wt.residue === "left" && wt.status !== "retained-locked") {
|
|
38261
38526
|
lines2.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
|
|
38262
38527
|
}
|
|
38528
|
+
if (wt.unlinkedReparsePoints?.length) {
|
|
38529
|
+
lines2.push(`pr merge: unlinked ${wt.unlinkedReparsePoints.length} reparse point(s) resolving outside ${wt.path}: ${wt.unlinkedReparsePoints.join(", ")}`);
|
|
38530
|
+
}
|
|
38263
38531
|
if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
|
|
38264
38532
|
lines2.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
|
|
38265
38533
|
} else if (wt.artifactsArchive?.status === "blocked" && wt.artifactsArchive.error) {
|
|
@@ -39321,7 +39589,7 @@ ${list}`);
|
|
|
39321
39589
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
39322
39590
|
if (result.status === "failed") process.exitCode = 1;
|
|
39323
39591
|
});
|
|
39324
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
39592
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
39325
39593
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
39326
39594
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
39327
39595
|
if (o.disableAuto) {
|
|
@@ -39381,7 +39649,23 @@ ${list}`);
|
|
|
39381
39649
|
const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
39382
39650
|
const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
|
|
39383
39651
|
const beforeWorktreesRead = await execFileP("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => ({ state: "ok", stdout: r.stdout })).catch((e) => ({ state: "failed", error: e.message || "git worktree list failed" }));
|
|
39384
|
-
|
|
39652
|
+
let beforeWorktrees = beforeWorktreesRead.state === "ok" ? parseGitWorktreePorcelain(beforeWorktreesRead.stdout) : [];
|
|
39653
|
+
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
|
|
39654
|
+
const targetRepo2 = repoForPostCleanup ? repoForPostCleanup.split("/").slice(-2).join("/") : void 0;
|
|
39655
|
+
const foreignCwd = Boolean(o.repo) && Boolean(targetRepo2) && cwdRepo?.toLowerCase() !== targetRepo2.toLowerCase();
|
|
39656
|
+
const remote = foreignCwd ? `https://github.com/${targetRepo2}.git` : "origin";
|
|
39657
|
+
let foreignCheckout;
|
|
39658
|
+
if (foreignCwd) {
|
|
39659
|
+
const sibling = (0, import_node_path39.join)((0, import_node_path39.dirname)(beforeWorktrees[0]?.path || startingPath || process.cwd()), targetRepo2.split("/")[1]);
|
|
39660
|
+
const siblingRepo = repoFromRemoteUrl(await gitOut(["-C", sibling, "remote", "get-url", "origin"]).catch(() => ""));
|
|
39661
|
+
if (siblingRepo?.toLowerCase() === targetRepo2.toLowerCase()) {
|
|
39662
|
+
const siblingWorktrees = await execFileP("git", ["-C", sibling, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseGitWorktreePorcelain(r.stdout)).catch(() => void 0);
|
|
39663
|
+
if (siblingWorktrees?.length) {
|
|
39664
|
+
foreignCheckout = sibling;
|
|
39665
|
+
beforeWorktrees = siblingWorktrees;
|
|
39666
|
+
}
|
|
39667
|
+
}
|
|
39668
|
+
}
|
|
39385
39669
|
const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
|
|
39386
39670
|
const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
|
|
39387
39671
|
if (o.wait) {
|
|
@@ -39439,7 +39723,7 @@ ${list}`);
|
|
|
39439
39723
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
39440
39724
|
if (guard.note) console.warn(`pr merge: ${guard.note}`);
|
|
39441
39725
|
}
|
|
39442
|
-
const remoteBefore = await remoteBranchExists2(headRef);
|
|
39726
|
+
const remoteBefore = await remoteBranchExists2(headRef, { remote });
|
|
39443
39727
|
let upgradedToAuto = false;
|
|
39444
39728
|
let remoteNotAttemptedReason = "preserved-delayed-cleanup";
|
|
39445
39729
|
const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
@@ -39558,30 +39842,34 @@ ${list}`);
|
|
|
39558
39842
|
}
|
|
39559
39843
|
const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
|
|
39560
39844
|
let localCleanup;
|
|
39561
|
-
|
|
39562
|
-
localCleanup =
|
|
39563
|
-
|
|
39564
|
-
|
|
39565
|
-
|
|
39566
|
-
|
|
39567
|
-
|
|
39568
|
-
|
|
39569
|
-
|
|
39570
|
-
|
|
39571
|
-
|
|
39572
|
-
|
|
39573
|
-
|
|
39574
|
-
|
|
39575
|
-
|
|
39576
|
-
|
|
39577
|
-
|
|
39578
|
-
|
|
39579
|
-
|
|
39580
|
-
|
|
39581
|
-
|
|
39582
|
-
|
|
39583
|
-
|
|
39584
|
-
|
|
39845
|
+
if (foreignCwd && !foreignCheckout) {
|
|
39846
|
+
localCleanup = { branch: headRef, localBranch: { name: headRef, status: "not-attempted", reason: "skipped-foreign-cwd" } };
|
|
39847
|
+
} else {
|
|
39848
|
+
try {
|
|
39849
|
+
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
39850
|
+
beforeWorktrees,
|
|
39851
|
+
startingPath,
|
|
39852
|
+
baseRef,
|
|
39853
|
+
primaryRoot,
|
|
39854
|
+
preserveWorktree: o.preserveWorktree,
|
|
39855
|
+
gcAcknowledged: o.gc,
|
|
39856
|
+
expectedHeadOid: headRefOid,
|
|
39857
|
+
pathExists: (p) => (0, import_node_fs42.existsSync)(p),
|
|
39858
|
+
// #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
|
|
39859
|
+
// standing in is removed mid-cleanup, so a cwd-relative invocation fails with
|
|
39860
|
+
// 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
|
|
39861
|
+
execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout
|
|
39862
|
+
});
|
|
39863
|
+
} catch (e) {
|
|
39864
|
+
localCleanup = {
|
|
39865
|
+
branch: headRef,
|
|
39866
|
+
localBranch: {
|
|
39867
|
+
name: headRef,
|
|
39868
|
+
status: "failed",
|
|
39869
|
+
error: e instanceof Error ? e.message : String(e)
|
|
39870
|
+
}
|
|
39871
|
+
};
|
|
39872
|
+
}
|
|
39585
39873
|
}
|
|
39586
39874
|
const remoteBranch = await deleteMergedRemoteBranch({
|
|
39587
39875
|
branch: headRef,
|
|
@@ -39592,7 +39880,9 @@ ${list}`);
|
|
|
39592
39880
|
// #5899: this leg runs after worktree teardown — anchor it to the main checkout so a cwd inside
|
|
39593
39881
|
// the removed worktree cannot turn the delete into 'fatal: not a git repository' + manual remediation.
|
|
39594
39882
|
execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
39595
|
-
branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
|
|
39883
|
+
branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot, remote }),
|
|
39884
|
+
// #6148: aim the delete and its proof at `--repo`, not the cwd checkout's origin.
|
|
39885
|
+
remote
|
|
39596
39886
|
});
|
|
39597
39887
|
const worktree = localCleanup?.worktree;
|
|
39598
39888
|
const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
|
|
@@ -39655,6 +39945,7 @@ ${list}`);
|
|
|
39655
39945
|
...methodField ? { method: methodField } : {},
|
|
39656
39946
|
remoteBranch,
|
|
39657
39947
|
housekeeping,
|
|
39948
|
+
...foreignCwd ? { foreignCwd: true, ...foreignCheckout ? { foreignCheckout } : {} } : {},
|
|
39658
39949
|
...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
|
|
39659
39950
|
...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
|
|
39660
39951
|
...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
|
|
@@ -39970,7 +40261,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
39970
40261
|
var import_node_child_process16 = require("node:child_process");
|
|
39971
40262
|
var import_node_crypto12 = require("node:crypto");
|
|
39972
40263
|
var import_node_fs45 = require("node:fs");
|
|
39973
|
-
var
|
|
40264
|
+
var import_node_os20 = require("node:os");
|
|
39974
40265
|
var import_node_path41 = require("node:path");
|
|
39975
40266
|
|
|
39976
40267
|
// ../scripts/distribution-digest.mjs
|
|
@@ -40121,7 +40412,7 @@ function rebuildTo(packageRoot, outDir) {
|
|
|
40121
40412
|
});
|
|
40122
40413
|
}
|
|
40123
40414
|
function runDistStatus(root) {
|
|
40124
|
-
const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path41.join)((0,
|
|
40415
|
+
const stage = (0, import_node_fs45.mkdtempSync)((0, import_node_path41.join)((0, import_node_os20.tmpdir)(), "mmi-dist-drift-"));
|
|
40125
40416
|
let overlayCount = 0;
|
|
40126
40417
|
try {
|
|
40127
40418
|
const cliOut = (0, import_node_path41.join)(stage, "cli-dist");
|
|
@@ -40564,8 +40855,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40564
40855
|
registerSchedulesCommands(program3);
|
|
40565
40856
|
registerSchedulesLiftCommand(program3);
|
|
40566
40857
|
const docs = program3.command("docs").description("generated docs surfaces \u2014 the routing index (org knowledge layer)");
|
|
40567
|
-
docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").action(async (o) => {
|
|
40858
|
+
docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs index reads the current checkout").hideHelp()).action(async (o) => {
|
|
40568
40859
|
try {
|
|
40860
|
+
if (o.repo) {
|
|
40861
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs index ${o.check ? "--check" : "--write"}`, "docs index");
|
|
40862
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40863
|
+
}
|
|
40569
40864
|
const root = await repoRoot();
|
|
40570
40865
|
const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
|
|
40571
40866
|
if (o.check) {
|
|
@@ -40580,8 +40875,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40580
40875
|
await failGraceful(e.message);
|
|
40581
40876
|
}
|
|
40582
40877
|
});
|
|
40583
|
-
docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by \u2014 -->` comment across docs/** + README.md + architecture.md must resolve, or exit 1 (#3339)").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").action(async (o) => {
|
|
40878
|
+
docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by \u2014 -->` comment across docs/** + README.md + architecture.md must resolve, or exit 1 (#3339)").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs refs reads the current checkout").hideHelp()).action(async (o) => {
|
|
40584
40879
|
try {
|
|
40880
|
+
if (o.repo) {
|
|
40881
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs refs${o.json ? " --json" : ""}`, "docs refs");
|
|
40882
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40883
|
+
}
|
|
40585
40884
|
const root = await repoRoot();
|
|
40586
40885
|
const commandPaths = new Set(
|
|
40587
40886
|
buildCommandManifest(program3).index.map((entry) => entry.path)
|
|
@@ -40607,8 +40906,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40607
40906
|
}
|
|
40608
40907
|
});
|
|
40609
40908
|
const spawnCmd = program3.command("spawn").description("this repo's process-spawn contract \u2014 every child process must be unable to pop a console window");
|
|
40610
|
-
spawnCmd.command("policy").description("enforce the windowsHide contract across this repo's tracked source: a child_process call must set windowsHide, or take its options from a named constant, a type annotation, or a forwarded caller bag that does. Waive a call the scan cannot classify with a `// windows-hide-exempt: <reason>` comment on the line above it (#3979)").option("--json", "machine-readable result: { ok, scannedCount, findings[] }").action(async (o) => {
|
|
40909
|
+
spawnCmd.command("policy").description("enforce the windowsHide contract across this repo's tracked source: a child_process call must set windowsHide, or take its options from a named constant, a type annotation, or a forwarded caller bag that does. Waive a call the scan cannot classify with a `// windows-hide-exempt: <reason>` comment on the line above it (#3979)").option("--json", "machine-readable result: { ok, scannedCount, findings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 spawn policy reads the current checkout").hideHelp()).action(async (o) => {
|
|
40611
40910
|
try {
|
|
40911
|
+
if (o.repo) {
|
|
40912
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli spawn policy${o.json ? " --json" : ""}`, "spawn policy");
|
|
40913
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40914
|
+
}
|
|
40612
40915
|
const root = await repoRoot();
|
|
40613
40916
|
const result = runSpawnPolicy(root);
|
|
40614
40917
|
if (o.json) {
|
|
@@ -40630,8 +40933,13 @@ function registerDeveloperCommands(program3) {
|
|
|
40630
40933
|
}
|
|
40631
40934
|
});
|
|
40632
40935
|
const tests = program3.command("tests").description("a repo's test-policy.json \u2014 the opt-in test contract and its enforcement");
|
|
40633
|
-
tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, policySource: { ref, sha, path }, root, changedCount, mandatoryCount, matchedMandatoryCount, matchedMandatoryGlobs, testCommandsAllowed, commandClasses, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").option("--policy-ref <commit>", "load
|
|
40936
|
+
tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, policySource: { ref, sha, path }, root, changedCount, mandatoryCount, matchedMandatoryCount, matchedMandatoryGlobs, testCommandsAllowed, commandClasses, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").option("--policy-ref <commit>", "load test-policy.json from the exact fetched origin/development commit and audit its protected/satisfiedBy paths against that commit's tree, not this checkout; valid only with --base origin/main").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 tests policy reads the current checkout").hideHelp()).action(async (o) => {
|
|
40634
40937
|
try {
|
|
40938
|
+
if (o.repo) {
|
|
40939
|
+
const rerun = `mmi-cli tests policy${o.base ? ` --base ${o.base}` : ""}${o.policyRef ? ` --policy-ref ${o.policyRef}` : ""}${o.json ? " --json" : ""}`;
|
|
40940
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun, "tests policy");
|
|
40941
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40942
|
+
}
|
|
40635
40943
|
const root = await repoRoot();
|
|
40636
40944
|
const result = runTestPolicy(root, { base: o.base, policyRef: o.policyRef });
|
|
40637
40945
|
if (o.json) {
|
|
@@ -40661,8 +40969,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40661
40969
|
}
|
|
40662
40970
|
});
|
|
40663
40971
|
const distCmd = program3.command("dist").description("this repo's committed dist/BOM drift receipt \u2014 whether cli/dist, updater/dist and distribution-bom.json still match a fresh build of source");
|
|
40664
|
-
distCmd.command("status").description("rebuild every committed dist artifact to a temp dir and report committed vs rebuilt-expected sha256 plus the BOM's recorded dist identities \u2014 a visible, non-blocking receipt. Development checkouts may lag source until the release fold; drift NEVER fails the run, and nothing is refreshed for you (#5576)").option("--json", "machine-readable receipt: { ok, staleCount, artifacts[], bom, summary } (full hashes; drift still exits 0)").action(async (o) => {
|
|
40972
|
+
distCmd.command("status").description("rebuild every committed dist artifact to a temp dir and report committed vs rebuilt-expected sha256 plus the BOM's recorded dist identities \u2014 a visible, non-blocking receipt. Development checkouts may lag source until the release fold; drift NEVER fails the run, and nothing is refreshed for you (#5576)").option("--json", "machine-readable receipt: { ok, staleCount, artifacts[], bom, summary } (full hashes; drift still exits 0)").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 dist status reads the current checkout").hideHelp()).action(async (o) => {
|
|
40665
40973
|
try {
|
|
40974
|
+
if (o.repo) {
|
|
40975
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli dist status${o.json ? " --json" : ""}`, "dist status");
|
|
40976
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40977
|
+
}
|
|
40666
40978
|
const root = await repoRoot();
|
|
40667
40979
|
const receipt = runDistStatus(root);
|
|
40668
40980
|
if (o.json) {
|
|
@@ -40957,7 +41269,7 @@ async function resolveHotfixDeployModel(deps, ctx) {
|
|
|
40957
41269
|
}
|
|
40958
41270
|
async function hotfixPreflight(deps, ctx, verb, targetTag) {
|
|
40959
41271
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
40960
|
-
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
41272
|
+
const deployModel = await preflight(deps, ctx, "main", meta, "hotfix");
|
|
40961
41273
|
const root = await hotfixCheckoutRoot(deps);
|
|
40962
41274
|
const begin = await beginReleaseLedger(deps, root, targetTag);
|
|
40963
41275
|
if (!begin.ok) throw new Error(`hotfix ${verb} refused before any mutation: ${begin.error}`);
|
|
@@ -41316,6 +41628,16 @@ function hotfixDispatchFromRuns(runs, note) {
|
|
|
41316
41628
|
const tenantDeploy2 = runs.find((r) => r.workflow === "tenant-deploy.yml");
|
|
41317
41629
|
return { note, deployStatus, workflowRuns, ...tenantDeploy2?.runId != null ? { runId: tenantDeploy2.runId } : {}, ...tenantDeploy2?.url ? { runUrl: tenantDeploy2.url } : {} };
|
|
41318
41630
|
}
|
|
41631
|
+
async function appendHotfixGatewayRun(deps, repo, tag, sha, runs, note) {
|
|
41632
|
+
const input = hotfixDispatchFromRuns(runs, note);
|
|
41633
|
+
const gateway = await appendJervGatewayReleaseDeploy(deps, repo, tag, sha, input);
|
|
41634
|
+
if (gateway === input) return note;
|
|
41635
|
+
const row = gateway.workflowRuns?.at(-1);
|
|
41636
|
+
if (row?.workflow === "jerv-gateway") {
|
|
41637
|
+
runs.push({ workflow: row.workflow, ...row.runUrlNote ? { runUrlNote: row.runUrlNote } : {}, conclusion: row.conclusion });
|
|
41638
|
+
}
|
|
41639
|
+
return gateway.note;
|
|
41640
|
+
}
|
|
41319
41641
|
function hotfixPhaseInputsFromRuns(deployModel, runs, note, publishDispatch, publishRequired, opts) {
|
|
41320
41642
|
const dispatch = hotfixDispatchFromRuns(runs, note);
|
|
41321
41643
|
return {
|
|
@@ -41559,6 +41881,7 @@ ${decision.note}`);
|
|
|
41559
41881
|
} else {
|
|
41560
41882
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
41561
41883
|
}
|
|
41884
|
+
deployNote = await appendHotfixGatewayRun(deps, ctx.repo, tag, mergedSha, runs, deployNote);
|
|
41562
41885
|
const ledgerInputs = hotfixPhaseInputsFromRuns(
|
|
41563
41886
|
deployModel,
|
|
41564
41887
|
runs,
|
|
@@ -41751,6 +42074,9 @@ function hotfixStatusRunsFromLedger(ledger) {
|
|
|
41751
42074
|
workflow: record.workflow ?? phase,
|
|
41752
42075
|
...record.runId != null ? { runId: record.runId } : {},
|
|
41753
42076
|
...record.runUrl ? { url: record.runUrl } : {},
|
|
42077
|
+
// #912: the operator-host Gateway leg carries no run id by design — without its own note the
|
|
42078
|
+
// renderer's missing-URL fallback reads it as 'run absent or unreadable'.
|
|
42079
|
+
...record.workflow === "jerv-gateway" ? { runUrlNote: JERV_GATEWAY_RUN_URL_NOTE } : {},
|
|
41754
42080
|
conclusion: record.state === "complete" ? "success" : record.state === "failed" ? "failure" : "pending"
|
|
41755
42081
|
}];
|
|
41756
42082
|
});
|
|
@@ -42231,6 +42557,7 @@ function renderReleaseResume(r) {
|
|
|
42231
42557
|
if (r.devRollForward) lines2.push(` development: ${r.devRollForward.note}`);
|
|
42232
42558
|
if (r.rcAlignment) lines2.push(` rc: ${r.rcAlignment.note}`);
|
|
42233
42559
|
if (r.checkout) lines2.push(` checkout: ${r.checkout.note}`);
|
|
42560
|
+
if (r.projectInfoSync) lines2.push(` project info: ${r.projectInfoSync.note}`);
|
|
42234
42561
|
if (r.ledger) lines2.push(...formatReleaseLedgerReport(r.ledger).map((l, i) => i === 0 ? ` ${l}` : ` ${l}`));
|
|
42235
42562
|
return lines2.join("\n");
|
|
42236
42563
|
}
|
|
@@ -42258,7 +42585,8 @@ function releaseFollowUpLegs(result, projectInfoSync) {
|
|
|
42258
42585
|
const legs = [
|
|
42259
42586
|
{
|
|
42260
42587
|
leg: "project-info",
|
|
42261
|
-
|
|
42588
|
+
// #6180: a transient Hub read outage is PENDING (rerun the manual verb), never a failed follow-up.
|
|
42589
|
+
status: projectInfoSync && "error" in projectInfoSync ? projectInfoSync.pending ? "pending" : "failed" : "success",
|
|
42262
42590
|
...projectInfoSync && "error" in projectInfoSync ? { error: projectInfoSync.error } : {}
|
|
42263
42591
|
}
|
|
42264
42592
|
];
|
|
@@ -42297,6 +42625,24 @@ function releaseFollowUpLegs(result, projectInfoSync) {
|
|
|
42297
42625
|
}
|
|
42298
42626
|
return legs;
|
|
42299
42627
|
}
|
|
42628
|
+
function hotfixFollowUpLegs(runs, foldPort, alignment, foldNote, deployNote) {
|
|
42629
|
+
const legs = runs.map((run) => ({
|
|
42630
|
+
leg: run.workflow,
|
|
42631
|
+
status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
|
|
42632
|
+
// #912 lane parity with releaseFollowUpLegs: the operator-host Gateway leg has no workflow run to
|
|
42633
|
+
// read, so its deploy note IS the error — a flat "workflow reported failure" names nothing.
|
|
42634
|
+
...run.conclusion === "failure" ? { error: run.workflow === "jerv-gateway" && deployNote ? deployNote : "workflow reported failure" } : {}
|
|
42635
|
+
}));
|
|
42636
|
+
if (foldPort === "failure") {
|
|
42637
|
+
legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
|
|
42638
|
+
}
|
|
42639
|
+
if (alignment === "unresolved") {
|
|
42640
|
+
legs.push({ leg: "development-fold-alignment", status: "pending" });
|
|
42641
|
+
} else if (alignment === "failure" && foldPort !== "failure") {
|
|
42642
|
+
legs.push({ leg: "development-fold-alignment", status: "failed", error: foldNote ?? "development fold alignment failed" });
|
|
42643
|
+
}
|
|
42644
|
+
return legs;
|
|
42645
|
+
}
|
|
42300
42646
|
var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
|
|
42301
42647
|
async function runPostReleaseJervDoctor(repo) {
|
|
42302
42648
|
if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
|
|
@@ -42312,10 +42658,35 @@ async function runPostReleaseJervDoctor(repo) {
|
|
|
42312
42658
|
};
|
|
42313
42659
|
}
|
|
42314
42660
|
}
|
|
42661
|
+
async function runProjectInfoSyncLeg(cb, repo, sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))) {
|
|
42662
|
+
for (let attempt = 0; ; attempt++) {
|
|
42663
|
+
try {
|
|
42664
|
+
return await cb(repo, true);
|
|
42665
|
+
} catch (e) {
|
|
42666
|
+
const error = e.message;
|
|
42667
|
+
if (!(e instanceof ProjectInfoReadUnavailableError)) return { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
42668
|
+
if (attempt === 0) {
|
|
42669
|
+
await sleep2(PROJECT_INFO_RETRY_MS);
|
|
42670
|
+
continue;
|
|
42671
|
+
}
|
|
42672
|
+
return {
|
|
42673
|
+
applied: false,
|
|
42674
|
+
pending: true,
|
|
42675
|
+
note: `PENDING \u2014 ${error}; rerun \`mmi-cli oracle org project sync-info ${repo} --apply\``,
|
|
42676
|
+
error
|
|
42677
|
+
};
|
|
42678
|
+
}
|
|
42679
|
+
}
|
|
42680
|
+
}
|
|
42681
|
+
var PROJECT_INFO_RETRY_MS = 3e3;
|
|
42682
|
+
function projectInfoOutcome(projectInfoSync) {
|
|
42683
|
+
if (!projectInfoSync || !("error" in projectInfoSync)) return "ok";
|
|
42684
|
+
return projectInfoSync.pending ? "unresolved" : "failure";
|
|
42685
|
+
}
|
|
42315
42686
|
function buildReleaseVerdict(commandName, result, projectInfoSync) {
|
|
42316
42687
|
const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
|
|
42317
42688
|
const followUpStatus = deriveTrainFollowUpStatus({
|
|
42318
|
-
projectInfo: projectInfoSync
|
|
42689
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42319
42690
|
deploy: deployFollowUpOutcome(result.deployStatus),
|
|
42320
42691
|
rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
|
|
42321
42692
|
alignment: alignmentPending ? "unresolved" : "ok",
|
|
@@ -42496,8 +42867,17 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42496
42867
|
}
|
|
42497
42868
|
const raw = await runReleaseResume(trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
|
|
42498
42869
|
const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
|
|
42499
|
-
|
|
42500
|
-
|
|
42870
|
+
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42871
|
+
const resumed = { ...result, projectInfoSync };
|
|
42872
|
+
emitTrainResult("release --resume", o.json ? JSON.stringify(resumed, null, 2) : renderReleaseResume(resumed), o.out);
|
|
42873
|
+
const resumeStatus = resumeFollowUpOf(result.state);
|
|
42874
|
+
applyTrainFollowUpExit(deriveTrainFollowUpStatus({
|
|
42875
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42876
|
+
deploy: "ok",
|
|
42877
|
+
rcRetirement: "ok",
|
|
42878
|
+
alignment: resumeStatus === "failed" ? "failure" : resumeStatus === "pending" ? "unresolved" : "ok",
|
|
42879
|
+
foldPort: "ok"
|
|
42880
|
+
}));
|
|
42501
42881
|
return;
|
|
42502
42882
|
} catch (e) {
|
|
42503
42883
|
applyTrainFollowUpExit("failed");
|
|
@@ -42535,12 +42915,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42535
42915
|
let projectInfoSync;
|
|
42536
42916
|
const postReleaseJervDoctor = commandName === "release" ? await runPostReleaseJervDoctor(result.repo) : void 0;
|
|
42537
42917
|
if (commandName === "release") {
|
|
42538
|
-
|
|
42539
|
-
projectInfoSync = await runProjectInfoSyncCallback(result.repo, true);
|
|
42540
|
-
} catch (e) {
|
|
42541
|
-
const error = e.message;
|
|
42542
|
-
projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
42543
|
-
}
|
|
42918
|
+
projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42544
42919
|
}
|
|
42545
42920
|
const { followUpStatus, releaseVerdict } = buildReleaseVerdict(commandName, result, projectInfoSync);
|
|
42546
42921
|
const reported = {
|
|
@@ -42637,22 +43012,6 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42637
43012
|
if (conclusion === "failure") return "failure";
|
|
42638
43013
|
return "unresolved";
|
|
42639
43014
|
}
|
|
42640
|
-
function hotfixFollowUpLegs(runs, foldPort, alignment, foldNote) {
|
|
42641
|
-
const legs = runs.map((run) => ({
|
|
42642
|
-
leg: run.workflow,
|
|
42643
|
-
status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
|
|
42644
|
-
...run.conclusion === "failure" ? { error: "workflow reported failure" } : {}
|
|
42645
|
-
}));
|
|
42646
|
-
if (foldPort === "failure") {
|
|
42647
|
-
legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
|
|
42648
|
-
}
|
|
42649
|
-
if (alignment === "unresolved") {
|
|
42650
|
-
legs.push({ leg: "development-fold-alignment", status: "pending" });
|
|
42651
|
-
} else if (alignment === "failure" && foldPort !== "failure") {
|
|
42652
|
-
legs.push({ leg: "development-fold-alignment", status: "failed", error: foldNote ?? "development fold alignment failed" });
|
|
42653
|
-
}
|
|
42654
|
-
return legs;
|
|
42655
|
-
}
|
|
42656
43015
|
async function runHotfixSub(sub, body, o, render) {
|
|
42657
43016
|
try {
|
|
42658
43017
|
await requireFreshTrainCli("hotfix");
|
|
@@ -42664,11 +43023,12 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42664
43023
|
const ledger = result.ledger;
|
|
42665
43024
|
const alignment = result.alignmentStatus ?? ledgerAlignmentFollowUpOutcome(ledger);
|
|
42666
43025
|
const projectInfoSync = result.projectInfoSync;
|
|
42667
|
-
const
|
|
43026
|
+
const deployNote = result.deployNote ?? ledger?.phases.deploy.error ?? ledger?.phases.deploy.note;
|
|
43027
|
+
const legs = runs ? hotfixFollowUpLegs(runs, foldPort, alignment, result.foldNote, deployNote) : void 0;
|
|
42668
43028
|
const json = o.json || Boolean(hotfixCmd.opts().json);
|
|
42669
43029
|
emitTrainResult(`hotfix ${sub}`, json ? JSON.stringify(legs ? Object.assign({}, result, { legs }) : result, null, 2) : render(result), o.out);
|
|
42670
43030
|
if (runs) applyTrainFollowUpExit(deriveTrainFollowUpStatus({
|
|
42671
|
-
projectInfo: projectInfoSync
|
|
43031
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42672
43032
|
deploy: reduceFollowUpOutcomes(runs.map((r) => hotfixRunOutcome(r.conclusion))),
|
|
42673
43033
|
rcRetirement: "ok",
|
|
42674
43034
|
alignment,
|
|
@@ -42692,13 +43052,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42692
43052
|
hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated 3-6 line Hub Slack summary; required for a NEW MMI-Hub hotfix Release (#883/#3901/#6068)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").option("--out <path>", "write the result to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)").action(async (version, o) => runHotfixSub("release", async () => {
|
|
42693
43053
|
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
|
|
42694
43054
|
const postReleaseJervDoctor = await runPostReleaseJervDoctor(result.repo);
|
|
42695
|
-
|
|
42696
|
-
try {
|
|
42697
|
-
projectInfoSync = await runProjectInfoSyncCallback(result.repo, true);
|
|
42698
|
-
} catch (e) {
|
|
42699
|
-
const error = e.message;
|
|
42700
|
-
projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
42701
|
-
}
|
|
43055
|
+
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42702
43056
|
return { ...result, projectInfoSync, ...postReleaseJervDoctor ? { postReleaseJervDoctor } : {} };
|
|
42703
43057
|
}, o, renderHotfixRelease));
|
|
42704
43058
|
function hotfixStatusDeps() {
|
|
@@ -42742,7 +43096,7 @@ function envHealLockPath(home) {
|
|
|
42742
43096
|
async function withEnvHealLock(what, run) {
|
|
42743
43097
|
try {
|
|
42744
43098
|
return await withFileLock(
|
|
42745
|
-
envHealLockPath((0,
|
|
43099
|
+
envHealLockPath((0, import_node_os21.homedir)()),
|
|
42746
43100
|
{ staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
|
|
42747
43101
|
run
|
|
42748
43102
|
);
|
|
@@ -42838,7 +43192,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
42838
43192
|
const configRoot = surfaceConfigRoot(surface);
|
|
42839
43193
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
42840
43194
|
const plan = buildPluginCachePlan(
|
|
42841
|
-
(0,
|
|
43195
|
+
(0, import_node_os21.homedir)(),
|
|
42842
43196
|
running,
|
|
42843
43197
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
42844
43198
|
{ configRoot, includeStaging: surface !== "codex" }
|
|
@@ -42866,7 +43220,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
42866
43220
|
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
42867
43221
|
const installed = installedActivePluginVersion(surface);
|
|
42868
43222
|
const plan = buildPluginCachePlan(
|
|
42869
|
-
(0,
|
|
43223
|
+
(0, import_node_os21.homedir)(),
|
|
42870
43224
|
running,
|
|
42871
43225
|
pluginCacheFsDeps(configRoot, () => 0),
|
|
42872
43226
|
{ configRoot, includeStaging: surface !== "codex", installedVersion: installed }
|
|
@@ -43279,7 +43633,7 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
|
|
|
43279
43633
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
43280
43634
|
}
|
|
43281
43635
|
});
|
|
43282
|
-
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints").action(async (repo, stage) => {
|
|
43636
|
+
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints; an auth-walled root (401/403) is re-probed at /health and the `url` field names the endpoint that answered (#6137)").action(async (repo, stage) => {
|
|
43283
43637
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
43284
43638
|
const cfg = await loadConfig();
|
|
43285
43639
|
const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
@@ -43321,26 +43675,13 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
43321
43675
|
return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
|
|
43322
43676
|
}
|
|
43323
43677
|
});
|
|
43324
|
-
async function probeHttpBounded2(url, timeoutMs = 5e3) {
|
|
43325
|
-
const controller = new AbortController();
|
|
43326
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
43327
|
-
timeout.unref?.();
|
|
43328
|
-
try {
|
|
43329
|
-
const res = await fetch(url, { method: "GET", signal: controller.signal });
|
|
43330
|
-
return { ok: res.ok, status: res.status };
|
|
43331
|
-
} catch (e) {
|
|
43332
|
-
return { ok: false, error: e.message };
|
|
43333
|
-
} finally {
|
|
43334
|
-
clearTimeout(timeout);
|
|
43335
|
-
}
|
|
43336
|
-
}
|
|
43337
43678
|
async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
43338
43679
|
const slug = slugOf(target);
|
|
43339
43680
|
const reg = registryClientDeps(cfg);
|
|
43340
43681
|
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
43341
43682
|
const deploy = facts?.stages[stage] ?? null;
|
|
43342
43683
|
const publicUrl = publicUrlFromDeployFact(deploy);
|
|
43343
|
-
const publicProbe = publicUrl ? await
|
|
43684
|
+
const publicProbe = publicUrl ? await probePublicHealth(publicUrl) : void 0;
|
|
43344
43685
|
return buildTenantRuntimeStatus({
|
|
43345
43686
|
repo: target,
|
|
43346
43687
|
slug,
|
|
@@ -43368,9 +43709,9 @@ async function runProjectInfoSync(target, apply) {
|
|
|
43368
43709
|
fetchProjectBySlugChecked(slugOf(targetRepo2), registry2),
|
|
43369
43710
|
fetchProjectsList(registry2)
|
|
43370
43711
|
]);
|
|
43371
|
-
if (!read.ok) throw new
|
|
43712
|
+
if (!read.ok) throw new ProjectInfoReadUnavailableError(`org project sync-info: Hub registry read failed (${read.error})`);
|
|
43372
43713
|
if (!read.project) throw new Error(`org project sync-info: no registry META for ${targetRepo2}`);
|
|
43373
|
-
if (!projects) throw new
|
|
43714
|
+
if (!projects) throw new ProjectInfoReadUnavailableError("org project sync-info: Hub project list unavailable");
|
|
43374
43715
|
if (apply) {
|
|
43375
43716
|
const authority = await fetchTrainAuthority(targetRepo2, registry2);
|
|
43376
43717
|
if (!authority.ok) throw new Error(`org project sync-info: train authority unverified (${authority.error})`);
|
|
@@ -44017,7 +44358,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
44017
44358
|
return;
|
|
44018
44359
|
}
|
|
44019
44360
|
const plan = buildPluginCachePlan(
|
|
44020
|
-
(0,
|
|
44361
|
+
(0, import_node_os21.homedir)(),
|
|
44021
44362
|
running,
|
|
44022
44363
|
pluginCacheFsDeps(configRoot, directoryBytes),
|
|
44023
44364
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|