@mutmutco/cli 4.3.25 → 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 +122 -42
- 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
|
}
|
|
@@ -14074,7 +14083,7 @@ var GATE_RUNTIME_DEFAULTS = {
|
|
|
14074
14083
|
node: { cmd: DEFAULT_GATE_CMD, install: "npm ci" },
|
|
14075
14084
|
python: { cmd: "pytest", install: 'pip install -e ".[dev]"' }
|
|
14076
14085
|
};
|
|
14077
|
-
function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches) {
|
|
14086
|
+
function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches, requiredChecks) {
|
|
14078
14087
|
const rt = GATE_RUNTIME_DEFAULTS[runtime] ?? GATE_RUNTIME_DEFAULTS.node;
|
|
14079
14088
|
const runtimeVars = {
|
|
14080
14089
|
GATE_RUNTIME: runtime,
|
|
@@ -14094,12 +14103,14 @@ function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches
|
|
|
14094
14103
|
const trackBranches = track === "trunk" ? ["main"] : track === "direct" ? ["development", "main"] : ["development", "rc", "main"];
|
|
14095
14104
|
const rulesetBranches = requiredCheckBranches?.length ? [...requiredCheckBranches] : trackBranches;
|
|
14096
14105
|
const rulesetRefs = JSON.stringify(rulesetBranches.map((branch) => `refs/heads/${branch}`));
|
|
14106
|
+
const rulesetContexts = JSON.stringify((requiredChecks?.length ? requiredChecks : ["gate"]).map((context) => ({ context })));
|
|
14097
14107
|
if (track === "trunk") {
|
|
14098
14108
|
return {
|
|
14099
14109
|
...runtimeVars,
|
|
14100
14110
|
GATE_PUSH_BRANCHES_YAML: "[main]",
|
|
14101
14111
|
GATE_FULL_RUN_BRANCH: "main",
|
|
14102
|
-
GATE_RULESET_BRANCH_REFS_JSON: rulesetRefs
|
|
14112
|
+
GATE_RULESET_BRANCH_REFS_JSON: rulesetRefs,
|
|
14113
|
+
GATE_RULESET_CONTEXTS_JSON: rulesetContexts
|
|
14103
14114
|
};
|
|
14104
14115
|
}
|
|
14105
14116
|
if (track === "direct") {
|
|
@@ -14107,17 +14118,19 @@ function gateSeedVars(cls, releaseTrack, runtime = "node", requiredCheckBranches
|
|
|
14107
14118
|
...runtimeVars,
|
|
14108
14119
|
GATE_PUSH_BRANCHES_YAML: "[development, main]",
|
|
14109
14120
|
GATE_FULL_RUN_BRANCH: "development",
|
|
14110
|
-
GATE_RULESET_BRANCH_REFS_JSON: rulesetRefs
|
|
14121
|
+
GATE_RULESET_BRANCH_REFS_JSON: rulesetRefs,
|
|
14122
|
+
GATE_RULESET_CONTEXTS_JSON: rulesetContexts
|
|
14111
14123
|
};
|
|
14112
14124
|
}
|
|
14113
14125
|
return {
|
|
14114
14126
|
...runtimeVars,
|
|
14115
14127
|
GATE_PUSH_BRANCHES_YAML: "[development, rc, main]",
|
|
14116
14128
|
GATE_FULL_RUN_BRANCH: "development",
|
|
14117
|
-
GATE_RULESET_BRANCH_REFS_JSON: rulesetRefs
|
|
14129
|
+
GATE_RULESET_BRANCH_REFS_JSON: rulesetRefs,
|
|
14130
|
+
GATE_RULESET_CONTEXTS_JSON: rulesetContexts
|
|
14118
14131
|
};
|
|
14119
14132
|
}
|
|
14120
|
-
function withDerivedRepoVars(vars, parsed, cls, releaseTrack, requiredCheckBranches) {
|
|
14133
|
+
function withDerivedRepoVars(vars, parsed, cls, releaseTrack, requiredCheckBranches, requiredChecks) {
|
|
14121
14134
|
const out = { ...vars };
|
|
14122
14135
|
out.REPO_NAME ??= parsed.name;
|
|
14123
14136
|
out.REPO_SLUG ??= parsed.slug;
|
|
@@ -14125,7 +14138,7 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack, requiredCheckBranc
|
|
|
14125
14138
|
out.PROJECT_OWNER ??= parsed.owner;
|
|
14126
14139
|
const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
|
|
14127
14140
|
const runtime = out.GATE_RUNTIME === "python" ? "python" : "node";
|
|
14128
|
-
for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime, requiredCheckBranches))) {
|
|
14141
|
+
for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime, requiredCheckBranches, requiredChecks))) {
|
|
14129
14142
|
out[key] ??= value;
|
|
14130
14143
|
}
|
|
14131
14144
|
if (!out.GATE_AFFECTED_CMD.trim()) out.GATE_AFFECTED_CMD = out.GATE_CMD;
|
|
@@ -15522,10 +15535,10 @@ var rollout_plan_default = {
|
|
|
15522
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)."
|
|
15523
15536
|
},
|
|
15524
15537
|
baseline: {
|
|
15525
|
-
version: "4.3.
|
|
15526
|
-
tag: "v4.3.
|
|
15527
|
-
commit: "
|
|
15528
|
-
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"
|
|
15529
15542
|
},
|
|
15530
15543
|
exitCriterion: "fleet-n-of-n",
|
|
15531
15544
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15542,14 +15555,14 @@ var rollout_plan_default = {
|
|
|
15542
15555
|
repo: "mutmutco/mmi-hub",
|
|
15543
15556
|
role: "canary",
|
|
15544
15557
|
schedule: "train",
|
|
15545
|
-
v3Target: "v4.3.
|
|
15558
|
+
v3Target: "v4.3.27"
|
|
15546
15559
|
}
|
|
15547
15560
|
],
|
|
15548
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.",
|
|
15549
15562
|
rollback: {
|
|
15550
15563
|
independent: true,
|
|
15551
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15552
|
-
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)"
|
|
15553
15566
|
}
|
|
15554
15567
|
},
|
|
15555
15568
|
{
|
|
@@ -24213,7 +24226,8 @@ async function seedGateYml(repo, deps, meta, result) {
|
|
|
24213
24226
|
parsed,
|
|
24214
24227
|
"deployable",
|
|
24215
24228
|
releaseTrack,
|
|
24216
|
-
meta?.requiredCheckBranches
|
|
24229
|
+
meta?.requiredCheckBranches,
|
|
24230
|
+
registryRequiredContexts(meta) ?? void 0
|
|
24217
24231
|
);
|
|
24218
24232
|
if (await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH)) {
|
|
24219
24233
|
return await seedRulesetRefIfMissing(repo, deps, refOnlyVars(), baseBranch, result);
|
|
@@ -24240,7 +24254,8 @@ async function seedGateYml(repo, deps, meta, result) {
|
|
|
24240
24254
|
parsed,
|
|
24241
24255
|
"deployable",
|
|
24242
24256
|
releaseTrack,
|
|
24243
|
-
meta?.requiredCheckBranches
|
|
24257
|
+
meta?.requiredCheckBranches,
|
|
24258
|
+
registryRequiredContexts(meta) ?? void 0
|
|
24244
24259
|
);
|
|
24245
24260
|
const rendered = renderSeedBody(deps, GATE_TEMPLATE_SEED, PRODUCT_GATE_PATH, derivedVars);
|
|
24246
24261
|
if (rendered == null) {
|
|
@@ -25569,11 +25584,12 @@ function registerBoardCommands(program3) {
|
|
|
25569
25584
|
function claimVerdict(ref, result) {
|
|
25570
25585
|
const holder = formatClaimHolder(result.holder);
|
|
25571
25586
|
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
25587
|
+
const reclaimed = result.reclaimedFrom ? ` (reclaimed from ${result.reclaimedFrom})` : "";
|
|
25572
25588
|
if (result.checked) {
|
|
25573
25589
|
if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
|
|
25574
25590
|
if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
|
|
25575
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})`;
|
|
25576
|
-
return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
|
|
25592
|
+
return `Check ${ref}: free - claim would proceed for ${holder}${reclaimed} (nothing written)`;
|
|
25577
25593
|
}
|
|
25578
25594
|
if (result.partial) {
|
|
25579
25595
|
if (result.outcome === "took-over") return `Partially took over ${ref} from ${previousHolder}: ${result.warning}`;
|
|
@@ -25583,12 +25599,12 @@ function registerBoardCommands(program3) {
|
|
|
25583
25599
|
if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
|
|
25584
25600
|
if (result.outcome === "resumed") return `Resumed ${ref} from ${previousHolder} for ${holder} - In Progress (${result.resumeEvidence})`;
|
|
25585
25601
|
if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
|
|
25586
|
-
return `Claimed ${ref} for ${holder} - In Progress`;
|
|
25602
|
+
return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
|
|
25587
25603
|
}
|
|
25588
25604
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
25589
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));
|
|
25590
25606
|
withExamples(mutating(
|
|
25591
|
-
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"),
|
|
25592
25608
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
25593
25609
|
).action(async (issueRefs, o) => {
|
|
25594
25610
|
if (issueRefs.length === 1) {
|
|
@@ -26590,8 +26606,9 @@ function deployStagesForBootstrap(deployModel, track) {
|
|
|
26590
26606
|
return branchesForTrack(track).map((branch) => branch === "development" ? "dev" : branch);
|
|
26591
26607
|
}
|
|
26592
26608
|
var missingBaseDetail = (base) => `${base} is missing; no source ref exists from which bootstrap can create the train lanes`;
|
|
26593
|
-
function productRulesetMatches(fact, branches) {
|
|
26594
|
-
|
|
26609
|
+
function productRulesetMatches(fact, branches, contexts) {
|
|
26610
|
+
const wantedContexts = contexts?.length ? contexts : ["gate"];
|
|
26611
|
+
if (fact?.enforcement !== "active" || !sameNames(fact.contexts, wantedContexts)) return false;
|
|
26595
26612
|
const wanted = branches.map((branch) => branch.startsWith("refs/") ? branch : `refs/heads/${branch}`);
|
|
26596
26613
|
return sameNames(fact.branches, wanted);
|
|
26597
26614
|
}
|
|
@@ -26627,7 +26644,7 @@ function planBootstrapControlPlane(input) {
|
|
|
26627
26644
|
code: "bootstrap-gap",
|
|
26628
26645
|
detail: [input.github.gateError, input.github.rulesetError].filter(Boolean).join("; ")
|
|
26629
26646
|
});
|
|
26630
|
-
} else if (productRulesetMatches(input.github.productRuleset, input.requiredCheckBranches)) {
|
|
26647
|
+
} else if (productRulesetMatches(input.github.productRuleset, input.requiredCheckBranches, input.requiredChecks)) {
|
|
26631
26648
|
rows.push({ kind: "product-ruleset", target: "mmi-product-required-checks", action: "skip", detail: "active gate context and branch scope already match the seeded authority" });
|
|
26632
26649
|
} else if (!input.github.gateExists) {
|
|
26633
26650
|
rows.push({ kind: "product-ruleset", target: "mmi-product-required-checks", action: "park", detail: `${PRODUCT_GATE_PATH2} is not present on ${base}; install disabled and activate on the next apply after the seed lands` });
|
|
@@ -27639,6 +27656,7 @@ function registerBootstrapCommands(program3) {
|
|
|
27639
27656
|
repoClass: cls,
|
|
27640
27657
|
releaseTrack: track,
|
|
27641
27658
|
requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(meta, cls), repo),
|
|
27659
|
+
requiredChecks: meta?.requiredChecks ?? void 0,
|
|
27642
27660
|
deployModel: meta?.deployModel,
|
|
27643
27661
|
owners: controlOwners,
|
|
27644
27662
|
github,
|
|
@@ -27778,7 +27796,7 @@ function registerBootstrapCommands(program3) {
|
|
|
27778
27796
|
for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
|
|
27779
27797
|
} catch {
|
|
27780
27798
|
}
|
|
27781
|
-
const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack, registryRequiredCheckBranches);
|
|
27799
|
+
const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack, registryRequiredCheckBranches, registryMeta?.requiredChecks);
|
|
27782
27800
|
if (!vars.PROJECT_ID) {
|
|
27783
27801
|
try {
|
|
27784
27802
|
const r = await gh(linkedProjectsQueryArgs(parsedRepo.owner, parsedRepo.name));
|
|
@@ -27837,6 +27855,7 @@ function registerBootstrapCommands(program3) {
|
|
|
27837
27855
|
repoClass: o.class,
|
|
27838
27856
|
releaseTrack: effectiveTrack,
|
|
27839
27857
|
requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
|
|
27858
|
+
requiredChecks: registryMeta?.requiredChecks ?? void 0,
|
|
27840
27859
|
deployModel: applyDeployModel,
|
|
27841
27860
|
owners: controlOwners,
|
|
27842
27861
|
github,
|
|
@@ -28186,6 +28205,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
28186
28205
|
repoClass: o.class,
|
|
28187
28206
|
releaseTrack: effectiveTrack,
|
|
28188
28207
|
requiredCheckBranches: resolveRequiredCheckBranches(metaWithDeclaredClass(registryMeta, o.class), repo),
|
|
28208
|
+
requiredChecks: registryMeta?.requiredChecks ?? void 0,
|
|
28189
28209
|
deployModel: applyDeployModel,
|
|
28190
28210
|
owners: controlOwners,
|
|
28191
28211
|
github,
|
|
@@ -28296,7 +28316,7 @@ LIVE apply to ${repo}:
|
|
|
28296
28316
|
if (seed.source.startsWith("seed:")) {
|
|
28297
28317
|
const project2 = projects.find((p) => (p.repos ?? []).some((repo) => repo.toLowerCase() === r.repo.toLowerCase()));
|
|
28298
28318
|
const track = resolveReleaseTrack(project2, void 0, r.repo);
|
|
28299
|
-
const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass, track, project2?.requiredCheckBranches);
|
|
28319
|
+
const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass, track, project2?.requiredCheckBranches, project2?.requiredChecks);
|
|
28300
28320
|
const resolved = resolveSeedWriteContent(seed, vars, readSeedFile2, content);
|
|
28301
28321
|
if (!resolved.ok || resolved.content == null) {
|
|
28302
28322
|
return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.ok ? "rendered no content" : resolved.reason} \u2014 refusing an incomplete per-repo render`);
|
|
@@ -29984,6 +30004,19 @@ function stageExtraEnv(config, stagePort) {
|
|
|
29984
30004
|
function stageProcessEnv(stagePort, extraEnv) {
|
|
29985
30005
|
return { ...stagePort != null ? { STAGE_PORT: String(stagePort) } : {}, ...extraEnv };
|
|
29986
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
|
+
}
|
|
29987
30020
|
async function ensureStageRuntimeEnv(config, opts, cwd) {
|
|
29988
30021
|
if (!config.ensureEnv) return;
|
|
29989
30022
|
const target = (0, import_node_path27.join)(cwd, config.ensureEnv.target);
|
|
@@ -30059,10 +30092,19 @@ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
|
|
|
30059
30092
|
writeState(statePath, reservation);
|
|
30060
30093
|
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, reservation);
|
|
30061
30094
|
}
|
|
30062
|
-
|
|
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) {
|
|
30063
30100
|
await killTree(state.pid);
|
|
30064
30101
|
if (state.teardown?.command.trim()) {
|
|
30065
|
-
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
|
+
);
|
|
30066
30108
|
}
|
|
30067
30109
|
for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
|
|
30068
30110
|
(0, import_node_fs28.rmSync)(path2, { force: true });
|
|
@@ -30120,13 +30162,20 @@ async function stopStage(opts = {}) {
|
|
|
30120
30162
|
}
|
|
30121
30163
|
const usingGlobalState = state === globalState;
|
|
30122
30164
|
const recordedStatePath = state.statePath ?? statePath;
|
|
30123
|
-
|
|
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
|
+
);
|
|
30124
30173
|
return {
|
|
30125
30174
|
ok: true,
|
|
30126
30175
|
action: "stop",
|
|
30127
30176
|
statePath: recordedStatePath,
|
|
30128
30177
|
pid: state.pid,
|
|
30129
|
-
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)"}` : "")
|
|
30130
30179
|
};
|
|
30131
30180
|
}
|
|
30132
30181
|
async function startStage(config = {}, opts = {}) {
|
|
@@ -30146,8 +30195,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30146
30195
|
const sub = (s) => substituteStagePort(s, stagePort);
|
|
30147
30196
|
if (!opts.envPrepared) await ensureStageRuntimeEnv(config, opts, cwd);
|
|
30148
30197
|
if (stagePort != null && portGuard) await ensureStagePortAvailable(stagePort, cwd, portGuard);
|
|
30149
|
-
const
|
|
30150
|
-
const vaultProcessEnv = opts.vaultEnvMerge ?? {};
|
|
30198
|
+
const composeEnv = stageComposeEnv(config, stagePort, opts.vaultEnvMerge, cwd);
|
|
30151
30199
|
let up = sub(config.up.trim());
|
|
30152
30200
|
if (opts.forceRecreate) up = appendForceRecreate(up);
|
|
30153
30201
|
const identity = await resolveStageIdentity(cwd);
|
|
@@ -30160,8 +30208,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30160
30208
|
detached: process.platform !== "win32",
|
|
30161
30209
|
windowsHide: true,
|
|
30162
30210
|
stdio: "ignore",
|
|
30163
|
-
|
|
30164
|
-
env: { ...process.env, ...vaultProcessEnv, ...stageProcessEnv(stagePort, extraEnv) }
|
|
30211
|
+
env: { ...process.env, ...composeEnv }
|
|
30165
30212
|
});
|
|
30166
30213
|
const state = {
|
|
30167
30214
|
pid: child2.pid ?? 0,
|
|
@@ -30172,7 +30219,10 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30172
30219
|
healthUrl: sub(config.healthUrl?.trim()) || void 0,
|
|
30173
30220
|
port: stagePort,
|
|
30174
30221
|
identity,
|
|
30175
|
-
|
|
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
|
|
30176
30226
|
};
|
|
30177
30227
|
writeState(statePath, state);
|
|
30178
30228
|
if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, state);
|
|
@@ -30180,7 +30230,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30180
30230
|
if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
|
|
30181
30231
|
else await waitForProcessStability(child2);
|
|
30182
30232
|
} catch (e) {
|
|
30183
|
-
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd);
|
|
30233
|
+
await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd, composeEnv);
|
|
30184
30234
|
throw e;
|
|
30185
30235
|
}
|
|
30186
30236
|
const result = {
|
|
@@ -30211,7 +30261,12 @@ async function runStage(config = {}, opts = {}) {
|
|
|
30211
30261
|
const statePath = opts.statePath ?? stageStatePath(cwd);
|
|
30212
30262
|
const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
|
|
30213
30263
|
const portGuard = resolveStagePortGuard(opts);
|
|
30214
|
-
await stopStage({
|
|
30264
|
+
await stopStage({
|
|
30265
|
+
...opts,
|
|
30266
|
+
cwd,
|
|
30267
|
+
requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd,
|
|
30268
|
+
vaultEnvMerge: stageComposeEnv(config, opts.stagePort, opts.vaultEnvMerge, cwd)
|
|
30269
|
+
});
|
|
30215
30270
|
const reserved = await reservedPortsForWorktree(cwd);
|
|
30216
30271
|
let stagePort = opts.stagePort;
|
|
30217
30272
|
if (stagePort != null) {
|
|
@@ -30223,14 +30278,13 @@ async function runStage(config = {}, opts = {}) {
|
|
|
30223
30278
|
if (stagePort != null) {
|
|
30224
30279
|
writeStagePortReservation(stagePort, cwd, statePath, globalStatePath, opts.now ?? (() => /* @__PURE__ */ new Date()));
|
|
30225
30280
|
}
|
|
30226
|
-
const extraEnv = stageExtraEnv(config, stagePort);
|
|
30227
30281
|
const build = config.build?.trim();
|
|
30228
30282
|
const ranBuild = Boolean(build);
|
|
30229
30283
|
try {
|
|
30230
30284
|
await ensureStageRuntimeEnv(config, opts, cwd);
|
|
30231
30285
|
if (build) {
|
|
30232
30286
|
await shell(sub(build), cwd, timeoutMs, {
|
|
30233
|
-
...
|
|
30287
|
+
...stageComposeEnv(config, stagePort, opts.vaultEnvMerge, cwd),
|
|
30234
30288
|
...opts.buildEnvMerge ?? {}
|
|
30235
30289
|
});
|
|
30236
30290
|
}
|
|
@@ -42451,6 +42505,30 @@ ${recovery.note}; the full required-check wall did not revalidate`);
|
|
|
42451
42505
|
}, `the immutable ${anchors.tag} pushed at ${mergedSha.slice(0, 12)}`);
|
|
42452
42506
|
return checks;
|
|
42453
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
|
+
}
|
|
42454
42532
|
async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTrainDoctor) {
|
|
42455
42533
|
await doctor({ lane: "hotfix", heal: true, train: deps, refuse: true });
|
|
42456
42534
|
const ctx = await buildTrainApplyContext(deps);
|
|
@@ -42484,6 +42562,8 @@ async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTr
|
|
|
42484
42562
|
assertTagAddressableRequiredContexts(deps, required, ctx.repo);
|
|
42485
42563
|
if (deployModel === "hub-serverless") {
|
|
42486
42564
|
await deps.run("node", ["scripts/release-distribution.mjs", "assert-release-lineage", version, "--release-commit", mergedSha]);
|
|
42565
|
+
} else {
|
|
42566
|
+
await assertConsumerBomLineage(deps, mergedSha, tag);
|
|
42487
42567
|
}
|
|
42488
42568
|
const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
|
|
42489
42569
|
if (!releaseExists && isHubControlRepo(ctx.repo)) {
|
package/package.json
CHANGED