@mutmutco/cli 4.3.23 → 4.3.25
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 +341 -392
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -6862,6 +6862,9 @@ async function tenantReconcile(payload, deps) {
|
|
|
6862
6862
|
async function tenantDeploy(payload, deps) {
|
|
6863
6863
|
return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
|
|
6864
6864
|
}
|
|
6865
|
+
async function actionsCanary(payload, deps) {
|
|
6866
|
+
return postJson("/actions-canary", payload, deps, "POST", { noRetry: true });
|
|
6867
|
+
}
|
|
6865
6868
|
|
|
6866
6869
|
// src/config-discovery.ts
|
|
6867
6870
|
function stripMutableBoardConfig(cfg) {
|
|
@@ -15519,10 +15522,10 @@ var rollout_plan_default = {
|
|
|
15519
15522
|
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)."
|
|
15520
15523
|
},
|
|
15521
15524
|
baseline: {
|
|
15522
|
-
version: "4.3.
|
|
15523
|
-
tag: "v4.3.
|
|
15524
|
-
commit: "
|
|
15525
|
-
npm: "@mutmutco/cli@4.3.
|
|
15525
|
+
version: "4.3.25",
|
|
15526
|
+
tag: "v4.3.25",
|
|
15527
|
+
commit: "fc9a141cca17",
|
|
15528
|
+
npm: "@mutmutco/cli@4.3.25"
|
|
15526
15529
|
},
|
|
15527
15530
|
exitCriterion: "fleet-n-of-n",
|
|
15528
15531
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15539,14 +15542,14 @@ var rollout_plan_default = {
|
|
|
15539
15542
|
repo: "mutmutco/mmi-hub",
|
|
15540
15543
|
role: "canary",
|
|
15541
15544
|
schedule: "train",
|
|
15542
|
-
v3Target: "v4.3.
|
|
15545
|
+
v3Target: "v4.3.25"
|
|
15543
15546
|
}
|
|
15544
15547
|
],
|
|
15545
15548
|
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.",
|
|
15546
15549
|
rollback: {
|
|
15547
15550
|
independent: true,
|
|
15548
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15549
|
-
v3Target: "v4.3.
|
|
15551
|
+
mechanism: "npm dist-tag latest -> 4.3.25 and redeploy the Hub Lambda from tag v4.3.25 (fc9a141cca17); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15552
|
+
v3Target: "v4.3.25 (@mutmutco/cli@4.3.25, tag commit fc9a141cca17 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15550
15553
|
}
|
|
15551
15554
|
},
|
|
15552
15555
|
{
|
|
@@ -18914,6 +18917,15 @@ function interpretActionsJobStart(input) {
|
|
|
18914
18917
|
}
|
|
18915
18918
|
return "pending";
|
|
18916
18919
|
}
|
|
18920
|
+
var CANARY_ACCESS_BLOCK_RE = /HTTP 404|HTTP 403|Not Found|Resource not accessible|must have admin rights|not authorized|no identity|401/i;
|
|
18921
|
+
function isCanaryAccessBlockText(text) {
|
|
18922
|
+
return !isActionsBillingBlockText(text) && CANARY_ACCESS_BLOCK_RE.test(text);
|
|
18923
|
+
}
|
|
18924
|
+
function actionsCanaryUndispatchedRefusal(detail) {
|
|
18925
|
+
return new Error(
|
|
18926
|
+
`release refused: the hosted-job canary (#5604) was not dispatched, so a hosted job start is UNPROVEN \u2014 this is NOT a billing verdict. ${detail} The Hub dispatches ${CANARY_WORKFLOW} with its App token for a train-authorized caller (#6319): refresh the Hub session (\`gh auth login\`) and retry, and confirm \`mmi-cli oracle org access role <owner/repo>\` reports train authority. Nothing was tagged or pushed \u2014 see docs/Guides/train-troubleshooting.md#actions-job-start-canary`
|
|
18927
|
+
);
|
|
18928
|
+
}
|
|
18917
18929
|
function actionsBillingRefusal(detail) {
|
|
18918
18930
|
return new Error(
|
|
18919
18931
|
`release refused: GitHub Actions cannot start a hosted job (billing/spending). ${detail} Fix Billing & plans / the org spending limit, then rerun. Do not mint a new tag. If a tag is already on origin, use \`mmi-cli devops release --retry-publish <run-id> --apply\` on that exact run \u2014 never recut.`
|
|
@@ -19008,14 +19020,16 @@ async function correlateCanaryRun(deps, nonce) {
|
|
|
19008
19020
|
`could not correlate ${CANARY_WORKFLOW} on ${CANARY_REPO} (nonce ${nonce}): ${lastError}`
|
|
19009
19021
|
);
|
|
19010
19022
|
}
|
|
19011
|
-
async function
|
|
19012
|
-
const
|
|
19013
|
-
if (
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19023
|
+
async function dispatchCanary(deps, targetRepo2, nonce) {
|
|
19024
|
+
const attempts = [];
|
|
19025
|
+
if (deps.dispatchActionsCanary) {
|
|
19026
|
+
try {
|
|
19027
|
+
await deps.dispatchActionsCanary({ repo: targetRepo2, nonce });
|
|
19028
|
+
return;
|
|
19029
|
+
} catch (e) {
|
|
19030
|
+
attempts.push(`Hub App dispatch: ${e instanceof Error ? e.message : String(e)}`);
|
|
19031
|
+
}
|
|
19017
19032
|
}
|
|
19018
|
-
const nonce = `5604-${(deps.now ?? Date.now)().toString(36)}`;
|
|
19019
19033
|
try {
|
|
19020
19034
|
await deps.run("gh", [
|
|
19021
19035
|
"workflow",
|
|
@@ -19026,11 +19040,27 @@ async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
|
19026
19040
|
"-f",
|
|
19027
19041
|
`nonce=${nonce}`
|
|
19028
19042
|
]);
|
|
19043
|
+
return;
|
|
19029
19044
|
} catch (e) {
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19045
|
+
attempts.push(`gh workflow run --repo ${CANARY_REPO}: ${e instanceof Error ? e.message : String(e)}`);
|
|
19046
|
+
}
|
|
19047
|
+
const detail = `could not dispatch ${CANARY_WORKFLOW} on ${CANARY_REPO} \u2014 ${attempts.join(" | ")}.`;
|
|
19048
|
+
if (attempts.some((a) => isActionsBillingBlockText(a))) {
|
|
19049
|
+
throw actionsBillingRefusal(`${detail} A hosted publish job may not start (the v1.54.2 class).`);
|
|
19050
|
+
}
|
|
19051
|
+
throw actionsCanaryUndispatchedRefusal(
|
|
19052
|
+
attempts.every((a) => isCanaryAccessBlockText(a)) ? `${detail} Every door answered with an access refusal \u2014 the caller may read MMI-Hub but not start its workflows.` : detail
|
|
19053
|
+
);
|
|
19054
|
+
}
|
|
19055
|
+
async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
19056
|
+
const scanned = await scanRepoForBillingBlock(deps, targetRepo2);
|
|
19057
|
+
if (scanned) throw actionsBillingRefusal(scanned);
|
|
19058
|
+
if (targetRepo2.toLowerCase() !== CANARY_REPO.toLowerCase()) {
|
|
19059
|
+
const hubScan = await scanRepoForBillingBlock(deps, CANARY_REPO);
|
|
19060
|
+
if (hubScan) throw actionsBillingRefusal(hubScan);
|
|
19033
19061
|
}
|
|
19062
|
+
const nonce = `5604-${(deps.now ?? Date.now)().toString(36)}`;
|
|
19063
|
+
await dispatchCanary(deps, targetRepo2, nonce);
|
|
19034
19064
|
const runId = await correlateCanaryRun(deps, nonce);
|
|
19035
19065
|
const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
19036
19066
|
let last = "pending";
|
|
@@ -19111,6 +19141,7 @@ Resume it: ${resumeCommand}
|
|
|
19111
19141
|
}
|
|
19112
19142
|
|
|
19113
19143
|
// src/train-doctor.ts
|
|
19144
|
+
var import_node_child_process9 = require("node:child_process");
|
|
19114
19145
|
var import_node_fs23 = require("node:fs");
|
|
19115
19146
|
var import_node_os13 = require("node:os");
|
|
19116
19147
|
var import_node_path22 = require("node:path");
|
|
@@ -21202,19 +21233,18 @@ var TRAIN_LANES = ["release", "rcand", "hotfix"];
|
|
|
21202
21233
|
var TROUBLESHOOTING_GUIDE = "docs/Guides/train-troubleshooting.md";
|
|
21203
21234
|
var SCRATCH_BRANCH_GLOBS = ["train/check/*", "hotfix-fold/*--port-*"];
|
|
21204
21235
|
var COMPOSE_GUARD_HARD_FAIL = /^secrets preflight: .*noEnvFile is (not )?true.*$/m;
|
|
21205
|
-
function
|
|
21206
|
-
const
|
|
21207
|
-
let
|
|
21236
|
+
function readOriginWorkflowsDefault(root, ref) {
|
|
21237
|
+
const git3 = (args) => (0, import_node_child_process9.execFileSync)("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
21238
|
+
let paths;
|
|
21208
21239
|
try {
|
|
21209
|
-
|
|
21210
|
-
} catch
|
|
21211
|
-
if (e.code === "ENOENT") return [];
|
|
21240
|
+
paths = git3(["ls-tree", "--name-only", ref, ".github/workflows/"]).split("\n").map((l) => l.trim()).filter((l) => /\.ya?ml$/i.test(l));
|
|
21241
|
+
} catch {
|
|
21212
21242
|
return null;
|
|
21213
21243
|
}
|
|
21214
21244
|
const files = [];
|
|
21215
|
-
for (const
|
|
21245
|
+
for (const path2 of paths) {
|
|
21216
21246
|
try {
|
|
21217
|
-
files.push({ path:
|
|
21247
|
+
files.push({ path: path2, body: git3(["show", `${ref}:${path2}`]) });
|
|
21218
21248
|
} catch {
|
|
21219
21249
|
}
|
|
21220
21250
|
}
|
|
@@ -21250,7 +21280,7 @@ var defaultDeps2 = {
|
|
|
21250
21280
|
selfConverge: selfConvergeTrainCli,
|
|
21251
21281
|
readGitignore,
|
|
21252
21282
|
writeGitignore,
|
|
21253
|
-
readWorkflows:
|
|
21283
|
+
readWorkflows: readOriginWorkflowsDefault,
|
|
21254
21284
|
ledgerPath: releaseLedgerPath,
|
|
21255
21285
|
readLedgerRaw: (path2) => {
|
|
21256
21286
|
try {
|
|
@@ -21277,10 +21307,10 @@ function message(e) {
|
|
|
21277
21307
|
function laneStage(lane) {
|
|
21278
21308
|
return lane === "rcand" ? "rc" : "main";
|
|
21279
21309
|
}
|
|
21280
|
-
function laneStartBranch(lane, track) {
|
|
21310
|
+
function laneStartBranch(lane, track, dev) {
|
|
21281
21311
|
if (lane === "hotfix") return void 0;
|
|
21282
21312
|
if (lane === "rcand") return "development";
|
|
21283
|
-
return track === "full" ? "rc" : "development";
|
|
21313
|
+
return track === "full" && !dev ? "rc" : "development";
|
|
21284
21314
|
}
|
|
21285
21315
|
function defaultLane(track, currentBranch2) {
|
|
21286
21316
|
if (track === "full") return currentBranch2 === "rc" ? "release" : "rcand";
|
|
@@ -21361,7 +21391,8 @@ async function runTrainDoctor(input) {
|
|
|
21361
21391
|
} catch {
|
|
21362
21392
|
currentBranch2 = "";
|
|
21363
21393
|
}
|
|
21364
|
-
const lane = input.lane ?? defaultLane(track, currentBranch2);
|
|
21394
|
+
const lane = input.lane ?? (input.dev === true ? "release" : defaultLane(track, currentBranch2));
|
|
21395
|
+
const dev = input.dev === true && lane === "release";
|
|
21365
21396
|
if (lane === "rcand" && track === "direct") {
|
|
21366
21397
|
add({ code: "lane-mismatch", severity: "blocker", source: "origin", title: `${repo} is direct-track (no rc) \u2014 rcand does not apply`, remedy: "run `mmi-cli devops release --apply` from development instead of rcand" });
|
|
21367
21398
|
} else if (track === "trunk") {
|
|
@@ -21449,7 +21480,7 @@ async function runTrainDoctor(input) {
|
|
|
21449
21480
|
if (restored.length) add({ code: "tree-churn", severity: "healed", source: "local", title: `restored content-identical churn: ${restored.join(", ")}`, remedy: "nothing \u2014 the paths carried no content change (mode/EOL churn)" });
|
|
21450
21481
|
if (left.length) add({ code: "tree-churn", severity: "blocker", source: "local", title: `content-identical churn blocks the clean-tree gate: ${left.join(", ")}`, remedy: `run \`mmi-cli devops train doctor --heal\` or \`git checkout -- ${left.join(" ")}\`` });
|
|
21451
21482
|
}
|
|
21452
|
-
const startBranch = laneStartBranch(lane, track);
|
|
21483
|
+
const startBranch = laneStartBranch(lane, track, dev);
|
|
21453
21484
|
if (startBranch && currentBranch2 !== startBranch) {
|
|
21454
21485
|
let linked = false;
|
|
21455
21486
|
try {
|
|
@@ -21460,15 +21491,17 @@ async function runTrainDoctor(input) {
|
|
|
21460
21491
|
add(linked ? { code: "worktree-isolated", severity: "blocker", source: "local", title: `this is a linked worktree on ${currentBranch2 || "(detached)"}, not the lane's start branch ${startBranch} (#2770)`, remedy: `run the ${lane} lane from the primary checkout on ${startBranch}; a linked worktree never carries the train` } : { code: "branch-mismatch", severity: "blocker", source: "local", title: `on ${currentBranch2 || "(detached)"}; the ${lane} lane starts from ${startBranch}`, remedy: `git checkout ${startBranch}, then rerun` });
|
|
21461
21492
|
}
|
|
21462
21493
|
const stage = laneStage(lane);
|
|
21463
|
-
const
|
|
21494
|
+
const workflowsRef = `origin/${startBranch ?? "main"}`;
|
|
21495
|
+
const workflows = deps.readWorkflows(cwd, workflowsRef);
|
|
21464
21496
|
if (workflows === null) {
|
|
21465
|
-
add({ code: "workflows-unreadable", severity: "blocker", source: "local", title: `.github/workflows
|
|
21497
|
+
add({ code: "workflows-unreadable", severity: "blocker", source: "local", title: `.github/workflows at ${workflowsRef} could not be read from ${cwd} \u2014 the tag-addressability (#5428) and npm-major (#5666) preflights did not run`, remedy: `run from a checkout whose git can read ${workflowsRef} (fetch origin first), then rerun; an unread workflow set is unverified, not green` });
|
|
21466
21498
|
}
|
|
21467
21499
|
if (hints) {
|
|
21468
21500
|
const missing = [
|
|
21469
21501
|
...!hints.hasDevelopmentBranch && lane !== "hotfix" ? ["development"] : [],
|
|
21470
21502
|
...!hints.hasMainBranch ? ["main"] : [],
|
|
21471
|
-
|
|
21503
|
+
// #6318: a `--dev` release never reads, merges or tags rc — a missing rc branch cannot block it.
|
|
21504
|
+
...!hints.hasRcBranch && track === "full" && !dev ? ["rc"] : []
|
|
21472
21505
|
];
|
|
21473
21506
|
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` });
|
|
21474
21507
|
}
|
|
@@ -23410,7 +23443,7 @@ async function resolveHotfixVersion(deps, env = process.env) {
|
|
|
23410
23443
|
}
|
|
23411
23444
|
async function runTrainApply(command, deps, options = {}) {
|
|
23412
23445
|
const watch = options.watch ?? false;
|
|
23413
|
-
await runTrainDoctor({ lane: command === "rcand" ? "rcand" : "release", heal: true, train: deps, refuse: true });
|
|
23446
|
+
await runTrainDoctor({ lane: command === "rcand" ? "rcand" : "release", dev: command === "release" && options.dev === true, heal: true, train: deps, refuse: true });
|
|
23414
23447
|
const ctx = await buildTrainApplyContext(deps);
|
|
23415
23448
|
await requireCleanTree(deps);
|
|
23416
23449
|
await runGitRemoteRead(deps, ["fetch", "origin"]);
|
|
@@ -23458,6 +23491,7 @@ var GATE_GREEN_ON_BASE_LABEL = "gate is green on the default branch (#3819)";
|
|
|
23458
23491
|
var REQUIRED_CONTEXTS_EMITTED_LABEL = "required check contexts match PR workflows";
|
|
23459
23492
|
var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
|
|
23460
23493
|
var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
|
|
23494
|
+
var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
|
|
23461
23495
|
var RECONCILE_SEED_BRANCH_PREFIX = "bootstrap-seed-ruleset-ref";
|
|
23462
23496
|
function slugFromRepo(repo) {
|
|
23463
23497
|
return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
@@ -23524,6 +23558,10 @@ function gateWorkflowFiles(prWorkflowPaths) {
|
|
|
23524
23558
|
return files.length ? files : DEFAULT_GATE_FILES;
|
|
23525
23559
|
}
|
|
23526
23560
|
async function resolveEmittedPrContexts(deps, repo, branch) {
|
|
23561
|
+
const workflows = await resolveBranchWorkflows(deps, repo, branch);
|
|
23562
|
+
return workflows === void 0 ? void 0 : collectPullRequestWorkflowContexts(workflows);
|
|
23563
|
+
}
|
|
23564
|
+
async function resolveBranchWorkflows(deps, repo, branch) {
|
|
23527
23565
|
const paths = await listWorkflowPaths(deps, repo, branch);
|
|
23528
23566
|
if (paths === void 0) return void 0;
|
|
23529
23567
|
const workflows = [];
|
|
@@ -23531,7 +23569,7 @@ async function resolveEmittedPrContexts(deps, repo, branch) {
|
|
|
23531
23569
|
const body = await fetchFileContent(deps, repo, branch, path2);
|
|
23532
23570
|
if (body) workflows.push({ path: path2, body });
|
|
23533
23571
|
}
|
|
23534
|
-
return
|
|
23572
|
+
return workflows;
|
|
23535
23573
|
}
|
|
23536
23574
|
function registryRequiredContexts(meta) {
|
|
23537
23575
|
const raw = meta?.requiredChecks;
|
|
@@ -23544,6 +23582,9 @@ function refPatternCovers(pattern, ref) {
|
|
|
23544
23582
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
23545
23583
|
return new RegExp(`^${escaped}$`).test(ref);
|
|
23546
23584
|
}
|
|
23585
|
+
function uncoveredTrackBranches(liveIncludes, trackBranches) {
|
|
23586
|
+
return trackBranches.filter((branch) => !liveIncludes.some((pattern) => refPatternCovers(pattern, `refs/heads/${branch}`)));
|
|
23587
|
+
}
|
|
23547
23588
|
function rulesetCoversReleaseBranches(payload, track) {
|
|
23548
23589
|
const conditions = payload.conditions;
|
|
23549
23590
|
const include = conditions?.ref_name?.include;
|
|
@@ -23753,10 +23794,14 @@ async function auditRepoCi(repo, deps) {
|
|
|
23753
23794
|
prWorkflowPaths = listedWorkflowPaths === void 0 ? [] : await filterPullRequestTriggered(deps, repo, baseBranch, listedWorkflowPaths);
|
|
23754
23795
|
}
|
|
23755
23796
|
const hasGateWorkflow = hasCanonicalGateWorkflow || prWorkflowPaths.length > 0;
|
|
23756
|
-
let
|
|
23797
|
+
let branchWorkflows;
|
|
23798
|
+
const getBranchWorkflows = async () => {
|
|
23799
|
+
branchWorkflows ??= await resolveBranchWorkflows(deps, repo, baseBranch);
|
|
23800
|
+
return branchWorkflows;
|
|
23801
|
+
};
|
|
23757
23802
|
const getEmittedPrContexts = async () => {
|
|
23758
|
-
|
|
23759
|
-
return
|
|
23803
|
+
const workflows = await getBranchWorkflows();
|
|
23804
|
+
return workflows === void 0 ? void 0 : collectPullRequestWorkflowContexts(workflows);
|
|
23760
23805
|
};
|
|
23761
23806
|
const committedReferenceRaw = deployableGated ? await fetchFileContent(deps, repo, baseBranch, PRODUCT_RULESET_REF) : null;
|
|
23762
23807
|
let authoritativeRuleset = null;
|
|
@@ -23840,6 +23885,18 @@ async function auditRepoCi(repo, deps) {
|
|
|
23840
23885
|
});
|
|
23841
23886
|
}
|
|
23842
23887
|
}
|
|
23888
|
+
if (productRuleset != null) {
|
|
23889
|
+
const missingTrackBranches = [];
|
|
23890
|
+
for (const branch of uncoveredTrackBranches(liveBranchIncludes, branchesForTrack(resolveReleaseTrack(meta, void 0, repo)))) {
|
|
23891
|
+
if (await branchPresence(deps, repo, branch) === true) missingTrackBranches.push(branch);
|
|
23892
|
+
}
|
|
23893
|
+
checks.push({
|
|
23894
|
+
ok: missingTrackBranches.length === 0,
|
|
23895
|
+
label: RULESET_TRACK_SCOPE_LABEL,
|
|
23896
|
+
detail: missingTrackBranches.length ? `live ${PRODUCT_RULESET_NAME} includes [${liveBranchIncludes.join(", ")}] but the release track also requires [${missingTrackBranches.join(", ")}] \u2014 those branches tag and merge with no required check` : void 0,
|
|
23897
|
+
remediation: missingTrackBranches.length ? `mmi-cli oracle org project set ${repo} --var requiredCheckBranches=${JSON.stringify(branchesForTrack(resolveReleaseTrack(meta, void 0, repo)))} then mmi-cli devops bootstrap apply ${repo} --execute` : void 0
|
|
23898
|
+
});
|
|
23899
|
+
}
|
|
23843
23900
|
const contextAuthority = authoritativeRuleset ?? registryAuthorityWithoutReference(meta, repo);
|
|
23844
23901
|
if (contextAuthority != null) {
|
|
23845
23902
|
const emitted = await getEmittedPrContexts();
|
|
@@ -23854,11 +23911,12 @@ async function auditRepoCi(repo, deps) {
|
|
|
23854
23911
|
});
|
|
23855
23912
|
}
|
|
23856
23913
|
const prOnly = contextAuthority.coversReleaseBranches ? contextAuthority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context)) : [];
|
|
23914
|
+
const tagBlind = contextAuthority.coversReleaseBranches ? contextsUnreachableOnTagPush(contextAuthority.contexts, await getBranchWorkflows() ?? [], TRAIN_PR_ONLY_CONTEXTS) : [];
|
|
23857
23915
|
checks.push({
|
|
23858
|
-
ok: prOnly.length === 0,
|
|
23916
|
+
ok: prOnly.length === 0 && tagBlind.length === 0,
|
|
23859
23917
|
label: TAG_ADDRESSABLE_CONTEXTS_LABEL,
|
|
23860
|
-
detail: prOnly.length ? `${contextAuthority.source} requires [${prOnly.join(", ")}] on rc/main, but those contexts can only materialize for pull_request` : void 0,
|
|
23861
|
-
remediation: prOnly.length ? `Remove [${prOnly.join(", ")}] from rc/main requirements in registry META or ${PRODUCT_RULESET_REF}` : void 0
|
|
23918
|
+
detail: prOnly.length ? `${contextAuthority.source} requires [${prOnly.join(", ")}] on rc/main, but those contexts can only materialize for pull_request` : tagBlind.length ? `${contextAuthority.source} requires [${tagBlind.join(", ")}] on rc/main, but their workflows never trigger on a v* tag push (#5428) \u2014 the train would tag and wait forever` : void 0,
|
|
23919
|
+
remediation: prOnly.length ? `Remove [${prOnly.join(", ")}] from rc/main requirements in registry META or ${PRODUCT_RULESET_REF}` : tagBlind.length ? `Add push: tags: ['v*'] to the workflow(s) emitting [${tagBlind.join(", ")}] on ${baseBranch} (MMI-Hub .github/workflows/gate.yml is the shape)` : void 0
|
|
23862
23920
|
});
|
|
23863
23921
|
if (productRuleset != null && liveContexts.length > 0) {
|
|
23864
23922
|
const releaseBranches = releaseBranchRefsCovered(productRuleset.conditions, resolveReleaseTrack(meta, void 0, repo));
|
|
@@ -25448,10 +25506,10 @@ async function loadConfigForBoardSelector2(selector, repoOption) {
|
|
|
25448
25506
|
}
|
|
25449
25507
|
|
|
25450
25508
|
// src/statusline-invalidate.ts
|
|
25451
|
-
var
|
|
25509
|
+
var import_node_child_process10 = require("node:child_process");
|
|
25452
25510
|
function invalidateStatuslineBoardCache() {
|
|
25453
25511
|
try {
|
|
25454
|
-
const child2 = (0,
|
|
25512
|
+
const child2 = (0, import_node_child_process10.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
|
|
25455
25513
|
detached: true,
|
|
25456
25514
|
stdio: "ignore",
|
|
25457
25515
|
windowsHide: true
|
|
@@ -28326,6 +28384,7 @@ LIVE apply to ${repo}:
|
|
|
28326
28384
|
branchExists = false;
|
|
28327
28385
|
}
|
|
28328
28386
|
if (!branchExists) await gh(["api", `repos/${rec.repo}/git/refs`, "-f", `ref=refs/heads/${branch}`, "-f", `sha=${baseSha}`]);
|
|
28387
|
+
else await gh(["api", "-X", "PATCH", `repos/${rec.repo}/git/refs/heads/${branch}`, "-f", `sha=${baseSha}`, "-F", "force=true"]);
|
|
28329
28388
|
let existingSha;
|
|
28330
28389
|
try {
|
|
28331
28390
|
const cur = await gh(["api", `repos/${rec.repo}/contents/${enc(seed.target)}?ref=${branch}`]);
|
|
@@ -29525,7 +29584,7 @@ function deriveComposeProjectName(worktreePath) {
|
|
|
29525
29584
|
}
|
|
29526
29585
|
|
|
29527
29586
|
// src/stage-runner.ts
|
|
29528
|
-
var
|
|
29587
|
+
var import_node_child_process11 = require("node:child_process");
|
|
29529
29588
|
var import_node_fs28 = require("node:fs");
|
|
29530
29589
|
var import_node_path27 = require("node:path");
|
|
29531
29590
|
var import_node_net = require("node:net");
|
|
@@ -29574,7 +29633,7 @@ function findEnvFiles(root) {
|
|
|
29574
29633
|
}
|
|
29575
29634
|
|
|
29576
29635
|
// src/stage-runner.ts
|
|
29577
|
-
var execFileP3 = (0, import_node_util5.promisify)(
|
|
29636
|
+
var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process11.execFile);
|
|
29578
29637
|
var DOCKER_TIMEOUT_MS = 15e3;
|
|
29579
29638
|
var EARLY_EXIT_GRACE_MS = 2e3;
|
|
29580
29639
|
function earlyExitGraceMs() {
|
|
@@ -30092,7 +30151,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
30092
30151
|
let up = sub(config.up.trim());
|
|
30093
30152
|
if (opts.forceRecreate) up = appendForceRecreate(up);
|
|
30094
30153
|
const identity = await resolveStageIdentity(cwd);
|
|
30095
|
-
const child2 = (0,
|
|
30154
|
+
const child2 = (0, import_node_child_process11.spawn)(up, {
|
|
30096
30155
|
cwd,
|
|
30097
30156
|
shell: true,
|
|
30098
30157
|
// POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
|
|
@@ -34612,9 +34671,9 @@ function writeError(res) {
|
|
|
34612
34671
|
|
|
34613
34672
|
// src/schedules-commands.ts
|
|
34614
34673
|
var import_promises5 = require("node:fs/promises");
|
|
34615
|
-
var
|
|
34674
|
+
var import_node_child_process12 = require("node:child_process");
|
|
34616
34675
|
var import_node_util6 = require("node:util");
|
|
34617
|
-
var execFileP4 = (0, import_node_util6.promisify)(
|
|
34676
|
+
var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process12.execFile);
|
|
34618
34677
|
var AWS_REGION = "eu-central-1";
|
|
34619
34678
|
var AWS_TIMEOUT_MS = 3e4;
|
|
34620
34679
|
var AWS_RETRY_DELAY_MS = 1500;
|
|
@@ -36134,7 +36193,7 @@ function renderVerifySecrets(body) {
|
|
|
36134
36193
|
}
|
|
36135
36194
|
|
|
36136
36195
|
// src/command-register-collaboration.ts
|
|
36137
|
-
var
|
|
36196
|
+
var import_node_child_process17 = require("node:child_process");
|
|
36138
36197
|
var import_node_fs43 = require("node:fs");
|
|
36139
36198
|
var import_promises7 = require("node:fs/promises");
|
|
36140
36199
|
|
|
@@ -36349,14 +36408,6 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
36349
36408
|
};
|
|
36350
36409
|
}
|
|
36351
36410
|
}
|
|
36352
|
-
const queue = await deps.queueMerge?.(prNumber, repo);
|
|
36353
|
-
if (queue) return {
|
|
36354
|
-
...base,
|
|
36355
|
-
queue,
|
|
36356
|
-
status: queue.state === "merged" ? "merged" : "failed",
|
|
36357
|
-
mergeStatus: queue.state === "merged" ? "merged" : "failed",
|
|
36358
|
-
...queue.state === "merged" ? {} : { error: queue.detail }
|
|
36359
|
-
};
|
|
36360
36411
|
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
36361
36412
|
base.ciPolicy = ciPolicy;
|
|
36362
36413
|
const checksWaitError = (checksWait) => {
|
|
@@ -36516,12 +36567,12 @@ function boardAdvanceFailureMessage(result) {
|
|
|
36516
36567
|
}
|
|
36517
36568
|
|
|
36518
36569
|
// src/test-policy-core.ts
|
|
36519
|
-
var
|
|
36570
|
+
var import_node_child_process14 = require("node:child_process");
|
|
36520
36571
|
var import_node_fs36 = require("node:fs");
|
|
36521
36572
|
var import_node_path33 = require("node:path");
|
|
36522
36573
|
|
|
36523
36574
|
// src/test-command-policy-shared.mjs
|
|
36524
|
-
var
|
|
36575
|
+
var import_node_child_process13 = require("node:child_process");
|
|
36525
36576
|
var TEST_COMMAND_CLASS = "test";
|
|
36526
36577
|
var TRAILER_KEY = "Test-Policy-Override";
|
|
36527
36578
|
var OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
|
|
@@ -36621,7 +36672,7 @@ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, overrid
|
|
|
36621
36672
|
};
|
|
36622
36673
|
}
|
|
36623
36674
|
function git(args, cwd) {
|
|
36624
|
-
return (0,
|
|
36675
|
+
return (0, import_node_child_process13.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
36625
36676
|
}
|
|
36626
36677
|
function parseScope(value) {
|
|
36627
36678
|
const scoped = /^\[([^\]]*)\]\s*([\s\S]*)$/.exec(value);
|
|
@@ -37120,14 +37171,14 @@ function evaluate(changed, policy, present = () => false) {
|
|
|
37120
37171
|
return findings;
|
|
37121
37172
|
}
|
|
37122
37173
|
function git2(args, cwd) {
|
|
37123
|
-
return (0,
|
|
37174
|
+
return (0, import_node_child_process14.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
37124
37175
|
}
|
|
37125
37176
|
var COAUTHOR_KEY = "Co-authored-by";
|
|
37126
37177
|
var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
|
|
37127
37178
|
var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
|
|
37128
37179
|
function parseTrailers(message2, cwd) {
|
|
37129
37180
|
try {
|
|
37130
|
-
return (0,
|
|
37181
|
+
return (0, import_node_child_process14.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
|
|
37131
37182
|
windowsHide: true,
|
|
37132
37183
|
cwd,
|
|
37133
37184
|
input: message2,
|
|
@@ -37668,287 +37719,9 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37668
37719
|
};
|
|
37669
37720
|
}
|
|
37670
37721
|
|
|
37671
|
-
// src/
|
|
37672
|
-
var import_node_child_process14 = require("node:child_process");
|
|
37722
|
+
// src/post-merge-recon.ts
|
|
37673
37723
|
var import_node_fs38 = require("node:fs");
|
|
37674
|
-
var import_node_os19 = require("node:os");
|
|
37675
37724
|
var import_node_path35 = require("node:path");
|
|
37676
|
-
var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
|
|
37677
|
-
var REVIEW_VERDICTS = ["PROCEED", "CORRECT", "ESCALATE"];
|
|
37678
|
-
function isReviewVerdict(value) {
|
|
37679
|
-
return typeof value === "string" && REVIEW_VERDICTS.includes(value);
|
|
37680
|
-
}
|
|
37681
|
-
function renderReviewVerdictComment(input) {
|
|
37682
|
-
const payload = {
|
|
37683
|
-
v: 1,
|
|
37684
|
-
patch: input.patch,
|
|
37685
|
-
head: input.head,
|
|
37686
|
-
verdict: input.verdict,
|
|
37687
|
-
scope: input.scope,
|
|
37688
|
-
risk: input.risk,
|
|
37689
|
-
unverified: input.unverified,
|
|
37690
|
-
reviewer: input.reviewer
|
|
37691
|
-
};
|
|
37692
|
-
const findings = input.findings?.trim();
|
|
37693
|
-
return `${REVIEW_VERDICT_MARKER}
|
|
37694
|
-
\`\`\`json
|
|
37695
|
-
${JSON.stringify(payload, null, 2)}
|
|
37696
|
-
\`\`\`
|
|
37697
|
-
${findings ? `
|
|
37698
|
-
${findings}
|
|
37699
|
-
` : ""}`;
|
|
37700
|
-
}
|
|
37701
|
-
function isReviewVerdictComment(body) {
|
|
37702
|
-
return body.trimStart().startsWith(REVIEW_VERDICT_MARKER);
|
|
37703
|
-
}
|
|
37704
|
-
var FENCE_RE = /^(?:`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n(?:`{3,}|~{3,})\s*$/m;
|
|
37705
|
-
function parseReviewVerdictComment(body) {
|
|
37706
|
-
if (!isReviewVerdictComment(body)) return void 0;
|
|
37707
|
-
const rest = body.trimStart().slice(REVIEW_VERDICT_MARKER.length);
|
|
37708
|
-
const fence = FENCE_RE.exec(rest);
|
|
37709
|
-
if (!fence) return void 0;
|
|
37710
|
-
let parsed;
|
|
37711
|
-
try {
|
|
37712
|
-
parsed = JSON.parse(fence[1]);
|
|
37713
|
-
} catch {
|
|
37714
|
-
return void 0;
|
|
37715
|
-
}
|
|
37716
|
-
if (!parsed || typeof parsed !== "object") return void 0;
|
|
37717
|
-
const p = parsed;
|
|
37718
|
-
if (p.v !== 1 || !isReviewVerdict(p.verdict)) return void 0;
|
|
37719
|
-
if (typeof p.patch !== "string" || !/^[0-9a-f]{40}$/.test(p.patch)) return void 0;
|
|
37720
|
-
if (typeof p.head !== "string" || typeof p.scope !== "string" || typeof p.risk !== "string" || typeof p.reviewer !== "string") return void 0;
|
|
37721
|
-
const unverified = Array.isArray(p.unverified) ? p.unverified.filter((u) => typeof u === "string") : [];
|
|
37722
|
-
return { v: 1, patch: p.patch, head: p.head, verdict: p.verdict, scope: p.scope, risk: p.risk, unverified, reviewer: p.reviewer };
|
|
37723
|
-
}
|
|
37724
|
-
function latestReviewComment(comments) {
|
|
37725
|
-
let latest;
|
|
37726
|
-
for (const c of comments) {
|
|
37727
|
-
if (!isReviewVerdictComment(c.body)) continue;
|
|
37728
|
-
if (!latest || c.createdAt > latest.createdAt || c.createdAt === latest.createdAt && (c.id ?? 0) >= (latest.id ?? 0)) latest = c;
|
|
37729
|
-
}
|
|
37730
|
-
return latest;
|
|
37731
|
-
}
|
|
37732
|
-
function evaluateReviewVerdict(comments, currentPatchId) {
|
|
37733
|
-
const latest = latestReviewComment(comments);
|
|
37734
|
-
if (!latest) return { ok: false, reason: "none" };
|
|
37735
|
-
const payload = parseReviewVerdictComment(latest.body);
|
|
37736
|
-
if (!payload) return { ok: false, reason: "malformed" };
|
|
37737
|
-
if (payload.verdict !== "PROCEED") return { ok: false, reason: "not-proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37738
|
-
if (payload.patch !== currentPatchId) return { ok: false, reason: "stale", verdict: payload.verdict, patch: payload.patch };
|
|
37739
|
-
return { ok: true, reason: "proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37740
|
-
}
|
|
37741
|
-
function evaluateTrustedReviewVerdict(comments, patch, head) {
|
|
37742
|
-
const latest = latestReviewComment(comments);
|
|
37743
|
-
if (!latest) return { ok: false, reason: "none" };
|
|
37744
|
-
const identity = { commentId: latest.id, commentCreatedAt: latest.createdAt };
|
|
37745
|
-
if (latest.author?.toLowerCase() !== "jervaise") return { ...identity, ok: false, reason: "untrusted-author" };
|
|
37746
|
-
const evaluation = evaluateReviewVerdict([latest], patch);
|
|
37747
|
-
if (!evaluation.ok) return { ...identity, ok: false, reason: evaluation.reason };
|
|
37748
|
-
if (parseReviewVerdictComment(latest.body)?.head !== head) return { ...identity, ok: false, reason: "stale-head" };
|
|
37749
|
-
return { ...identity, ok: true, reason: "proceed" };
|
|
37750
|
-
}
|
|
37751
|
-
async function checkPrReview(number, repo, head, deps = {
|
|
37752
|
-
readHead: readPrHeadSha,
|
|
37753
|
-
readComments: readPrIssueComments,
|
|
37754
|
-
computePatch: computePrPatchId
|
|
37755
|
-
}) {
|
|
37756
|
-
const base = { repo, number, head };
|
|
37757
|
-
if (!/^[1-9][0-9]*$/.test(number) || !/^[\w.-]+\/[\w.-]+$/.test(repo) || !/^[0-9a-f]{40}$/.test(head)) {
|
|
37758
|
-
return { ...base, ok: false, reason: "invalid-input", detail: "expected a PR number, owner/repo and exact lowercase 40-character head SHA" };
|
|
37759
|
-
}
|
|
37760
|
-
try {
|
|
37761
|
-
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
37762
|
-
const [comments, patch] = await Promise.all([deps.readComments(number, repo), deps.computePatch(number, repo)]);
|
|
37763
|
-
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
37764
|
-
return { ...base, patch, ...evaluateTrustedReviewVerdict(comments, patch, head) };
|
|
37765
|
-
} catch (e) {
|
|
37766
|
-
return { ...base, ok: false, reason: "unreadable", detail: e.message };
|
|
37767
|
-
}
|
|
37768
|
-
}
|
|
37769
|
-
function computePrPatchId(number, repo) {
|
|
37770
|
-
return new Promise((resolve7, reject) => {
|
|
37771
|
-
const gh = (0, import_node_child_process14.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
37772
|
-
const git3 = (0, import_node_child_process14.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
37773
|
-
let out = "";
|
|
37774
|
-
let ghErr = "";
|
|
37775
|
-
let gitErr = "";
|
|
37776
|
-
const timer = setTimeout(() => {
|
|
37777
|
-
gh.kill();
|
|
37778
|
-
git3.kill();
|
|
37779
|
-
reject(new Error(`patch-id: timed out after ${GC_GH_TIMEOUT_MS4}ms`));
|
|
37780
|
-
}, GC_GH_TIMEOUT_MS4);
|
|
37781
|
-
gh.stdout.pipe(git3.stdin);
|
|
37782
|
-
gh.stderr.on("data", (d) => {
|
|
37783
|
-
ghErr += d.toString();
|
|
37784
|
-
});
|
|
37785
|
-
git3.stderr.on("data", (d) => {
|
|
37786
|
-
gitErr += d.toString();
|
|
37787
|
-
});
|
|
37788
|
-
git3.stdout.on("data", (d) => {
|
|
37789
|
-
out += d.toString();
|
|
37790
|
-
});
|
|
37791
|
-
gh.on("error", (e) => {
|
|
37792
|
-
clearTimeout(timer);
|
|
37793
|
-
reject(e);
|
|
37794
|
-
});
|
|
37795
|
-
git3.on("error", (e) => {
|
|
37796
|
-
clearTimeout(timer);
|
|
37797
|
-
reject(e);
|
|
37798
|
-
});
|
|
37799
|
-
let ghCode;
|
|
37800
|
-
let gitCode;
|
|
37801
|
-
const finish = () => {
|
|
37802
|
-
if (ghCode === void 0 || gitCode === void 0) return;
|
|
37803
|
-
clearTimeout(timer);
|
|
37804
|
-
if (ghCode !== 0) return reject(Object.assign(new Error(`gh pr diff ${number} --repo ${repo} exited ${ghCode}: ${ghErr.trim()}`), { stderr: ghErr }));
|
|
37805
|
-
if (gitCode !== 0) return reject(new Error(`git patch-id --stable exited ${gitCode}: ${gitErr.trim()}`));
|
|
37806
|
-
const id = out.trim().split(/\s+/)[0];
|
|
37807
|
-
if (!/^[0-9a-f]{40}$/.test(id ?? "")) return reject(new Error(`patch-id: PR #${number} diff is empty or unreadable (gh: ${ghErr.trim() || "no stderr"})`));
|
|
37808
|
-
resolve7(id);
|
|
37809
|
-
};
|
|
37810
|
-
gh.on("close", (code) => {
|
|
37811
|
-
ghCode = code;
|
|
37812
|
-
finish();
|
|
37813
|
-
});
|
|
37814
|
-
git3.on("close", (code) => {
|
|
37815
|
-
gitCode = code;
|
|
37816
|
-
finish();
|
|
37817
|
-
});
|
|
37818
|
-
git3.stdin.on("error", (e) => {
|
|
37819
|
-
clearTimeout(timer);
|
|
37820
|
-
gh.kill();
|
|
37821
|
-
git3.kill();
|
|
37822
|
-
reject(e);
|
|
37823
|
-
});
|
|
37824
|
-
});
|
|
37825
|
-
}
|
|
37826
|
-
async function readPrHeadSha(number, repo) {
|
|
37827
|
-
const { stdout } = await execFileP("gh", ["api", `repos/${repo}/pulls/${number}`, "--jq", ".head.sha"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
37828
|
-
const sha = stdout.trim();
|
|
37829
|
-
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`could not read PR #${number} head sha`);
|
|
37830
|
-
return sha;
|
|
37831
|
-
}
|
|
37832
|
-
async function readPrIssueComments(number, repo) {
|
|
37833
|
-
const { stdout } = await execFileP("gh", ["api", "--paginate", `repos/${repo}/issues/${number}/comments?per_page=100`, "--jq", ".[] | {id, body, createdAt: .created_at, author: .user.login}"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
37834
|
-
const comments = parseNdjsonLines(stdout);
|
|
37835
|
-
if (comments.some((c) => !c || !Number.isSafeInteger(c.id) || c.id <= 0 || typeof c.body !== "string" || typeof c.createdAt !== "string" || !Number.isFinite(Date.parse(c.createdAt)) || c.author !== null && typeof c.author !== "string")) throw new Error("unreadable PR comment identity");
|
|
37836
|
-
return comments;
|
|
37837
|
-
}
|
|
37838
|
-
async function postPrCommentFromFile(number, repo, body) {
|
|
37839
|
-
const dir = (0, import_node_fs38.mkdtempSync)((0, import_node_path35.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
|
|
37840
|
-
const path2 = (0, import_node_path35.join)(dir, "body.md");
|
|
37841
|
-
try {
|
|
37842
|
-
(0, import_node_fs38.writeFileSync)(path2, body, "utf8");
|
|
37843
|
-
const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
37844
|
-
return stdout.trim();
|
|
37845
|
-
} finally {
|
|
37846
|
-
try {
|
|
37847
|
-
(0, import_node_fs38.rmSync)(dir, { recursive: true, force: true });
|
|
37848
|
-
} catch {
|
|
37849
|
-
}
|
|
37850
|
-
}
|
|
37851
|
-
}
|
|
37852
|
-
|
|
37853
|
-
// src/pr-mergify-queue.ts
|
|
37854
|
-
var MERGIFY_PILOT_REPO = "mutmutco/Jerv-JervCode";
|
|
37855
|
-
var MERGIFY_PILOT_VARIABLE = "JERV_BATCH_QUEUE_PILOT";
|
|
37856
|
-
var MERGIFY_READY_LABEL = "ready-to-merge";
|
|
37857
|
-
async function ghJson(args) {
|
|
37858
|
-
return JSON.parse((await execFileP("gh", ["api", ...args], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
|
|
37859
|
-
}
|
|
37860
|
-
async function usesMergifyPilot(repo, base, readVariable = async () => ghJson([`repos/${repo}/actions/variables/${MERGIFY_PILOT_VARIABLE}`])) {
|
|
37861
|
-
if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase() || base !== "development") return false;
|
|
37862
|
-
const variable = await readVariable();
|
|
37863
|
-
if (variable?.value === "true") return true;
|
|
37864
|
-
if (variable?.value === "false") return false;
|
|
37865
|
-
throw new Error(`Mergify pilot activation is missing or malformed on ${repo}; refusing native merge`);
|
|
37866
|
-
}
|
|
37867
|
-
function queueCheckState(checks, head) {
|
|
37868
|
-
const check = checks.filter((c) => c.name === "Mergify Merge Queue" && c.head_sha === head && c.app?.id === 10562 && c.app.slug === "mergify").sort((a, b) => b.id - a.id)[0];
|
|
37869
|
-
if (!check) return "refused";
|
|
37870
|
-
if (check.conclusion && !["success", "neutral"].includes(check.conclusion)) return "refused";
|
|
37871
|
-
if (["In merge queue", "Running merge queue checks"].includes(check.output?.title ?? "")) return "pending";
|
|
37872
|
-
return "requested";
|
|
37873
|
-
}
|
|
37874
|
-
async function requestMergifyMerge(number, repo, guardedText, expectedHead, deps = {
|
|
37875
|
-
readPull: () => ghJson([`repos/${repo}/pulls/${number}`]),
|
|
37876
|
-
readChecks: async (head) => parseNdjsonLines((await execFileP("gh", [
|
|
37877
|
-
"api",
|
|
37878
|
-
"--paginate",
|
|
37879
|
-
`repos/${repo}/commits/${head}/check-runs?per_page=100`,
|
|
37880
|
-
"--jq",
|
|
37881
|
-
".check_runs[]"
|
|
37882
|
-
], { timeout: GC_GH_TIMEOUT_MS4 })).stdout),
|
|
37883
|
-
addLabel: async () => {
|
|
37884
|
-
await ghJson([`repos/${repo}/issues/${number}/labels`, "--method", "POST", "-f", `labels[]=${MERGIFY_READY_LABEL}`]);
|
|
37885
|
-
},
|
|
37886
|
-
review: (head) => checkPrReview(number, repo, head),
|
|
37887
|
-
now: () => Date.now(),
|
|
37888
|
-
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
|
|
37889
|
-
}, timeoutMs = 6e5) {
|
|
37890
|
-
let head = expectedHead ?? "";
|
|
37891
|
-
let state = "refused";
|
|
37892
|
-
const receipt = (detail) => ({ provider: "mergify", state, head, detail });
|
|
37893
|
-
try {
|
|
37894
|
-
if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase()) return receipt("repository is outside the Mergify pilot");
|
|
37895
|
-
const initial = await deps.readPull();
|
|
37896
|
-
head = expectedHead ?? initial.head.sha;
|
|
37897
|
-
const sameTarget = (pr) => /^[0-9a-f]{40}$/.test(head) && pr.head.sha === head && pr.base.ref === "development";
|
|
37898
|
-
if (!sameTarget(initial)) return receipt("PR head or target base changed");
|
|
37899
|
-
if (initial.merged === true) {
|
|
37900
|
-
state = "merged";
|
|
37901
|
-
return receipt("GitHub confirms the PR merged");
|
|
37902
|
-
}
|
|
37903
|
-
const validate = (pr) => pr.state === "open" && sameTarget(pr) && `${pr.title}
|
|
37904
|
-
${pr.body ?? ""}` === guardedText;
|
|
37905
|
-
if (!validate(initial)) return receipt("PR state, head, base or guarded title/body changed");
|
|
37906
|
-
const initialState = queueCheckState(await deps.readChecks(head), head);
|
|
37907
|
-
if (initialState === "refused") return receipt("trusted current-head Mergify check is absent or failed");
|
|
37908
|
-
if (!(await deps.review(head)).ok) return receipt("current trusted review verdict does not permit queue admission");
|
|
37909
|
-
if (!validate(await deps.readPull())) return receipt("PR changed before queue request");
|
|
37910
|
-
let mutationDetail = "";
|
|
37911
|
-
if (!initial.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
|
|
37912
|
-
try {
|
|
37913
|
-
await deps.addLabel();
|
|
37914
|
-
} catch {
|
|
37915
|
-
mutationDetail = "Label response was ambiguous; no replay was attempted. ";
|
|
37916
|
-
}
|
|
37917
|
-
}
|
|
37918
|
-
state = "requested";
|
|
37919
|
-
const deadline = deps.now() + timeoutMs;
|
|
37920
|
-
while (true) {
|
|
37921
|
-
const pr = await deps.readPull();
|
|
37922
|
-
if (!sameTarget(pr)) {
|
|
37923
|
-
state = "refused";
|
|
37924
|
-
return receipt("PR head or target base changed while queued");
|
|
37925
|
-
}
|
|
37926
|
-
if (pr.merged === true) {
|
|
37927
|
-
state = "merged";
|
|
37928
|
-
return receipt("GitHub confirms the PR merged");
|
|
37929
|
-
}
|
|
37930
|
-
if (!validate(pr)) {
|
|
37931
|
-
state = "refused";
|
|
37932
|
-
return receipt("PR state, head, base or guarded title/body changed while queued");
|
|
37933
|
-
}
|
|
37934
|
-
state = queueCheckState(await deps.readChecks(head), head);
|
|
37935
|
-
if (state === "refused") return receipt("trusted current-head Mergify check is absent or failed");
|
|
37936
|
-
if (!pr.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
|
|
37937
|
-
state = "refused";
|
|
37938
|
-
return receipt(`${mutationDetail}Queue request label is absent`);
|
|
37939
|
-
}
|
|
37940
|
-
if (deps.now() >= deadline) return receipt(`${mutationDetail}${state === "pending" ? "Mergify confirms queue admission" : "Queue requested; Mergify has not confirmed admission"}; PR has not merged within the wait window`);
|
|
37941
|
-
await deps.sleep(3e4);
|
|
37942
|
-
}
|
|
37943
|
-
} catch {
|
|
37944
|
-
state = "refused";
|
|
37945
|
-
return receipt("Mergify or GitHub state is unreadable; no native merge was attempted");
|
|
37946
|
-
}
|
|
37947
|
-
}
|
|
37948
|
-
|
|
37949
|
-
// src/post-merge-recon.ts
|
|
37950
|
-
var import_node_fs39 = require("node:fs");
|
|
37951
|
-
var import_node_path36 = require("node:path");
|
|
37952
37725
|
|
|
37953
37726
|
// src/cross-repo-filing-issue.ts
|
|
37954
37727
|
function crossRepoFilingRetryCommand(prRepo, prNumber) {
|
|
@@ -38114,16 +37887,16 @@ function buildPostMergeReconRecovery(input) {
|
|
|
38114
37887
|
}
|
|
38115
37888
|
function writePostMergeReconRecovery(cwd, recovery) {
|
|
38116
37889
|
const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
|
|
38117
|
-
(0,
|
|
38118
|
-
(0,
|
|
37890
|
+
(0, import_node_fs38.mkdirSync)((0, import_node_path35.dirname)(path2), { recursive: true });
|
|
37891
|
+
(0, import_node_fs38.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
|
|
38119
37892
|
`, "utf8");
|
|
38120
37893
|
return path2;
|
|
38121
37894
|
}
|
|
38122
37895
|
function clearPostMergeReconRecovery(cwd, repo, pr) {
|
|
38123
37896
|
const path2 = postMergeReconStatePath(cwd, repo, pr);
|
|
38124
|
-
if (!(0,
|
|
37897
|
+
if (!(0, import_node_fs38.existsSync)(path2)) return;
|
|
38125
37898
|
try {
|
|
38126
|
-
(0,
|
|
37899
|
+
(0, import_node_fs38.unlinkSync)(path2);
|
|
38127
37900
|
} catch {
|
|
38128
37901
|
}
|
|
38129
37902
|
}
|
|
@@ -38153,15 +37926,197 @@ function postMergeReconWarnings(input) {
|
|
|
38153
37926
|
return lines2;
|
|
38154
37927
|
}
|
|
38155
37928
|
|
|
38156
|
-
// src/
|
|
37929
|
+
// src/review-verdict.ts
|
|
38157
37930
|
var import_node_child_process15 = require("node:child_process");
|
|
37931
|
+
var import_node_fs39 = require("node:fs");
|
|
37932
|
+
var import_node_os19 = require("node:os");
|
|
37933
|
+
var import_node_path36 = require("node:path");
|
|
37934
|
+
var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
|
|
37935
|
+
var REVIEW_VERDICTS = ["PROCEED", "CORRECT", "ESCALATE"];
|
|
37936
|
+
function isReviewVerdict(value) {
|
|
37937
|
+
return typeof value === "string" && REVIEW_VERDICTS.includes(value);
|
|
37938
|
+
}
|
|
37939
|
+
function renderReviewVerdictComment(input) {
|
|
37940
|
+
const payload = {
|
|
37941
|
+
v: 1,
|
|
37942
|
+
patch: input.patch,
|
|
37943
|
+
head: input.head,
|
|
37944
|
+
verdict: input.verdict,
|
|
37945
|
+
scope: input.scope,
|
|
37946
|
+
risk: input.risk,
|
|
37947
|
+
unverified: input.unverified,
|
|
37948
|
+
reviewer: input.reviewer
|
|
37949
|
+
};
|
|
37950
|
+
const findings = input.findings?.trim();
|
|
37951
|
+
return `${REVIEW_VERDICT_MARKER}
|
|
37952
|
+
\`\`\`json
|
|
37953
|
+
${JSON.stringify(payload, null, 2)}
|
|
37954
|
+
\`\`\`
|
|
37955
|
+
${findings ? `
|
|
37956
|
+
${findings}
|
|
37957
|
+
` : ""}`;
|
|
37958
|
+
}
|
|
37959
|
+
function isReviewVerdictComment(body) {
|
|
37960
|
+
return body.trimStart().startsWith(REVIEW_VERDICT_MARKER);
|
|
37961
|
+
}
|
|
37962
|
+
var FENCE_RE = /^(?:`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n(?:`{3,}|~{3,})\s*$/m;
|
|
37963
|
+
function parseReviewVerdictComment(body) {
|
|
37964
|
+
if (!isReviewVerdictComment(body)) return void 0;
|
|
37965
|
+
const rest = body.trimStart().slice(REVIEW_VERDICT_MARKER.length);
|
|
37966
|
+
const fence = FENCE_RE.exec(rest);
|
|
37967
|
+
if (!fence) return void 0;
|
|
37968
|
+
let parsed;
|
|
37969
|
+
try {
|
|
37970
|
+
parsed = JSON.parse(fence[1]);
|
|
37971
|
+
} catch {
|
|
37972
|
+
return void 0;
|
|
37973
|
+
}
|
|
37974
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
37975
|
+
const p = parsed;
|
|
37976
|
+
if (p.v !== 1 || !isReviewVerdict(p.verdict)) return void 0;
|
|
37977
|
+
if (typeof p.patch !== "string" || !/^[0-9a-f]{40}$/.test(p.patch)) return void 0;
|
|
37978
|
+
if (typeof p.head !== "string" || typeof p.scope !== "string" || typeof p.risk !== "string" || typeof p.reviewer !== "string") return void 0;
|
|
37979
|
+
const unverified = Array.isArray(p.unverified) ? p.unverified.filter((u) => typeof u === "string") : [];
|
|
37980
|
+
return { v: 1, patch: p.patch, head: p.head, verdict: p.verdict, scope: p.scope, risk: p.risk, unverified, reviewer: p.reviewer };
|
|
37981
|
+
}
|
|
37982
|
+
function latestReviewComment(comments) {
|
|
37983
|
+
let latest;
|
|
37984
|
+
for (const c of comments) {
|
|
37985
|
+
if (!isReviewVerdictComment(c.body)) continue;
|
|
37986
|
+
if (!latest || c.createdAt > latest.createdAt || c.createdAt === latest.createdAt && (c.id ?? 0) >= (latest.id ?? 0)) latest = c;
|
|
37987
|
+
}
|
|
37988
|
+
return latest;
|
|
37989
|
+
}
|
|
37990
|
+
function evaluateReviewVerdict(comments, currentPatchId) {
|
|
37991
|
+
const latest = latestReviewComment(comments);
|
|
37992
|
+
if (!latest) return { ok: false, reason: "none" };
|
|
37993
|
+
const payload = parseReviewVerdictComment(latest.body);
|
|
37994
|
+
if (!payload) return { ok: false, reason: "malformed" };
|
|
37995
|
+
if (payload.verdict !== "PROCEED") return { ok: false, reason: "not-proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37996
|
+
if (payload.patch !== currentPatchId) return { ok: false, reason: "stale", verdict: payload.verdict, patch: payload.patch };
|
|
37997
|
+
return { ok: true, reason: "proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37998
|
+
}
|
|
37999
|
+
function evaluateTrustedReviewVerdict(comments, patch, head) {
|
|
38000
|
+
const latest = latestReviewComment(comments);
|
|
38001
|
+
if (!latest) return { ok: false, reason: "none" };
|
|
38002
|
+
const identity = { commentId: latest.id, commentCreatedAt: latest.createdAt };
|
|
38003
|
+
if (latest.author?.toLowerCase() !== "jervaise") return { ...identity, ok: false, reason: "untrusted-author" };
|
|
38004
|
+
const evaluation = evaluateReviewVerdict([latest], patch);
|
|
38005
|
+
if (!evaluation.ok) return { ...identity, ok: false, reason: evaluation.reason };
|
|
38006
|
+
if (parseReviewVerdictComment(latest.body)?.head !== head) return { ...identity, ok: false, reason: "stale-head" };
|
|
38007
|
+
return { ...identity, ok: true, reason: "proceed" };
|
|
38008
|
+
}
|
|
38009
|
+
async function checkPrReview(number, repo, head, deps = {
|
|
38010
|
+
readHead: readPrHeadSha,
|
|
38011
|
+
readComments: readPrIssueComments,
|
|
38012
|
+
computePatch: computePrPatchId
|
|
38013
|
+
}) {
|
|
38014
|
+
const base = { repo, number, head };
|
|
38015
|
+
if (!/^[1-9][0-9]*$/.test(number) || !/^[\w.-]+\/[\w.-]+$/.test(repo) || !/^[0-9a-f]{40}$/.test(head)) {
|
|
38016
|
+
return { ...base, ok: false, reason: "invalid-input", detail: "expected a PR number, owner/repo and exact lowercase 40-character head SHA" };
|
|
38017
|
+
}
|
|
38018
|
+
try {
|
|
38019
|
+
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
38020
|
+
const [comments, patch] = await Promise.all([deps.readComments(number, repo), deps.computePatch(number, repo)]);
|
|
38021
|
+
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
38022
|
+
return { ...base, patch, ...evaluateTrustedReviewVerdict(comments, patch, head) };
|
|
38023
|
+
} catch (e) {
|
|
38024
|
+
return { ...base, ok: false, reason: "unreadable", detail: e.message };
|
|
38025
|
+
}
|
|
38026
|
+
}
|
|
38027
|
+
function computePrPatchId(number, repo) {
|
|
38028
|
+
return new Promise((resolve7, reject) => {
|
|
38029
|
+
const gh = (0, import_node_child_process15.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
38030
|
+
const git3 = (0, import_node_child_process15.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
38031
|
+
let out = "";
|
|
38032
|
+
let ghErr = "";
|
|
38033
|
+
let gitErr = "";
|
|
38034
|
+
const timer = setTimeout(() => {
|
|
38035
|
+
gh.kill();
|
|
38036
|
+
git3.kill();
|
|
38037
|
+
reject(new Error(`patch-id: timed out after ${GC_GH_TIMEOUT_MS4}ms`));
|
|
38038
|
+
}, GC_GH_TIMEOUT_MS4);
|
|
38039
|
+
gh.stdout.pipe(git3.stdin);
|
|
38040
|
+
gh.stderr.on("data", (d) => {
|
|
38041
|
+
ghErr += d.toString();
|
|
38042
|
+
});
|
|
38043
|
+
git3.stderr.on("data", (d) => {
|
|
38044
|
+
gitErr += d.toString();
|
|
38045
|
+
});
|
|
38046
|
+
git3.stdout.on("data", (d) => {
|
|
38047
|
+
out += d.toString();
|
|
38048
|
+
});
|
|
38049
|
+
gh.on("error", (e) => {
|
|
38050
|
+
clearTimeout(timer);
|
|
38051
|
+
reject(e);
|
|
38052
|
+
});
|
|
38053
|
+
git3.on("error", (e) => {
|
|
38054
|
+
clearTimeout(timer);
|
|
38055
|
+
reject(e);
|
|
38056
|
+
});
|
|
38057
|
+
let ghCode;
|
|
38058
|
+
let gitCode;
|
|
38059
|
+
const finish = () => {
|
|
38060
|
+
if (ghCode === void 0 || gitCode === void 0) return;
|
|
38061
|
+
clearTimeout(timer);
|
|
38062
|
+
if (ghCode !== 0) return reject(Object.assign(new Error(`gh pr diff ${number} --repo ${repo} exited ${ghCode}: ${ghErr.trim()}`), { stderr: ghErr }));
|
|
38063
|
+
if (gitCode !== 0) return reject(new Error(`git patch-id --stable exited ${gitCode}: ${gitErr.trim()}`));
|
|
38064
|
+
const id = out.trim().split(/\s+/)[0];
|
|
38065
|
+
if (!/^[0-9a-f]{40}$/.test(id ?? "")) return reject(new Error(`patch-id: PR #${number} diff is empty or unreadable (gh: ${ghErr.trim() || "no stderr"})`));
|
|
38066
|
+
resolve7(id);
|
|
38067
|
+
};
|
|
38068
|
+
gh.on("close", (code) => {
|
|
38069
|
+
ghCode = code;
|
|
38070
|
+
finish();
|
|
38071
|
+
});
|
|
38072
|
+
git3.on("close", (code) => {
|
|
38073
|
+
gitCode = code;
|
|
38074
|
+
finish();
|
|
38075
|
+
});
|
|
38076
|
+
git3.stdin.on("error", (e) => {
|
|
38077
|
+
clearTimeout(timer);
|
|
38078
|
+
gh.kill();
|
|
38079
|
+
git3.kill();
|
|
38080
|
+
reject(e);
|
|
38081
|
+
});
|
|
38082
|
+
});
|
|
38083
|
+
}
|
|
38084
|
+
async function readPrHeadSha(number, repo) {
|
|
38085
|
+
const { stdout } = await execFileP("gh", ["api", `repos/${repo}/pulls/${number}`, "--jq", ".head.sha"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
38086
|
+
const sha = stdout.trim();
|
|
38087
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`could not read PR #${number} head sha`);
|
|
38088
|
+
return sha;
|
|
38089
|
+
}
|
|
38090
|
+
async function readPrIssueComments(number, repo) {
|
|
38091
|
+
const { stdout } = await execFileP("gh", ["api", "--paginate", `repos/${repo}/issues/${number}/comments?per_page=100`, "--jq", ".[] | {id, body, createdAt: .created_at, author: .user.login}"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
38092
|
+
const comments = parseNdjsonLines(stdout);
|
|
38093
|
+
if (comments.some((c) => !c || !Number.isSafeInteger(c.id) || c.id <= 0 || typeof c.body !== "string" || typeof c.createdAt !== "string" || !Number.isFinite(Date.parse(c.createdAt)) || c.author !== null && typeof c.author !== "string")) throw new Error("unreadable PR comment identity");
|
|
38094
|
+
return comments;
|
|
38095
|
+
}
|
|
38096
|
+
async function postPrCommentFromFile(number, repo, body) {
|
|
38097
|
+
const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path36.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
|
|
38098
|
+
const path2 = (0, import_node_path36.join)(dir, "body.md");
|
|
38099
|
+
try {
|
|
38100
|
+
(0, import_node_fs39.writeFileSync)(path2, body, "utf8");
|
|
38101
|
+
const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
38102
|
+
return stdout.trim();
|
|
38103
|
+
} finally {
|
|
38104
|
+
try {
|
|
38105
|
+
(0, import_node_fs39.rmSync)(dir, { recursive: true, force: true });
|
|
38106
|
+
} catch {
|
|
38107
|
+
}
|
|
38108
|
+
}
|
|
38109
|
+
}
|
|
38110
|
+
|
|
38111
|
+
// src/pr-create-docs-check.ts
|
|
38112
|
+
var import_node_child_process16 = require("node:child_process");
|
|
38158
38113
|
var GIT_TIMEOUT_MS2 = 15e3;
|
|
38159
38114
|
function catFileBatch(root, ref, paths) {
|
|
38160
38115
|
if (paths.length === 0) return Promise.resolve([]);
|
|
38161
38116
|
return new Promise((resolve7) => {
|
|
38162
38117
|
const chunks = [];
|
|
38163
38118
|
let settled = false;
|
|
38164
|
-
const child2 = (0,
|
|
38119
|
+
const child2 = (0, import_node_child_process16.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
|
|
38165
38120
|
const finish = () => {
|
|
38166
38121
|
if (settled) return;
|
|
38167
38122
|
settled = true;
|
|
@@ -39277,7 +39232,7 @@ function ciAuditDeps() {
|
|
|
39277
39232
|
}
|
|
39278
39233
|
};
|
|
39279
39234
|
}
|
|
39280
|
-
async function
|
|
39235
|
+
async function ghJson(args, timeout = 1e4) {
|
|
39281
39236
|
const { stdout } = await execFileP("gh", args, { timeout });
|
|
39282
39237
|
return JSON.parse(stdout);
|
|
39283
39238
|
}
|
|
@@ -39355,7 +39310,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
39355
39310
|
try {
|
|
39356
39311
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
|
|
39357
39312
|
if (o.repo) args.push("--repo", o.repo);
|
|
39358
|
-
spawnDetachedSelf(args, { spawn:
|
|
39313
|
+
spawnDetachedSelf(args, { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
|
|
39359
39314
|
} catch {
|
|
39360
39315
|
}
|
|
39361
39316
|
}
|
|
@@ -39564,7 +39519,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39564
39519
|
async function readParentField(number, repo) {
|
|
39565
39520
|
let payload;
|
|
39566
39521
|
try {
|
|
39567
|
-
payload = await
|
|
39522
|
+
payload = await ghJson(["api", `repos/${repo}/issues/${number}`]);
|
|
39568
39523
|
} catch (e) {
|
|
39569
39524
|
const err = e;
|
|
39570
39525
|
return { parentReadError: (err.stderr || err.message || String(e)).trim() };
|
|
@@ -39596,7 +39551,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39596
39551
|
emit(data2, await readParentField(n, repo));
|
|
39597
39552
|
return;
|
|
39598
39553
|
}
|
|
39599
|
-
const data = await
|
|
39554
|
+
const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", gh.ghFields]);
|
|
39600
39555
|
emit(data, await readParentField(n, repo));
|
|
39601
39556
|
} catch (e) {
|
|
39602
39557
|
const err = e;
|
|
@@ -39611,7 +39566,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39611
39566
|
const repo = await resolveRepo(o.repo);
|
|
39612
39567
|
if (!repo) return fail("issue discover-related: could not resolve repo");
|
|
39613
39568
|
try {
|
|
39614
|
-
const issues = await
|
|
39569
|
+
const issues = await ghJson([
|
|
39615
39570
|
"issue",
|
|
39616
39571
|
"list",
|
|
39617
39572
|
"--repo",
|
|
@@ -39626,7 +39581,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39626
39581
|
const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
|
|
39627
39582
|
if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
|
|
39628
39583
|
if (!candidates.length) return;
|
|
39629
|
-
const viewed = await
|
|
39584
|
+
const viewed = await ghJson([
|
|
39630
39585
|
"issue",
|
|
39631
39586
|
"view",
|
|
39632
39587
|
String(number),
|
|
@@ -39700,7 +39655,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39700
39655
|
const checked = o.off !== true;
|
|
39701
39656
|
let body;
|
|
39702
39657
|
try {
|
|
39703
|
-
const viewed = await
|
|
39658
|
+
const viewed = await ghJson(["issue", "view", String(parsed.number), "--repo", repo, "--json", "body"]);
|
|
39704
39659
|
body = viewed.body ?? "";
|
|
39705
39660
|
} catch (e) {
|
|
39706
39661
|
return fail(`issue check: could not read ${repo}#${parsed.number}: ${e.message}`);
|
|
@@ -39802,7 +39757,7 @@ ${list}`);
|
|
|
39802
39757
|
if (!o.force) {
|
|
39803
39758
|
let openLessons = [];
|
|
39804
39759
|
try {
|
|
39805
|
-
openLessons = await
|
|
39760
|
+
openLessons = await ghJson([
|
|
39806
39761
|
"issue",
|
|
39807
39762
|
"list",
|
|
39808
39763
|
"--repo",
|
|
@@ -39908,7 +39863,7 @@ ${list}`);
|
|
|
39908
39863
|
console.log(JSON.stringify(data2));
|
|
39909
39864
|
return;
|
|
39910
39865
|
}
|
|
39911
|
-
const data = await
|
|
39866
|
+
const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", effective]);
|
|
39912
39867
|
console.log(JSON.stringify(data));
|
|
39913
39868
|
} catch (e) {
|
|
39914
39869
|
const err = e;
|
|
@@ -40173,7 +40128,7 @@ ${list}`);
|
|
|
40173
40128
|
await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
|
|
40174
40129
|
async (n) => {
|
|
40175
40130
|
if (!landRepoForGuard) return void 0;
|
|
40176
|
-
const viewed = await
|
|
40131
|
+
const viewed = await ghJson(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
|
|
40177
40132
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
40178
40133
|
}
|
|
40179
40134
|
);
|
|
@@ -40191,14 +40146,6 @@ ${list}`);
|
|
|
40191
40146
|
}
|
|
40192
40147
|
if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
|
|
40193
40148
|
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
40194
|
-
queueMerge: async (prNumber, repo) => {
|
|
40195
|
-
const meta = await ghJson2(["pr", "view", prNumber, "--repo", repo, "--json", "baseRefName,headRefOid"]);
|
|
40196
|
-
if (!await usesMergifyPilot(repo, meta.baseRefName)) return void 0;
|
|
40197
|
-
if (!landClosingGuardInput) throw new Error("pr land: Mergify requires a readable closing-keyword guard");
|
|
40198
|
-
const guard = evaluateClosingGuard(landClosingGuardInput, { force: o.force, context: "pr land", squashBodyText: landClosingGuardInput.text });
|
|
40199
|
-
if (guard.blocked) throw new Error(guard.message);
|
|
40200
|
-
return requestMergifyMerge(prNumber, repo, landClosingGuardInput.text, meta.headRefOid);
|
|
40201
|
-
},
|
|
40202
40149
|
resolveRepo: async (prNumber, repoOpt) => {
|
|
40203
40150
|
const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
|
|
40204
40151
|
const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS4 })).stdout.trim();
|
|
@@ -40356,23 +40303,20 @@ ${list}`);
|
|
|
40356
40303
|
const baseRef = prMeta.base;
|
|
40357
40304
|
const headRefOid = (prMeta.oid ?? "").trim() || void 0;
|
|
40358
40305
|
if (!repoForPostCleanup) throw new Error("pr merge: cannot resolve target repository");
|
|
40359
|
-
const mergifyPilot = await usesMergifyPilot(repoForPostCleanup, baseRef);
|
|
40360
|
-
if (mergifyPilot && (method !== "--squash" || o.squashBodyFile)) throw new Error("pr merge: Mergify pilot uses the protected squash title/body policy; custom merge methods and body files are unsupported");
|
|
40361
40306
|
const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
|
|
40362
40307
|
const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
|
|
40363
40308
|
const closingGuardInput = await withAlreadyClosedCommitTargets(
|
|
40364
40309
|
await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
|
|
40365
40310
|
async (n) => {
|
|
40366
40311
|
if (!repoForPostCleanup) return void 0;
|
|
40367
|
-
const viewed = await
|
|
40312
|
+
const viewed = await ghJson(["issue", "view", String(n), "--repo", repoForPostCleanup, "--json", "state"]);
|
|
40368
40313
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
40369
40314
|
}
|
|
40370
40315
|
);
|
|
40371
40316
|
if (o.squashBodyFile && method !== "--squash") {
|
|
40372
40317
|
throw new Error("pr merge: --squash-body-file applies only to squash merges");
|
|
40373
40318
|
}
|
|
40374
|
-
|
|
40375
|
-
const mergeSquashBody = mergifyPilot ? closingGuardInput.text : squashBodyTextForMerge(
|
|
40319
|
+
const mergeSquashBody = squashBodyTextForMerge(
|
|
40376
40320
|
closingGuardInput,
|
|
40377
40321
|
method === "--squash",
|
|
40378
40322
|
o.squashBodyFile ? (0, import_node_fs43.readFileSync)(o.squashBodyFile, "utf8") : void 0
|
|
@@ -40470,7 +40414,7 @@ ${list}`);
|
|
|
40470
40414
|
}
|
|
40471
40415
|
return true;
|
|
40472
40416
|
};
|
|
40473
|
-
if (
|
|
40417
|
+
if (o.wait && !await runWaitGate()) return;
|
|
40474
40418
|
if (ciPolicy.policy === "no-ci") {
|
|
40475
40419
|
const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
|
|
40476
40420
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
@@ -40479,7 +40423,7 @@ ${list}`);
|
|
|
40479
40423
|
const remoteBefore = await remoteBranchExists2(headRef, { remote });
|
|
40480
40424
|
let upgradedToAuto = false;
|
|
40481
40425
|
let remoteNotAttemptedReason = "preserved-delayed-cleanup";
|
|
40482
|
-
const overrideBody =
|
|
40426
|
+
const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
40483
40427
|
number,
|
|
40484
40428
|
repoArgs,
|
|
40485
40429
|
async (a, t) => (await execFileP("gh", a, { timeout: t })).stdout,
|
|
@@ -40559,15 +40503,7 @@ ${list}`);
|
|
|
40559
40503
|
});
|
|
40560
40504
|
try {
|
|
40561
40505
|
try {
|
|
40562
|
-
|
|
40563
|
-
const queue = await requestMergifyMerge(number, repoForPostCleanup, closingGuardInput.text, headRefOid);
|
|
40564
|
-
if (queue.state !== "merged") {
|
|
40565
|
-
console.log(JSON.stringify({ mergeStatus: "failed", pr: number, repo: repoForPostCleanup, queue }));
|
|
40566
|
-
process.exitCode = queue.state === "refused" ? 1 : PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
40567
|
-
return;
|
|
40568
|
-
}
|
|
40569
|
-
remoteNotAttemptedReason = "pr-already-merged";
|
|
40570
|
-
} else await mergeOnce();
|
|
40506
|
+
await mergeOnce();
|
|
40571
40507
|
} catch (e) {
|
|
40572
40508
|
if (!(e instanceof PrHeadBehindBaseError)) throw e;
|
|
40573
40509
|
const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
|
|
@@ -41038,7 +40974,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
41038
40974
|
}
|
|
41039
40975
|
|
|
41040
40976
|
// src/dist-drift.ts
|
|
41041
|
-
var
|
|
40977
|
+
var import_node_child_process18 = require("node:child_process");
|
|
41042
40978
|
var import_node_crypto13 = require("node:crypto");
|
|
41043
40979
|
var import_node_fs46 = require("node:fs");
|
|
41044
40980
|
var import_node_os21 = require("node:os");
|
|
@@ -41183,7 +41119,7 @@ function bomPathFor(root) {
|
|
|
41183
41119
|
}
|
|
41184
41120
|
}
|
|
41185
41121
|
function rebuildTo(packageRoot, outDir) {
|
|
41186
|
-
(0,
|
|
41122
|
+
(0, import_node_child_process18.execFileSync)(process.execPath, ["build.mjs"], {
|
|
41187
41123
|
cwd: packageRoot,
|
|
41188
41124
|
env: { ...process.env, MMI_DIST_OUTDIR: outDir },
|
|
41189
41125
|
windowsHide: true,
|
|
@@ -41401,7 +41337,7 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
|
41401
41337
|
}
|
|
41402
41338
|
|
|
41403
41339
|
// src/spawn-policy-core.ts
|
|
41404
|
-
var
|
|
41340
|
+
var import_node_child_process19 = require("node:child_process");
|
|
41405
41341
|
var import_node_fs47 = require("node:fs");
|
|
41406
41342
|
var import_node_path44 = require("node:path");
|
|
41407
41343
|
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
@@ -41472,7 +41408,7 @@ function findViolationsInSource(raw) {
|
|
|
41472
41408
|
return found;
|
|
41473
41409
|
}
|
|
41474
41410
|
function policedFiles(root) {
|
|
41475
|
-
const r = (0,
|
|
41411
|
+
const r = (0, import_node_child_process19.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
41476
41412
|
cwd: root,
|
|
41477
41413
|
encoding: "utf8",
|
|
41478
41414
|
windowsHide: true,
|
|
@@ -43009,12 +42945,12 @@ async function findInFlightHotfixVersion(deps, ctx, latestMainTag, workflows = H
|
|
|
43009
42945
|
}
|
|
43010
42946
|
|
|
43011
42947
|
// src/hotfix-coverage.ts
|
|
43012
|
-
var
|
|
42948
|
+
var import_node_child_process20 = require("node:child_process");
|
|
43013
42949
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
43014
42950
|
function checkHotfixCoverage(options = {}) {
|
|
43015
42951
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
43016
42952
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
43017
|
-
const git3 = options.git ?? ((args, opts) => (0,
|
|
42953
|
+
const git3 = options.git ?? ((args, opts) => (0, import_node_child_process20.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
43018
42954
|
const revList = (range) => {
|
|
43019
42955
|
const out = git3(["rev-list", "--no-merges", range]).trim();
|
|
43020
42956
|
return out ? out.split("\n") : [];
|
|
@@ -43082,7 +43018,7 @@ function checkHotfixCoverage(options = {}) {
|
|
|
43082
43018
|
}
|
|
43083
43019
|
function checkHotfixCarries(options) {
|
|
43084
43020
|
const { cwd = process.cwd(), branch, baseRef, targets } = options;
|
|
43085
|
-
const git3 = options.git ?? ((args, opts) => (0,
|
|
43021
|
+
const git3 = options.git ?? ((args, opts) => (0, import_node_child_process20.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
43086
43022
|
const isAncestor = (sha, ref) => {
|
|
43087
43023
|
try {
|
|
43088
43024
|
git3(["merge-base", "--is-ancestor", sha, ref]);
|
|
@@ -43134,10 +43070,13 @@ function readRepoVersion() {
|
|
|
43134
43070
|
}
|
|
43135
43071
|
function registerTrainCommands(program3, trainDeps) {
|
|
43136
43072
|
const train = program3.command("train").description("release-train observability \u2014 track position, version lag, and the shared preflight-and-heal doctor (#2688, #6067)");
|
|
43137
|
-
train.command("doctor").description("one shared preflight-and-heal verdict for the release, rcand and hotfix lanes; read-only unless --heal (#6067)").addOption(new Option("--lane <lane>", "train lane to check (defaults from the release track: direct \u2192 release)").choices([...TRAIN_LANES])).option("--heal", "repair safe local reconstructible state (scratch gitignore, churn, stray local tags, ff-only branch lag, finished scratch branches, stale alignment ledger leg)").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").option("--json", "machine-readable output").addHelpText("after", "\nExit codes:\n 0 ready \u2014 no blocker and origin verified\n 1 a blocker remains, or origin could not be verified (never green on an unread origin)\n\nEvery finding carries code, severity (blocker|warning|healed|info), source (local|origin), remedy and a\ndocs/Guides/train-troubleshooting.md#<code> anchor. `release --apply` and `rcand --apply` run this at step 0 with --heal.\n").action(async (o) => {
|
|
43073
|
+
train.command("doctor").description("one shared preflight-and-heal verdict for the release, rcand and hotfix lanes; read-only unless --heal (#6067)").addOption(new Option("--lane <lane>", "train lane to check (defaults from the release track: direct \u2192 release)").choices([...TRAIN_LANES])).option("--dev", "judge the `release --dev` lane (development -> main, skipping rc): the lane starts from development and is read on development's workflows (#6318)").option("--heal", "repair safe local reconstructible state (scratch gitignore, churn, stray local tags, ff-only branch lag, finished scratch branches, stale alignment ledger leg)").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").option("--json", "machine-readable output").addHelpText("after", "\nExit codes:\n 0 ready \u2014 no blocker and origin verified\n 1 a blocker remains, or origin could not be verified (never green on an unread origin)\n\nEvery finding carries code, severity (blocker|warning|healed|info), source (local|origin), remedy and a\ndocs/Guides/train-troubleshooting.md#<code> anchor. `release --apply` and `rcand --apply` run this at step 0 with --heal.\n").action(async (o) => {
|
|
43138
43074
|
try {
|
|
43075
|
+
if (o.dev && o.lane && o.lane !== "release") {
|
|
43076
|
+
return failGraceful(`train doctor: --dev applies only to the release lane \u2014 it names the development -> main variant that skips rc, which the ${o.lane} lane does not have`);
|
|
43077
|
+
}
|
|
43139
43078
|
if (o.repo) {
|
|
43140
|
-
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops train doctor${o.lane ? ` --lane ${o.lane}` : ""}${o.heal ? " --heal" : ""}${o.json ? " --json" : ""}`, "train doctor");
|
|
43079
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli devops train doctor${o.lane ? ` --lane ${o.lane}` : ""}${o.dev ? " --dev" : ""}${o.heal ? " --heal" : ""}${o.json ? " --json" : ""}`, "train doctor");
|
|
43141
43080
|
if (!guard.ok) return failGraceful(`train doctor: ${guard.message}`);
|
|
43142
43081
|
}
|
|
43143
43082
|
const root = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
@@ -43153,7 +43092,7 @@ function registerTrainCommands(program3, trainDeps) {
|
|
|
43153
43092
|
}
|
|
43154
43093
|
}
|
|
43155
43094
|
};
|
|
43156
|
-
const verdict = await runTrainDoctor({ lane: o.lane, heal: o.heal === true, train: deps, repo: o.repo, cwd: root, argv: INVOKED_ARGV });
|
|
43095
|
+
const verdict = await runTrainDoctor({ lane: o.lane, dev: o.dev === true, heal: o.heal === true, train: deps, repo: o.repo, cwd: root, argv: INVOKED_ARGV });
|
|
43157
43096
|
if (verdict.reexecCode !== void 0) return process.exit(verdict.reexecCode);
|
|
43158
43097
|
console.log(o.json ? JSON.stringify(verdict, null, 2) : formatTrainDoctor(verdict));
|
|
43159
43098
|
if (!verdict.ready) process.exitCode = 1;
|
|
@@ -43262,6 +43201,16 @@ function trainApplyDeps() {
|
|
|
43262
43201
|
throw new Error(`tenant deploy dispatch failed: ${detail}`);
|
|
43263
43202
|
}
|
|
43264
43203
|
},
|
|
43204
|
+
// Hub-App-authority dispatch of the #5604 hosted-job canary (#6319) — the Hub fires the
|
|
43205
|
+
// workflow_dispatch with its App token, so a train-authorized project-admin (pull-only on MMI-Hub)
|
|
43206
|
+
// runs the probe instead of being refused with a 404 misread as a billing block (#6320).
|
|
43207
|
+
dispatchActionsCanary: async ({ repo, nonce }) => {
|
|
43208
|
+
const res = await actionsCanary({ repo, nonce }, registryClientDeps(await loadConfig()));
|
|
43209
|
+
if (!res.ok) {
|
|
43210
|
+
const detail = res.body?.error ?? res.error ?? `HTTP ${res.status}`;
|
|
43211
|
+
throw new Error(`Hub /actions-canary refused: ${detail}`);
|
|
43212
|
+
}
|
|
43213
|
+
},
|
|
43265
43214
|
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
43266
43215
|
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
43267
43216
|
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
@@ -43735,7 +43684,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
43735
43684
|
let doctor;
|
|
43736
43685
|
if (repo && (!o.repo || (await resolveRepo())?.toLowerCase() === repo.toLowerCase())) {
|
|
43737
43686
|
try {
|
|
43738
|
-
doctor = await runTrainDoctor({ lane: commandName, heal: false, train: trainApplyDeps(), repo });
|
|
43687
|
+
doctor = await runTrainDoctor({ lane: commandName, dev: commandName === "release" && o.dev === true, heal: false, train: trainApplyDeps(), repo });
|
|
43739
43688
|
} catch (e) {
|
|
43740
43689
|
consoleIo.err(`mmi-cli: train doctor did not complete: ${e.message}`);
|
|
43741
43690
|
}
|
package/package.json
CHANGED