@mutmutco/cli 4.3.26 → 4.3.27
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 +99 -29
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -10939,9 +10939,15 @@ function boardNotFoundError(ref, board, opts = {}) {
|
|
|
10939
10939
|
function evaluateClaim(item, login) {
|
|
10940
10940
|
const others = item.assignees.filter((a) => a.toLowerCase() !== login.toLowerCase());
|
|
10941
10941
|
const mine = item.assignees.some((a) => a.toLowerCase() === login.toLowerCase());
|
|
10942
|
+
if (others.length && item.status === "In Review") {
|
|
10943
|
+
return {
|
|
10944
|
+
ok: false,
|
|
10945
|
+
reason: `${item.ref} is not claimable: In Review and held by @${others.join(", @")} \u2014 ask the holder, or wait for the review to land`
|
|
10946
|
+
};
|
|
10947
|
+
}
|
|
10942
10948
|
if (others.length) return { ok: false, reason: `${item.ref} is already assigned to @${others.join(", @")}` };
|
|
10943
10949
|
if (item.status === "In Progress" && mine) return { ok: true, alreadyClaimed: true };
|
|
10944
|
-
if (item.status !== "Todo" && item.status !== "In Progress") {
|
|
10950
|
+
if (item.status !== "Todo" && item.status !== "In Progress" && item.status !== "In Review") {
|
|
10945
10951
|
return { ok: false, reason: `${item.ref} is not claimable: Status is ${item.status}` };
|
|
10946
10952
|
}
|
|
10947
10953
|
return { ok: true, alreadyClaimed: false };
|
|
@@ -11064,7 +11070,7 @@ async function repoCanPush(repo, client) {
|
|
|
11064
11070
|
}
|
|
11065
11071
|
}
|
|
11066
11072
|
async function resolveWritableReposForClaimables(items, client) {
|
|
11067
|
-
const candidateRepos = [...new Set(items.filter((item) => (item.status === "Todo" || item.status === "In Progress") && item.assignees.length === 0).map((item) => item.repository))];
|
|
11073
|
+
const candidateRepos = [...new Set(items.filter((item) => (item.status === "Todo" || item.status === "In Progress" || item.status === "In Review") && item.assignees.length === 0).map((item) => item.repository))];
|
|
11068
11074
|
const repos = /* @__PURE__ */ new Set();
|
|
11069
11075
|
const unknown = /* @__PURE__ */ new Set();
|
|
11070
11076
|
const warnings = [];
|
|
@@ -12177,7 +12183,7 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
|
|
|
12177
12183
|
async function claimOneBoardItem(ctx, selector, options) {
|
|
12178
12184
|
const { cfg, client, report } = ctx;
|
|
12179
12185
|
const flatItem = findBoardItem(ctx.items, selector, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
12180
|
-
const wouldWrite = flatItem.assignees.length === 0 && (flatItem.status === "Todo" || flatItem.status === "In Progress");
|
|
12186
|
+
const wouldWrite = flatItem.assignees.length === 0 && (flatItem.status === "Todo" || flatItem.status === "In Progress" || flatItem.status === "In Review");
|
|
12181
12187
|
if (wouldWrite && !ctx.writable.has(flatItem.repository.toLowerCase())) {
|
|
12182
12188
|
throw new Error(
|
|
12183
12189
|
`${flatItem.ref} is not claimable: the token from ${describeTokenSource()} reports no write access to ${flatItem.repository}`
|
|
@@ -12216,7 +12222,10 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12216
12222
|
};
|
|
12217
12223
|
let previousHolder;
|
|
12218
12224
|
let resumeEvidence;
|
|
12219
|
-
const claimedReceipt = () =>
|
|
12225
|
+
const claimedReceipt = () => ({
|
|
12226
|
+
...item.status === "In Review" ? { reclaimedFrom: "In Review" } : {},
|
|
12227
|
+
...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder }
|
|
12228
|
+
});
|
|
12220
12229
|
const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
|
|
12221
12230
|
if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
|
|
12222
12231
|
const pre = evaluateClaim(flatItem, assignedLogin);
|
|
@@ -12274,7 +12283,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12274
12283
|
} catch (e) {
|
|
12275
12284
|
const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
|
|
12276
12285
|
if (!options.allowPartial) throw new Error(warning);
|
|
12277
|
-
return { item, viewer: report.viewer, repo: report.repo, status:
|
|
12286
|
+
return { item, viewer: report.viewer, repo: report.repo, status: item.status, partial: true, warning, ...claimedReceipt() };
|
|
12278
12287
|
}
|
|
12279
12288
|
return {
|
|
12280
12289
|
item: {
|
|
@@ -12320,7 +12329,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12320
12329
|
const ref = `${selector.repo}#${selector.number}`;
|
|
12321
12330
|
try {
|
|
12322
12331
|
const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
|
|
12323
|
-
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12332
|
+
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12324
12333
|
} catch (e) {
|
|
12325
12334
|
results[index] = { ref, claimed: false, reason: e.message };
|
|
12326
12335
|
}
|
|
@@ -15526,10 +15535,10 @@ var rollout_plan_default = {
|
|
|
15526
15535
|
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)."
|
|
15527
15536
|
},
|
|
15528
15537
|
baseline: {
|
|
15529
|
-
version: "4.3.
|
|
15530
|
-
tag: "v4.3.
|
|
15531
|
-
commit: "
|
|
15532
|
-
npm: "@mutmutco/cli@4.3.
|
|
15538
|
+
version: "4.3.27",
|
|
15539
|
+
tag: "v4.3.27",
|
|
15540
|
+
commit: "924b5a7761c4",
|
|
15541
|
+
npm: "@mutmutco/cli@4.3.27"
|
|
15533
15542
|
},
|
|
15534
15543
|
exitCriterion: "fleet-n-of-n",
|
|
15535
15544
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15546,14 +15555,14 @@ var rollout_plan_default = {
|
|
|
15546
15555
|
repo: "mutmutco/mmi-hub",
|
|
15547
15556
|
role: "canary",
|
|
15548
15557
|
schedule: "train",
|
|
15549
|
-
v3Target: "v4.3.
|
|
15558
|
+
v3Target: "v4.3.27"
|
|
15550
15559
|
}
|
|
15551
15560
|
],
|
|
15552
15561
|
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.",
|
|
15553
15562
|
rollback: {
|
|
15554
15563
|
independent: true,
|
|
15555
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15556
|
-
v3Target: "v4.3.
|
|
15564
|
+
mechanism: "npm dist-tag latest -> 4.3.27 and redeploy the Hub Lambda from tag v4.3.27 (924b5a7761c4); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15565
|
+
v3Target: "v4.3.27 (@mutmutco/cli@4.3.27, tag commit 924b5a7761c4 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15557
15566
|
}
|
|
15558
15567
|
},
|
|
15559
15568
|
{
|
|
@@ -25575,11 +25584,12 @@ function registerBoardCommands(program3) {
|
|
|
25575
25584
|
function claimVerdict(ref, result) {
|
|
25576
25585
|
const holder = formatClaimHolder(result.holder);
|
|
25577
25586
|
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
25587
|
+
const reclaimed = result.reclaimedFrom ? ` (reclaimed from ${result.reclaimedFrom})` : "";
|
|
25578
25588
|
if (result.checked) {
|
|
25579
25589
|
if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
|
|
25580
25590
|
if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
|
|
25581
25591
|
if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
|
|
25582
|
-
return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
|
|
25592
|
+
return `Check ${ref}: free - claim would proceed for ${holder}${reclaimed} (nothing written)`;
|
|
25583
25593
|
}
|
|
25584
25594
|
if (result.partial) {
|
|
25585
25595
|
if (result.outcome === "took-over") return `Partially took over ${ref} from ${previousHolder}: ${result.warning}`;
|
|
@@ -25589,12 +25599,12 @@ function registerBoardCommands(program3) {
|
|
|
25589
25599
|
if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
|
|
25590
25600
|
if (result.outcome === "resumed") return `Resumed ${ref} from ${previousHolder} for ${holder} - In Progress (${result.resumeEvidence})`;
|
|
25591
25601
|
if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
|
|
25592
|
-
return `Claimed ${ref} for ${holder} - In Progress`;
|
|
25602
|
+
return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
|
|
25593
25603
|
}
|
|
25594
25604
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
25595
25605
|
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));
|
|
25596
25606
|
withExamples(mutating(
|
|
25597
|
-
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"),
|
|
25607
|
+
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\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\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"),
|
|
25598
25608
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
25599
25609
|
).action(async (issueRefs, o) => {
|
|
25600
25610
|
if (issueRefs.length === 1) {
|
|
@@ -29994,6 +30004,19 @@ function stageExtraEnv(config, stagePort) {
|
|
|
29994
30004
|
function stageProcessEnv(stagePort, extraEnv) {
|
|
29995
30005
|
return { ...stagePort != null ? { STAGE_PORT: String(stagePort) } : {}, ...extraEnv };
|
|
29996
30006
|
}
|
|
30007
|
+
function composeResolvesPort(cwd) {
|
|
30008
|
+
if (process.env.PORT) return true;
|
|
30009
|
+
const envFile = (0, import_node_path27.join)(cwd, ".env");
|
|
30010
|
+
return (0, import_node_fs28.existsSync)(envFile) && envFileKeys((0, import_node_fs28.readFileSync)(envFile, "utf8")).has("PORT");
|
|
30011
|
+
}
|
|
30012
|
+
function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
|
|
30013
|
+
return {
|
|
30014
|
+
// Vault secrets first so the stage-selection contract (MMI_STAGE/MMI_PORT/…) always wins on any collision.
|
|
30015
|
+
...vaultEnvMerge ?? {},
|
|
30016
|
+
...stageProcessEnv(stagePort, stageExtraEnv(config, stagePort)),
|
|
30017
|
+
...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {}
|
|
30018
|
+
};
|
|
30019
|
+
}
|
|
29997
30020
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
29998
30021
|
if (!config.ensureEnv) return;
|
|
29999
30022
|
const target = (0, import_node_path27.join)(cwd, config.ensureEnv.target);
|
|
@@ -30069,10 +30092,19 @@ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
|
30069
30092
|
writeState(statePath, reservation);
|
|
30070
30093
|
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, reservation);
|
|
30071
30094
|
}
|
|
30072
|
-
|
|
30095
|
+
function teardownEnv(state, currentEnv) {
|
|
30096
|
+
const env = { ...state.teardown?.env ?? {}, ...currentEnv ?? {} };
|
|
30097
|
+
return Object.keys(env).length ? env : void 0;
|
|
30098
|
+
}
|
|
30099
|
+
async function cleanupStageState(state, paths, timeoutMs, fallbackCwd, currentEnv) {
|
|
30073
30100
|
await killTree(state.pid);
|
|
30074
30101
|
if (state.teardown?.command.trim()) {
|
|
30075
|
-
await shell(
|
|
30102
|
+
await shell(
|
|
30103
|
+
state.teardown.command.trim(),
|
|
30104
|
+
state.teardown.cwd || state.cwd || fallbackCwd,
|
|
30105
|
+
Math.max(timeoutMs, 1e4),
|
|
30106
|
+
teardownEnv(state, currentEnv)
|
|
30107
|
+
);
|
|
30076
30108
|
}
|
|
30077
30109
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
30078
30110
|
(0, import_node_fs28.rmSync)(path2, { force: true });
|
|
@@ -30130,13 +30162,20 @@ async function stopStage(opts = {}) {
|
|
|
30130
30162
|
}
|
|
30131
30163
|
const usingGlobalState = state === globalState;
|
|
30132
30164
|
const recordedStatePath = state.statePath ?? statePath;
|
|
30133
|
-
|
|
30165
|
+
const recordedTeardownEnv = Boolean(state.teardown?.env);
|
|
30166
|
+
await cleanupStageState(
|
|
30167
|
+
state,
|
|
30168
|
+
[statePath, recordedStatePath, usingGlobalState || !opts.requiredIdentityCwd ? globalStatePath : void 0],
|
|
30169
|
+
opts.timeoutMs ?? 6e4,
|
|
30170
|
+
cwd,
|
|
30171
|
+
opts.vaultEnvMerge
|
|
30172
|
+
);
|
|
30134
30173
|
return {
|
|
30135
30174
|
ok: true,
|
|
30136
30175
|
action: "stop",
|
|
30137
30176
|
statePath: recordedStatePath,
|
|
30138
30177
|
pid: state.pid,
|
|
30139
|
-
message: `stopped previous stage pid ${state.pid}
|
|
30178
|
+
message: `stopped previous stage pid ${state.pid}` + (state.teardown?.command.trim() ? ` and ran teardown${recordedTeardownEnv ? "" : " (no recorded teardown env \u2014 used the current environment)"}` : "")
|
|
30140
30179
|
};
|
|
30141
30180
|
}
|
|
30142
30181
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -30156,8 +30195,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30156
30195
|
const sub = (s) => substituteStagePort(s, stagePort);
|
|
30157
30196
|
if (!opts.envPrepared) await ensureStageRuntimeEnv(config, opts, cwd);
|
|
30158
30197
|
if (stagePort != null && portGuard) await ensureStagePortAvailable(stagePort, cwd, portGuard);
|
|
30159
|
-
const
|
|
30160
|
-
const vaultProcessEnv = opts.vaultEnvMerge ?? {};
|
|
30198
|
+
const composeEnv = stageComposeEnv(config, stagePort, opts.vaultEnvMerge, cwd);
|
|
30161
30199
|
let up = sub(config.up.trim());
|
|
30162
30200
|
if (opts.forceRecreate) up = appendForceRecreate(up);
|
|
30163
30201
|
const identity = await resolveStageIdentity(cwd);
|
|
@@ -30170,8 +30208,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30170
30208
|
detached: process.platform !== "win32",
|
|
30171
30209
|
windowsHide: true,
|
|
30172
30210
|
stdio: "ignore",
|
|
30173
|
-
|
|
30174
|
-
env: { ...process.env, ...vaultProcessEnv, ...stageProcessEnv(stagePort, extraEnv) }
|
|
30211
|
+
env: { ...process.env, ...composeEnv }
|
|
30175
30212
|
});
|
|
30176
30213
|
const state = {
|
|
30177
30214
|
pid: child2.pid ?? 0,
|
|
@@ -30182,7 +30219,10 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30182
30219
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
30183
30220
|
port: stagePort,
|
|
30184
30221
|
identity,
|
|
30185
|
-
|
|
30222
|
+
// #6343: record the non-secret half of the interpolation env so a LATER `stage stop` can run
|
|
30223
|
+
// `docker compose down` against the same file. `vaultEnvMerge` is omitted deliberately — no secret
|
|
30224
|
+
// value ever reaches disk (#2655); the stopping invocation re-fetches them.
|
|
30225
|
+
teardown: config.teardown?.trim() ? { command: sub(config.teardown.trim()), cwd, env: stageComposeEnv(config, stagePort, void 0, cwd) } : void 0
|
|
30186
30226
|
};
|
|
30187
30227
|
writeState(statePath, state);
|
|
30188
30228
|
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, state);
|
|
@@ -30190,7 +30230,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30190
30230
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
30191
30231
|
else await waitForProcessStability(child2);
|
|
30192
30232
|
} catch (e) {
|
|
30193
|
-
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd);
|
|
30233
|
+
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd, composeEnv);
|
|
30194
30234
|
throw e;
|
|
30195
30235
|
}
|
|
30196
30236
|
const result = {
|
|
@@ -30221,7 +30261,12 @@ async function runStage(config = {}, opts = {}) {
|
|
|
30221
30261
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
30222
30262
|
const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
|
|
30223
30263
|
const portGuard = resolveStagePortGuard(opts);
|
|
30224
|
-
await stopStage({
|
|
30264
|
+
await stopStage({
|
|
30265
|
+
...opts,
|
|
30266
|
+
cwd,
|
|
30267
|
+
requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd,
|
|
30268
|
+
vaultEnvMerge: stageComposeEnv(config, opts.stagePort, opts.vaultEnvMerge, cwd)
|
|
30269
|
+
});
|
|
30225
30270
|
const reserved = await reservedPortsForWorktree(cwd);
|
|
30226
30271
|
let stagePort = opts.stagePort;
|
|
30227
30272
|
if (stagePort != null) {
|
|
@@ -30233,14 +30278,13 @@ async function runStage(config = {}, opts = {}) {
|
|
|
30233
30278
|
if (stagePort != null) {
|
|
30234
30279
|
writeStagePortReservation(stagePort, cwd, statePath, globalStatePath, opts.now ?? (() => /* @__PURE__ */ new Date()));
|
|
30235
30280
|
}
|
|
30236
|
-
const extraEnv = stageExtraEnv(config, stagePort);
|
|
30237
30281
|
const build = config.build?.trim();
|
|
30238
30282
|
const ranBuild = Boolean(build);
|
|
30239
30283
|
try {
|
|
30240
30284
|
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
30241
30285
|
if (build) {
|
|
30242
30286
|
await shell(sub(build), cwd, timeoutMs, {
|
|
30243
|
-
...
|
|
30287
|
+
...stageComposeEnv(config, stagePort, opts.vaultEnvMerge, cwd),
|
|
30244
30288
|
...opts.buildEnvMerge ?? {}
|
|
30245
30289
|
});
|
|
30246
30290
|
}
|
|
@@ -42461,6 +42505,30 @@ ${recovery.note}; the full required-check wall did not revalidate`);
|
|
|
42461
42505
|
}, `the immutable ${anchors.tag} pushed at ${mergedSha.slice(0, 12)}`);
|
|
42462
42506
|
return checks;
|
|
42463
42507
|
}
|
|
42508
|
+
async function assertConsumerBomLineage(deps, mergedSha, tag) {
|
|
42509
|
+
let bomText;
|
|
42510
|
+
try {
|
|
42511
|
+
bomText = await deps.run("git", ["show", `${mergedSha}:distribution-bom.json`]);
|
|
42512
|
+
} catch {
|
|
42513
|
+
return;
|
|
42514
|
+
}
|
|
42515
|
+
let stamp;
|
|
42516
|
+
try {
|
|
42517
|
+
stamp = JSON.parse(bomText).sourceCommit;
|
|
42518
|
+
} catch (e) {
|
|
42519
|
+
throw new Error(`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} is unparseable (${e.message}) \u2014 the updater would refuse this release; fix the BOM on development and rerun from a fresh port`);
|
|
42520
|
+
}
|
|
42521
|
+
if (typeof stamp !== "string" || !/^[0-9a-f]{40}$/.test(stamp)) {
|
|
42522
|
+
throw new Error(`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} carries no 40-hex sourceCommit \u2014 the updater would refuse this release`);
|
|
42523
|
+
}
|
|
42524
|
+
try {
|
|
42525
|
+
await deps.run("git", ["merge-base", "--is-ancestor", stamp, mergedSha]);
|
|
42526
|
+
} catch {
|
|
42527
|
+
throw new Error(
|
|
42528
|
+
`hotfix ${tag}: distribution-bom.json at ${mergedSha.slice(0, 12)} stamps sourceCommit ${stamp.slice(0, 12)}, which the merged main commit does not contain \u2014 every updater would reject ${tag} (gateCandidate ancestry). The fold was stamped on the hotfix branch and squash-merged away: make the consumer stamp a durable merge-base (Jerv-Hub#1084), land that on development, and port it with the fix \u2014 see docs/Guides/train-troubleshooting.md#hotfix-bom-lineage`
|
|
42529
|
+
);
|
|
42530
|
+
}
|
|
42531
|
+
}
|
|
42464
42532
|
async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTrainDoctor) {
|
|
42465
42533
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
42466
42534
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -42494,6 +42562,8 @@ async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTr
|
|
|
42494
42562
|
assertTagAddressableRequiredContexts(deps, required, ctx.repo);
|
|
42495
42563
|
if (deployModel === "hub-serverless") {
|
|
42496
42564
|
await deps.run("node", ["scripts/release-distribution.mjs", "assert-release-lineage", version, "--release-commit", mergedSha]);
|
|
42565
|
+
} else {
|
|
42566
|
+
await assertConsumerBomLineage(deps, mergedSha, tag);
|
|
42497
42567
|
}
|
|
42498
42568
|
const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
|
|
42499
42569
|
if (!releaseExists && isHubControlRepo(ctx.repo)) {
|
package/package.json
CHANGED