@mutmutco/cli 4.3.7 → 4.3.8
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 +503 -200
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -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.8",
|
|
15454
|
+
tag: "v4.3.8",
|
|
15455
|
+
commit: "b109b86b97d1",
|
|
15456
|
+
npm: "@mutmutco/cli@4.3.8"
|
|
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.8"
|
|
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.8 and redeploy the Hub Lambda from tag v4.3.8 (b109b86b97d1); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15480
|
+
v3Target: "v4.3.8 (@mutmutco/cli@4.3.8, tag commit b109b86b97d1 \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 {
|
|
@@ -20599,8 +20647,11 @@ function deployPhaseInput(model, dispatch, opts = {}) {
|
|
|
20599
20647
|
note: deployRun ? `${deployRun.workflow} release run` : dispatch.note
|
|
20600
20648
|
};
|
|
20601
20649
|
}
|
|
20602
|
-
case "registry-publish":
|
|
20650
|
+
case "registry-publish": {
|
|
20651
|
+
const hostRun = (dispatch.workflowRuns ?? []).find((r) => r.workflow === "jerv-gateway");
|
|
20652
|
+
if (hostRun) return { state: runRowState(hostRun, dispatch.deployStatus), ...sha ? { sha } : {}, workflow: hostRun.workflow, note: dispatch.note };
|
|
20603
20653
|
return { state: "skipped", ...sha ? { sha } : {}, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" };
|
|
20654
|
+
}
|
|
20604
20655
|
case "tenant-container":
|
|
20605
20656
|
case "solo-container":
|
|
20606
20657
|
case "static-cdn":
|
|
@@ -20661,6 +20712,7 @@ function releasePhaseInputsFromDispatch(model, dispatch, publishDispatch, publis
|
|
|
20661
20712
|
function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
|
|
20662
20713
|
const deployRun = rows.find((r) => r.workflow === "deploy.yml");
|
|
20663
20714
|
const publishRun = rows.find((r) => r.workflow === "publish.yml");
|
|
20715
|
+
const hostRun = rows.find((r) => r.workflow === "jerv-gateway");
|
|
20664
20716
|
switch (model) {
|
|
20665
20717
|
case "hub-serverless":
|
|
20666
20718
|
return {
|
|
@@ -20679,7 +20731,9 @@ function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
|
|
|
20679
20731
|
};
|
|
20680
20732
|
case "registry-publish":
|
|
20681
20733
|
return {
|
|
20682
|
-
|
|
20734
|
+
// #884/#6145: an operator-host deploy leg (Jerv-Hub's Gateway) outranks 'skipped' here exactly as
|
|
20735
|
+
// in deployPhaseInput — it carries no runId, so no run metadata and no resume re-verification.
|
|
20736
|
+
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
20737
|
publish: publishRun ? {
|
|
20684
20738
|
state: runRowState(publishRun, "pending"),
|
|
20685
20739
|
sha: tagSha,
|
|
@@ -21046,12 +21100,14 @@ async function selfConvergeTrainCli(input) {
|
|
|
21046
21100
|
var TRAIN_LANES = ["release", "rcand", "hotfix"];
|
|
21047
21101
|
var TROUBLESHOOTING_GUIDE = "docs/Guides/train-troubleshooting.md";
|
|
21048
21102
|
var SCRATCH_BRANCH_GLOBS = ["train/check/*", "hotfix-fold/*--port-*"];
|
|
21103
|
+
var COMPOSE_GUARD_HARD_FAIL = /^secrets preflight: .*noEnvFile is (not )?true.*$/m;
|
|
21049
21104
|
function readLocalWorkflowsDefault(root) {
|
|
21050
21105
|
const dir = (0, import_node_path22.join)(root, ".github", "workflows");
|
|
21051
21106
|
let names;
|
|
21052
21107
|
try {
|
|
21053
21108
|
names = (0, import_node_fs23.readdirSync)(dir);
|
|
21054
|
-
} catch {
|
|
21109
|
+
} catch (e) {
|
|
21110
|
+
if (e.code === "ENOENT") return [];
|
|
21055
21111
|
return null;
|
|
21056
21112
|
}
|
|
21057
21113
|
const files = [];
|
|
@@ -21281,12 +21337,12 @@ async function runTrainDoctor(input) {
|
|
|
21281
21337
|
...!hints.hasMainBranch ? ["main"] : [],
|
|
21282
21338
|
...!hints.hasRcBranch && track === "full" ? ["rc"] : []
|
|
21283
21339
|
];
|
|
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` });
|
|
21340
|
+
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
21341
|
}
|
|
21286
21342
|
try {
|
|
21287
21343
|
const required = await discoverRequiredCheckContexts(train, ctx, stage);
|
|
21288
21344
|
if (required.length === 0) {
|
|
21289
|
-
add({ code: "bootstrap-gap", severity: "
|
|
21345
|
+
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
21346
|
} else {
|
|
21291
21347
|
try {
|
|
21292
21348
|
assertTagAddressableRequiredContexts({ readWorkflows: () => workflows }, required, repo);
|
|
@@ -21358,12 +21414,14 @@ async function runTrainDoctor(input) {
|
|
|
21358
21414
|
}
|
|
21359
21415
|
}
|
|
21360
21416
|
try {
|
|
21361
|
-
await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo]);
|
|
21417
|
+
await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
|
|
21362
21418
|
} catch (e) {
|
|
21363
21419
|
const err = e;
|
|
21364
21420
|
const text = [typeof err.stdout === "string" ? err.stdout : "", typeof err.stderr === "string" ? err.stderr : "", message(e)].join("\n");
|
|
21365
21421
|
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}\`` });
|
|
21422
|
+
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" : ""}\`` });
|
|
21423
|
+
} else if (COMPOSE_GUARD_HARD_FAIL.test(text)) {
|
|
21424
|
+
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
21425
|
} else {
|
|
21368
21426
|
unverified(`the ${stage} secrets preflight`, clean2(text) || e);
|
|
21369
21427
|
}
|
|
@@ -21553,7 +21611,7 @@ function enforceGateBudget(deps, repo) {
|
|
|
21553
21611
|
);
|
|
21554
21612
|
}
|
|
21555
21613
|
}
|
|
21556
|
-
async function preflight(deps, ctx, stage, meta) {
|
|
21614
|
+
async function preflight(deps, ctx, stage, meta, lane) {
|
|
21557
21615
|
const model = requireDeployModel(meta, ctx.repo);
|
|
21558
21616
|
if (model === "content") {
|
|
21559
21617
|
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 +21619,7 @@ async function preflight(deps, ctx, stage, meta) {
|
|
|
21561
21619
|
if (model === "none") {
|
|
21562
21620
|
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
21621
|
}
|
|
21564
|
-
await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo]);
|
|
21622
|
+
await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
|
|
21565
21623
|
await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
|
|
21566
21624
|
await assertActionsJobsCanStart(deps, ctx.repo);
|
|
21567
21625
|
enforceGateBudget(deps, ctx.repo);
|
|
@@ -22207,7 +22265,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22207
22265
|
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
22208
22266
|
const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
|
|
22209
22267
|
const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
|
|
22210
|
-
const
|
|
22268
|
+
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
22211
22269
|
deps,
|
|
22212
22270
|
ledger,
|
|
22213
22271
|
ledgerAnchors,
|
|
@@ -22220,6 +22278,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22220
22278
|
workflow: failedReleaseRun?.workflow ?? (isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow")
|
|
22221
22279
|
}
|
|
22222
22280
|
);
|
|
22281
|
+
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch);
|
|
22223
22282
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha })), {
|
|
22224
22283
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
22225
22284
|
});
|
|
@@ -22244,7 +22303,6 @@ ${recovery.note}`), recoveryInput);
|
|
|
22244
22303
|
landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
22245
22304
|
});
|
|
22246
22305
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
22247
|
-
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
|
|
22248
22306
|
if (publishSkipNote) dispatch = { ...dispatch, note: `${dispatch.note}; tenant-publish.yml skipped (${publishSkipNote})` };
|
|
22249
22307
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors };
|
|
22250
22308
|
}
|
|
@@ -22520,17 +22578,18 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
22520
22578
|
historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : recovered.deployStatus;
|
|
22521
22579
|
historicalNote = recovered.note;
|
|
22522
22580
|
}
|
|
22581
|
+
const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
|
|
22582
|
+
note: historicalNote,
|
|
22583
|
+
deployStatus: historicalDeploy,
|
|
22584
|
+
workflowRuns: historicalRows
|
|
22585
|
+
});
|
|
22523
22586
|
if (persisted) {
|
|
22524
|
-
const phaseInputs = phaseInputsFromRunRows(deployModel, historicalRows, tagSha, { repo: ctx.repo });
|
|
22587
|
+
const phaseInputs = phaseInputsFromRunRows(deployModel, dispatch2.workflowRuns ?? historicalRows, tagSha, { repo: ctx.repo });
|
|
22525
22588
|
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
22589
|
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
22590
|
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
22591
|
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
22592
|
}
|
|
22530
|
-
const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
|
|
22531
|
-
note: historicalNote,
|
|
22532
|
-
deployStatus: historicalDeploy
|
|
22533
|
-
});
|
|
22534
22593
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
22535
22594
|
if (persisted) {
|
|
22536
22595
|
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 +22671,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22612
22671
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
22613
22672
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
22614
22673
|
const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
|
|
22615
|
-
const
|
|
22674
|
+
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
22616
22675
|
deps,
|
|
22617
22676
|
ledger,
|
|
22618
22677
|
anchors,
|
|
@@ -22625,6 +22684,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22625
22684
|
workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow"
|
|
22626
22685
|
}
|
|
22627
22686
|
);
|
|
22687
|
+
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
22628
22688
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
22629
22689
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha })), {
|
|
22630
22690
|
strict: ledgerMode === "strict",
|
|
@@ -22643,8 +22703,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22643
22703
|
strict: ledgerMode === "strict",
|
|
22644
22704
|
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
22645
22705
|
});
|
|
22646
|
-
|
|
22647
|
-
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, dispatch);
|
|
22706
|
+
const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
22648
22707
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
22649
22708
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
22650
22709
|
const rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
@@ -25269,6 +25328,13 @@ function registerBoardCommands(program3) {
|
|
|
25269
25328
|
const lane = holder.surface && holder.session && holder.host ? ` (${holder.surface}/${holder.session}@${holder.host})` : "";
|
|
25270
25329
|
return `@${holder.login}${lane}`;
|
|
25271
25330
|
}
|
|
25331
|
+
function printClaimWarnings(warnings, toStderr = false) {
|
|
25332
|
+
for (const warning of warnings ?? []) {
|
|
25333
|
+
if (toStderr) process.stderr.write(`Warning: ${warning}
|
|
25334
|
+
`);
|
|
25335
|
+
else console.log(`Warning: ${warning}`);
|
|
25336
|
+
}
|
|
25337
|
+
}
|
|
25272
25338
|
function claimVerdict(ref, result) {
|
|
25273
25339
|
const holder = formatClaimHolder(result.holder);
|
|
25274
25340
|
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
@@ -25291,24 +25357,26 @@ function registerBoardCommands(program3) {
|
|
|
25291
25357
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
25292
25358
|
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
25359
|
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"),
|
|
25360
|
+
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
25361
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
25296
25362
|
).action(async (issueRefs, o) => {
|
|
25297
25363
|
if (issueRefs.length === 1) {
|
|
25298
25364
|
const issueRef = issueRefs[0];
|
|
25299
25365
|
try {
|
|
25366
|
+
const config = await loadConfigForBoardSelector2(issueRef, o.repo);
|
|
25300
25367
|
const result = await claimBoardIssue({
|
|
25301
|
-
config
|
|
25368
|
+
config,
|
|
25302
25369
|
selector: issueRef,
|
|
25303
25370
|
repo: o.repo,
|
|
25304
25371
|
assignee: o.for,
|
|
25305
25372
|
force: o.force,
|
|
25306
25373
|
check: o.check,
|
|
25307
25374
|
allowPartial: o.allowPartial
|
|
25308
|
-
});
|
|
25375
|
+
}, { snapshot: registryClientDeps(config) });
|
|
25309
25376
|
if (!result.checked) invalidateStatuslineBoardCache();
|
|
25310
25377
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25311
25378
|
console.log(claimVerdict(result.item.ref, result));
|
|
25379
|
+
printClaimWarnings(result.warnings);
|
|
25312
25380
|
} catch (e) {
|
|
25313
25381
|
if (refuseRateLimited(e, o.json)) return;
|
|
25314
25382
|
return failGraceful(`board claim failed: ${e.message}`);
|
|
@@ -25316,22 +25384,25 @@ function registerBoardCommands(program3) {
|
|
|
25316
25384
|
return;
|
|
25317
25385
|
}
|
|
25318
25386
|
try {
|
|
25387
|
+
const config = await loadConfigForBoardSelector2(issueRefs[0], o.repo);
|
|
25319
25388
|
const bulk = await claimBoardIssues({
|
|
25320
|
-
config
|
|
25389
|
+
config,
|
|
25321
25390
|
selectors: issueRefs,
|
|
25322
25391
|
repo: o.repo,
|
|
25323
25392
|
assignee: o.for,
|
|
25324
25393
|
force: o.force,
|
|
25325
25394
|
check: o.check,
|
|
25326
25395
|
allowPartial: o.allowPartial
|
|
25327
|
-
});
|
|
25396
|
+
}, { snapshot: registryClientDeps(config) });
|
|
25328
25397
|
if (bulk.results.some((r) => r.claimed && !r.checked)) invalidateStatuslineBoardCache();
|
|
25329
25398
|
if (o.json) {
|
|
25330
25399
|
console.log(JSON.stringify(bulk.results));
|
|
25400
|
+
printClaimWarnings(bulk.warnings, true);
|
|
25331
25401
|
} else {
|
|
25332
25402
|
for (const result of bulk.results) {
|
|
25333
25403
|
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
25334
25404
|
}
|
|
25405
|
+
printClaimWarnings(bulk.warnings);
|
|
25335
25406
|
}
|
|
25336
25407
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
25337
25408
|
} catch (e) {
|
|
@@ -25644,7 +25715,7 @@ function trainPlan(command, options = {}) {
|
|
|
25644
25715
|
{ label: "verify the fix is merged on development (the only hotfix origin)", gated: true },
|
|
25645
25716
|
// #6068: hotfix start/release run the SAME shared preflight as release/rcand, before any git mutation.
|
|
25646
25717
|
{ 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 },
|
|
25718
|
+
{ label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo> --lane hotfix", gated: true },
|
|
25648
25719
|
{ 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
25720
|
{ 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
25721
|
{ label: "branch hotfix from main and cherry-pick the dev commits", command: "git cherry-pick -x <dev-sha>", gated: true },
|
|
@@ -28252,7 +28323,7 @@ function formatDeployStatus(r) {
|
|
|
28252
28323
|
`running version: ${r.runningVersion ?? "none stamped"}`,
|
|
28253
28324
|
`last deploy run: ${formatLastRun(r)}`,
|
|
28254
28325
|
`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"}`,
|
|
28326
|
+
`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
28327
|
`deploy state: ${r.deployOk === void 0 ? "not stamped" : r.deployOk ? "ok" : "failed"}`
|
|
28257
28328
|
];
|
|
28258
28329
|
if (r.hints.length) {
|
|
@@ -28263,22 +28334,9 @@ function formatDeployStatus(r) {
|
|
|
28263
28334
|
|
|
28264
28335
|
// src/deploy-commands.ts
|
|
28265
28336
|
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
28337
|
function registerDeployCommands(program3) {
|
|
28280
28338
|
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) => {
|
|
28339
|
+
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
28340
|
if (!STAGES2.includes(stage)) {
|
|
28283
28341
|
return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
|
|
28284
28342
|
}
|
|
@@ -28290,7 +28348,7 @@ function registerDeployCommands(program3) {
|
|
|
28290
28348
|
const deployFacts = await fetchDeployFactsBySlug(slug, reg);
|
|
28291
28349
|
const publicUrl = publicUrlFromDeployFact(deployFacts?.stages[stage] ?? null);
|
|
28292
28350
|
const [health, lastRun] = await Promise.all([
|
|
28293
|
-
publicUrl ?
|
|
28351
|
+
publicUrl ? probePublicHealth(publicUrl) : Promise.resolve(void 0),
|
|
28294
28352
|
fetchLastTenantDeployRun(slug, stage)
|
|
28295
28353
|
]);
|
|
28296
28354
|
const report = buildDeployStatusReport({
|
|
@@ -29021,7 +29079,8 @@ function composeMarksRuntimeEnvTrue(composeText) {
|
|
|
29021
29079
|
return false;
|
|
29022
29080
|
});
|
|
29023
29081
|
}
|
|
29024
|
-
function promotionSourceBranch(stage, releaseTrack) {
|
|
29082
|
+
function promotionSourceBranch(stage, releaseTrack, lane) {
|
|
29083
|
+
if (lane === "hotfix") return "main";
|
|
29025
29084
|
if (stage === "main") return releaseTrack === "direct" ? "development" : "rc";
|
|
29026
29085
|
return "development";
|
|
29027
29086
|
}
|
|
@@ -29922,7 +29981,7 @@ async function collectStatus() {
|
|
|
29922
29981
|
try {
|
|
29923
29982
|
const cfg = await loadConfigOrDiscover();
|
|
29924
29983
|
if (cfg.sagaApiUrl) {
|
|
29925
|
-
const report = await readBoard({ config: cfg });
|
|
29984
|
+
const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
|
|
29926
29985
|
claimedItems = report.primary.userOwned.map((item) => ({
|
|
29927
29986
|
number: item.number,
|
|
29928
29987
|
title: item.title,
|
|
@@ -29969,8 +30028,8 @@ var PRIORITY_RANK = {
|
|
|
29969
30028
|
};
|
|
29970
30029
|
async function recommendNext(repo, deps) {
|
|
29971
30030
|
const load = deps?.loadConfig ?? loadConfigForRepo;
|
|
29972
|
-
const reader = deps?.readBoard ?? readBoard;
|
|
29973
30031
|
const cfg = await load(repo);
|
|
30032
|
+
const reader = deps?.readBoard ?? ((opts) => readBoard(opts, { snapshot: registryClientDeps(cfg) }));
|
|
29974
30033
|
if (!cfg.sagaApiUrl) throw new Error("Hub API URL not configured \u2014 the board was NOT read (run `mmi-cli doctor`)");
|
|
29975
30034
|
let report;
|
|
29976
30035
|
try {
|
|
@@ -30016,7 +30075,7 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
30016
30075
|
let board = { ok: false, detail: "no config" };
|
|
30017
30076
|
try {
|
|
30018
30077
|
if (cfg.sagaApiUrl) {
|
|
30019
|
-
const report = await readBoard({ config: cfg });
|
|
30078
|
+
const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
|
|
30020
30079
|
const total = report.primary.claimable.length + report.primary.userOwned.length + report.primary.taken.length;
|
|
30021
30080
|
board = { ok: true, detail: `board has ${total} active items (${report.primary.claimable.length} claimable, ${report.primary.userOwned.length} yours)` };
|
|
30022
30081
|
} else {
|
|
@@ -32577,6 +32636,10 @@ function validateBatchSpecs(specs) {
|
|
|
32577
32636
|
spec.labels = [...spec.labels ?? [], ...alias];
|
|
32578
32637
|
delete spec.label;
|
|
32579
32638
|
}
|
|
32639
|
+
if (spec.labels !== void 0 && (!Array.isArray(spec.labels) || spec.labels.some((l) => typeof l !== "string" || !l.trim()))) {
|
|
32640
|
+
errors.push({ row, error: "labels must be an array of non-empty strings" });
|
|
32641
|
+
continue;
|
|
32642
|
+
}
|
|
32580
32643
|
if (spec.repo !== void 0 && !/^[\w.-]+\/[\w.-]+$/.test(spec.repo)) {
|
|
32581
32644
|
errors.push({ row, error: `bad repo "${spec.repo}" \u2014 expected owner/repo` });
|
|
32582
32645
|
continue;
|
|
@@ -32585,13 +32648,17 @@ function validateBatchSpecs(specs) {
|
|
|
32585
32648
|
errors.push({ row, error: `unknown type "${spec.type}" \u2014 expected one of: ${validTypes.join(", ")}` });
|
|
32586
32649
|
continue;
|
|
32587
32650
|
}
|
|
32588
|
-
if (
|
|
32651
|
+
if (typeof spec.title !== "string" || !spec.title.trim()) {
|
|
32589
32652
|
errors.push({ row, error: "missing or empty title" });
|
|
32590
32653
|
continue;
|
|
32591
32654
|
}
|
|
32655
|
+
if (spec.body !== void 0 && typeof spec.body !== "string") {
|
|
32656
|
+
errors.push({ row, error: "body must be a string" });
|
|
32657
|
+
continue;
|
|
32658
|
+
}
|
|
32592
32659
|
let priority;
|
|
32593
32660
|
try {
|
|
32594
|
-
priority = spec.priority ? normalizePriority(spec.priority) : "medium";
|
|
32661
|
+
priority = spec.priority ? normalizePriority(String(spec.priority)) : "medium";
|
|
32595
32662
|
} catch (e) {
|
|
32596
32663
|
errors.push({ row, error: e.message });
|
|
32597
32664
|
continue;
|
|
@@ -32604,6 +32671,14 @@ function validateBatchSpecs(specs) {
|
|
|
32604
32671
|
if (!labelsCarrySurface(spec.labels)) spec.labels = [...spec.labels ?? [], surfaceLabel(spec.surface)];
|
|
32605
32672
|
delete spec.surface;
|
|
32606
32673
|
}
|
|
32674
|
+
if (spec.parent !== void 0) {
|
|
32675
|
+
try {
|
|
32676
|
+
parseIssueRef(spec.parent);
|
|
32677
|
+
} catch (e) {
|
|
32678
|
+
errors.push({ row, error: e.message });
|
|
32679
|
+
continue;
|
|
32680
|
+
}
|
|
32681
|
+
}
|
|
32607
32682
|
validated.push({ row, spec, priority, type: spec.type });
|
|
32608
32683
|
}
|
|
32609
32684
|
return { ok: errors.length === 0, errors, validated };
|
|
@@ -34030,6 +34105,8 @@ var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!,
|
|
|
34030
34105
|
projectV2 { id }
|
|
34031
34106
|
}
|
|
34032
34107
|
}`;
|
|
34108
|
+
var ProjectInfoReadUnavailableError = class extends Error {
|
|
34109
|
+
};
|
|
34033
34110
|
function shortDescriptionFromReadme(markdown) {
|
|
34034
34111
|
const lines2 = markdown.replace(/\r/g, "").split("\n");
|
|
34035
34112
|
const h1 = lines2.findIndex((line) => /^#\s+\S/.test(line.trim()));
|
|
@@ -34751,10 +34828,13 @@ function registerSecretsCommands(program3) {
|
|
|
34751
34828
|
const ok = body !== void 0 ? await secretsOrgCatalogSet(d, body, { replace: o.replace, remove: o.remove }) : await secretsOrgCatalogRemove(d, o.remove ?? []);
|
|
34752
34829
|
if (!ok) process.exitCode = 1;
|
|
34753
34830
|
}));
|
|
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) => {
|
|
34831
|
+
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
34832
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
34756
34833
|
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
34757
34834
|
}
|
|
34835
|
+
if (o.lane !== void 0 && o.lane !== "hotfix") {
|
|
34836
|
+
return fail("secrets preflight: --lane must be hotfix (the only lane that changes the compose source)");
|
|
34837
|
+
}
|
|
34758
34838
|
const cfg = await loadConfig();
|
|
34759
34839
|
if (!cfg.sagaApiUrl) {
|
|
34760
34840
|
fail("secrets: Hub API URL not configured");
|
|
@@ -34775,7 +34855,7 @@ function registerSecretsCommands(program3) {
|
|
|
34775
34855
|
let filelessOk = true;
|
|
34776
34856
|
if (meta && centralContainer && !o.skipComposeGuard) {
|
|
34777
34857
|
const stage = o.stage;
|
|
34778
|
-
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
|
|
34858
|
+
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo), o.lane);
|
|
34779
34859
|
const cwdRepo = repoFromRemoteUrl((await execFileP("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
|
|
34780
34860
|
const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
|
|
34781
34861
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
@@ -34981,10 +35061,11 @@ async function resolveBoardConfig2(repoOption) {
|
|
|
34981
35061
|
const floor = await loadConfig();
|
|
34982
35062
|
if (!floor.sagaApiUrl) return null;
|
|
34983
35063
|
const slug = repoOption ? (repoOption.replace(/\.git$/, "").split("/").pop() ?? repoOption).toLowerCase() : await repoSlug();
|
|
34984
|
-
const
|
|
35064
|
+
const registry2 = registryClientDeps(floor);
|
|
35065
|
+
const read = await fetchProjectBySlugChecked(slug, registry2);
|
|
34985
35066
|
if (!read.ok || !read.project) return null;
|
|
34986
35067
|
const cfg = boardConfigFromProject(read.project, floor);
|
|
34987
|
-
return readBoard({ config: cfg, repo: repoOption, allowPartial: true });
|
|
35068
|
+
return readBoard({ config: cfg, repo: repoOption, allowPartial: true }, { snapshot: registry2 });
|
|
34988
35069
|
}
|
|
34989
35070
|
function registerSessionReport(program3) {
|
|
34990
35071
|
const report = program3.commands.find((c) => c.name() === "report");
|
|
@@ -36753,24 +36834,31 @@ function runTestPolicy(root, deps = {}) {
|
|
|
36753
36834
|
` + (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
36835
|
}] : [];
|
|
36755
36836
|
const present = (path2) => exists((0, import_node_path33.join)(root, path2));
|
|
36837
|
+
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;
|
|
36838
|
+
const policyTreeExists = policyTree ? (abs) => policyTree.has(abs) : exists;
|
|
36839
|
+
const where = policySource.sha ? `${policySource.ref}@${policySource.sha}` : "this worktree";
|
|
36756
36840
|
const removedByThisDiff = removedPaths(changed);
|
|
36757
36841
|
const staleFindings = [];
|
|
36758
|
-
const unresolved = unresolvedProtectedEntries(policy, root,
|
|
36842
|
+
const unresolved = unresolvedProtectedEntries(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
|
|
36759
36843
|
if (unresolved.length > 0) {
|
|
36760
36844
|
staleFindings.push({
|
|
36761
36845
|
kind: "stale-protected-entry",
|
|
36762
36846
|
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") +
|
|
36847
|
+
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist on ${where}:
|
|
36848
|
+
` + unresolved.map((p) => ` ${p}`).join("\n") + `
|
|
36849
|
+
An entry naming a missing file reads as protection while protecting nothing.
|
|
36850
|
+
Restore the file on ${where}, or remove its entry.`
|
|
36765
36851
|
});
|
|
36766
36852
|
}
|
|
36767
|
-
const staleSatisfiers = unresolvedSatisfiers(policy, root,
|
|
36853
|
+
const staleSatisfiers = unresolvedSatisfiers(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
|
|
36768
36854
|
if (staleSatisfiers.length > 0) {
|
|
36769
36855
|
staleFindings.push({
|
|
36770
36856
|
kind: "stale-satisfied-by",
|
|
36771
36857
|
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") +
|
|
36858
|
+
detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist on ${where}:
|
|
36859
|
+
` + staleSatisfiers.map((p) => ` ${p}`).join("\n") + `
|
|
36860
|
+
A glob discharged by coverage that is not there is a glob enforcing nothing, quietly.
|
|
36861
|
+
Restore the file on ${where}, or drop it from satisfiedBy so the glob asks for a test again.`
|
|
36774
36862
|
});
|
|
36775
36863
|
}
|
|
36776
36864
|
const override = lookup.override;
|
|
@@ -37085,15 +37173,17 @@ function cleanupGitArgs(cwd, args) {
|
|
|
37085
37173
|
}
|
|
37086
37174
|
async function remoteBranchExists2(branch, options = {}) {
|
|
37087
37175
|
if (!branch) return void 0;
|
|
37176
|
+
const remote = options.remote ?? "origin";
|
|
37088
37177
|
try {
|
|
37089
|
-
if (options.prune) await execFileP("git", cleanupGitArgs(options.cwd, ["fetch",
|
|
37090
|
-
return (await execFileP("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads",
|
|
37178
|
+
if (options.prune) await execFileP("git", cleanupGitArgs(options.cwd, ["fetch", remote, "--prune"]), { timeout: GIT_TIMEOUT_MS });
|
|
37179
|
+
return (await execFileP("git", cleanupGitArgs(options.cwd, ["ls-remote", "--heads", remote, branch]), { timeout: GIT_TIMEOUT_MS })).stdout.trim().length > 0;
|
|
37091
37180
|
} catch {
|
|
37092
37181
|
return void 0;
|
|
37093
37182
|
}
|
|
37094
37183
|
}
|
|
37095
37184
|
async function deleteMergedRemoteBranch(options) {
|
|
37096
|
-
const
|
|
37185
|
+
const remote = options.remote ?? "origin";
|
|
37186
|
+
const remediation = `git push ${remote} --delete ${options.branch}`;
|
|
37097
37187
|
if (!options.branch) {
|
|
37098
37188
|
return {
|
|
37099
37189
|
branch: options.branch,
|
|
@@ -37122,13 +37212,13 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37122
37212
|
existedBefore: false,
|
|
37123
37213
|
attempted: false,
|
|
37124
37214
|
status: "failed",
|
|
37125
|
-
error: `could not verify absence of
|
|
37215
|
+
error: `could not verify absence of ${remote}/${options.branch}`,
|
|
37126
37216
|
remediation
|
|
37127
37217
|
};
|
|
37128
37218
|
}
|
|
37129
37219
|
}
|
|
37130
37220
|
try {
|
|
37131
|
-
await options.execGit(["push",
|
|
37221
|
+
await options.execGit(["push", remote, "--delete", options.branch]);
|
|
37132
37222
|
} catch (e) {
|
|
37133
37223
|
const exists2 = await options.branchExists(options.branch);
|
|
37134
37224
|
if (exists2 === false) {
|
|
@@ -37162,7 +37252,7 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37162
37252
|
existedBefore: options.existedBefore,
|
|
37163
37253
|
attempted: true,
|
|
37164
37254
|
status: "failed",
|
|
37165
|
-
error: exists ?
|
|
37255
|
+
error: exists ? `${remote} still reports ${options.branch} after deletion` : `could not verify deletion of ${remote}/${options.branch}`,
|
|
37166
37256
|
remediation
|
|
37167
37257
|
};
|
|
37168
37258
|
}
|
|
@@ -37504,11 +37594,13 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
37504
37594
|
const actor = deps.actor ?? describeSessionIdentity();
|
|
37505
37595
|
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
37506
37596
|
for (const number of issues) {
|
|
37597
|
+
const state = await client.rest("GET", `repos/${repo}/issues/${number}`).then((issue) => issue?.state, () => void 0);
|
|
37598
|
+
if (state?.toLowerCase() === "closed") continue;
|
|
37507
37599
|
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
37508
37600
|
if (!contest.contested) continue;
|
|
37509
37601
|
const ref = `${repo}#${number}`;
|
|
37510
37602
|
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`;
|
|
37603
|
+
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
37604
|
}
|
|
37513
37605
|
return void 0;
|
|
37514
37606
|
}
|
|
@@ -37908,6 +38000,69 @@ function safeRemoveTree(path2) {
|
|
|
37908
38000
|
}
|
|
37909
38001
|
(0, import_node_fs41.unlinkSync)(path2);
|
|
37910
38002
|
}
|
|
38003
|
+
function errorMessage(e) {
|
|
38004
|
+
return e instanceof Error ? e.message : String(e);
|
|
38005
|
+
}
|
|
38006
|
+
function resolvedOrRaw(path2) {
|
|
38007
|
+
try {
|
|
38008
|
+
return normPath2((0, import_node_fs41.realpathSync)(path2));
|
|
38009
|
+
} catch {
|
|
38010
|
+
return normPath2(path2);
|
|
38011
|
+
}
|
|
38012
|
+
}
|
|
38013
|
+
function unlinkEscapingReparsePoints(root, primaryRoot) {
|
|
38014
|
+
let realRoot;
|
|
38015
|
+
try {
|
|
38016
|
+
if ((0, import_node_fs41.lstatSync)(root).isSymbolicLink()) {
|
|
38017
|
+
return { ok: false, error: `delete root ${normPath2(root)} is a reparse point resolving to ${resolvedOrRaw(root)}` };
|
|
38018
|
+
}
|
|
38019
|
+
realRoot = normPath2((0, import_node_fs41.realpathSync)(root));
|
|
38020
|
+
} catch (e) {
|
|
38021
|
+
if (e.code === "ENOENT") return { ok: true, unlinked: [] };
|
|
38022
|
+
return { ok: false, error: `cannot resolve delete root ${normPath2(root)}: ${errorMessage(e)}` };
|
|
38023
|
+
}
|
|
38024
|
+
const realPrimary = resolvedOrRaw(primaryRoot);
|
|
38025
|
+
const forbidden = isPathAtOrWithin(realPrimary, realRoot) ? realPrimary : [`${realPrimary}/node_modules`, `${realPrimary}/packages`].find((p) => isPathAtOrWithin(realRoot, p));
|
|
38026
|
+
if (forbidden) {
|
|
38027
|
+
return { ok: false, error: `delete root ${normPath2(root)} (resolved ${realRoot}) is or contains the primary checkout path ${forbidden}` };
|
|
38028
|
+
}
|
|
38029
|
+
const unlinked = [];
|
|
38030
|
+
const stack = [realRoot];
|
|
38031
|
+
while (stack.length) {
|
|
38032
|
+
const dir = stack.pop();
|
|
38033
|
+
let entries;
|
|
38034
|
+
try {
|
|
38035
|
+
entries = (0, import_node_fs41.readdirSync)(dir, { withFileTypes: true });
|
|
38036
|
+
} catch (e) {
|
|
38037
|
+
return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
|
|
38038
|
+
}
|
|
38039
|
+
for (const entry of entries) {
|
|
38040
|
+
const child2 = (0, import_node_path38.join)(dir, entry.name);
|
|
38041
|
+
if (entry.isSymbolicLink()) {
|
|
38042
|
+
let target = "";
|
|
38043
|
+
try {
|
|
38044
|
+
target = normPath2((0, import_node_fs41.realpathSync)(child2));
|
|
38045
|
+
} catch {
|
|
38046
|
+
target = "";
|
|
38047
|
+
}
|
|
38048
|
+
if (target && isPathAtOrWithin(target, realRoot)) continue;
|
|
38049
|
+
try {
|
|
38050
|
+
safeRemoveTree(child2);
|
|
38051
|
+
} catch (e) {
|
|
38052
|
+
return { ok: false, error: `cannot unlink reparse point ${normPath2(child2)} -> ${target || "unresolvable"}: ${errorMessage(e)}` };
|
|
38053
|
+
}
|
|
38054
|
+
unlinked.push(normPath2(child2));
|
|
38055
|
+
continue;
|
|
38056
|
+
}
|
|
38057
|
+
if (entry.isDirectory()) stack.push(child2);
|
|
38058
|
+
}
|
|
38059
|
+
}
|
|
38060
|
+
return { ok: true, unlinked };
|
|
38061
|
+
}
|
|
38062
|
+
function reparseEscapeRemediation(wtPath) {
|
|
38063
|
+
const quote = (value) => value.replace(/'/g, "''");
|
|
38064
|
+
return `inspect '${quote(wtPath)}' manually \u2014 a reparse point resolves outside the worktree; never remove it recursively`;
|
|
38065
|
+
}
|
|
37911
38066
|
async function describePreCleanFailure(wtPath, execGit, error) {
|
|
37912
38067
|
const dryRun = await execGit(["-C", wtPath, "clean", "-ndX"]).catch(() => "");
|
|
37913
38068
|
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 +38097,25 @@ async function verifyBranchHead(git3, branch, expectedHeadOid) {
|
|
|
37942
38097
|
}
|
|
37943
38098
|
return { ok: true };
|
|
37944
38099
|
}
|
|
38100
|
+
async function switchPrimaryCheckoutOffMergedBranch(wtPath, branch, baseRef, execGit, expectedHeadOid) {
|
|
38101
|
+
const git3 = (args) => execGit(["-C", wtPath, ...args]);
|
|
38102
|
+
const headCheck = await verifyBranchHead(git3, branch, expectedHeadOid);
|
|
38103
|
+
if (!headCheck.ok) return { ok: false, error: headCheck.error ? `${headCheck.reason}: ${headCheck.error}` : headCheck.reason };
|
|
38104
|
+
const porcelain = await git3(["status", "--porcelain"]).catch(() => void 0);
|
|
38105
|
+
if (porcelain === void 0) return { ok: false, error: "could not read the primary checkout status" };
|
|
38106
|
+
if (porcelainHasBlockingChanges(porcelain)) return { ok: false, error: "dirty-worktree" };
|
|
38107
|
+
try {
|
|
38108
|
+
await git3(["switch", baseRef]);
|
|
38109
|
+
} catch (e) {
|
|
38110
|
+
return { ok: false, error: `switch to ${baseRef} failed: ${formatGitCommandError(e)}` };
|
|
38111
|
+
}
|
|
38112
|
+
try {
|
|
38113
|
+
await git3(["branch", "-D", branch]);
|
|
38114
|
+
} catch (e) {
|
|
38115
|
+
return { ok: false, switchedTo: baseRef, error: `branch -D ${branch} failed after switching to ${baseRef}: ${formatGitCommandError(e)}` };
|
|
38116
|
+
}
|
|
38117
|
+
return { ok: true, switchedTo: baseRef };
|
|
38118
|
+
}
|
|
37945
38119
|
async function teardownWorktreeStage(worktreePath) {
|
|
37946
38120
|
try {
|
|
37947
38121
|
const result = await stopStage({ cwd: worktreePath, requiredIdentityCwd: worktreePath, globalStatePath: false });
|
|
@@ -38035,7 +38209,7 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
38035
38209
|
}
|
|
38036
38210
|
function isPrMergeWorktreePartial(cleanup) {
|
|
38037
38211
|
const worktree = cleanup?.worktree;
|
|
38038
|
-
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked");
|
|
38212
|
+
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked" && worktree.status !== "switched-primary");
|
|
38039
38213
|
}
|
|
38040
38214
|
function prMergeLocalCleanupExitCode(cleanup) {
|
|
38041
38215
|
return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
|
|
@@ -38075,10 +38249,29 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38075
38249
|
const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
|
|
38076
38250
|
if (!wtPath || mainWorktreeTarget) {
|
|
38077
38251
|
if (wtPath && mainWorktreeTarget) {
|
|
38252
|
+
const switched = await switchPrimaryCheckoutOffMergedBranch(wtPath, branch, options.baseRef, execGit, options.expectedHeadOid);
|
|
38253
|
+
if (switched.ok) {
|
|
38254
|
+
report.worktree = { path: wtPath, status: "switched-primary", reason: "main-worktree", switchedTo: switched.switchedTo };
|
|
38255
|
+
report.localBranch = { name: branch, status: "deleted" };
|
|
38256
|
+
return report;
|
|
38257
|
+
}
|
|
38258
|
+
if (switched.switchedTo) {
|
|
38259
|
+
report.worktree = {
|
|
38260
|
+
path: wtPath,
|
|
38261
|
+
status: "switched-primary",
|
|
38262
|
+
reason: "main-worktree",
|
|
38263
|
+
switchedTo: switched.switchedTo,
|
|
38264
|
+
error: switched.error,
|
|
38265
|
+
remediation: primaryCheckoutBranchRemediation(options.primaryRoot, options.baseRef, branch)
|
|
38266
|
+
};
|
|
38267
|
+
report.localBranch = { name: branch, status: "failed", error: switched.error };
|
|
38268
|
+
return report;
|
|
38269
|
+
}
|
|
38078
38270
|
report.worktree = {
|
|
38079
38271
|
path: wtPath,
|
|
38080
38272
|
status: "not-attempted",
|
|
38081
38273
|
reason: "main-worktree",
|
|
38274
|
+
error: switched.error,
|
|
38082
38275
|
remediation: primaryCheckoutBranchRemediation(options.primaryRoot, options.baseRef, branch)
|
|
38083
38276
|
};
|
|
38084
38277
|
}
|
|
@@ -38098,7 +38291,12 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38098
38291
|
return report;
|
|
38099
38292
|
}
|
|
38100
38293
|
const porcelain = await execGit(["-C", wtPath, "status", "--porcelain"]).catch(() => void 0);
|
|
38101
|
-
if (porcelain
|
|
38294
|
+
if (porcelain === void 0) {
|
|
38295
|
+
report.worktree = { path: wtPath, status: "refused", reason: "status-unreadable", error: "could not read the worktree status" };
|
|
38296
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "status-unreadable" };
|
|
38297
|
+
return report;
|
|
38298
|
+
}
|
|
38299
|
+
if (porcelain.trim()) {
|
|
38102
38300
|
report.worktree = { path: wtPath, status: "refused", reason: "dirty-worktree" };
|
|
38103
38301
|
report.localBranch = { name: branch, status: "not-attempted", reason: "dirty-worktree" };
|
|
38104
38302
|
return report;
|
|
@@ -38148,6 +38346,25 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38148
38346
|
report.localBranch = { name: branch, status: "not-attempted", reason: "archive-failed" };
|
|
38149
38347
|
return report;
|
|
38150
38348
|
}
|
|
38349
|
+
const unlinkedReparsePoints = [];
|
|
38350
|
+
const refuseReparseEscape = (error) => {
|
|
38351
|
+
report.worktree = {
|
|
38352
|
+
path: wtPath,
|
|
38353
|
+
status: "refused",
|
|
38354
|
+
reason: "reparse-escape",
|
|
38355
|
+
error,
|
|
38356
|
+
artifactsArchive,
|
|
38357
|
+
stageTeardown,
|
|
38358
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38359
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38360
|
+
remediation: reparseEscapeRemediation(wtPath)
|
|
38361
|
+
};
|
|
38362
|
+
report.localBranch = { name: branch, status: "not-attempted", reason: "reparse-escape" };
|
|
38363
|
+
return report;
|
|
38364
|
+
};
|
|
38365
|
+
const preHelperGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
|
|
38366
|
+
if (!preHelperGuard.ok) return refuseReparseEscape(preHelperGuard.error);
|
|
38367
|
+
unlinkedReparsePoints.push(...preHelperGuard.unlinked);
|
|
38151
38368
|
if (pathExists((0, import_node_path38.join)(wtPath, "node_modules"))) {
|
|
38152
38369
|
const nmRemoved = await (options.removeRealNodeModules ?? ((p) => removeWorktreeNodeModulesViaHelper(p, { cwd: mainWorktreePath })))(wtPath);
|
|
38153
38370
|
if (!nmRemoved.ok) {
|
|
@@ -38159,6 +38376,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38159
38376
|
artifactsArchive,
|
|
38160
38377
|
stageTeardown,
|
|
38161
38378
|
tmpEvidenceCount: tmpEvidence.length,
|
|
38379
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38162
38380
|
remediation: `jervcode worktree-node-modules-cleanup --worktree '${wtPath.replace(/'/g, "''")}'`
|
|
38163
38381
|
};
|
|
38164
38382
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
@@ -38175,12 +38393,16 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38175
38393
|
artifactsArchive,
|
|
38176
38394
|
stageTeardown,
|
|
38177
38395
|
tmpEvidenceCount: tmpEvidence.length,
|
|
38396
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38178
38397
|
// #6076: name the surviving node_modules remover when the dry run found one, else the clean.
|
|
38179
38398
|
remediation: preClean.remediation ?? `git -C '${wtPath.replace(/'/g, "''")}' clean -ffdX`
|
|
38180
38399
|
};
|
|
38181
38400
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
38182
38401
|
return report;
|
|
38183
38402
|
}
|
|
38403
|
+
const preRemoveGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
|
|
38404
|
+
if (!preRemoveGuard.ok) return refuseReparseEscape(preRemoveGuard.error);
|
|
38405
|
+
unlinkedReparsePoints.push(...preRemoveGuard.unlinked);
|
|
38184
38406
|
moveCwdToSafeWorktree(wtPath, safeCwd);
|
|
38185
38407
|
const removal = await removeWorktreeWithReconcile(
|
|
38186
38408
|
wtPath,
|
|
@@ -38195,7 +38417,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38195
38417
|
error: removal.error,
|
|
38196
38418
|
artifactsArchive,
|
|
38197
38419
|
stageTeardown,
|
|
38198
|
-
tmpEvidenceCount: tmpEvidence.length
|
|
38420
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38421
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
|
|
38199
38422
|
};
|
|
38200
38423
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-failed" };
|
|
38201
38424
|
return report;
|
|
@@ -38206,7 +38429,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38206
38429
|
...removal.reason ? { reason: removal.reason } : {},
|
|
38207
38430
|
artifactsArchive,
|
|
38208
38431
|
stageTeardown,
|
|
38209
|
-
tmpEvidenceCount: tmpEvidence.length
|
|
38432
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38433
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
|
|
38210
38434
|
};
|
|
38211
38435
|
if (pathExists(wtPath)) {
|
|
38212
38436
|
const residue = await (options.removeResidueDir?.(wtPath) ?? removeResidueDirectory(wtPath));
|
|
@@ -38254,12 +38478,18 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
38254
38478
|
lines2.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
|
|
38255
38479
|
} else if (wt.status === "retained-locked") {
|
|
38256
38480
|
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"}`);
|
|
38481
|
+
} else if (wt.status === "switched-primary") {
|
|
38482
|
+
const now = wt.switchedTo ? `to ${wt.switchedTo}` : "to the base branch";
|
|
38483
|
+
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
38484
|
} 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"}`);
|
|
38485
|
+
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
38486
|
}
|
|
38260
38487
|
if (wt.residue === "left" && wt.status !== "retained-locked") {
|
|
38261
38488
|
lines2.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
|
|
38262
38489
|
}
|
|
38490
|
+
if (wt.unlinkedReparsePoints?.length) {
|
|
38491
|
+
lines2.push(`pr merge: unlinked ${wt.unlinkedReparsePoints.length} reparse point(s) resolving outside ${wt.path}: ${wt.unlinkedReparsePoints.join(", ")}`);
|
|
38492
|
+
}
|
|
38263
38493
|
if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
|
|
38264
38494
|
lines2.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
|
|
38265
38495
|
} else if (wt.artifactsArchive?.status === "blocked" && wt.artifactsArchive.error) {
|
|
@@ -39321,7 +39551,7 @@ ${list}`);
|
|
|
39321
39551
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
39322
39552
|
if (result.status === "failed") process.exitCode = 1;
|
|
39323
39553
|
});
|
|
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) => {
|
|
39554
|
+
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
39555
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
39326
39556
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
39327
39557
|
if (o.disableAuto) {
|
|
@@ -39381,7 +39611,23 @@ ${list}`);
|
|
|
39381
39611
|
const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
39382
39612
|
const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
|
|
39383
39613
|
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
|
-
|
|
39614
|
+
let beforeWorktrees = beforeWorktreesRead.state === "ok" ? parseGitWorktreePorcelain(beforeWorktreesRead.stdout) : [];
|
|
39615
|
+
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]).catch(() => ""));
|
|
39616
|
+
const targetRepo2 = repoForPostCleanup ? repoForPostCleanup.split("/").slice(-2).join("/") : void 0;
|
|
39617
|
+
const foreignCwd = Boolean(o.repo) && Boolean(targetRepo2) && cwdRepo?.toLowerCase() !== targetRepo2.toLowerCase();
|
|
39618
|
+
const remote = foreignCwd ? `https://github.com/${targetRepo2}.git` : "origin";
|
|
39619
|
+
let foreignCheckout;
|
|
39620
|
+
if (foreignCwd) {
|
|
39621
|
+
const sibling = (0, import_node_path39.join)((0, import_node_path39.dirname)(beforeWorktrees[0]?.path || startingPath || process.cwd()), targetRepo2.split("/")[1]);
|
|
39622
|
+
const siblingRepo = repoFromRemoteUrl(await gitOut(["-C", sibling, "remote", "get-url", "origin"]).catch(() => ""));
|
|
39623
|
+
if (siblingRepo?.toLowerCase() === targetRepo2.toLowerCase()) {
|
|
39624
|
+
const siblingWorktrees = await execFileP("git", ["-C", sibling, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseGitWorktreePorcelain(r.stdout)).catch(() => void 0);
|
|
39625
|
+
if (siblingWorktrees?.length) {
|
|
39626
|
+
foreignCheckout = sibling;
|
|
39627
|
+
beforeWorktrees = siblingWorktrees;
|
|
39628
|
+
}
|
|
39629
|
+
}
|
|
39630
|
+
}
|
|
39385
39631
|
const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
|
|
39386
39632
|
const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
|
|
39387
39633
|
if (o.wait) {
|
|
@@ -39439,7 +39685,7 @@ ${list}`);
|
|
|
39439
39685
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
39440
39686
|
if (guard.note) console.warn(`pr merge: ${guard.note}`);
|
|
39441
39687
|
}
|
|
39442
|
-
const remoteBefore = await remoteBranchExists2(headRef);
|
|
39688
|
+
const remoteBefore = await remoteBranchExists2(headRef, { remote });
|
|
39443
39689
|
let upgradedToAuto = false;
|
|
39444
39690
|
let remoteNotAttemptedReason = "preserved-delayed-cleanup";
|
|
39445
39691
|
const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
@@ -39558,30 +39804,34 @@ ${list}`);
|
|
|
39558
39804
|
}
|
|
39559
39805
|
const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
|
|
39560
39806
|
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
|
-
|
|
39807
|
+
if (foreignCwd && !foreignCheckout) {
|
|
39808
|
+
localCleanup = { branch: headRef, localBranch: { name: headRef, status: "not-attempted", reason: "skipped-foreign-cwd" } };
|
|
39809
|
+
} else {
|
|
39810
|
+
try {
|
|
39811
|
+
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
39812
|
+
beforeWorktrees,
|
|
39813
|
+
startingPath,
|
|
39814
|
+
baseRef,
|
|
39815
|
+
primaryRoot,
|
|
39816
|
+
preserveWorktree: o.preserveWorktree,
|
|
39817
|
+
gcAcknowledged: o.gc,
|
|
39818
|
+
expectedHeadOid: headRefOid,
|
|
39819
|
+
pathExists: (p) => (0, import_node_fs42.existsSync)(p),
|
|
39820
|
+
// #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
|
|
39821
|
+
// standing in is removed mid-cleanup, so a cwd-relative invocation fails with
|
|
39822
|
+
// 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
|
|
39823
|
+
execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout
|
|
39824
|
+
});
|
|
39825
|
+
} catch (e) {
|
|
39826
|
+
localCleanup = {
|
|
39827
|
+
branch: headRef,
|
|
39828
|
+
localBranch: {
|
|
39829
|
+
name: headRef,
|
|
39830
|
+
status: "failed",
|
|
39831
|
+
error: e instanceof Error ? e.message : String(e)
|
|
39832
|
+
}
|
|
39833
|
+
};
|
|
39834
|
+
}
|
|
39585
39835
|
}
|
|
39586
39836
|
const remoteBranch = await deleteMergedRemoteBranch({
|
|
39587
39837
|
branch: headRef,
|
|
@@ -39592,7 +39842,9 @@ ${list}`);
|
|
|
39592
39842
|
// #5899: this leg runs after worktree teardown — anchor it to the main checkout so a cwd inside
|
|
39593
39843
|
// the removed worktree cannot turn the delete into 'fatal: not a git repository' + manual remediation.
|
|
39594
39844
|
execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
39595
|
-
branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
|
|
39845
|
+
branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot, remote }),
|
|
39846
|
+
// #6148: aim the delete and its proof at `--repo`, not the cwd checkout's origin.
|
|
39847
|
+
remote
|
|
39596
39848
|
});
|
|
39597
39849
|
const worktree = localCleanup?.worktree;
|
|
39598
39850
|
const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
|
|
@@ -39655,6 +39907,7 @@ ${list}`);
|
|
|
39655
39907
|
...methodField ? { method: methodField } : {},
|
|
39656
39908
|
remoteBranch,
|
|
39657
39909
|
housekeeping,
|
|
39910
|
+
...foreignCwd ? { foreignCwd: true, ...foreignCheckout ? { foreignCheckout } : {} } : {},
|
|
39658
39911
|
...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
|
|
39659
39912
|
...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
|
|
39660
39913
|
...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
|
|
@@ -40564,8 +40817,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40564
40817
|
registerSchedulesCommands(program3);
|
|
40565
40818
|
registerSchedulesLiftCommand(program3);
|
|
40566
40819
|
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) => {
|
|
40820
|
+
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
40821
|
try {
|
|
40822
|
+
if (o.repo) {
|
|
40823
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs index ${o.check ? "--check" : "--write"}`, "docs index");
|
|
40824
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40825
|
+
}
|
|
40569
40826
|
const root = await repoRoot();
|
|
40570
40827
|
const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
|
|
40571
40828
|
if (o.check) {
|
|
@@ -40580,8 +40837,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40580
40837
|
await failGraceful(e.message);
|
|
40581
40838
|
}
|
|
40582
40839
|
});
|
|
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) => {
|
|
40840
|
+
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
40841
|
try {
|
|
40842
|
+
if (o.repo) {
|
|
40843
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs refs${o.json ? " --json" : ""}`, "docs refs");
|
|
40844
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40845
|
+
}
|
|
40585
40846
|
const root = await repoRoot();
|
|
40586
40847
|
const commandPaths = new Set(
|
|
40587
40848
|
buildCommandManifest(program3).index.map((entry) => entry.path)
|
|
@@ -40607,8 +40868,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40607
40868
|
}
|
|
40608
40869
|
});
|
|
40609
40870
|
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) => {
|
|
40871
|
+
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
40872
|
try {
|
|
40873
|
+
if (o.repo) {
|
|
40874
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli spawn policy${o.json ? " --json" : ""}`, "spawn policy");
|
|
40875
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40876
|
+
}
|
|
40612
40877
|
const root = await repoRoot();
|
|
40613
40878
|
const result = runSpawnPolicy(root);
|
|
40614
40879
|
if (o.json) {
|
|
@@ -40630,8 +40895,13 @@ function registerDeveloperCommands(program3) {
|
|
|
40630
40895
|
}
|
|
40631
40896
|
});
|
|
40632
40897
|
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
|
|
40898
|
+
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
40899
|
try {
|
|
40900
|
+
if (o.repo) {
|
|
40901
|
+
const rerun = `mmi-cli tests policy${o.base ? ` --base ${o.base}` : ""}${o.policyRef ? ` --policy-ref ${o.policyRef}` : ""}${o.json ? " --json" : ""}`;
|
|
40902
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun, "tests policy");
|
|
40903
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40904
|
+
}
|
|
40635
40905
|
const root = await repoRoot();
|
|
40636
40906
|
const result = runTestPolicy(root, { base: o.base, policyRef: o.policyRef });
|
|
40637
40907
|
if (o.json) {
|
|
@@ -40661,8 +40931,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40661
40931
|
}
|
|
40662
40932
|
});
|
|
40663
40933
|
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) => {
|
|
40934
|
+
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
40935
|
try {
|
|
40936
|
+
if (o.repo) {
|
|
40937
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli dist status${o.json ? " --json" : ""}`, "dist status");
|
|
40938
|
+
if (!guard.ok) return failGraceful(guard.message);
|
|
40939
|
+
}
|
|
40666
40940
|
const root = await repoRoot();
|
|
40667
40941
|
const receipt = runDistStatus(root);
|
|
40668
40942
|
if (o.json) {
|
|
@@ -40957,7 +41231,7 @@ async function resolveHotfixDeployModel(deps, ctx) {
|
|
|
40957
41231
|
}
|
|
40958
41232
|
async function hotfixPreflight(deps, ctx, verb, targetTag) {
|
|
40959
41233
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
40960
|
-
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
41234
|
+
const deployModel = await preflight(deps, ctx, "main", meta, "hotfix");
|
|
40961
41235
|
const root = await hotfixCheckoutRoot(deps);
|
|
40962
41236
|
const begin = await beginReleaseLedger(deps, root, targetTag);
|
|
40963
41237
|
if (!begin.ok) throw new Error(`hotfix ${verb} refused before any mutation: ${begin.error}`);
|
|
@@ -41316,6 +41590,16 @@ function hotfixDispatchFromRuns(runs, note) {
|
|
|
41316
41590
|
const tenantDeploy2 = runs.find((r) => r.workflow === "tenant-deploy.yml");
|
|
41317
41591
|
return { note, deployStatus, workflowRuns, ...tenantDeploy2?.runId != null ? { runId: tenantDeploy2.runId } : {}, ...tenantDeploy2?.url ? { runUrl: tenantDeploy2.url } : {} };
|
|
41318
41592
|
}
|
|
41593
|
+
async function appendHotfixGatewayRun(deps, repo, tag, sha, runs, note) {
|
|
41594
|
+
const input = hotfixDispatchFromRuns(runs, note);
|
|
41595
|
+
const gateway = await appendJervGatewayReleaseDeploy(deps, repo, tag, sha, input);
|
|
41596
|
+
if (gateway === input) return note;
|
|
41597
|
+
const row = gateway.workflowRuns?.at(-1);
|
|
41598
|
+
if (row?.workflow === "jerv-gateway") {
|
|
41599
|
+
runs.push({ workflow: row.workflow, ...row.runUrlNote ? { runUrlNote: row.runUrlNote } : {}, conclusion: row.conclusion });
|
|
41600
|
+
}
|
|
41601
|
+
return gateway.note;
|
|
41602
|
+
}
|
|
41319
41603
|
function hotfixPhaseInputsFromRuns(deployModel, runs, note, publishDispatch, publishRequired, opts) {
|
|
41320
41604
|
const dispatch = hotfixDispatchFromRuns(runs, note);
|
|
41321
41605
|
return {
|
|
@@ -41559,6 +41843,7 @@ ${decision.note}`);
|
|
|
41559
41843
|
} else {
|
|
41560
41844
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
41561
41845
|
}
|
|
41846
|
+
deployNote = await appendHotfixGatewayRun(deps, ctx.repo, tag, mergedSha, runs, deployNote);
|
|
41562
41847
|
const ledgerInputs = hotfixPhaseInputsFromRuns(
|
|
41563
41848
|
deployModel,
|
|
41564
41849
|
runs,
|
|
@@ -41751,6 +42036,9 @@ function hotfixStatusRunsFromLedger(ledger) {
|
|
|
41751
42036
|
workflow: record.workflow ?? phase,
|
|
41752
42037
|
...record.runId != null ? { runId: record.runId } : {},
|
|
41753
42038
|
...record.runUrl ? { url: record.runUrl } : {},
|
|
42039
|
+
// #912: the operator-host Gateway leg carries no run id by design — without its own note the
|
|
42040
|
+
// renderer's missing-URL fallback reads it as 'run absent or unreadable'.
|
|
42041
|
+
...record.workflow === "jerv-gateway" ? { runUrlNote: JERV_GATEWAY_RUN_URL_NOTE } : {},
|
|
41754
42042
|
conclusion: record.state === "complete" ? "success" : record.state === "failed" ? "failure" : "pending"
|
|
41755
42043
|
}];
|
|
41756
42044
|
});
|
|
@@ -42231,6 +42519,7 @@ function renderReleaseResume(r) {
|
|
|
42231
42519
|
if (r.devRollForward) lines2.push(` development: ${r.devRollForward.note}`);
|
|
42232
42520
|
if (r.rcAlignment) lines2.push(` rc: ${r.rcAlignment.note}`);
|
|
42233
42521
|
if (r.checkout) lines2.push(` checkout: ${r.checkout.note}`);
|
|
42522
|
+
if (r.projectInfoSync) lines2.push(` project info: ${r.projectInfoSync.note}`);
|
|
42234
42523
|
if (r.ledger) lines2.push(...formatReleaseLedgerReport(r.ledger).map((l, i) => i === 0 ? ` ${l}` : ` ${l}`));
|
|
42235
42524
|
return lines2.join("\n");
|
|
42236
42525
|
}
|
|
@@ -42258,7 +42547,8 @@ function releaseFollowUpLegs(result, projectInfoSync) {
|
|
|
42258
42547
|
const legs = [
|
|
42259
42548
|
{
|
|
42260
42549
|
leg: "project-info",
|
|
42261
|
-
|
|
42550
|
+
// #6180: a transient Hub read outage is PENDING (rerun the manual verb), never a failed follow-up.
|
|
42551
|
+
status: projectInfoSync && "error" in projectInfoSync ? projectInfoSync.pending ? "pending" : "failed" : "success",
|
|
42262
42552
|
...projectInfoSync && "error" in projectInfoSync ? { error: projectInfoSync.error } : {}
|
|
42263
42553
|
}
|
|
42264
42554
|
];
|
|
@@ -42297,6 +42587,24 @@ function releaseFollowUpLegs(result, projectInfoSync) {
|
|
|
42297
42587
|
}
|
|
42298
42588
|
return legs;
|
|
42299
42589
|
}
|
|
42590
|
+
function hotfixFollowUpLegs(runs, foldPort, alignment, foldNote, deployNote) {
|
|
42591
|
+
const legs = runs.map((run) => ({
|
|
42592
|
+
leg: run.workflow,
|
|
42593
|
+
status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
|
|
42594
|
+
// #912 lane parity with releaseFollowUpLegs: the operator-host Gateway leg has no workflow run to
|
|
42595
|
+
// read, so its deploy note IS the error — a flat "workflow reported failure" names nothing.
|
|
42596
|
+
...run.conclusion === "failure" ? { error: run.workflow === "jerv-gateway" && deployNote ? deployNote : "workflow reported failure" } : {}
|
|
42597
|
+
}));
|
|
42598
|
+
if (foldPort === "failure") {
|
|
42599
|
+
legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
|
|
42600
|
+
}
|
|
42601
|
+
if (alignment === "unresolved") {
|
|
42602
|
+
legs.push({ leg: "development-fold-alignment", status: "pending" });
|
|
42603
|
+
} else if (alignment === "failure" && foldPort !== "failure") {
|
|
42604
|
+
legs.push({ leg: "development-fold-alignment", status: "failed", error: foldNote ?? "development fold alignment failed" });
|
|
42605
|
+
}
|
|
42606
|
+
return legs;
|
|
42607
|
+
}
|
|
42300
42608
|
var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
|
|
42301
42609
|
async function runPostReleaseJervDoctor(repo) {
|
|
42302
42610
|
if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
|
|
@@ -42312,10 +42620,35 @@ async function runPostReleaseJervDoctor(repo) {
|
|
|
42312
42620
|
};
|
|
42313
42621
|
}
|
|
42314
42622
|
}
|
|
42623
|
+
async function runProjectInfoSyncLeg(cb, repo, sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))) {
|
|
42624
|
+
for (let attempt = 0; ; attempt++) {
|
|
42625
|
+
try {
|
|
42626
|
+
return await cb(repo, true);
|
|
42627
|
+
} catch (e) {
|
|
42628
|
+
const error = e.message;
|
|
42629
|
+
if (!(e instanceof ProjectInfoReadUnavailableError)) return { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
42630
|
+
if (attempt === 0) {
|
|
42631
|
+
await sleep2(PROJECT_INFO_RETRY_MS);
|
|
42632
|
+
continue;
|
|
42633
|
+
}
|
|
42634
|
+
return {
|
|
42635
|
+
applied: false,
|
|
42636
|
+
pending: true,
|
|
42637
|
+
note: `PENDING \u2014 ${error}; rerun \`mmi-cli oracle org project sync-info ${repo} --apply\``,
|
|
42638
|
+
error
|
|
42639
|
+
};
|
|
42640
|
+
}
|
|
42641
|
+
}
|
|
42642
|
+
}
|
|
42643
|
+
var PROJECT_INFO_RETRY_MS = 3e3;
|
|
42644
|
+
function projectInfoOutcome(projectInfoSync) {
|
|
42645
|
+
if (!projectInfoSync || !("error" in projectInfoSync)) return "ok";
|
|
42646
|
+
return projectInfoSync.pending ? "unresolved" : "failure";
|
|
42647
|
+
}
|
|
42315
42648
|
function buildReleaseVerdict(commandName, result, projectInfoSync) {
|
|
42316
42649
|
const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
|
|
42317
42650
|
const followUpStatus = deriveTrainFollowUpStatus({
|
|
42318
|
-
projectInfo: projectInfoSync
|
|
42651
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42319
42652
|
deploy: deployFollowUpOutcome(result.deployStatus),
|
|
42320
42653
|
rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
|
|
42321
42654
|
alignment: alignmentPending ? "unresolved" : "ok",
|
|
@@ -42496,8 +42829,17 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42496
42829
|
}
|
|
42497
42830
|
const raw = await runReleaseResume(trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
|
|
42498
42831
|
const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
|
|
42499
|
-
|
|
42500
|
-
|
|
42832
|
+
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42833
|
+
const resumed = { ...result, projectInfoSync };
|
|
42834
|
+
emitTrainResult("release --resume", o.json ? JSON.stringify(resumed, null, 2) : renderReleaseResume(resumed), o.out);
|
|
42835
|
+
const resumeStatus = resumeFollowUpOf(result.state);
|
|
42836
|
+
applyTrainFollowUpExit(deriveTrainFollowUpStatus({
|
|
42837
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42838
|
+
deploy: "ok",
|
|
42839
|
+
rcRetirement: "ok",
|
|
42840
|
+
alignment: resumeStatus === "failed" ? "failure" : resumeStatus === "pending" ? "unresolved" : "ok",
|
|
42841
|
+
foldPort: "ok"
|
|
42842
|
+
}));
|
|
42501
42843
|
return;
|
|
42502
42844
|
} catch (e) {
|
|
42503
42845
|
applyTrainFollowUpExit("failed");
|
|
@@ -42535,12 +42877,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42535
42877
|
let projectInfoSync;
|
|
42536
42878
|
const postReleaseJervDoctor = commandName === "release" ? await runPostReleaseJervDoctor(result.repo) : void 0;
|
|
42537
42879
|
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
|
-
}
|
|
42880
|
+
projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42544
42881
|
}
|
|
42545
42882
|
const { followUpStatus, releaseVerdict } = buildReleaseVerdict(commandName, result, projectInfoSync);
|
|
42546
42883
|
const reported = {
|
|
@@ -42637,22 +42974,6 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42637
42974
|
if (conclusion === "failure") return "failure";
|
|
42638
42975
|
return "unresolved";
|
|
42639
42976
|
}
|
|
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
42977
|
async function runHotfixSub(sub, body, o, render) {
|
|
42657
42978
|
try {
|
|
42658
42979
|
await requireFreshTrainCli("hotfix");
|
|
@@ -42664,11 +42985,12 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42664
42985
|
const ledger = result.ledger;
|
|
42665
42986
|
const alignment = result.alignmentStatus ?? ledgerAlignmentFollowUpOutcome(ledger);
|
|
42666
42987
|
const projectInfoSync = result.projectInfoSync;
|
|
42667
|
-
const
|
|
42988
|
+
const deployNote = result.deployNote ?? ledger?.phases.deploy.error ?? ledger?.phases.deploy.note;
|
|
42989
|
+
const legs = runs ? hotfixFollowUpLegs(runs, foldPort, alignment, result.foldNote, deployNote) : void 0;
|
|
42668
42990
|
const json = o.json || Boolean(hotfixCmd.opts().json);
|
|
42669
42991
|
emitTrainResult(`hotfix ${sub}`, json ? JSON.stringify(legs ? Object.assign({}, result, { legs }) : result, null, 2) : render(result), o.out);
|
|
42670
42992
|
if (runs) applyTrainFollowUpExit(deriveTrainFollowUpStatus({
|
|
42671
|
-
projectInfo: projectInfoSync
|
|
42993
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42672
42994
|
deploy: reduceFollowUpOutcomes(runs.map((r) => hotfixRunOutcome(r.conclusion))),
|
|
42673
42995
|
rcRetirement: "ok",
|
|
42674
42996
|
alignment,
|
|
@@ -42692,13 +43014,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42692
43014
|
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
43015
|
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
|
|
42694
43016
|
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
|
-
}
|
|
43017
|
+
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42702
43018
|
return { ...result, projectInfoSync, ...postReleaseJervDoctor ? { postReleaseJervDoctor } : {} };
|
|
42703
43019
|
}, o, renderHotfixRelease));
|
|
42704
43020
|
function hotfixStatusDeps() {
|
|
@@ -43279,7 +43595,7 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
|
|
|
43279
43595
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
43280
43596
|
}
|
|
43281
43597
|
});
|
|
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) => {
|
|
43598
|
+
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
43599
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
43284
43600
|
const cfg = await loadConfig();
|
|
43285
43601
|
const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
@@ -43321,26 +43637,13 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
43321
43637
|
return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
|
|
43322
43638
|
}
|
|
43323
43639
|
});
|
|
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
43640
|
async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
43338
43641
|
const slug = slugOf(target);
|
|
43339
43642
|
const reg = registryClientDeps(cfg);
|
|
43340
43643
|
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
43341
43644
|
const deploy = facts?.stages[stage] ?? null;
|
|
43342
43645
|
const publicUrl = publicUrlFromDeployFact(deploy);
|
|
43343
|
-
const publicProbe = publicUrl ? await
|
|
43646
|
+
const publicProbe = publicUrl ? await probePublicHealth(publicUrl) : void 0;
|
|
43344
43647
|
return buildTenantRuntimeStatus({
|
|
43345
43648
|
repo: target,
|
|
43346
43649
|
slug,
|
|
@@ -43368,9 +43671,9 @@ async function runProjectInfoSync(target, apply) {
|
|
|
43368
43671
|
fetchProjectBySlugChecked(slugOf(targetRepo2), registry2),
|
|
43369
43672
|
fetchProjectsList(registry2)
|
|
43370
43673
|
]);
|
|
43371
|
-
if (!read.ok) throw new
|
|
43674
|
+
if (!read.ok) throw new ProjectInfoReadUnavailableError(`org project sync-info: Hub registry read failed (${read.error})`);
|
|
43372
43675
|
if (!read.project) throw new Error(`org project sync-info: no registry META for ${targetRepo2}`);
|
|
43373
|
-
if (!projects) throw new
|
|
43676
|
+
if (!projects) throw new ProjectInfoReadUnavailableError("org project sync-info: Hub project list unavailable");
|
|
43374
43677
|
if (apply) {
|
|
43375
43678
|
const authority = await fetchTrainAuthority(targetRepo2, registry2);
|
|
43376
43679
|
if (!authority.ok) throw new Error(`org project sync-info: train authority unverified (${authority.error})`);
|