@mutmutco/cli 4.3.6 → 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 +537 -256
- package/package.json +2 -4
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
|
}
|
|
@@ -9347,8 +9361,6 @@ function normalizeTitle(title) {
|
|
|
9347
9361
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
9348
9362
|
}
|
|
9349
9363
|
var REPORT_TIMEOUT_MS = 8e3;
|
|
9350
|
-
var RED_TEAM_SLA_DAYS = 14;
|
|
9351
|
-
var RED_TEAM_SLA_MS = RED_TEAM_SLA_DAYS * 24 * 60 * 60 * 1e3;
|
|
9352
9364
|
async function fileReport(deps, req) {
|
|
9353
9365
|
const res = await deps.fetch(`${deps.apiUrl}/reports`, {
|
|
9354
9366
|
method: "POST",
|
|
@@ -9437,7 +9449,7 @@ function invalidReference(ref) {
|
|
|
9437
9449
|
return new Error(`invalid reference "${ref}" \u2014 expected ${ISSUE_REF_SHAPES}`);
|
|
9438
9450
|
}
|
|
9439
9451
|
function parseIssueRef(ref, expectedRepo) {
|
|
9440
|
-
const trimmed = ref.trim();
|
|
9452
|
+
const trimmed = String(ref).trim();
|
|
9441
9453
|
const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)$/i);
|
|
9442
9454
|
const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#(\d+)$/);
|
|
9443
9455
|
const bare = trimmed.match(/^#?(\d+)$/);
|
|
@@ -11063,14 +11075,7 @@ function writableOrUnknown(writable) {
|
|
|
11063
11075
|
function renderBoardSource() {
|
|
11064
11076
|
return "source: live";
|
|
11065
11077
|
}
|
|
11066
|
-
async function
|
|
11067
|
-
const cfg = resolveBoardConfig(options.config);
|
|
11068
|
-
const client = deps.client ?? defaultGitHubClient();
|
|
11069
|
-
let collected;
|
|
11070
|
-
let writable;
|
|
11071
|
-
let pullRequests;
|
|
11072
|
-
let github;
|
|
11073
|
-
let snapshotFallback;
|
|
11078
|
+
async function collectBoardPreferringSnapshot(cfg, options, deps) {
|
|
11074
11079
|
const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
|
|
11075
11080
|
{
|
|
11076
11081
|
owner: cfg.projectOwner,
|
|
@@ -11096,19 +11101,37 @@ async function readBoard(options, deps = {}) {
|
|
|
11096
11101
|
warnings.push(message2);
|
|
11097
11102
|
partial = true;
|
|
11098
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) {
|
|
11099
11131
|
for (const unreadable of snapshot.unreadableRepos) {
|
|
11100
|
-
warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
|
|
11101
|
-
partial = true;
|
|
11132
|
+
collected.warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
|
|
11133
|
+
collected.partial = true;
|
|
11102
11134
|
}
|
|
11103
|
-
collected = {
|
|
11104
|
-
items: read.items,
|
|
11105
|
-
viewer: snapshot.viewer,
|
|
11106
|
-
repo: currentRepo,
|
|
11107
|
-
projectId: snapshot.project.id,
|
|
11108
|
-
projectTitle: snapshot.project.title,
|
|
11109
|
-
warnings,
|
|
11110
|
-
partial
|
|
11111
|
-
};
|
|
11112
11135
|
writable = {
|
|
11113
11136
|
repos: new Set(snapshot.writableRepos.map((repo) => repo.toLowerCase())),
|
|
11114
11137
|
unknown: new Set(snapshot.unreadableRepos.map((entry) => entry.repo.toLowerCase()))
|
|
@@ -11116,11 +11139,6 @@ async function readBoard(options, deps = {}) {
|
|
|
11116
11139
|
pullRequests = snapshot.pullRequests;
|
|
11117
11140
|
github = snapshot.github;
|
|
11118
11141
|
} else {
|
|
11119
|
-
if (attempt?.state === "unavailable") snapshotFallback = attempt.reason;
|
|
11120
|
-
collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
|
|
11121
|
-
if (snapshotFallback) {
|
|
11122
|
-
collected.warnings.push(`Hub board snapshot unavailable (${snapshotFallback}) \u2014 served by the direct user-auth read (emergency fallback)`);
|
|
11123
|
-
}
|
|
11124
11142
|
const probed = await resolveWritableReposForClaimables(collected.items, client);
|
|
11125
11143
|
collected.warnings.push(...probed.warnings);
|
|
11126
11144
|
collected.partial = collected.partial || probed.partial;
|
|
@@ -12059,21 +12077,45 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
12059
12077
|
partial: false
|
|
12060
12078
|
};
|
|
12061
12079
|
}
|
|
12062
|
-
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) {
|
|
12063
12103
|
const cfg = resolveBoardConfig(options.config);
|
|
12064
12104
|
const client = deps.client ?? defaultGitHubClient();
|
|
12065
12105
|
const board = { owner: cfg.projectOwner, number: cfg.projectNumber };
|
|
12106
|
+
const unscanned = [];
|
|
12066
12107
|
for (const selector of selectors) {
|
|
12067
12108
|
try {
|
|
12068
12109
|
findBoardItem(collected.items, selector, board);
|
|
12069
12110
|
} catch {
|
|
12070
12111
|
const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
|
|
12071
|
-
if (fallback)
|
|
12112
|
+
if (fallback) {
|
|
12113
|
+
collected.items.push(fallback);
|
|
12114
|
+
unscanned.push(fallback);
|
|
12115
|
+
}
|
|
12072
12116
|
}
|
|
12073
12117
|
}
|
|
12074
|
-
const writable = await
|
|
12075
|
-
collected.warnings.push(...writable.warnings);
|
|
12076
|
-
collected.partial = collected.partial || writable.partial;
|
|
12118
|
+
const writable = await resolveClaimWritable(collected, client, snapshot, unscanned);
|
|
12077
12119
|
const report = {
|
|
12078
12120
|
project: { owner: cfg.projectOwner, number: cfg.projectNumber, id: collected.projectId, title: collected.projectTitle || String(cfg.projectNumber) },
|
|
12079
12121
|
viewer: collected.viewer,
|
|
@@ -12211,14 +12253,15 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12211
12253
|
}
|
|
12212
12254
|
async function claimBoardIssue(options, deps = {}) {
|
|
12213
12255
|
const cfg = resolveBoardConfig(options.config);
|
|
12214
|
-
const collected = await
|
|
12256
|
+
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
12215
12257
|
const selector = parseIssueSelector(options.selector, collected.repo, options.repo);
|
|
12216
|
-
const ctx = await prepareClaimContext(options, [selector], deps, collected);
|
|
12217
|
-
|
|
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;
|
|
12218
12261
|
}
|
|
12219
12262
|
async function claimBoardIssues(options, deps = {}) {
|
|
12220
12263
|
const cfg = resolveBoardConfig(options.config);
|
|
12221
|
-
const collected = await
|
|
12264
|
+
const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
|
|
12222
12265
|
const selectors = [];
|
|
12223
12266
|
const seen = /* @__PURE__ */ new Set();
|
|
12224
12267
|
for (const raw of options.selectors) {
|
|
@@ -12228,7 +12271,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12228
12271
|
seen.add(key);
|
|
12229
12272
|
selectors.push(selector);
|
|
12230
12273
|
}
|
|
12231
|
-
const ctx = await prepareClaimContext(options, selectors, deps, collected);
|
|
12274
|
+
const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
|
|
12232
12275
|
const results = new Array(selectors.length);
|
|
12233
12276
|
let next = 0;
|
|
12234
12277
|
const worker = async () => {
|
|
@@ -12249,7 +12292,9 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12249
12292
|
viewer: ctx.report.viewer,
|
|
12250
12293
|
repo: ctx.report.repo,
|
|
12251
12294
|
results,
|
|
12252
|
-
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] } : {}
|
|
12253
12298
|
};
|
|
12254
12299
|
}
|
|
12255
12300
|
async function moveBoardIssues(options, deps = {}) {
|
|
@@ -14321,6 +14366,12 @@ function trainFollowUpNote(status) {
|
|
|
14321
14366
|
return `follow-up ${status}: the exit code is never the verdict, read workflowRuns \u2014 see docs/Guides/train-troubleshooting.md#follow-up-pending`;
|
|
14322
14367
|
}
|
|
14323
14368
|
var TRAIN_DEPLOY_FAILED_NOTE = "promotion stands; retry the deploy, do not re-tag \u2014 see docs/Guides/train-troubleshooting.md#deploy-failed";
|
|
14369
|
+
function missingRunUrlNote(runId) {
|
|
14370
|
+
return runId != null ? `no run URL emitted \u2014 GitHub Actions returned run ${runId} without its URL` : "no run URL emitted \u2014 run absent or unreadable";
|
|
14371
|
+
}
|
|
14372
|
+
function workflowRunWithEvidence(run) {
|
|
14373
|
+
return run.runUrl || run.runUrlNote ? run : { ...run, runUrlNote: missingRunUrlNote(run.runId) };
|
|
14374
|
+
}
|
|
14324
14375
|
function isCentralDispatchModel(model) {
|
|
14325
14376
|
return model === "tenant-container" || model === "solo-container" || model === "static-cdn";
|
|
14326
14377
|
}
|
|
@@ -15399,10 +15450,10 @@ var rollout_plan_default = {
|
|
|
15399
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)."
|
|
15400
15451
|
},
|
|
15401
15452
|
baseline: {
|
|
15402
|
-
version: "4.3.
|
|
15403
|
-
tag: "v4.3.
|
|
15404
|
-
commit: "
|
|
15405
|
-
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"
|
|
15406
15457
|
},
|
|
15407
15458
|
exitCriterion: "fleet-n-of-n",
|
|
15408
15459
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15419,14 +15470,14 @@ var rollout_plan_default = {
|
|
|
15419
15470
|
repo: "mutmutco/mmi-hub",
|
|
15420
15471
|
role: "canary",
|
|
15421
15472
|
schedule: "train",
|
|
15422
|
-
v3Target: "v4.3.
|
|
15473
|
+
v3Target: "v4.3.8"
|
|
15423
15474
|
}
|
|
15424
15475
|
],
|
|
15425
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.",
|
|
15426
15477
|
rollback: {
|
|
15427
15478
|
independent: true,
|
|
15428
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15429
|
-
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)"
|
|
15430
15481
|
}
|
|
15431
15482
|
},
|
|
15432
15483
|
{
|
|
@@ -18276,12 +18327,14 @@ function appendPublishDispatch(deploy, publish) {
|
|
|
18276
18327
|
deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
|
|
18277
18328
|
};
|
|
18278
18329
|
}
|
|
18330
|
+
var JERV_GATEWAY_RUN_URL_NOTE = "no run URL emitted \u2014 host health leg runs outside GitHub Actions";
|
|
18279
18331
|
async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch) {
|
|
18280
18332
|
if (!isJervHubRepo(repo)) return dispatch;
|
|
18281
18333
|
if (dispatch.deployStatus !== "success") {
|
|
18282
18334
|
return {
|
|
18283
18335
|
...dispatch,
|
|
18284
|
-
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" }]
|
|
18285
18338
|
};
|
|
18286
18339
|
}
|
|
18287
18340
|
try {
|
|
@@ -18299,7 +18352,7 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
18299
18352
|
return {
|
|
18300
18353
|
...dispatch,
|
|
18301
18354
|
note: `${dispatch.note}; Jerv Gateway deployed ${tagSha.slice(0, 12)} and passed health checks`,
|
|
18302
|
-
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", conclusion: "success" }],
|
|
18355
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "success" }],
|
|
18303
18356
|
deployStatus: "success"
|
|
18304
18357
|
};
|
|
18305
18358
|
} catch (error) {
|
|
@@ -18308,7 +18361,7 @@ async function appendJervGatewayReleaseDeploy(deps, repo, tag, tagSha, dispatch)
|
|
|
18308
18361
|
...dispatch,
|
|
18309
18362
|
// #6095: the helper's receipt, health or rollback failed after a proven publish — the release resumes, never re-tags.
|
|
18310
18363
|
note: `${dispatch.note}; Jerv Gateway deploy FAILED (${detail}) \u2014 see docs/Guides/train-troubleshooting.md#jerv-gateway-deploy`,
|
|
18311
|
-
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", conclusion: "failure" }],
|
|
18364
|
+
workflowRuns: [...dispatch.workflowRuns ?? [], { workflow: "jerv-gateway", runUrlNote: JERV_GATEWAY_RUN_URL_NOTE, conclusion: "failure" }],
|
|
18312
18365
|
deployStatus: "failure"
|
|
18313
18366
|
};
|
|
18314
18367
|
}
|
|
@@ -20594,8 +20647,11 @@ function deployPhaseInput(model, dispatch, opts = {}) {
|
|
|
20594
20647
|
note: deployRun ? `${deployRun.workflow} release run` : dispatch.note
|
|
20595
20648
|
};
|
|
20596
20649
|
}
|
|
20597
|
-
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 };
|
|
20598
20653
|
return { state: "skipped", ...sha ? { sha } : {}, note: "registry-publish deploys by publishing \u2014 the release-event publish.yml run is the deploy plane" };
|
|
20654
|
+
}
|
|
20599
20655
|
case "tenant-container":
|
|
20600
20656
|
case "solo-container":
|
|
20601
20657
|
case "static-cdn":
|
|
@@ -20656,6 +20712,7 @@ function releasePhaseInputsFromDispatch(model, dispatch, publishDispatch, publis
|
|
|
20656
20712
|
function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
|
|
20657
20713
|
const deployRun = rows.find((r) => r.workflow === "deploy.yml");
|
|
20658
20714
|
const publishRun = rows.find((r) => r.workflow === "publish.yml");
|
|
20715
|
+
const hostRun = rows.find((r) => r.workflow === "jerv-gateway");
|
|
20659
20716
|
switch (model) {
|
|
20660
20717
|
case "hub-serverless":
|
|
20661
20718
|
return {
|
|
@@ -20674,7 +20731,9 @@ function phaseInputsFromRunRows(model, rows, tagSha, opts = {}) {
|
|
|
20674
20731
|
};
|
|
20675
20732
|
case "registry-publish":
|
|
20676
20733
|
return {
|
|
20677
|
-
|
|
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" },
|
|
20678
20737
|
publish: publishRun ? {
|
|
20679
20738
|
state: runRowState(publishRun, "pending"),
|
|
20680
20739
|
sha: tagSha,
|
|
@@ -21041,12 +21100,14 @@ async function selfConvergeTrainCli(input) {
|
|
|
21041
21100
|
var TRAIN_LANES = ["release", "rcand", "hotfix"];
|
|
21042
21101
|
var TROUBLESHOOTING_GUIDE = "docs/Guides/train-troubleshooting.md";
|
|
21043
21102
|
var SCRATCH_BRANCH_GLOBS = ["train/check/*", "hotfix-fold/*--port-*"];
|
|
21103
|
+
var COMPOSE_GUARD_HARD_FAIL = /^secrets preflight: .*noEnvFile is (not )?true.*$/m;
|
|
21044
21104
|
function readLocalWorkflowsDefault(root) {
|
|
21045
21105
|
const dir = (0, import_node_path22.join)(root, ".github", "workflows");
|
|
21046
21106
|
let names;
|
|
21047
21107
|
try {
|
|
21048
21108
|
names = (0, import_node_fs23.readdirSync)(dir);
|
|
21049
|
-
} catch {
|
|
21109
|
+
} catch (e) {
|
|
21110
|
+
if (e.code === "ENOENT") return [];
|
|
21050
21111
|
return null;
|
|
21051
21112
|
}
|
|
21052
21113
|
const files = [];
|
|
@@ -21276,12 +21337,12 @@ async function runTrainDoctor(input) {
|
|
|
21276
21337
|
...!hints.hasMainBranch ? ["main"] : [],
|
|
21277
21338
|
...!hints.hasRcBranch && track === "full" ? ["rc"] : []
|
|
21278
21339
|
];
|
|
21279
|
-
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` });
|
|
21280
21341
|
}
|
|
21281
21342
|
try {
|
|
21282
21343
|
const required = await discoverRequiredCheckContexts(train, ctx, stage);
|
|
21283
21344
|
if (required.length === 0) {
|
|
21284
|
-
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}\`` });
|
|
21285
21346
|
} else {
|
|
21286
21347
|
try {
|
|
21287
21348
|
assertTagAddressableRequiredContexts({ readWorkflows: () => workflows }, required, repo);
|
|
@@ -21353,12 +21414,14 @@ async function runTrainDoctor(input) {
|
|
|
21353
21414
|
}
|
|
21354
21415
|
}
|
|
21355
21416
|
try {
|
|
21356
|
-
await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo]);
|
|
21417
|
+
await train.runSelf(["secrets", "preflight", "--stage", stage, "--repo", repo, ...lane === "hotfix" ? ["--lane", "hotfix"] : []]);
|
|
21357
21418
|
} catch (e) {
|
|
21358
21419
|
const err = e;
|
|
21359
21420
|
const text = [typeof err.stdout === "string" ? err.stdout : "", typeof err.stderr === "string" ? err.stderr : "", message(e)].join("\n");
|
|
21360
21421
|
if (/^missing /m.test(text)) {
|
|
21361
|
-
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)` });
|
|
21362
21425
|
} else {
|
|
21363
21426
|
unverified(`the ${stage} secrets preflight`, clean2(text) || e);
|
|
21364
21427
|
}
|
|
@@ -21548,7 +21611,7 @@ function enforceGateBudget(deps, repo) {
|
|
|
21548
21611
|
);
|
|
21549
21612
|
}
|
|
21550
21613
|
}
|
|
21551
|
-
async function preflight(deps, ctx, stage, meta) {
|
|
21614
|
+
async function preflight(deps, ctx, stage, meta, lane) {
|
|
21552
21615
|
const model = requireDeployModel(meta, ctx.repo);
|
|
21553
21616
|
if (model === "content") {
|
|
21554
21617
|
throw new Error(`${ctx.repo} is a content repo (deployModel=content) \u2014 the release train does not apply (trunk-based; PR to main)`);
|
|
@@ -21556,7 +21619,7 @@ async function preflight(deps, ctx, stage, meta) {
|
|
|
21556
21619
|
if (model === "none") {
|
|
21557
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`);
|
|
21558
21621
|
}
|
|
21559
|
-
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"] : []]);
|
|
21560
21623
|
await assertNpmMajorPreflightFromWorkflows(deps, ctx.repo);
|
|
21561
21624
|
await assertActionsJobsCanStart(deps, ctx.repo);
|
|
21562
21625
|
enforceGateBudget(deps, ctx.repo);
|
|
@@ -22202,7 +22265,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22202
22265
|
const deployRunRepo = releaseRunRepoFor(deployModel, ctx.repo);
|
|
22203
22266
|
const failedReleaseRun = deployDispatch0.workflowRuns?.find((run) => run.conclusion === "failure");
|
|
22204
22267
|
const dispatchFailurePhase = failedReleaseRun?.workflow === "publish.yml" || deployModel === "registry-publish" ? "publish" : "deploy";
|
|
22205
|
-
const
|
|
22268
|
+
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
22206
22269
|
deps,
|
|
22207
22270
|
ledger,
|
|
22208
22271
|
ledgerAnchors,
|
|
@@ -22215,6 +22278,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22215
22278
|
workflow: failedReleaseRun?.workflow ?? (isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow")
|
|
22216
22279
|
}
|
|
22217
22280
|
);
|
|
22281
|
+
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, recoveredDeployDispatch);
|
|
22218
22282
|
await recordPhase(ledger, deps, ledgerAnchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha })), {
|
|
22219
22283
|
landed: "the immutable tag, the green required-check wall, the origin/main fast-forward, and the verified GitHub Release"
|
|
22220
22284
|
});
|
|
@@ -22239,7 +22303,6 @@ ${recovery.note}`), recoveryInput);
|
|
|
22239
22303
|
landed: "the immutable tag, the green required-check wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
22240
22304
|
});
|
|
22241
22305
|
let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
22242
|
-
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, releaseSha, dispatch);
|
|
22243
22306
|
if (publishSkipNote) dispatch = { ...dispatch, note: `${dispatch.note}; tenant-publish.yml skipped (${publishSkipNote})` };
|
|
22244
22307
|
return { checks, releaseUrl, announceNote, dispatch, ledgerAnchors };
|
|
22245
22308
|
}
|
|
@@ -22515,17 +22578,18 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
|
|
|
22515
22578
|
historicalDeploy = historicalTargets.length ? aggregateWorkflowRuns(historicalRows) : recovered.deployStatus;
|
|
22516
22579
|
historicalNote = recovered.note;
|
|
22517
22580
|
}
|
|
22581
|
+
const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
|
|
22582
|
+
note: historicalNote,
|
|
22583
|
+
deployStatus: historicalDeploy,
|
|
22584
|
+
workflowRuns: historicalRows
|
|
22585
|
+
});
|
|
22518
22586
|
if (persisted) {
|
|
22519
|
-
const phaseInputs = phaseInputsFromRunRows(deployModel, historicalRows, tagSha, { repo: ctx.repo });
|
|
22587
|
+
const phaseInputs = phaseInputsFromRunRows(deployModel, dispatch2.workflowRuns ?? historicalRows, tagSha, { repo: ctx.repo });
|
|
22520
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" });
|
|
22521
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" });
|
|
22522
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)" });
|
|
22523
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" });
|
|
22524
22592
|
}
|
|
22525
|
-
const dispatch2 = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, {
|
|
22526
|
-
note: historicalNote,
|
|
22527
|
-
deployStatus: historicalDeploy
|
|
22528
|
-
});
|
|
22529
22593
|
if (isJervHubRepo(ctx.repo)) steps2.push(dispatch2.note);
|
|
22530
22594
|
if (persisted) {
|
|
22531
22595
|
await recordPhase(ledger, deps, anchors, "alignment", phaseEntry(alignmentPhaseEntry(devRollForward2, rcAlignment2)), { strict: true, landed: "the released, verified, deploy-resolved release (alignment PRs reused/landed)" });
|
|
@@ -22607,7 +22671,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22607
22671
|
const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
22608
22672
|
const autoRunSince = (deps.now ?? Date.now)();
|
|
22609
22673
|
const deployDispatch0 = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, tagSha, "report", meta.publishDir);
|
|
22610
|
-
const
|
|
22674
|
+
const recoveredDeployDispatch = await recoverFailedDispatchPhase(
|
|
22611
22675
|
deps,
|
|
22612
22676
|
ledger,
|
|
22613
22677
|
anchors,
|
|
@@ -22620,6 +22684,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22620
22684
|
workflow: isCentralDispatchModel(deployModel) ? "tenant-deploy.yml" : "deploy workflow"
|
|
22621
22685
|
}
|
|
22622
22686
|
);
|
|
22687
|
+
const deployDispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, recoveredDeployDispatch);
|
|
22623
22688
|
steps.push(`dispatched the ${deployModel} deploy path`);
|
|
22624
22689
|
await recordPhase(ledger, deps, anchors, "deploy", phaseEntry(deployPhaseInput(deployModel, deployDispatch, { releaseSha: tagSha })), {
|
|
22625
22690
|
strict: ledgerMode === "strict",
|
|
@@ -22638,8 +22703,7 @@ ${recovery.note}`), recoveryInput);
|
|
|
22638
22703
|
strict: ledgerMode === "strict",
|
|
22639
22704
|
landed: "the immutable tag, the re-proven wall, origin/main, the verified GitHub Release, and the dispatched deploy path"
|
|
22640
22705
|
});
|
|
22641
|
-
|
|
22642
|
-
dispatch = await appendJervGatewayReleaseDeploy(deps, ctx.repo, tag, tagSha, dispatch);
|
|
22706
|
+
const dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
|
|
22643
22707
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
22644
22708
|
steps.push(`development roll-forward: ${devRollForward.status}`);
|
|
22645
22709
|
const rcAlignment = !directTrack && branchHints.hasRcBranch ? await alignRcForward(deps, ctx, tag) : void 0;
|
|
@@ -25264,6 +25328,13 @@ function registerBoardCommands(program3) {
|
|
|
25264
25328
|
const lane = holder.surface && holder.session && holder.host ? ` (${holder.surface}/${holder.session}@${holder.host})` : "";
|
|
25265
25329
|
return `@${holder.login}${lane}`;
|
|
25266
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
|
+
}
|
|
25267
25338
|
function claimVerdict(ref, result) {
|
|
25268
25339
|
const holder = formatClaimHolder(result.holder);
|
|
25269
25340
|
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
@@ -25286,24 +25357,26 @@ function registerBoardCommands(program3) {
|
|
|
25286
25357
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
25287
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));
|
|
25288
25359
|
withExamples(mutating(
|
|
25289
|
-
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"),
|
|
25290
25361
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
25291
25362
|
).action(async (issueRefs, o) => {
|
|
25292
25363
|
if (issueRefs.length === 1) {
|
|
25293
25364
|
const issueRef = issueRefs[0];
|
|
25294
25365
|
try {
|
|
25366
|
+
const config = await loadConfigForBoardSelector2(issueRef, o.repo);
|
|
25295
25367
|
const result = await claimBoardIssue({
|
|
25296
|
-
config
|
|
25368
|
+
config,
|
|
25297
25369
|
selector: issueRef,
|
|
25298
25370
|
repo: o.repo,
|
|
25299
25371
|
assignee: o.for,
|
|
25300
25372
|
force: o.force,
|
|
25301
25373
|
check: o.check,
|
|
25302
25374
|
allowPartial: o.allowPartial
|
|
25303
|
-
});
|
|
25375
|
+
}, { snapshot: registryClientDeps(config) });
|
|
25304
25376
|
if (!result.checked) invalidateStatuslineBoardCache();
|
|
25305
25377
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25306
25378
|
console.log(claimVerdict(result.item.ref, result));
|
|
25379
|
+
printClaimWarnings(result.warnings);
|
|
25307
25380
|
} catch (e) {
|
|
25308
25381
|
if (refuseRateLimited(e, o.json)) return;
|
|
25309
25382
|
return failGraceful(`board claim failed: ${e.message}`);
|
|
@@ -25311,22 +25384,25 @@ function registerBoardCommands(program3) {
|
|
|
25311
25384
|
return;
|
|
25312
25385
|
}
|
|
25313
25386
|
try {
|
|
25387
|
+
const config = await loadConfigForBoardSelector2(issueRefs[0], o.repo);
|
|
25314
25388
|
const bulk = await claimBoardIssues({
|
|
25315
|
-
config
|
|
25389
|
+
config,
|
|
25316
25390
|
selectors: issueRefs,
|
|
25317
25391
|
repo: o.repo,
|
|
25318
25392
|
assignee: o.for,
|
|
25319
25393
|
force: o.force,
|
|
25320
25394
|
check: o.check,
|
|
25321
25395
|
allowPartial: o.allowPartial
|
|
25322
|
-
});
|
|
25396
|
+
}, { snapshot: registryClientDeps(config) });
|
|
25323
25397
|
if (bulk.results.some((r) => r.claimed && !r.checked)) invalidateStatuslineBoardCache();
|
|
25324
25398
|
if (o.json) {
|
|
25325
25399
|
console.log(JSON.stringify(bulk.results));
|
|
25400
|
+
printClaimWarnings(bulk.warnings, true);
|
|
25326
25401
|
} else {
|
|
25327
25402
|
for (const result of bulk.results) {
|
|
25328
25403
|
console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
|
|
25329
25404
|
}
|
|
25405
|
+
printClaimWarnings(bulk.warnings);
|
|
25330
25406
|
}
|
|
25331
25407
|
if (bulk.failed > 0) process.exitCode = 1;
|
|
25332
25408
|
} catch (e) {
|
|
@@ -25639,7 +25715,7 @@ function trainPlan(command, options = {}) {
|
|
|
25639
25715
|
{ label: "verify the fix is merged on development (the only hotfix origin)", gated: true },
|
|
25640
25716
|
// #6068: hotfix start/release run the SAME shared preflight as release/rcand, before any git mutation.
|
|
25641
25717
|
{ label: "verify registry META for this project", command: "mmi-cli oracle org project get <owner/repo>", gated: true },
|
|
25642
|
-
{ 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 },
|
|
25643
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 },
|
|
25644
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 },
|
|
25645
25721
|
{ label: "branch hotfix from main and cherry-pick the dev commits", command: "git cherry-pick -x <dev-sha>", gated: true },
|
|
@@ -28247,7 +28323,7 @@ function formatDeployStatus(r) {
|
|
|
28247
28323
|
`running version: ${r.runningVersion ?? "none stamped"}`,
|
|
28248
28324
|
`last deploy run: ${formatLastRun(r)}`,
|
|
28249
28325
|
`public URL: ${r.publicUrl ?? "none"}`,
|
|
28250
|
-
`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"}`,
|
|
28251
28327
|
`deploy state: ${r.deployOk === void 0 ? "not stamped" : r.deployOk ? "ok" : "failed"}`
|
|
28252
28328
|
];
|
|
28253
28329
|
if (r.hints.length) {
|
|
@@ -28258,22 +28334,9 @@ function formatDeployStatus(r) {
|
|
|
28258
28334
|
|
|
28259
28335
|
// src/deploy-commands.ts
|
|
28260
28336
|
var STAGES2 = ["dev", "rc", "main"];
|
|
28261
|
-
async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
28262
|
-
const controller = new AbortController();
|
|
28263
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
28264
|
-
timeout.unref?.();
|
|
28265
|
-
try {
|
|
28266
|
-
const res = await fetch(url, { method: "GET", signal: controller.signal });
|
|
28267
|
-
return { ok: res.ok, status: res.status };
|
|
28268
|
-
} catch (e) {
|
|
28269
|
-
return { ok: false, error: e.message };
|
|
28270
|
-
} finally {
|
|
28271
|
-
clearTimeout(timeout);
|
|
28272
|
-
}
|
|
28273
|
-
}
|
|
28274
28337
|
function registerDeployCommands(program3) {
|
|
28275
28338
|
const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
|
|
28276
|
-
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) => {
|
|
28277
28340
|
if (!STAGES2.includes(stage)) {
|
|
28278
28341
|
return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
|
|
28279
28342
|
}
|
|
@@ -28285,7 +28348,7 @@ function registerDeployCommands(program3) {
|
|
|
28285
28348
|
const deployFacts = await fetchDeployFactsBySlug(slug, reg);
|
|
28286
28349
|
const publicUrl = publicUrlFromDeployFact(deployFacts?.stages[stage] ?? null);
|
|
28287
28350
|
const [health, lastRun] = await Promise.all([
|
|
28288
|
-
publicUrl ?
|
|
28351
|
+
publicUrl ? probePublicHealth(publicUrl) : Promise.resolve(void 0),
|
|
28289
28352
|
fetchLastTenantDeployRun(slug, stage)
|
|
28290
28353
|
]);
|
|
28291
28354
|
const report = buildDeployStatusReport({
|
|
@@ -29016,7 +29079,8 @@ function composeMarksRuntimeEnvTrue(composeText) {
|
|
|
29016
29079
|
return false;
|
|
29017
29080
|
});
|
|
29018
29081
|
}
|
|
29019
|
-
function promotionSourceBranch(stage, releaseTrack) {
|
|
29082
|
+
function promotionSourceBranch(stage, releaseTrack, lane) {
|
|
29083
|
+
if (lane === "hotfix") return "main";
|
|
29020
29084
|
if (stage === "main") return releaseTrack === "direct" ? "development" : "rc";
|
|
29021
29085
|
return "development";
|
|
29022
29086
|
}
|
|
@@ -29917,7 +29981,7 @@ async function collectStatus() {
|
|
|
29917
29981
|
try {
|
|
29918
29982
|
const cfg = await loadConfigOrDiscover();
|
|
29919
29983
|
if (cfg.sagaApiUrl) {
|
|
29920
|
-
const report = await readBoard({ config: cfg });
|
|
29984
|
+
const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
|
|
29921
29985
|
claimedItems = report.primary.userOwned.map((item) => ({
|
|
29922
29986
|
number: item.number,
|
|
29923
29987
|
title: item.title,
|
|
@@ -29964,8 +30028,8 @@ var PRIORITY_RANK = {
|
|
|
29964
30028
|
};
|
|
29965
30029
|
async function recommendNext(repo, deps) {
|
|
29966
30030
|
const load = deps?.loadConfig ?? loadConfigForRepo;
|
|
29967
|
-
const reader = deps?.readBoard ?? readBoard;
|
|
29968
30031
|
const cfg = await load(repo);
|
|
30032
|
+
const reader = deps?.readBoard ?? ((opts) => readBoard(opts, { snapshot: registryClientDeps(cfg) }));
|
|
29969
30033
|
if (!cfg.sagaApiUrl) throw new Error("Hub API URL not configured \u2014 the board was NOT read (run `mmi-cli doctor`)");
|
|
29970
30034
|
let report;
|
|
29971
30035
|
try {
|
|
@@ -30011,7 +30075,7 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
30011
30075
|
let board = { ok: false, detail: "no config" };
|
|
30012
30076
|
try {
|
|
30013
30077
|
if (cfg.sagaApiUrl) {
|
|
30014
|
-
const report = await readBoard({ config: cfg });
|
|
30078
|
+
const report = await readBoard({ config: cfg }, { snapshot: registryClientDeps(cfg) });
|
|
30015
30079
|
const total = report.primary.claimable.length + report.primary.userOwned.length + report.primary.taken.length;
|
|
30016
30080
|
board = { ok: true, detail: `board has ${total} active items (${report.primary.claimable.length} claimable, ${report.primary.userOwned.length} yours)` };
|
|
30017
30081
|
} else {
|
|
@@ -30230,14 +30294,6 @@ var surfaces_default = {
|
|
|
30230
30294
|
sourcePath: "skills",
|
|
30231
30295
|
targetPath: "packages/claude-plugin/skills"
|
|
30232
30296
|
},
|
|
30233
|
-
{
|
|
30234
|
-
mode: "files",
|
|
30235
|
-
sourcePath: "scripts",
|
|
30236
|
-
targetPath: "packages/claude-plugin/scripts",
|
|
30237
|
-
include: [
|
|
30238
|
-
"edit-tool-paths.mjs"
|
|
30239
|
-
]
|
|
30240
|
-
},
|
|
30241
30297
|
{
|
|
30242
30298
|
mode: "files",
|
|
30243
30299
|
sourcePath: "bin",
|
|
@@ -30313,14 +30369,6 @@ var surfaces_default = {
|
|
|
30313
30369
|
sourcePath: "skills",
|
|
30314
30370
|
targetPath: "packages/codex-plugin/skills"
|
|
30315
30371
|
},
|
|
30316
|
-
{
|
|
30317
|
-
mode: "files",
|
|
30318
|
-
sourcePath: "scripts",
|
|
30319
|
-
targetPath: "packages/codex-plugin/scripts",
|
|
30320
|
-
include: [
|
|
30321
|
-
"edit-tool-paths.mjs"
|
|
30322
|
-
]
|
|
30323
|
-
},
|
|
30324
30372
|
{
|
|
30325
30373
|
mode: "files",
|
|
30326
30374
|
sourcePath: "bin",
|
|
@@ -30396,14 +30444,6 @@ var surfaces_default = {
|
|
|
30396
30444
|
sourcePath: "skills",
|
|
30397
30445
|
targetPath: "packages/cursor-plugin/skills"
|
|
30398
30446
|
},
|
|
30399
|
-
{
|
|
30400
|
-
mode: "files",
|
|
30401
|
-
sourcePath: "scripts",
|
|
30402
|
-
targetPath: "packages/cursor-plugin/scripts",
|
|
30403
|
-
include: [
|
|
30404
|
-
"edit-tool-paths.mjs"
|
|
30405
|
-
]
|
|
30406
|
-
},
|
|
30407
30447
|
{
|
|
30408
30448
|
mode: "files",
|
|
30409
30449
|
sourcePath: "bin",
|
|
@@ -30482,14 +30522,6 @@ var surfaces_default = {
|
|
|
30482
30522
|
excludeNamePrefixes: [
|
|
30483
30523
|
"_"
|
|
30484
30524
|
]
|
|
30485
|
-
},
|
|
30486
|
-
{
|
|
30487
|
-
mode: "files",
|
|
30488
|
-
sourcePath: "scripts",
|
|
30489
|
-
targetPath: ".pi-plugin/scripts",
|
|
30490
|
-
include: [
|
|
30491
|
-
"edit-tool-paths.mjs"
|
|
30492
|
-
]
|
|
30493
30525
|
}
|
|
30494
30526
|
]
|
|
30495
30527
|
},
|
|
@@ -30549,14 +30581,6 @@ var surfaces_default = {
|
|
|
30549
30581
|
mode: "directories",
|
|
30550
30582
|
sourcePath: "skills",
|
|
30551
30583
|
targetPath: "packages/hermes-plugin/skills"
|
|
30552
|
-
},
|
|
30553
|
-
{
|
|
30554
|
-
mode: "files",
|
|
30555
|
-
sourcePath: "scripts",
|
|
30556
|
-
targetPath: "packages/hermes-plugin/scripts",
|
|
30557
|
-
include: [
|
|
30558
|
-
"edit-tool-paths.mjs"
|
|
30559
|
-
]
|
|
30560
30584
|
}
|
|
30561
30585
|
]
|
|
30562
30586
|
},
|
|
@@ -32612,6 +32636,10 @@ function validateBatchSpecs(specs) {
|
|
|
32612
32636
|
spec.labels = [...spec.labels ?? [], ...alias];
|
|
32613
32637
|
delete spec.label;
|
|
32614
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
|
+
}
|
|
32615
32643
|
if (spec.repo !== void 0 && !/^[\w.-]+\/[\w.-]+$/.test(spec.repo)) {
|
|
32616
32644
|
errors.push({ row, error: `bad repo "${spec.repo}" \u2014 expected owner/repo` });
|
|
32617
32645
|
continue;
|
|
@@ -32620,13 +32648,17 @@ function validateBatchSpecs(specs) {
|
|
|
32620
32648
|
errors.push({ row, error: `unknown type "${spec.type}" \u2014 expected one of: ${validTypes.join(", ")}` });
|
|
32621
32649
|
continue;
|
|
32622
32650
|
}
|
|
32623
|
-
if (
|
|
32651
|
+
if (typeof spec.title !== "string" || !spec.title.trim()) {
|
|
32624
32652
|
errors.push({ row, error: "missing or empty title" });
|
|
32625
32653
|
continue;
|
|
32626
32654
|
}
|
|
32655
|
+
if (spec.body !== void 0 && typeof spec.body !== "string") {
|
|
32656
|
+
errors.push({ row, error: "body must be a string" });
|
|
32657
|
+
continue;
|
|
32658
|
+
}
|
|
32627
32659
|
let priority;
|
|
32628
32660
|
try {
|
|
32629
|
-
priority = spec.priority ? normalizePriority(spec.priority) : "medium";
|
|
32661
|
+
priority = spec.priority ? normalizePriority(String(spec.priority)) : "medium";
|
|
32630
32662
|
} catch (e) {
|
|
32631
32663
|
errors.push({ row, error: e.message });
|
|
32632
32664
|
continue;
|
|
@@ -32639,6 +32671,14 @@ function validateBatchSpecs(specs) {
|
|
|
32639
32671
|
if (!labelsCarrySurface(spec.labels)) spec.labels = [...spec.labels ?? [], surfaceLabel(spec.surface)];
|
|
32640
32672
|
delete spec.surface;
|
|
32641
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
|
+
}
|
|
32642
32682
|
validated.push({ row, spec, priority, type: spec.type });
|
|
32643
32683
|
}
|
|
32644
32684
|
return { ok: errors.length === 0, errors, validated };
|
|
@@ -34065,6 +34105,8 @@ var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!,
|
|
|
34065
34105
|
projectV2 { id }
|
|
34066
34106
|
}
|
|
34067
34107
|
}`;
|
|
34108
|
+
var ProjectInfoReadUnavailableError = class extends Error {
|
|
34109
|
+
};
|
|
34068
34110
|
function shortDescriptionFromReadme(markdown) {
|
|
34069
34111
|
const lines2 = markdown.replace(/\r/g, "").split("\n");
|
|
34070
34112
|
const h1 = lines2.findIndex((line) => /^#\s+\S/.test(line.trim()));
|
|
@@ -34786,10 +34828,13 @@ function registerSecretsCommands(program3) {
|
|
|
34786
34828
|
const ok = body !== void 0 ? await secretsOrgCatalogSet(d, body, { replace: o.replace, remove: o.remove }) : await secretsOrgCatalogRemove(d, o.remove ?? []);
|
|
34787
34829
|
if (!ok) process.exitCode = 1;
|
|
34788
34830
|
}));
|
|
34789
|
-
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) => {
|
|
34790
34832
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
34791
34833
|
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
34792
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
|
+
}
|
|
34793
34838
|
const cfg = await loadConfig();
|
|
34794
34839
|
if (!cfg.sagaApiUrl) {
|
|
34795
34840
|
fail("secrets: Hub API URL not configured");
|
|
@@ -34810,7 +34855,7 @@ function registerSecretsCommands(program3) {
|
|
|
34810
34855
|
let filelessOk = true;
|
|
34811
34856
|
if (meta && centralContainer && !o.skipComposeGuard) {
|
|
34812
34857
|
const stage = o.stage;
|
|
34813
|
-
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo));
|
|
34858
|
+
const branch = promotionSourceBranch(stage, resolveReleaseTrack(meta, void 0, repo), o.lane);
|
|
34814
34859
|
const cwdRepo = repoFromRemoteUrl((await execFileP("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
|
|
34815
34860
|
const { sameRepo: sameRepo2 } = resolvePreflightRepoScope(o.repo, cwdRepo);
|
|
34816
34861
|
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
@@ -35016,10 +35061,11 @@ async function resolveBoardConfig2(repoOption) {
|
|
|
35016
35061
|
const floor = await loadConfig();
|
|
35017
35062
|
if (!floor.sagaApiUrl) return null;
|
|
35018
35063
|
const slug = repoOption ? (repoOption.replace(/\.git$/, "").split("/").pop() ?? repoOption).toLowerCase() : await repoSlug();
|
|
35019
|
-
const
|
|
35064
|
+
const registry2 = registryClientDeps(floor);
|
|
35065
|
+
const read = await fetchProjectBySlugChecked(slug, registry2);
|
|
35020
35066
|
if (!read.ok || !read.project) return null;
|
|
35021
35067
|
const cfg = boardConfigFromProject(read.project, floor);
|
|
35022
|
-
return readBoard({ config: cfg, repo: repoOption, allowPartial: true });
|
|
35068
|
+
return readBoard({ config: cfg, repo: repoOption, allowPartial: true }, { snapshot: registry2 });
|
|
35023
35069
|
}
|
|
35024
35070
|
function registerSessionReport(program3) {
|
|
35025
35071
|
const report = program3.commands.find((c) => c.name() === "report");
|
|
@@ -36788,24 +36834,31 @@ function runTestPolicy(root, deps = {}) {
|
|
|
36788
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.`)
|
|
36789
36835
|
}] : [];
|
|
36790
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";
|
|
36791
36840
|
const removedByThisDiff = removedPaths(changed);
|
|
36792
36841
|
const staleFindings = [];
|
|
36793
|
-
const unresolved = unresolvedProtectedEntries(policy, root,
|
|
36842
|
+
const unresolved = unresolvedProtectedEntries(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
|
|
36794
36843
|
if (unresolved.length > 0) {
|
|
36795
36844
|
staleFindings.push({
|
|
36796
36845
|
kind: "stale-protected-entry",
|
|
36797
36846
|
paths: unresolved,
|
|
36798
|
-
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
|
|
36799
|
-
` + 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.`
|
|
36800
36851
|
});
|
|
36801
36852
|
}
|
|
36802
|
-
const staleSatisfiers = unresolvedSatisfiers(policy, root,
|
|
36853
|
+
const staleSatisfiers = unresolvedSatisfiers(policy, root, policyTreeExists).filter((p) => !removedByThisDiff.has(p));
|
|
36803
36854
|
if (staleSatisfiers.length > 0) {
|
|
36804
36855
|
staleFindings.push({
|
|
36805
36856
|
kind: "stale-satisfied-by",
|
|
36806
36857
|
paths: staleSatisfiers,
|
|
36807
|
-
detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist:
|
|
36808
|
-
` + 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.`
|
|
36809
36862
|
});
|
|
36810
36863
|
}
|
|
36811
36864
|
const override = lookup.override;
|
|
@@ -37120,15 +37173,17 @@ function cleanupGitArgs(cwd, args) {
|
|
|
37120
37173
|
}
|
|
37121
37174
|
async function remoteBranchExists2(branch, options = {}) {
|
|
37122
37175
|
if (!branch) return void 0;
|
|
37176
|
+
const remote = options.remote ?? "origin";
|
|
37123
37177
|
try {
|
|
37124
|
-
if (options.prune) await execFileP("git", cleanupGitArgs(options.cwd, ["fetch",
|
|
37125
|
-
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;
|
|
37126
37180
|
} catch {
|
|
37127
37181
|
return void 0;
|
|
37128
37182
|
}
|
|
37129
37183
|
}
|
|
37130
37184
|
async function deleteMergedRemoteBranch(options) {
|
|
37131
|
-
const
|
|
37185
|
+
const remote = options.remote ?? "origin";
|
|
37186
|
+
const remediation = `git push ${remote} --delete ${options.branch}`;
|
|
37132
37187
|
if (!options.branch) {
|
|
37133
37188
|
return {
|
|
37134
37189
|
branch: options.branch,
|
|
@@ -37157,13 +37212,13 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37157
37212
|
existedBefore: false,
|
|
37158
37213
|
attempted: false,
|
|
37159
37214
|
status: "failed",
|
|
37160
|
-
error: `could not verify absence of
|
|
37215
|
+
error: `could not verify absence of ${remote}/${options.branch}`,
|
|
37161
37216
|
remediation
|
|
37162
37217
|
};
|
|
37163
37218
|
}
|
|
37164
37219
|
}
|
|
37165
37220
|
try {
|
|
37166
|
-
await options.execGit(["push",
|
|
37221
|
+
await options.execGit(["push", remote, "--delete", options.branch]);
|
|
37167
37222
|
} catch (e) {
|
|
37168
37223
|
const exists2 = await options.branchExists(options.branch);
|
|
37169
37224
|
if (exists2 === false) {
|
|
@@ -37197,7 +37252,7 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37197
37252
|
existedBefore: options.existedBefore,
|
|
37198
37253
|
attempted: true,
|
|
37199
37254
|
status: "failed",
|
|
37200
|
-
error: exists ?
|
|
37255
|
+
error: exists ? `${remote} still reports ${options.branch} after deletion` : `could not verify deletion of ${remote}/${options.branch}`,
|
|
37201
37256
|
remediation
|
|
37202
37257
|
};
|
|
37203
37258
|
}
|
|
@@ -37539,11 +37594,13 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
37539
37594
|
const actor = deps.actor ?? describeSessionIdentity();
|
|
37540
37595
|
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
37541
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;
|
|
37542
37599
|
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
37543
37600
|
if (!contest.contested) continue;
|
|
37544
37601
|
const ref = `${repo}#${number}`;
|
|
37545
37602
|
const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : "another lane";
|
|
37546
|
-
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`;
|
|
37547
37604
|
}
|
|
37548
37605
|
return void 0;
|
|
37549
37606
|
}
|
|
@@ -37845,9 +37902,9 @@ function isPathAtOrWithin(path2, worktreePath) {
|
|
|
37845
37902
|
function isWindowsCwdLockResidueError(error) {
|
|
37846
37903
|
return process.platform === "win32" && /\bEPERM\b|access(?: is)? denied/i.test(error);
|
|
37847
37904
|
}
|
|
37848
|
-
function deferredCwdLockRemediation(
|
|
37905
|
+
function deferredCwdLockRemediation(wtPath) {
|
|
37849
37906
|
const quote = (path2) => path2.replace(/'/g, "''");
|
|
37850
|
-
return `After this command exits:
|
|
37907
|
+
return `After this command exits: jervcode worktree-cleanup --worktree '${quote(wtPath)}'`;
|
|
37851
37908
|
}
|
|
37852
37909
|
function primaryCheckoutBranchRemediation(primaryRoot, baseRef, branch) {
|
|
37853
37910
|
const quote = (value) => value.replace(/'/g, "''");
|
|
@@ -37943,6 +38000,69 @@ function safeRemoveTree(path2) {
|
|
|
37943
38000
|
}
|
|
37944
38001
|
(0, import_node_fs41.unlinkSync)(path2);
|
|
37945
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
|
+
}
|
|
37946
38066
|
async function describePreCleanFailure(wtPath, execGit, error) {
|
|
37947
38067
|
const dryRun = await execGit(["-C", wtPath, "clean", "-ndX"]).catch(() => "");
|
|
37948
38068
|
const remaining = dryRun.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("Would remove ")).map((line) => line.slice("Would remove ".length));
|
|
@@ -37977,6 +38097,25 @@ async function verifyBranchHead(git3, branch, expectedHeadOid) {
|
|
|
37977
38097
|
}
|
|
37978
38098
|
return { ok: true };
|
|
37979
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
|
+
}
|
|
37980
38119
|
async function teardownWorktreeStage(worktreePath) {
|
|
37981
38120
|
try {
|
|
37982
38121
|
const result = await stopStage({ cwd: worktreePath, requiredIdentityCwd: worktreePath, globalStatePath: false });
|
|
@@ -38070,7 +38209,7 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
38070
38209
|
}
|
|
38071
38210
|
function isPrMergeWorktreePartial(cleanup) {
|
|
38072
38211
|
const worktree = cleanup?.worktree;
|
|
38073
|
-
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");
|
|
38074
38213
|
}
|
|
38075
38214
|
function prMergeLocalCleanupExitCode(cleanup) {
|
|
38076
38215
|
return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
|
|
@@ -38110,10 +38249,29 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38110
38249
|
const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
|
|
38111
38250
|
if (!wtPath || mainWorktreeTarget) {
|
|
38112
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
|
+
}
|
|
38113
38270
|
report.worktree = {
|
|
38114
38271
|
path: wtPath,
|
|
38115
38272
|
status: "not-attempted",
|
|
38116
38273
|
reason: "main-worktree",
|
|
38274
|
+
error: switched.error,
|
|
38117
38275
|
remediation: primaryCheckoutBranchRemediation(options.primaryRoot, options.baseRef, branch)
|
|
38118
38276
|
};
|
|
38119
38277
|
}
|
|
@@ -38133,7 +38291,12 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38133
38291
|
return report;
|
|
38134
38292
|
}
|
|
38135
38293
|
const porcelain = await execGit(["-C", wtPath, "status", "--porcelain"]).catch(() => void 0);
|
|
38136
|
-
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()) {
|
|
38137
38300
|
report.worktree = { path: wtPath, status: "refused", reason: "dirty-worktree" };
|
|
38138
38301
|
report.localBranch = { name: branch, status: "not-attempted", reason: "dirty-worktree" };
|
|
38139
38302
|
return report;
|
|
@@ -38183,6 +38346,25 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38183
38346
|
report.localBranch = { name: branch, status: "not-attempted", reason: "archive-failed" };
|
|
38184
38347
|
return report;
|
|
38185
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);
|
|
38186
38368
|
if (pathExists((0, import_node_path38.join)(wtPath, "node_modules"))) {
|
|
38187
38369
|
const nmRemoved = await (options.removeRealNodeModules ?? ((p) => removeWorktreeNodeModulesViaHelper(p, { cwd: mainWorktreePath })))(wtPath);
|
|
38188
38370
|
if (!nmRemoved.ok) {
|
|
@@ -38194,6 +38376,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38194
38376
|
artifactsArchive,
|
|
38195
38377
|
stageTeardown,
|
|
38196
38378
|
tmpEvidenceCount: tmpEvidence.length,
|
|
38379
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38197
38380
|
remediation: `jervcode worktree-node-modules-cleanup --worktree '${wtPath.replace(/'/g, "''")}'`
|
|
38198
38381
|
};
|
|
38199
38382
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
@@ -38210,12 +38393,16 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38210
38393
|
artifactsArchive,
|
|
38211
38394
|
stageTeardown,
|
|
38212
38395
|
tmpEvidenceCount: tmpEvidence.length,
|
|
38396
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {},
|
|
38213
38397
|
// #6076: name the surviving node_modules remover when the dry run found one, else the clean.
|
|
38214
38398
|
remediation: preClean.remediation ?? `git -C '${wtPath.replace(/'/g, "''")}' clean -ffdX`
|
|
38215
38399
|
};
|
|
38216
38400
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-pre-clean-failed" };
|
|
38217
38401
|
return report;
|
|
38218
38402
|
}
|
|
38403
|
+
const preRemoveGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
|
|
38404
|
+
if (!preRemoveGuard.ok) return refuseReparseEscape(preRemoveGuard.error);
|
|
38405
|
+
unlinkedReparsePoints.push(...preRemoveGuard.unlinked);
|
|
38219
38406
|
moveCwdToSafeWorktree(wtPath, safeCwd);
|
|
38220
38407
|
const removal = await removeWorktreeWithReconcile(
|
|
38221
38408
|
wtPath,
|
|
@@ -38230,7 +38417,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38230
38417
|
error: removal.error,
|
|
38231
38418
|
artifactsArchive,
|
|
38232
38419
|
stageTeardown,
|
|
38233
|
-
tmpEvidenceCount: tmpEvidence.length
|
|
38420
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38421
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
|
|
38234
38422
|
};
|
|
38235
38423
|
report.localBranch = { name: branch, status: "not-attempted", reason: "worktree-removal-failed" };
|
|
38236
38424
|
return report;
|
|
@@ -38241,7 +38429,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38241
38429
|
...removal.reason ? { reason: removal.reason } : {},
|
|
38242
38430
|
artifactsArchive,
|
|
38243
38431
|
stageTeardown,
|
|
38244
|
-
tmpEvidenceCount: tmpEvidence.length
|
|
38432
|
+
tmpEvidenceCount: tmpEvidence.length,
|
|
38433
|
+
...unlinkedReparsePoints.length ? { unlinkedReparsePoints } : {}
|
|
38245
38434
|
};
|
|
38246
38435
|
if (pathExists(wtPath)) {
|
|
38247
38436
|
const residue = await (options.removeResidueDir?.(wtPath) ?? removeResidueDirectory(wtPath));
|
|
@@ -38253,7 +38442,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38253
38442
|
if (isWindowsCwdLockResidueError(residue.error) && isPathAtOrWithin(options.startingPath, wtPath)) {
|
|
38254
38443
|
report.worktree.status = "retained-locked";
|
|
38255
38444
|
report.worktree.reason = "parent-cwd-lock";
|
|
38256
|
-
report.worktree.remediation = deferredCwdLockRemediation(
|
|
38445
|
+
report.worktree.remediation = deferredCwdLockRemediation(wtPath);
|
|
38257
38446
|
} else {
|
|
38258
38447
|
report.worktree.status = "failed";
|
|
38259
38448
|
report.worktree.reason = "residue-remains";
|
|
@@ -38289,12 +38478,18 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
38289
38478
|
lines2.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
|
|
38290
38479
|
} else if (wt.status === "retained-locked") {
|
|
38291
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`);
|
|
38292
38484
|
} else if (wt.status === "not-attempted" && wt.reason === "main-worktree") {
|
|
38293
|
-
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"}`);
|
|
38294
38486
|
}
|
|
38295
38487
|
if (wt.residue === "left" && wt.status !== "retained-locked") {
|
|
38296
38488
|
lines2.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
|
|
38297
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
|
+
}
|
|
38298
38493
|
if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
|
|
38299
38494
|
lines2.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
|
|
38300
38495
|
} else if (wt.artifactsArchive?.status === "blocked" && wt.artifactsArchive.error) {
|
|
@@ -39356,7 +39551,7 @@ ${list}`);
|
|
|
39356
39551
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
39357
39552
|
if (result.status === "failed") process.exitCode = 1;
|
|
39358
39553
|
});
|
|
39359
|
-
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) => {
|
|
39360
39555
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
39361
39556
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
39362
39557
|
if (o.disableAuto) {
|
|
@@ -39416,7 +39611,23 @@ ${list}`);
|
|
|
39416
39611
|
const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
39417
39612
|
const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
|
|
39418
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" }));
|
|
39419
|
-
|
|
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
|
+
}
|
|
39420
39631
|
const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
|
|
39421
39632
|
const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
|
|
39422
39633
|
if (o.wait) {
|
|
@@ -39474,7 +39685,7 @@ ${list}`);
|
|
|
39474
39685
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
39475
39686
|
if (guard.note) console.warn(`pr merge: ${guard.note}`);
|
|
39476
39687
|
}
|
|
39477
|
-
const remoteBefore = await remoteBranchExists2(headRef);
|
|
39688
|
+
const remoteBefore = await remoteBranchExists2(headRef, { remote });
|
|
39478
39689
|
let upgradedToAuto = false;
|
|
39479
39690
|
let remoteNotAttemptedReason = "preserved-delayed-cleanup";
|
|
39480
39691
|
const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
@@ -39593,30 +39804,34 @@ ${list}`);
|
|
|
39593
39804
|
}
|
|
39594
39805
|
const primaryRoot = beforeWorktrees[0]?.path ?? (startingPath || process.cwd());
|
|
39595
39806
|
let localCleanup;
|
|
39596
|
-
|
|
39597
|
-
localCleanup =
|
|
39598
|
-
|
|
39599
|
-
|
|
39600
|
-
|
|
39601
|
-
|
|
39602
|
-
|
|
39603
|
-
|
|
39604
|
-
|
|
39605
|
-
|
|
39606
|
-
|
|
39607
|
-
|
|
39608
|
-
|
|
39609
|
-
|
|
39610
|
-
|
|
39611
|
-
|
|
39612
|
-
|
|
39613
|
-
|
|
39614
|
-
|
|
39615
|
-
|
|
39616
|
-
|
|
39617
|
-
|
|
39618
|
-
|
|
39619
|
-
|
|
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
|
+
}
|
|
39620
39835
|
}
|
|
39621
39836
|
const remoteBranch = await deleteMergedRemoteBranch({
|
|
39622
39837
|
branch: headRef,
|
|
@@ -39627,7 +39842,9 @@ ${list}`);
|
|
|
39627
39842
|
// #5899: this leg runs after worktree teardown — anchor it to the main checkout so a cwd inside
|
|
39628
39843
|
// the removed worktree cannot turn the delete into 'fatal: not a git repository' + manual remediation.
|
|
39629
39844
|
execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
39630
|
-
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
|
|
39631
39848
|
});
|
|
39632
39849
|
const worktree = localCleanup?.worktree;
|
|
39633
39850
|
const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
|
|
@@ -39690,6 +39907,7 @@ ${list}`);
|
|
|
39690
39907
|
...methodField ? { method: methodField } : {},
|
|
39691
39908
|
remoteBranch,
|
|
39692
39909
|
housekeeping,
|
|
39910
|
+
...foreignCwd ? { foreignCwd: true, ...foreignCheckout ? { foreignCheckout } : {} } : {},
|
|
39693
39911
|
...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
|
|
39694
39912
|
...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
|
|
39695
39913
|
...localCleanup?.localBranch ? { localBranch: localCleanup.localBranch } : {},
|
|
@@ -40599,8 +40817,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40599
40817
|
registerSchedulesCommands(program3);
|
|
40600
40818
|
registerSchedulesLiftCommand(program3);
|
|
40601
40819
|
const docs = program3.command("docs").description("generated docs surfaces \u2014 the routing index (org knowledge layer)");
|
|
40602
|
-
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) => {
|
|
40603
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
|
+
}
|
|
40604
40826
|
const root = await repoRoot();
|
|
40605
40827
|
const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
|
|
40606
40828
|
if (o.check) {
|
|
@@ -40615,8 +40837,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40615
40837
|
await failGraceful(e.message);
|
|
40616
40838
|
}
|
|
40617
40839
|
});
|
|
40618
|
-
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 (
|
|
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) => {
|
|
40619
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
|
+
}
|
|
40620
40846
|
const root = await repoRoot();
|
|
40621
40847
|
const commandPaths = new Set(
|
|
40622
40848
|
buildCommandManifest(program3).index.map((entry) => entry.path)
|
|
@@ -40642,8 +40868,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40642
40868
|
}
|
|
40643
40869
|
});
|
|
40644
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");
|
|
40645
|
-
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) => {
|
|
40646
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
|
+
}
|
|
40647
40877
|
const root = await repoRoot();
|
|
40648
40878
|
const result = runSpawnPolicy(root);
|
|
40649
40879
|
if (o.json) {
|
|
@@ -40665,8 +40895,13 @@ function registerDeveloperCommands(program3) {
|
|
|
40665
40895
|
}
|
|
40666
40896
|
});
|
|
40667
40897
|
const tests = program3.command("tests").description("a repo's test-policy.json \u2014 the opt-in test contract and its enforcement");
|
|
40668
|
-
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) => {
|
|
40669
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
|
+
}
|
|
40670
40905
|
const root = await repoRoot();
|
|
40671
40906
|
const result = runTestPolicy(root, { base: o.base, policyRef: o.policyRef });
|
|
40672
40907
|
if (o.json) {
|
|
@@ -40696,8 +40931,12 @@ function registerDeveloperCommands(program3) {
|
|
|
40696
40931
|
}
|
|
40697
40932
|
});
|
|
40698
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");
|
|
40699
|
-
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) => {
|
|
40700
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
|
+
}
|
|
40701
40940
|
const root = await repoRoot();
|
|
40702
40941
|
const receipt = runDistStatus(root);
|
|
40703
40942
|
if (o.json) {
|
|
@@ -40992,7 +41231,7 @@ async function resolveHotfixDeployModel(deps, ctx) {
|
|
|
40992
41231
|
}
|
|
40993
41232
|
async function hotfixPreflight(deps, ctx, verb, targetTag) {
|
|
40994
41233
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
40995
|
-
const deployModel = await preflight(deps, ctx, "main", meta);
|
|
41234
|
+
const deployModel = await preflight(deps, ctx, "main", meta, "hotfix");
|
|
40996
41235
|
const root = await hotfixCheckoutRoot(deps);
|
|
40997
41236
|
const begin = await beginReleaseLedger(deps, root, targetTag);
|
|
40998
41237
|
if (!begin.ok) throw new Error(`hotfix ${verb} refused before any mutation: ${begin.error}`);
|
|
@@ -41351,6 +41590,16 @@ function hotfixDispatchFromRuns(runs, note) {
|
|
|
41351
41590
|
const tenantDeploy2 = runs.find((r) => r.workflow === "tenant-deploy.yml");
|
|
41352
41591
|
return { note, deployStatus, workflowRuns, ...tenantDeploy2?.runId != null ? { runId: tenantDeploy2.runId } : {}, ...tenantDeploy2?.url ? { runUrl: tenantDeploy2.url } : {} };
|
|
41353
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
|
+
}
|
|
41354
41603
|
function hotfixPhaseInputsFromRuns(deployModel, runs, note, publishDispatch, publishRequired, opts) {
|
|
41355
41604
|
const dispatch = hotfixDispatchFromRuns(runs, note);
|
|
41356
41605
|
return {
|
|
@@ -41594,6 +41843,7 @@ ${decision.note}`);
|
|
|
41594
41843
|
} else {
|
|
41595
41844
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
41596
41845
|
}
|
|
41846
|
+
deployNote = await appendHotfixGatewayRun(deps, ctx.repo, tag, mergedSha, runs, deployNote);
|
|
41597
41847
|
const ledgerInputs = hotfixPhaseInputsFromRuns(
|
|
41598
41848
|
deployModel,
|
|
41599
41849
|
runs,
|
|
@@ -41786,6 +42036,9 @@ function hotfixStatusRunsFromLedger(ledger) {
|
|
|
41786
42036
|
workflow: record.workflow ?? phase,
|
|
41787
42037
|
...record.runId != null ? { runId: record.runId } : {},
|
|
41788
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 } : {},
|
|
41789
42042
|
conclusion: record.state === "complete" ? "success" : record.state === "failed" ? "failure" : "pending"
|
|
41790
42043
|
}];
|
|
41791
42044
|
});
|
|
@@ -42243,8 +42496,8 @@ function trainApplyDeps() {
|
|
|
42243
42496
|
};
|
|
42244
42497
|
}
|
|
42245
42498
|
function formatWorkflowRun(r) {
|
|
42246
|
-
const
|
|
42247
|
-
return `${
|
|
42499
|
+
const evidenced = workflowRunWithEvidence(r);
|
|
42500
|
+
return `${evidenced.workflow} ${evidenced.runUrl ?? evidenced.runUrlNote} ${evidenced.conclusion.toUpperCase()}`;
|
|
42248
42501
|
}
|
|
42249
42502
|
function renderDeployLine(d) {
|
|
42250
42503
|
const parts = [d.dispatch];
|
|
@@ -42266,6 +42519,7 @@ function renderReleaseResume(r) {
|
|
|
42266
42519
|
if (r.devRollForward) lines2.push(` development: ${r.devRollForward.note}`);
|
|
42267
42520
|
if (r.rcAlignment) lines2.push(` rc: ${r.rcAlignment.note}`);
|
|
42268
42521
|
if (r.checkout) lines2.push(` checkout: ${r.checkout.note}`);
|
|
42522
|
+
if (r.projectInfoSync) lines2.push(` project info: ${r.projectInfoSync.note}`);
|
|
42269
42523
|
if (r.ledger) lines2.push(...formatReleaseLedgerReport(r.ledger).map((l, i) => i === 0 ? ` ${l}` : ` ${l}`));
|
|
42270
42524
|
return lines2.join("\n");
|
|
42271
42525
|
}
|
|
@@ -42293,7 +42547,8 @@ function releaseFollowUpLegs(result, projectInfoSync) {
|
|
|
42293
42547
|
const legs = [
|
|
42294
42548
|
{
|
|
42295
42549
|
leg: "project-info",
|
|
42296
|
-
|
|
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",
|
|
42297
42552
|
...projectInfoSync && "error" in projectInfoSync ? { error: projectInfoSync.error } : {}
|
|
42298
42553
|
}
|
|
42299
42554
|
];
|
|
@@ -42332,6 +42587,24 @@ function releaseFollowUpLegs(result, projectInfoSync) {
|
|
|
42332
42587
|
}
|
|
42333
42588
|
return legs;
|
|
42334
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
|
+
}
|
|
42335
42608
|
var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
|
|
42336
42609
|
async function runPostReleaseJervDoctor(repo) {
|
|
42337
42610
|
if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
|
|
@@ -42347,10 +42620,35 @@ async function runPostReleaseJervDoctor(repo) {
|
|
|
42347
42620
|
};
|
|
42348
42621
|
}
|
|
42349
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
|
+
}
|
|
42350
42648
|
function buildReleaseVerdict(commandName, result, projectInfoSync) {
|
|
42351
42649
|
const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
|
|
42352
42650
|
const followUpStatus = deriveTrainFollowUpStatus({
|
|
42353
|
-
projectInfo: projectInfoSync
|
|
42651
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42354
42652
|
deploy: deployFollowUpOutcome(result.deployStatus),
|
|
42355
42653
|
rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
|
|
42356
42654
|
alignment: alignmentPending ? "unresolved" : "ok",
|
|
@@ -42517,7 +42815,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42517
42815
|
if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
|
|
42518
42816
|
try {
|
|
42519
42817
|
if (commandName === "rcand") {
|
|
42520
|
-
const
|
|
42818
|
+
const raw2 = await runRcandResume(trainApplyDeps(), { watch: o.watch });
|
|
42819
|
+
const result2 = raw2.workflowRuns ? { ...raw2, workflowRuns: raw2.workflowRuns.map(workflowRunWithEvidence) } : raw2;
|
|
42521
42820
|
emitTrainResult("rcand --resume", o.json ? JSON.stringify(result2, null, 2) : renderRcandResume(result2), o.out);
|
|
42522
42821
|
applyTrainFollowUpExit(deriveTrainFollowUpStatus({
|
|
42523
42822
|
projectInfo: "ok",
|
|
@@ -42528,9 +42827,19 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42528
42827
|
}));
|
|
42529
42828
|
return;
|
|
42530
42829
|
}
|
|
42531
|
-
const
|
|
42532
|
-
|
|
42533
|
-
|
|
42830
|
+
const raw = await runReleaseResume(trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile });
|
|
42831
|
+
const result = raw.dispatch?.workflowRuns ? { ...raw, dispatch: { ...raw.dispatch, workflowRuns: raw.dispatch.workflowRuns.map(workflowRunWithEvidence) } } : raw;
|
|
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
|
+
}));
|
|
42534
42843
|
return;
|
|
42535
42844
|
} catch (e) {
|
|
42536
42845
|
applyTrainFollowUpExit("failed");
|
|
@@ -42563,16 +42872,12 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42563
42872
|
if (o.apply) {
|
|
42564
42873
|
try {
|
|
42565
42874
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
42566
|
-
const
|
|
42875
|
+
const raw = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
|
|
42876
|
+
const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
|
|
42567
42877
|
let projectInfoSync;
|
|
42568
42878
|
const postReleaseJervDoctor = commandName === "release" ? await runPostReleaseJervDoctor(result.repo) : void 0;
|
|
42569
42879
|
if (commandName === "release") {
|
|
42570
|
-
|
|
42571
|
-
projectInfoSync = await runProjectInfoSyncCallback(result.repo, true);
|
|
42572
|
-
} catch (e) {
|
|
42573
|
-
const error = e.message;
|
|
42574
|
-
projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
42575
|
-
}
|
|
42880
|
+
projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42576
42881
|
}
|
|
42577
42882
|
const { followUpStatus, releaseVerdict } = buildReleaseVerdict(commandName, result, projectInfoSync);
|
|
42578
42883
|
const reported = {
|
|
@@ -42625,6 +42930,13 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42625
42930
|
function renderHotfixStart(r) {
|
|
42626
42931
|
return [`mmi-cli devops hotfix start: ${r.tag} (${r.branch}, from ${r.source})${r.reused ? " [reused]" : ""}`, ...r.notes.map((n) => ` - ${n}`)].join("\n");
|
|
42627
42932
|
}
|
|
42933
|
+
function hotfixRunWithEvidence(run) {
|
|
42934
|
+
return run.url || run.runUrlNote ? run : { ...run, runUrlNote: missingRunUrlNote(run.runId) };
|
|
42935
|
+
}
|
|
42936
|
+
function formatHotfixRun(run) {
|
|
42937
|
+
const evidenced = hotfixRunWithEvidence(run);
|
|
42938
|
+
return ` - ${evidenced.workflow}: ${evidenced.conclusion}${evidenced.conclusion === "failure" ? " \u2014 error: workflow reported failure" : ""} (${evidenced.url ?? evidenced.runUrlNote})`;
|
|
42939
|
+
}
|
|
42628
42940
|
function renderHotfixRelease(r) {
|
|
42629
42941
|
return [
|
|
42630
42942
|
`mmi-cli devops hotfix release: ${r.tag} at ${r.mergedSha.slice(0, 7)} on ${r.repo} (deployModel=${r.deployModel})`,
|
|
@@ -42632,7 +42944,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42632
42944
|
` - ${r.tagNote}`,
|
|
42633
42945
|
` - ${r.releaseNote}`,
|
|
42634
42946
|
` - deploy: ${r.deployNote}`,
|
|
42635
|
-
...r.runs.map(
|
|
42947
|
+
...r.runs.map(formatHotfixRun),
|
|
42636
42948
|
` - ${r.verifyNote}`,
|
|
42637
42949
|
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
42638
42950
|
` - fold: ${r.foldNote}`,
|
|
@@ -42649,7 +42961,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42649
42961
|
return [
|
|
42650
42962
|
`mmi-cli devops hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
|
|
42651
42963
|
` - branch: ${r.branchExists ? "pushed" : "absent"} \u2014 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \u2014 tag: ${r.tagPushed ? "pushed" : "absent"} \u2014 Release: ${r.releaseExists ? "exists" : "absent"}`,
|
|
42652
|
-
...r.runs.map(
|
|
42964
|
+
...r.runs.map(formatHotfixRun),
|
|
42653
42965
|
` - development fold alignment: ${r.alignmentStatus}${r.alignmentNote ? ` \u2014 ${r.alignmentNote}` : ""}`,
|
|
42654
42966
|
` - npm @mutmutco/cli: ${r.npmVersion}`,
|
|
42655
42967
|
` - next: ${r.next}`,
|
|
@@ -42662,36 +42974,23 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42662
42974
|
if (conclusion === "failure") return "failure";
|
|
42663
42975
|
return "unresolved";
|
|
42664
42976
|
}
|
|
42665
|
-
function hotfixFollowUpLegs(runs, foldPort, alignment, foldNote) {
|
|
42666
|
-
const legs = runs.map((run) => ({
|
|
42667
|
-
leg: run.workflow,
|
|
42668
|
-
status: followUpLegStatus(run.conclusion === "failure" ? "failure" : run.conclusion === "success" ? "success" : "pending"),
|
|
42669
|
-
...run.conclusion === "failure" ? { error: "workflow reported failure" } : {}
|
|
42670
|
-
}));
|
|
42671
|
-
if (foldPort === "failure") {
|
|
42672
|
-
legs.push({ leg: "development-fold-port", status: "failed", error: foldNote ?? "development fold port failed" });
|
|
42673
|
-
}
|
|
42674
|
-
if (alignment === "unresolved") {
|
|
42675
|
-
legs.push({ leg: "development-fold-alignment", status: "pending" });
|
|
42676
|
-
} else if (alignment === "failure" && foldPort !== "failure") {
|
|
42677
|
-
legs.push({ leg: "development-fold-alignment", status: "failed", error: foldNote ?? "development fold alignment failed" });
|
|
42678
|
-
}
|
|
42679
|
-
return legs;
|
|
42680
|
-
}
|
|
42681
42977
|
async function runHotfixSub(sub, body, o, render) {
|
|
42682
42978
|
try {
|
|
42683
42979
|
await requireFreshTrainCli("hotfix");
|
|
42684
|
-
const
|
|
42980
|
+
const raw = await body();
|
|
42981
|
+
const rawRuns = raw.runs;
|
|
42982
|
+
const result = rawRuns ? Object.assign({}, raw, { runs: rawRuns.map(hotfixRunWithEvidence) }) : raw;
|
|
42685
42983
|
const runs = result.runs;
|
|
42686
42984
|
const foldPort = result.foldStatus ?? "ok";
|
|
42687
42985
|
const ledger = result.ledger;
|
|
42688
42986
|
const alignment = result.alignmentStatus ?? ledgerAlignmentFollowUpOutcome(ledger);
|
|
42689
42987
|
const projectInfoSync = result.projectInfoSync;
|
|
42690
|
-
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;
|
|
42691
42990
|
const json = o.json || Boolean(hotfixCmd.opts().json);
|
|
42692
42991
|
emitTrainResult(`hotfix ${sub}`, json ? JSON.stringify(legs ? Object.assign({}, result, { legs }) : result, null, 2) : render(result), o.out);
|
|
42693
42992
|
if (runs) applyTrainFollowUpExit(deriveTrainFollowUpStatus({
|
|
42694
|
-
projectInfo: projectInfoSync
|
|
42993
|
+
projectInfo: projectInfoOutcome(projectInfoSync),
|
|
42695
42994
|
deploy: reduceFollowUpOutcomes(runs.map((r) => hotfixRunOutcome(r.conclusion))),
|
|
42696
42995
|
rcRetirement: "ok",
|
|
42697
42996
|
alignment,
|
|
@@ -42715,13 +43014,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
42715
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 () => {
|
|
42716
43015
|
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] });
|
|
42717
43016
|
const postReleaseJervDoctor = await runPostReleaseJervDoctor(result.repo);
|
|
42718
|
-
|
|
42719
|
-
try {
|
|
42720
|
-
projectInfoSync = await runProjectInfoSyncCallback(result.repo, true);
|
|
42721
|
-
} catch (e) {
|
|
42722
|
-
const error = e.message;
|
|
42723
|
-
projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
42724
|
-
}
|
|
43017
|
+
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
42725
43018
|
return { ...result, projectInfoSync, ...postReleaseJervDoctor ? { postReleaseJervDoctor } : {} };
|
|
42726
43019
|
}, o, renderHotfixRelease));
|
|
42727
43020
|
function hotfixStatusDeps() {
|
|
@@ -43302,7 +43595,7 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
|
|
|
43302
43595
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
43303
43596
|
}
|
|
43304
43597
|
});
|
|
43305
|
-
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) => {
|
|
43306
43599
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
43307
43600
|
const cfg = await loadConfig();
|
|
43308
43601
|
const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
@@ -43312,7 +43605,8 @@ tenant.command("status <owner/repo> <stage>").description("read tenant runtime r
|
|
|
43312
43605
|
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev \u2014 the promoted/staging ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
43313
43606
|
if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime tenant redeploy: <stage> must be dev, rc, or main");
|
|
43314
43607
|
try {
|
|
43315
|
-
const
|
|
43608
|
+
const raw = await runTenantRedeploy(trainApplyDeps(), { repo, stage, ref: o.ref, watch: o.watch });
|
|
43609
|
+
const result = raw.workflowRuns ? { ...raw, workflowRuns: raw.workflowRuns.map(workflowRunWithEvidence) } : raw;
|
|
43316
43610
|
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantRedeploy(result));
|
|
43317
43611
|
} catch (e) {
|
|
43318
43612
|
return failGraceful(`runtime tenant redeploy: ${e.message}`);
|
|
@@ -43343,26 +43637,13 @@ tenant.command("sweep-rc").description("discover (and optionally retire) running
|
|
|
43343
43637
|
return failGraceful(`runtime tenant sweep-rc: ${e.message}`);
|
|
43344
43638
|
}
|
|
43345
43639
|
});
|
|
43346
|
-
async function probeHttpBounded2(url, timeoutMs = 5e3) {
|
|
43347
|
-
const controller = new AbortController();
|
|
43348
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
43349
|
-
timeout.unref?.();
|
|
43350
|
-
try {
|
|
43351
|
-
const res = await fetch(url, { method: "GET", signal: controller.signal });
|
|
43352
|
-
return { ok: res.ok, status: res.status };
|
|
43353
|
-
} catch (e) {
|
|
43354
|
-
return { ok: false, error: e.message };
|
|
43355
|
-
} finally {
|
|
43356
|
-
clearTimeout(timeout);
|
|
43357
|
-
}
|
|
43358
|
-
}
|
|
43359
43640
|
async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
43360
43641
|
const slug = slugOf(target);
|
|
43361
43642
|
const reg = registryClientDeps(cfg);
|
|
43362
43643
|
const facts = await fetchDeployFactsBySlug(slug, reg);
|
|
43363
43644
|
const deploy = facts?.stages[stage] ?? null;
|
|
43364
43645
|
const publicUrl = publicUrlFromDeployFact(deploy);
|
|
43365
|
-
const publicProbe = publicUrl ? await
|
|
43646
|
+
const publicProbe = publicUrl ? await probePublicHealth(publicUrl) : void 0;
|
|
43366
43647
|
return buildTenantRuntimeStatus({
|
|
43367
43648
|
repo: target,
|
|
43368
43649
|
slug,
|
|
@@ -43390,9 +43671,9 @@ async function runProjectInfoSync(target, apply) {
|
|
|
43390
43671
|
fetchProjectBySlugChecked(slugOf(targetRepo2), registry2),
|
|
43391
43672
|
fetchProjectsList(registry2)
|
|
43392
43673
|
]);
|
|
43393
|
-
if (!read.ok) throw new
|
|
43674
|
+
if (!read.ok) throw new ProjectInfoReadUnavailableError(`org project sync-info: Hub registry read failed (${read.error})`);
|
|
43394
43675
|
if (!read.project) throw new Error(`org project sync-info: no registry META for ${targetRepo2}`);
|
|
43395
|
-
if (!projects) throw new
|
|
43676
|
+
if (!projects) throw new ProjectInfoReadUnavailableError("org project sync-info: Hub project list unavailable");
|
|
43396
43677
|
if (apply) {
|
|
43397
43678
|
const authority = await fetchTrainAuthority(targetRepo2, registry2);
|
|
43398
43679
|
if (!authority.ok) throw new Error(`org project sync-info: train authority unverified (${authority.error})`);
|