@mutmutco/cli 4.3.20 → 4.3.22
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 +761 -502
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5227,7 +5227,7 @@ function unknownCommandDomainGuide(parentPath, token) {
|
|
|
5227
5227
|
|
|
5228
5228
|
// src/command-composition.ts
|
|
5229
5229
|
var import_node_os22 = require("node:os");
|
|
5230
|
-
var
|
|
5230
|
+
var import_node_path47 = require("node:path");
|
|
5231
5231
|
|
|
5232
5232
|
// src/board-read.ts
|
|
5233
5233
|
var import_node_fs13 = require("node:fs");
|
|
@@ -12242,7 +12242,10 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12242
12242
|
}
|
|
12243
12243
|
return;
|
|
12244
12244
|
}
|
|
12245
|
-
if (!options.force)
|
|
12245
|
+
if (!options.force) {
|
|
12246
|
+
const refusal = laneContestMessage(item.ref, contest, "claim");
|
|
12247
|
+
throw new Error(options.bulk ? refusal : withPowerShellChainHint(refusal));
|
|
12248
|
+
}
|
|
12246
12249
|
previousHolder = displaced();
|
|
12247
12250
|
};
|
|
12248
12251
|
await refuseIfContested();
|
|
@@ -12313,7 +12316,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12313
12316
|
const selector = selectors[index];
|
|
12314
12317
|
const ref = `${selector.repo}#${selector.number}`;
|
|
12315
12318
|
try {
|
|
12316
|
-
const result = await claimOneBoardItem(ctx, selector, options);
|
|
12319
|
+
const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
|
|
12317
12320
|
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 };
|
|
12318
12321
|
} catch (e) {
|
|
12319
12322
|
results[index] = { ref, claimed: false, reason: e.message };
|
|
@@ -12902,6 +12905,29 @@ async function waitForPrChecks(deps) {
|
|
|
12902
12905
|
|
|
12903
12906
|
// src/bootstrap-ruleset.ts
|
|
12904
12907
|
var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
|
|
12908
|
+
var PRODUCT_RULESET_PATH = ".github/rulesets/mmi-product-required-checks.json";
|
|
12909
|
+
function reseedProductRulesetStrictness(current, seed) {
|
|
12910
|
+
const parse = (raw) => {
|
|
12911
|
+
const policy2 = JSON.parse(raw);
|
|
12912
|
+
if (!policy2 || typeof policy2 !== "object" || Array.isArray(policy2) || policy2.name !== PRODUCT_RULESET_NAME || policy2.target !== "branch" || !Array.isArray(policy2.rules) || policy2.rules.some((r) => !r || typeof r !== "object" || Array.isArray(r) || typeof r.type !== "string")) {
|
|
12913
|
+
throw new Error("product ruleset must be a branch ruleset object with well-formed rules");
|
|
12914
|
+
}
|
|
12915
|
+
const required = policy2.rules.filter((r) => r.type === "required_status_checks");
|
|
12916
|
+
const parameters2 = required[0]?.parameters;
|
|
12917
|
+
if (required.length !== 1 || !parameters2 || typeof parameters2 !== "object" || Array.isArray(parameters2) || !Array.isArray(parameters2.required_status_checks) || !parameters2.required_status_checks.length || parameters2.required_status_checks.some((c) => !c || typeof c !== "object" || Array.isArray(c) || typeof c.context !== "string" || !c.context.trim()) || parameters2.strict_required_status_checks_policy !== void 0 && typeof parameters2.strict_required_status_checks_policy !== "boolean") {
|
|
12918
|
+
throw new Error("product ruleset must carry exactly one well-formed required_status_checks rule");
|
|
12919
|
+
}
|
|
12920
|
+
return { policy: policy2, parameters: parameters2 };
|
|
12921
|
+
};
|
|
12922
|
+
const strict = parse(seed).parameters.strict_required_status_checks_policy;
|
|
12923
|
+
if (typeof strict !== "boolean") throw new Error("canonical product ruleset must declare boolean strictness");
|
|
12924
|
+
if (current === null) return seed;
|
|
12925
|
+
const { policy, parameters } = parse(current);
|
|
12926
|
+
if (parameters.strict_required_status_checks_policy === strict) return current;
|
|
12927
|
+
parameters.strict_required_status_checks_policy = strict;
|
|
12928
|
+
return `${JSON.stringify(policy, null, 2)}
|
|
12929
|
+
`;
|
|
12930
|
+
}
|
|
12905
12931
|
function stripRulesetComment(raw) {
|
|
12906
12932
|
const parsed = JSON.parse(raw);
|
|
12907
12933
|
delete parsed._comment;
|
|
@@ -12921,14 +12947,6 @@ function rulesetStrictPolicy(ruleset) {
|
|
|
12921
12947
|
const rules = (ruleset.rules ?? []).filter((rule) => rule.type === "required_status_checks");
|
|
12922
12948
|
return rules.length > 0 && rules.every((rule) => rule.parameters?.strict_required_status_checks_policy === true);
|
|
12923
12949
|
}
|
|
12924
|
-
function patchRulesetStrictPolicy(body) {
|
|
12925
|
-
const rules = (body.rules ?? []).map((rule) => {
|
|
12926
|
-
const r = rule;
|
|
12927
|
-
if (r.type !== "required_status_checks") return rule;
|
|
12928
|
-
return { ...r, parameters: { ...r.parameters, strict_required_status_checks_policy: true } };
|
|
12929
|
-
});
|
|
12930
|
-
return { ...body, rules };
|
|
12931
|
-
}
|
|
12932
12950
|
function rulesetBranchIncludes(ruleset) {
|
|
12933
12951
|
const raw = ruleset.conditions?.ref_name?.include;
|
|
12934
12952
|
return Array.isArray(raw) ? [...new Set(raw.filter((ref) => typeof ref === "string" && ref.length > 0))].sort((a, b) => a.localeCompare(b)) : [];
|
|
@@ -12948,8 +12966,7 @@ function patchRulesetRequiredContexts(body, contexts) {
|
|
|
12948
12966
|
...r,
|
|
12949
12967
|
parameters: {
|
|
12950
12968
|
...r.parameters,
|
|
12951
|
-
strict_required_status_checks_policy:
|
|
12952
|
-
// #6263: org policy — see patchRulesetStrictPolicy
|
|
12969
|
+
strict_required_status_checks_policy: r.parameters?.strict_required_status_checks_policy ?? false,
|
|
12953
12970
|
required_status_checks: sorted.map((context) => ({ context }))
|
|
12954
12971
|
}
|
|
12955
12972
|
};
|
|
@@ -13353,7 +13370,7 @@ function extractForwardRefs(markdown) {
|
|
|
13353
13370
|
});
|
|
13354
13371
|
return refs;
|
|
13355
13372
|
}
|
|
13356
|
-
function checkPins(root,
|
|
13373
|
+
function checkPins(root, readFile9, docs) {
|
|
13357
13374
|
const findings = [];
|
|
13358
13375
|
for (const [doc, markdown] of Object.entries(docs)) {
|
|
13359
13376
|
for (const pin of extractPins(markdown)) {
|
|
@@ -13361,7 +13378,7 @@ function checkPins(root, readFile10, docs) {
|
|
|
13361
13378
|
findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
|
|
13362
13379
|
continue;
|
|
13363
13380
|
}
|
|
13364
|
-
const source =
|
|
13381
|
+
const source = readFile9((0, import_node_path12.join)(root, pin.file));
|
|
13365
13382
|
if (source == null) {
|
|
13366
13383
|
findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
|
|
13367
13384
|
continue;
|
|
@@ -13576,7 +13593,7 @@ function defaultTrackedFirstSegments(root, firstSegments, exec = import_node_chi
|
|
|
13576
13593
|
}
|
|
13577
13594
|
}
|
|
13578
13595
|
function runDocRefs(root, deps = {}) {
|
|
13579
|
-
const
|
|
13596
|
+
const readFile9 = deps.readFile ?? readFileOrNull;
|
|
13580
13597
|
const exists = deps.exists ?? import_node_fs14.existsSync;
|
|
13581
13598
|
const listDocs = deps.listDocs ?? defaultListDocs;
|
|
13582
13599
|
const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
|
|
@@ -13585,11 +13602,11 @@ function runDocRefs(root, deps = {}) {
|
|
|
13585
13602
|
const walked = listDocs(root);
|
|
13586
13603
|
const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
|
|
13587
13604
|
const docs = Object.fromEntries(
|
|
13588
|
-
walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel,
|
|
13605
|
+
walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile9((0, import_node_path12.join)(root, rel))]).filter(([, body]) => body != null)
|
|
13589
13606
|
);
|
|
13590
13607
|
const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs);
|
|
13591
13608
|
const findings = [
|
|
13592
|
-
...checkPins(root,
|
|
13609
|
+
...checkPins(root, readFile9, docs).findings,
|
|
13593
13610
|
...refResult.findings
|
|
13594
13611
|
];
|
|
13595
13612
|
if (commandPaths == null) {
|
|
@@ -14186,22 +14203,22 @@ function labelsToPrune(orgLabelNames) {
|
|
|
14186
14203
|
const org = new Set(orgLabelNames);
|
|
14187
14204
|
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
14188
14205
|
}
|
|
14189
|
-
function resolveSeedContent(seed, vars,
|
|
14190
|
-
if (seed.source === "self") return
|
|
14206
|
+
function resolveSeedContent(seed, vars, readFile9) {
|
|
14207
|
+
if (seed.source === "self") return readFile9(seed.target);
|
|
14191
14208
|
if (seed.source.startsWith("seed:")) {
|
|
14192
|
-
const tmpl =
|
|
14209
|
+
const tmpl = readFile9(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
14193
14210
|
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
14194
14211
|
}
|
|
14195
14212
|
return null;
|
|
14196
14213
|
}
|
|
14197
|
-
function resolveSeedWriteContent(seed, vars,
|
|
14214
|
+
function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
|
|
14198
14215
|
if (!seed.managedBlock) {
|
|
14199
|
-
return { ok: true, content: resolveSeedContent(seed, vars,
|
|
14216
|
+
return { ok: true, content: resolveSeedContent(seed, vars, readFile9), managed: seed.source === "managed-block" };
|
|
14200
14217
|
}
|
|
14201
|
-
const base = remoteContent ?? resolveSeedContent(seed, vars,
|
|
14218
|
+
const base = remoteContent ?? resolveSeedContent(seed, vars, readFile9);
|
|
14202
14219
|
if (base == null) return { ok: true, content: null, managed: true };
|
|
14203
14220
|
const blockSeed = { ...seed, source: seed.managedBlock.source, managedBlock: void 0 };
|
|
14204
|
-
const desired = resolveSeedContent(blockSeed, vars,
|
|
14221
|
+
const desired = resolveSeedContent(blockSeed, vars, readFile9);
|
|
14205
14222
|
if (desired == null) return { ok: true, content: null, managed: true };
|
|
14206
14223
|
const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
|
|
14207
14224
|
return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
|
|
@@ -15502,10 +15519,10 @@ var rollout_plan_default = {
|
|
|
15502
15519
|
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)."
|
|
15503
15520
|
},
|
|
15504
15521
|
baseline: {
|
|
15505
|
-
version: "4.3.
|
|
15506
|
-
tag: "v4.3.
|
|
15507
|
-
commit: "
|
|
15508
|
-
npm: "@mutmutco/cli@4.3.
|
|
15522
|
+
version: "4.3.22",
|
|
15523
|
+
tag: "v4.3.22",
|
|
15524
|
+
commit: "ab0293be4361",
|
|
15525
|
+
npm: "@mutmutco/cli@4.3.22"
|
|
15509
15526
|
},
|
|
15510
15527
|
exitCriterion: "fleet-n-of-n",
|
|
15511
15528
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15522,14 +15539,14 @@ var rollout_plan_default = {
|
|
|
15522
15539
|
repo: "mutmutco/mmi-hub",
|
|
15523
15540
|
role: "canary",
|
|
15524
15541
|
schedule: "train",
|
|
15525
|
-
v3Target: "v4.3.
|
|
15542
|
+
v3Target: "v4.3.22"
|
|
15526
15543
|
}
|
|
15527
15544
|
],
|
|
15528
15545
|
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.",
|
|
15529
15546
|
rollback: {
|
|
15530
15547
|
independent: true,
|
|
15531
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15532
|
-
v3Target: "v4.3.
|
|
15548
|
+
mechanism: "npm dist-tag latest -> 4.3.22 and redeploy the Hub Lambda from tag v4.3.22 (ab0293be4361); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15549
|
+
v3Target: "v4.3.22 (@mutmutco/cli@4.3.22, tag commit ab0293be4361 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15533
15550
|
}
|
|
15534
15551
|
},
|
|
15535
15552
|
{
|
|
@@ -17897,8 +17914,29 @@ async function discoverRequiredCheckContexts(deps, ctx, branch) {
|
|
|
17897
17914
|
}
|
|
17898
17915
|
return [...contexts];
|
|
17899
17916
|
}
|
|
17900
|
-
|
|
17901
|
-
|
|
17917
|
+
function alignmentBranchName(target, tag) {
|
|
17918
|
+
return `release-align/${target}/${tag}`;
|
|
17919
|
+
}
|
|
17920
|
+
async function pushAlignmentBranch(deps, target, tag) {
|
|
17921
|
+
const branch = alignmentBranchName(target, tag);
|
|
17922
|
+
let tree;
|
|
17923
|
+
try {
|
|
17924
|
+
tree = clean2(await deps.run("git", ["merge-tree", "--write-tree", "--no-messages", `origin/${target}`, "origin/main"])).split("\n")[0] ?? "";
|
|
17925
|
+
} catch {
|
|
17926
|
+
const files = await runMergeTreePreflight(deps, `origin/${target}`, "origin/main");
|
|
17927
|
+
throw new Error(
|
|
17928
|
+
`the ${target} alignment of ${tag} could not merge origin/main${files.length ? ` \u2014 conflicts on ${files.join(", ")}` : ""} (#6288). Reconcile it on a branch cut from origin/${target} that merges origin/main, open that against ${target}, and land it with a true merge (\`mmi-cli devops pr merge <n> --auto --merge\`) \u2014 never hand-resolve on main, and never merge ${target} into main.`
|
|
17929
|
+
);
|
|
17930
|
+
}
|
|
17931
|
+
if (!tree) throw new Error(`git merge-tree wrote no tree for the ${target} alignment of ${tag} (#6288)`);
|
|
17932
|
+
const commit = clean2(await deps.run("git", ["commit-tree", tree, "-p", `origin/${target}`, "-p", "origin/main", "-m", `chore(release): align ${target} to ${tag}`]));
|
|
17933
|
+
if (!commit) throw new Error(`git commit-tree wrote no commit for the ${target} alignment of ${tag} (#6288)`);
|
|
17934
|
+
await deps.run("git", ["push", "origin", "--delete", branch]).catch(() => void 0);
|
|
17935
|
+
await runGitPush(deps, ["push", "origin", `${commit}:refs/heads/${branch}`]);
|
|
17936
|
+
return branch;
|
|
17937
|
+
}
|
|
17938
|
+
async function findAlignmentPr(deps, ctx, target, head) {
|
|
17939
|
+
const out = clean2(await deps.run("gh", ["pr", "list", "--repo", ctx.repo, "--base", target, "--head", head, "--state", "all", "--json", "number,url,state"]));
|
|
17902
17940
|
if (!out) return void 0;
|
|
17903
17941
|
const rows = JSON.parse(out);
|
|
17904
17942
|
const row = rows.filter((r) => typeof r.number === "number" && typeof r.url === "string" && (r.state === "OPEN" || r.state === "MERGED")).sort((a, b) => b.number - a.number)[0];
|
|
@@ -17919,23 +17957,28 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
17919
17957
|
return { status: "pushed", note: "development rolled forward to the released main (development has no required checks)" };
|
|
17920
17958
|
}
|
|
17921
17959
|
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/development..origin/main"]));
|
|
17922
|
-
const
|
|
17960
|
+
const branch = alignmentBranchName("development", tag);
|
|
17961
|
+
const existing = await findAlignmentPr(deps, ctx, "development", branch);
|
|
17923
17962
|
if (ahead === "0") {
|
|
17963
|
+
const merged = existing ?? await findAlignmentPr(deps, ctx, "development", "main");
|
|
17924
17964
|
return {
|
|
17925
17965
|
status: "aligned",
|
|
17926
17966
|
alignment: "already-merged",
|
|
17927
|
-
note:
|
|
17967
|
+
note: merged?.state === "MERGED" ? `alignment: already-merged \u2014 development already contains main via alignment PR #${merged.number}` : "alignment: already-merged \u2014 development already contains the released main; nothing to roll forward"
|
|
17928
17968
|
};
|
|
17929
17969
|
}
|
|
17930
17970
|
if (existing?.state === "OPEN") {
|
|
17931
17971
|
return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `alignment PR already open: ${existing.url}`);
|
|
17932
17972
|
}
|
|
17973
|
+
const superseded = await findAlignmentPr(deps, ctx, "development", "main");
|
|
17974
|
+
const supersededNote = superseded?.state === "OPEN" ? ` (supersedes the main-headed alignment PR #${superseded.number}, which GitHub closes as merged once this lands \u2014 #6288)` : "";
|
|
17975
|
+
await pushAlignmentBranch(deps, "development", tag);
|
|
17933
17976
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
17934
17977
|
|
|
17935
|
-
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
|
|
17936
|
-
const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head",
|
|
17978
|
+
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). The head is \`development\` with \`main\` merged into it, never \`main\` itself: a strict \`require branches to be up to date\` ruleset can never be satisfied by a main-headed PR, and satisfying it literally would promote every unreleased commit to \`main\` (#6288). Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
|
|
17979
|
+
const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", branch, "--title", `chore(release): align development to ${tag}`, "--body", body]));
|
|
17937
17980
|
const number = parsePrNumber(url);
|
|
17938
|
-
return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}`);
|
|
17981
|
+
return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}${supersededNote}`);
|
|
17939
17982
|
}
|
|
17940
17983
|
var ALIGNMENT_ARM_ATTEMPTS = 3;
|
|
17941
17984
|
var ALIGNMENT_ARM_BACKOFF_MS = 2e3;
|
|
@@ -17976,23 +18019,28 @@ async function alignRcForward(deps, ctx, tag) {
|
|
|
17976
18019
|
return { status: "pushed", note: "rc aligned to the released main (rc has no required checks)" };
|
|
17977
18020
|
}
|
|
17978
18021
|
const ahead = clean2(await deps.run("git", ["rev-list", "--count", "origin/rc..origin/main"]));
|
|
17979
|
-
const
|
|
18022
|
+
const branch = alignmentBranchName("rc", tag);
|
|
18023
|
+
const existing = await findAlignmentPr(deps, ctx, "rc", branch);
|
|
17980
18024
|
if (ahead === "0") {
|
|
18025
|
+
const merged = existing ?? await findAlignmentPr(deps, ctx, "rc", "main");
|
|
17981
18026
|
return {
|
|
17982
18027
|
status: "aligned",
|
|
17983
18028
|
alignment: "already-merged",
|
|
17984
|
-
note:
|
|
18029
|
+
note: merged?.state === "MERGED" ? `alignment: already-merged \u2014 rc already contains main via alignment PR #${merged.number}` : "alignment: already-merged \u2014 rc already contains the released main; nothing to align"
|
|
17985
18030
|
};
|
|
17986
18031
|
}
|
|
17987
18032
|
if (existing?.state === "OPEN") {
|
|
17988
18033
|
return enqueueAlignmentAutoMerge(deps, ctx, existing.number, existing.url, `rc alignment PR already open: ${existing.url}`);
|
|
17989
18034
|
}
|
|
18035
|
+
const superseded = await findAlignmentPr(deps, ctx, "rc", "main");
|
|
18036
|
+
const supersededNote = superseded?.state === "OPEN" ? ` (supersedes the main-headed alignment PR #${superseded.number}, which GitHub closes as merged once this lands \u2014 #6288)` : "";
|
|
18037
|
+
await pushAlignmentBranch(deps, "rc", tag);
|
|
17990
18038
|
const body = `Carries the ${tag} release from \`main\` back to \`rc\`.
|
|
17991
18039
|
|
|
17992
|
-
\`rc\` requires status checks, so the release train opens this alignment PR instead of bypassing branch protection with a direct push. Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives.`;
|
|
17993
|
-
const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "rc", "--head",
|
|
18040
|
+
\`rc\` requires status checks, so the release train opens this alignment PR instead of bypassing branch protection with a direct push. The head is \`rc\` with \`main\` merged into it, never \`main\` itself \u2014 a strict \`require branches to be up to date\` ruleset can never be satisfied by a main-headed PR (#6288). Land it with a **true merge** \u2014 \`mmi-cli devops pr merge <n> --auto --merge\` (not squash) so the merge parentage survives.`;
|
|
18041
|
+
const url = clean2(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "rc", "--head", branch, "--title", `chore(release): align rc to ${tag}`, "--body", body]));
|
|
17994
18042
|
const number = parsePrNumber(url);
|
|
17995
|
-
return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `rc requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}`);
|
|
18043
|
+
return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `rc requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}${supersededNote}`);
|
|
17996
18044
|
}
|
|
17997
18045
|
var FLEET_FOREIGN_VERDICT_GRACE_ATTEMPTS = 3;
|
|
17998
18046
|
function resolveContextState(context, checkRuns, statuses) {
|
|
@@ -21535,6 +21583,23 @@ async function runTrainDoctor(input) {
|
|
|
21535
21583
|
derived = deriveReleasePhaseStatus(ledger);
|
|
21536
21584
|
}
|
|
21537
21585
|
}
|
|
21586
|
+
if (derived.legs.some((l) => l.phase === "alignment" && l.state === "pending")) {
|
|
21587
|
+
for (const target of ["development", "rc"].filter((t) => t !== "rc" || track === "full")) {
|
|
21588
|
+
try {
|
|
21589
|
+
const stale = await findAlignmentPr(train, ctx, target, "main");
|
|
21590
|
+
if (stale?.state !== "OPEN") continue;
|
|
21591
|
+
add({
|
|
21592
|
+
code: "alignment-pr-main-headed",
|
|
21593
|
+
severity: "blocker",
|
|
21594
|
+
source: "origin",
|
|
21595
|
+
title: `alignment PR #${stale.number} (\`main\` -> ${target}) is headed by \`main\` \u2014 under a strict "branches must be up to date" ruleset it can never merge (#6288)`,
|
|
21596
|
+
remedy: `rerun \`mmi-cli devops release --resume\`: the train rebuilds the alignment as \`release-align/${target}/<tag>\` (${target} with \`main\` merged into it), which is up to date by construction; GitHub closes #${stale.number} as merged once that lands. Never merge ${target} into \`main\`, never hand-merge on \`main\`, and never add a bypass actor to the strict rule`
|
|
21597
|
+
});
|
|
21598
|
+
} catch (e) {
|
|
21599
|
+
unverified(`the ${target} alignment PR head`, e);
|
|
21600
|
+
}
|
|
21601
|
+
}
|
|
21602
|
+
}
|
|
21538
21603
|
if (derived.phaseStatus !== "phases-green") {
|
|
21539
21604
|
const open2 = derived.legs.filter((l) => l.state === "pending" || l.state === "failed").map((l) => `${l.phase}=${l.state}`).join(", ");
|
|
21540
21605
|
const failed = derived.phaseStatus === "phases-failed";
|
|
@@ -23527,12 +23592,12 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
|
|
|
23527
23592
|
const contexts = sortedUnique(registryContexts ?? committedContexts);
|
|
23528
23593
|
const explicitBranches = Array.isArray(meta?.requiredCheckBranches) && meta.requiredCheckBranches.length > 0 ? resolveRequiredCheckBranches(meta, repo) : null;
|
|
23529
23594
|
const committedStrict = rulesetStrictPolicy(committedPayload);
|
|
23530
|
-
let apiPayload =
|
|
23595
|
+
let apiPayload = registryContexts == null ? committedPayload : patchRulesetRequiredContexts(committedPayload, contexts);
|
|
23531
23596
|
if (explicitBranches) apiPayload = patchRulesetBranchIncludes(apiPayload, explicitBranches);
|
|
23532
23597
|
const branchIncludes = rulesetBranchIncludes(apiPayload);
|
|
23533
23598
|
const authoritativeFilePayload = {
|
|
23534
23599
|
...filePayload,
|
|
23535
|
-
...registryContexts == null
|
|
23600
|
+
...registryContexts == null ? {} : { rules: apiPayload.rules },
|
|
23536
23601
|
...explicitBranches == null ? {} : { conditions: apiPayload.conditions }
|
|
23537
23602
|
};
|
|
23538
23603
|
return {
|
|
@@ -23554,7 +23619,7 @@ function sameContexts(left, right) {
|
|
|
23554
23619
|
function resolveProductRulesetReconcilePlan(input) {
|
|
23555
23620
|
const liveNeedsContextConvergence = !sameContexts(input.liveContexts, input.authorityContexts);
|
|
23556
23621
|
const liveNeedsBranchConvergence = input.liveBranchIncludes !== void 0 && input.authorityBranchIncludes !== void 0 && !sameContexts(input.liveBranchIncludes, input.authorityBranchIncludes);
|
|
23557
|
-
const liveNeedsStrictConvergence = input.liveStrict
|
|
23622
|
+
const liveNeedsStrictConvergence = input.liveStrict !== void 0 && input.authorityStrict !== void 0 && input.liveStrict !== input.authorityStrict;
|
|
23558
23623
|
const liveIsActive = input.liveEnforcement === "active";
|
|
23559
23624
|
if (liveIsActive && !liveNeedsContextConvergence && !liveNeedsBranchConvergence && !liveNeedsStrictConvergence) {
|
|
23560
23625
|
return { shouldActivate: false, targetEnforcement: "active" };
|
|
@@ -23765,12 +23830,12 @@ async function auditRepoCi(repo, deps) {
|
|
|
23765
23830
|
const liveContextsAligned = sameContexts(liveContexts, authoritativeRuleset.contexts);
|
|
23766
23831
|
const fileBranchesAligned = sameContexts(authoritativeRuleset.committedBranchIncludes, authoritativeRuleset.branchIncludes);
|
|
23767
23832
|
const liveBranchesAligned = sameContexts(liveBranchIncludes, authoritativeRuleset.branchIncludes);
|
|
23768
|
-
const strictAligned = authoritativeRuleset.committedStrict
|
|
23833
|
+
const strictAligned = authoritativeRuleset.committedStrict === liveStrict;
|
|
23769
23834
|
const aligned = fileContextsAligned && liveContextsAligned && fileBranchesAligned && liveBranchesAligned && strictAligned;
|
|
23770
23835
|
checks.push({
|
|
23771
23836
|
ok: aligned,
|
|
23772
23837
|
label: RULESET_REFERENCE_MATCH_LABEL,
|
|
23773
|
-
detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy
|
|
23838
|
+
detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy differs from committed policy \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`),
|
|
23774
23839
|
remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
|
|
23775
23840
|
});
|
|
23776
23841
|
}
|
|
@@ -24360,6 +24425,7 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
24360
24425
|
liveBranchIncludes,
|
|
24361
24426
|
authorityBranchIncludes: authority.branchIncludes,
|
|
24362
24427
|
liveStrict: live == null ? void 0 : rulesetStrictPolicy(live),
|
|
24428
|
+
authorityStrict: authority.committedStrict,
|
|
24363
24429
|
gateProvenGreen: await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles),
|
|
24364
24430
|
unsafeContexts: unsafe
|
|
24365
24431
|
});
|
|
@@ -27167,11 +27233,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
27167
27233
|
label: "product required-check ruleset enforcement active",
|
|
27168
27234
|
detail: productRuleset?.enforcement !== "active" ? `${PRODUCT_RULESET_NAME} is ${productRuleset?.enforcement ?? "missing"} \u2014 run mmi-cli devops ci reconcile --apply --repo ${repo} once the gate is green` : void 0
|
|
27169
27235
|
});
|
|
27170
|
-
checks.push({
|
|
27171
|
-
ok: productRuleset != null && rulesetStrictPolicy(productRuleset),
|
|
27172
|
-
label: "product required-check ruleset requires an up-to-date branch",
|
|
27173
|
-
detail: productRuleset != null && rulesetStrictPolicy(productRuleset) ? void 0 : `${PRODUCT_RULESET_NAME} ${productRuleset == null ? "is missing" : "has strict_required_status_checks_policy=false"} \u2014 run mmi-cli devops ci reconcile --apply --repo ${repo} (#6263)`
|
|
27174
|
-
});
|
|
27175
27236
|
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
27176
27237
|
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
27177
27238
|
checks.push({
|
|
@@ -27225,6 +27286,100 @@ function renderBootstrapVerifyReport(report) {
|
|
|
27225
27286
|
return lines2.join("\n");
|
|
27226
27287
|
}
|
|
27227
27288
|
|
|
27289
|
+
// src/bootstrap-seed-delivery.ts
|
|
27290
|
+
var import_node_crypto9 = require("node:crypto");
|
|
27291
|
+
var SHA = /^[a-f0-9]{40}$/;
|
|
27292
|
+
async function readSeedFile(repo, target, ref, client) {
|
|
27293
|
+
try {
|
|
27294
|
+
const file = await client.rest(
|
|
27295
|
+
"GET",
|
|
27296
|
+
`repos/${repo}/contents/${target.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`
|
|
27297
|
+
);
|
|
27298
|
+
if (!file?.sha || !SHA.test(file.sha) || file.encoding !== "base64" || typeof file.content !== "string") {
|
|
27299
|
+
throw new Error("bootstrap apply: scoped seed file is unreadable");
|
|
27300
|
+
}
|
|
27301
|
+
return { sha: file.sha, content: decodeGitHubContents(file.content) };
|
|
27302
|
+
} catch (e) {
|
|
27303
|
+
if (e instanceof GitHubApiError && e.status === 404) return null;
|
|
27304
|
+
throw e;
|
|
27305
|
+
}
|
|
27306
|
+
}
|
|
27307
|
+
async function readTargetedSeedFiles(repo, plan, target, ref, client) {
|
|
27308
|
+
const base = await readSeedFile(repo, target, plan.baseSha, client);
|
|
27309
|
+
const candidate = ref === plan.baseSha ? base : await readSeedFile(repo, target, ref, client);
|
|
27310
|
+
return { base, candidate };
|
|
27311
|
+
}
|
|
27312
|
+
async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, client) {
|
|
27313
|
+
const [rules, base] = await Promise.all([
|
|
27314
|
+
client.rest("GET", `repos/${repo}/rules/branches/${encodeURIComponent(baseBranch)}`),
|
|
27315
|
+
client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(baseBranch)}`)
|
|
27316
|
+
]);
|
|
27317
|
+
if (!Array.isArray(rules) || rules.some((rule) => !rule || typeof rule.type !== "string" || !rule.type)) {
|
|
27318
|
+
throw new Error("bootstrap apply: base branch protection is unreadable");
|
|
27319
|
+
}
|
|
27320
|
+
const baseSha = base?.object?.sha;
|
|
27321
|
+
if (!baseSha || !SHA.test(baseSha) || !SHA.test(sourceSha)) throw new Error("bootstrap apply: seed identity is unreadable");
|
|
27322
|
+
const identity = (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify([repo.toLowerCase(), target, baseSha, sourceSha])).digest("hex").slice(0, 24);
|
|
27323
|
+
return { ...planSeedDelivery(rules, slug, baseBranch, `bootstrap-seed-${identity}`), baseSha, baseBranch };
|
|
27324
|
+
}
|
|
27325
|
+
async function verifyTargetedSeedBranch(repo, plan, target, client, allowMissing = false) {
|
|
27326
|
+
let ref;
|
|
27327
|
+
try {
|
|
27328
|
+
ref = await client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(plan.ref)}`);
|
|
27329
|
+
} catch (e) {
|
|
27330
|
+
if (allowMissing && e instanceof GitHubApiError && e.status === 404) return null;
|
|
27331
|
+
throw e;
|
|
27332
|
+
}
|
|
27333
|
+
const head = ref?.object?.sha;
|
|
27334
|
+
if (!head || !SHA.test(head)) throw new Error("bootstrap apply: scoped seed head is unreadable");
|
|
27335
|
+
if (head === plan.baseSha) return head;
|
|
27336
|
+
const diff = await client.rest("GET", `repos/${repo}/compare/${plan.baseSha}...${head}`);
|
|
27337
|
+
if (diff?.status !== "ahead" || diff.merge_base_commit?.sha !== plan.baseSha || !Array.isArray(diff.files) || diff.files.length !== 1 || diff.files[0].filename !== target || diff.files[0].previous_filename !== void 0 || !["added", "modified"].includes(diff.files[0].status ?? "")) {
|
|
27338
|
+
throw new Error("bootstrap apply: scoped seed branch contains changes outside the requested target or base");
|
|
27339
|
+
}
|
|
27340
|
+
return head;
|
|
27341
|
+
}
|
|
27342
|
+
async function prepareTargetedSeedBranch(repo, plan, target, client, gh) {
|
|
27343
|
+
const candidates = await client.rest(
|
|
27344
|
+
"GET",
|
|
27345
|
+
`repos/${repo}/pulls?state=open&head=${encodeURIComponent(`${repo.split("/")[0]}:${plan.branch}`)}&base=${encodeURIComponent(plan.baseBranch)}&per_page=2`
|
|
27346
|
+
);
|
|
27347
|
+
if (!Array.isArray(candidates) || candidates.length > 1) throw new Error("bootstrap apply: scoped seed PR identity is unreadable");
|
|
27348
|
+
for (const pr of candidates) {
|
|
27349
|
+
if (!Number.isInteger(pr?.number) || pr.number <= 0 || pr.head?.ref !== plan.branch || pr.base?.ref !== plan.baseBranch) {
|
|
27350
|
+
throw new Error("bootstrap apply: scoped seed PR identity is unreadable");
|
|
27351
|
+
}
|
|
27352
|
+
await gh(["pr", "merge", String(pr.number), "--repo", repo, "--disable-auto"]);
|
|
27353
|
+
const after = await client.rest("GET", `repos/${repo}/pulls/${pr.number}`);
|
|
27354
|
+
if (after?.auto_merge !== null) throw new Error("bootstrap apply: scoped seed PR auto-merge was not disabled");
|
|
27355
|
+
}
|
|
27356
|
+
const existing = await verifyTargetedSeedBranch(repo, plan, target, client, true);
|
|
27357
|
+
if (existing) return;
|
|
27358
|
+
try {
|
|
27359
|
+
await client.rest("POST", `repos/${repo}/git/refs`, { body: { ref: `refs/heads/${plan.branch}`, sha: plan.baseSha } });
|
|
27360
|
+
} catch (e) {
|
|
27361
|
+
if (!(e instanceof GitHubApiError) || e.status !== 422) throw e;
|
|
27362
|
+
}
|
|
27363
|
+
await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
27364
|
+
}
|
|
27365
|
+
async function enableTargetedSeedAutoMerge(repo, number, plan, target, expected, client, gh) {
|
|
27366
|
+
const head = await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
27367
|
+
const pr = await client.rest("GET", `repos/${repo}/pulls/${number}`);
|
|
27368
|
+
if (pr?.state !== "open" || pr.head?.ref !== plan.branch || pr.head?.sha !== head || pr.head.repo?.full_name?.toLowerCase() !== repo.toLowerCase() || pr.base?.ref !== plan.baseBranch || pr.base.sha !== plan.baseSha || pr.base.repo?.full_name?.toLowerCase() !== repo.toLowerCase() || pr.changed_files !== 1) throw new Error("bootstrap apply: scoped seed PR identity or diff changed");
|
|
27369
|
+
const files = await client.rest(
|
|
27370
|
+
"GET",
|
|
27371
|
+
`repos/${repo}/pulls/${number}/files?per_page=2`
|
|
27372
|
+
);
|
|
27373
|
+
if (!Array.isArray(files) || files.length !== 1 || files[0]?.filename !== target || files[0].previous_filename !== void 0) {
|
|
27374
|
+
throw new Error("bootstrap apply: scoped seed PR contains changes outside the requested target");
|
|
27375
|
+
}
|
|
27376
|
+
const file = await readSeedFile(repo, target, head, client);
|
|
27377
|
+
if (file?.content !== expected) {
|
|
27378
|
+
throw new Error("bootstrap apply: scoped seed content differs from this invocation");
|
|
27379
|
+
}
|
|
27380
|
+
await gh(["pr", "merge", String(number), "--repo", repo, "--auto", "--squash", "--match-head-commit", head]);
|
|
27381
|
+
}
|
|
27382
|
+
|
|
27228
27383
|
// src/bootstrap-commands.ts
|
|
27229
27384
|
var execGitForSeedSource = async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
27230
27385
|
function bootstrapTrainDoctorDeps() {
|
|
@@ -27537,7 +27692,7 @@ function registerBootstrapCommands(program3) {
|
|
|
27537
27692
|
}
|
|
27538
27693
|
const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
|
|
27539
27694
|
const gh = async (args) => execFileP("gh", args, { timeout: 2e4 });
|
|
27540
|
-
const
|
|
27695
|
+
const readFile9 = (p) => (0, import_node_fs26.existsSync)(p) ? (0, import_node_fs26.readFileSync)(p, "utf8") : null;
|
|
27541
27696
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
27542
27697
|
const putSeed = async (target, content, ref, sha) => {
|
|
27543
27698
|
const tmp = (0, import_node_path25.join)((0, import_node_os14.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
@@ -27632,14 +27787,23 @@ function registerBootstrapCommands(program3) {
|
|
|
27632
27787
|
});
|
|
27633
27788
|
}
|
|
27634
27789
|
let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
|
|
27790
|
+
let scopedPlan;
|
|
27791
|
+
let scopedContent;
|
|
27635
27792
|
let seededToBranch = 0;
|
|
27636
|
-
if (
|
|
27793
|
+
if (onlyTarget) {
|
|
27794
|
+
scopedPlan = await targetedSeedPlan(repo, slug, onlyTarget, baseBranch, seedSource.sha, controlClient);
|
|
27795
|
+
seedPlan = scopedPlan;
|
|
27796
|
+
if (o.execute && scopedPlan.branch) {
|
|
27797
|
+
await prepareTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient, gh);
|
|
27798
|
+
}
|
|
27799
|
+
} else if (o.execute) {
|
|
27637
27800
|
let branchRules = [];
|
|
27638
27801
|
try {
|
|
27639
27802
|
branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
|
|
27640
27803
|
} catch {
|
|
27641
|
-
|
|
27804
|
+
throw new Error("bootstrap apply: base branch protection is unreadable");
|
|
27642
27805
|
}
|
|
27806
|
+
if (!Array.isArray(branchRules)) throw new Error("bootstrap apply: base branch protection is unreadable");
|
|
27643
27807
|
seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
|
|
27644
27808
|
if (seedPlan.branch) {
|
|
27645
27809
|
const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
|
|
@@ -27651,6 +27815,7 @@ function registerBootstrapCommands(program3) {
|
|
|
27651
27815
|
}
|
|
27652
27816
|
}
|
|
27653
27817
|
const docsForIndex = [];
|
|
27818
|
+
const seedReadRef = !o.execute && scopedPlan ? scopedPlan.baseSha : seedPlan.ref;
|
|
27654
27819
|
for (const seed of seedsToApply) {
|
|
27655
27820
|
if (!seed.classes.includes(o.class)) continue;
|
|
27656
27821
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
@@ -27664,8 +27829,20 @@ function registerBootstrapCommands(program3) {
|
|
|
27664
27829
|
let exists = false;
|
|
27665
27830
|
let sha;
|
|
27666
27831
|
let remoteContent = null;
|
|
27667
|
-
|
|
27668
|
-
|
|
27832
|
+
let baseContent = null;
|
|
27833
|
+
const preserveRuleset = onlyTarget === PRODUCT_RULESET_PATH;
|
|
27834
|
+
if (scopedPlan) {
|
|
27835
|
+
try {
|
|
27836
|
+
const files = await readTargetedSeedFiles(repo, scopedPlan, resolved.target, seedReadRef, controlClient);
|
|
27837
|
+
baseContent = files.base?.content ?? null;
|
|
27838
|
+
exists = files.candidate !== null;
|
|
27839
|
+
sha = files.candidate?.sha;
|
|
27840
|
+
remoteContent = files.candidate?.content ?? null;
|
|
27841
|
+
} catch (e) {
|
|
27842
|
+
return failGraceful(`bootstrap apply: cannot read scoped seed file; no replacement made: ${e.message}`);
|
|
27843
|
+
}
|
|
27844
|
+
} else try {
|
|
27845
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedReadRef}`]);
|
|
27669
27846
|
exists = true;
|
|
27670
27847
|
try {
|
|
27671
27848
|
const parsed = JSON.parse(r.stdout);
|
|
@@ -27678,23 +27855,34 @@ function registerBootstrapCommands(program3) {
|
|
|
27678
27855
|
} catch {
|
|
27679
27856
|
exists = false;
|
|
27680
27857
|
}
|
|
27681
|
-
const planned = planSeedAction(resolved, exists);
|
|
27858
|
+
const planned = planSeedAction(resolved, scopedPlan ? baseContent !== null : exists);
|
|
27859
|
+
if (scopedPlan && planned.action !== "skip") planned.action = exists ? "update" : "create";
|
|
27682
27860
|
const isLegacyBlock = resolved.source === "managed-block";
|
|
27683
27861
|
let content = null;
|
|
27684
27862
|
let isManaged = isLegacyBlock || resolved.managedBlock != null;
|
|
27863
|
+
const preservedContent = scopedPlan ? baseContent : remoteContent;
|
|
27685
27864
|
if (planned.action === "create" || planned.action === "update") {
|
|
27686
27865
|
if (isLegacyBlock) {
|
|
27687
|
-
content = upsertManagedGitignoreBlock(
|
|
27866
|
+
content = upsertManagedGitignoreBlock(preservedContent).content;
|
|
27688
27867
|
} else {
|
|
27689
|
-
const writeContent = resolveSeedWriteContent(resolved, vars,
|
|
27868
|
+
const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, preservedContent);
|
|
27690
27869
|
if (!writeContent.ok) {
|
|
27691
27870
|
return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
27692
27871
|
}
|
|
27693
27872
|
content = writeContent.content;
|
|
27873
|
+
if (preserveRuleset) {
|
|
27874
|
+
if (content === null) return fail("bootstrap apply: canonical product ruleset seed is unreadable");
|
|
27875
|
+
try {
|
|
27876
|
+
content = reseedProductRulesetStrictness(baseContent, content);
|
|
27877
|
+
} catch (e) {
|
|
27878
|
+
return fail(`bootstrap apply: product ruleset preservation refused: ${e.message}`);
|
|
27879
|
+
}
|
|
27880
|
+
}
|
|
27694
27881
|
isManaged = writeContent.managed;
|
|
27695
27882
|
}
|
|
27696
27883
|
}
|
|
27697
27884
|
const action = reconcileSeedAction(planned, content, isManaged, remoteContent);
|
|
27885
|
+
if (onlyTarget && content !== null) scopedContent = content;
|
|
27698
27886
|
actions.push(action);
|
|
27699
27887
|
const docBody = content ?? remoteContent;
|
|
27700
27888
|
if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
|
|
@@ -27729,7 +27917,8 @@ function registerBootstrapCommands(program3) {
|
|
|
27729
27917
|
}
|
|
27730
27918
|
}
|
|
27731
27919
|
let seedPrUrl;
|
|
27732
|
-
|
|
27920
|
+
const scopedHead = o.execute && scopedPlan?.branch ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
|
|
27921
|
+
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
|
|
27733
27922
|
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]).catch(() => {
|
|
27734
27923
|
});
|
|
27735
27924
|
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
@@ -27758,7 +27947,12 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
27758
27947
|
seedPrUrl = created.url;
|
|
27759
27948
|
}
|
|
27760
27949
|
let autoMergeEnabled = true;
|
|
27761
|
-
|
|
27950
|
+
if (scopedPlan) {
|
|
27951
|
+
if (scopedContent === void 0) throw new Error("bootstrap apply: scoped seed content is unresolved");
|
|
27952
|
+
const number = Number(seedPrUrl.split("/").pop());
|
|
27953
|
+
if (!Number.isInteger(number) || number <= 0) throw new Error("bootstrap apply: scoped seed PR number is unreadable");
|
|
27954
|
+
await enableTargetedSeedAutoMerge(repo, number, scopedPlan, onlyTarget, scopedContent, controlClient, gh);
|
|
27955
|
+
} else await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
|
|
27762
27956
|
const message2 = String(e.message ?? "");
|
|
27763
27957
|
if (/already/i.test(message2)) return;
|
|
27764
27958
|
if (/clean status|enablePullRequestAutoMerge/i.test(message2)) {
|
|
@@ -27823,7 +28017,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
27823
28017
|
}
|
|
27824
28018
|
const rulesetSeed = repo.toLowerCase() === "mutmutco/mmi-hub" ? void 0 : manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
27825
28019
|
if (rulesetSeed) {
|
|
27826
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
28020
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile9);
|
|
27827
28021
|
if (rulesetContent) {
|
|
27828
28022
|
try {
|
|
27829
28023
|
const client = controlClient;
|
|
@@ -27941,7 +28135,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
27941
28135
|
deployFactsRead: controlDeployFacts !== null
|
|
27942
28136
|
});
|
|
27943
28137
|
}
|
|
27944
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, controlPlane: controlRows, applied, ddbWrites }, null, 2));
|
|
28138
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedBranch: seedPlan.branch, seedBaseSha: scopedPlan?.baseSha, seedPrUrl, actions, controlPlane: controlRows, applied, ddbWrites }, null, 2));
|
|
27945
28139
|
else {
|
|
27946
28140
|
console.log(renderSeedPlan(actions));
|
|
27947
28141
|
if (controlRows.length) console.log(`
|
|
@@ -27968,7 +28162,7 @@ LIVE apply to ${repo}:
|
|
|
27968
28162
|
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
27969
28163
|
if (!seed.managedBlock && !(0, import_node_fs26.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
|
|
27970
28164
|
const hubContent = seed.managedBlock ? null : (0, import_node_fs26.readFileSync)(seed.target, "utf8");
|
|
27971
|
-
const
|
|
28165
|
+
const readSeedFile2 = (path2) => (0, import_node_fs26.existsSync)(path2) ? (0, import_node_fs26.readFileSync)(path2, "utf8") : null;
|
|
27972
28166
|
const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
|
|
27973
28167
|
const cfg = await loadConfig();
|
|
27974
28168
|
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -28045,14 +28239,14 @@ LIVE apply to ${repo}:
|
|
|
28045
28239
|
const project2 = projects.find((p) => (p.repos ?? []).some((repo) => repo.toLowerCase() === r.repo.toLowerCase()));
|
|
28046
28240
|
const track = resolveReleaseTrack(project2, void 0, r.repo);
|
|
28047
28241
|
const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass, track, project2?.requiredCheckBranches);
|
|
28048
|
-
const resolved = resolveSeedWriteContent(seed, vars,
|
|
28242
|
+
const resolved = resolveSeedWriteContent(seed, vars, readSeedFile2, content);
|
|
28049
28243
|
if (!resolved.ok || resolved.content == null) {
|
|
28050
28244
|
return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.ok ? "rendered no content" : resolved.reason} \u2014 refusing an incomplete per-repo render`);
|
|
28051
28245
|
}
|
|
28052
28246
|
desired = resolved.content;
|
|
28053
28247
|
} else if (seed.managedBlock) {
|
|
28054
28248
|
const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass);
|
|
28055
|
-
const resolved = resolveSeedWriteContent(seed, vars,
|
|
28249
|
+
const resolved = resolveSeedWriteContent(seed, vars, readSeedFile2, content);
|
|
28056
28250
|
if (!resolved.ok) {
|
|
28057
28251
|
return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
28058
28252
|
}
|
|
@@ -31891,8 +32085,7 @@ var LOOP_PLAYBOOKS = {
|
|
|
31891
32085
|
{ label: "Apply the repository test policy, then build the touched package", command: "mmi-cli tests policy --base origin/development && npm run build" },
|
|
31892
32086
|
{ label: "Publish the branch", command: "git push origin <branch>:<branch>" },
|
|
31893
32087
|
{ label: "Open the development-base PR", command: 'mmi-cli devops pr create --title "<title>" --body-file .jerv/PR_BODY.md --base development' },
|
|
31894
|
-
{ label: "
|
|
31895
|
-
{ label: "Land to development (waits for checks itself and demands the review verdict)", command: "mmi-cli devops pr land <PR-number>" },
|
|
32088
|
+
{ label: "Land to development (waits for checks itself)", command: "mmi-cli devops pr land <PR-number>" },
|
|
31896
32089
|
{ label: "Release only after the gated train is authorized", command: "mmi-cli devops release --apply" },
|
|
31897
32090
|
// #5552: learning-tagged filings are cloud-agent owned — file and return to the current task.
|
|
31898
32091
|
{ label: "Learning reports are fire-and-forget (file, then return to the current task \u2014 never claim/poll/duplicate the learning issue)", command: 'mmi-cli learning report --title "<one-line>" --body "<what hurt>"' }
|
|
@@ -31913,14 +32106,14 @@ var LOOP_PLAYBOOKS = {
|
|
|
31913
32106
|
steps: [
|
|
31914
32107
|
{ label: "Publish the branch", command: "git push origin <branch>:<branch>" },
|
|
31915
32108
|
{ label: "Open the development-base PR", command: 'mmi-cli devops pr create --title "<title>" --body-file .jerv/PR_BODY.md --base development' },
|
|
31916
|
-
{ label: "Land the PR (merge to development \u2014 waits for checks itself
|
|
32109
|
+
{ label: "Land the PR (merge to development \u2014 waits for checks itself)", command: "mmi-cli devops pr land <PR-number>" }
|
|
31917
32110
|
]
|
|
31918
32111
|
},
|
|
31919
32112
|
"hotfix": {
|
|
31920
32113
|
title: "Hotfix",
|
|
31921
32114
|
steps: [
|
|
31922
32115
|
{ label: "Create the main-base hotfix PR from every already-merged fix this cycle carries", command: "mmi-cli devops hotfix start --from <pr#|sha>[,<pr#|sha>...]" },
|
|
31923
|
-
{ label: "Merge the main-base PR (waits for checks itself
|
|
32116
|
+
{ label: "Merge the main-base PR (waits for checks itself)", command: "mmi-cli devops pr merge <PR-number> --squash" },
|
|
31924
32117
|
{ label: "After the PR is merged, run the gated release", command: "mmi-cli devops hotfix release <vX.Y.Z> --carries <pr#|sha>[,<pr#|sha>...]" }
|
|
31925
32118
|
]
|
|
31926
32119
|
}
|
|
@@ -32193,7 +32386,7 @@ function parseOriginRepo(remoteUrl) {
|
|
|
32193
32386
|
|
|
32194
32387
|
// src/issue-commands.ts
|
|
32195
32388
|
var import_node_fs31 = require("node:fs");
|
|
32196
|
-
var
|
|
32389
|
+
var import_node_crypto10 = require("node:crypto");
|
|
32197
32390
|
|
|
32198
32391
|
// src/issue-body.ts
|
|
32199
32392
|
var import_node_os16 = require("node:os");
|
|
@@ -32723,7 +32916,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
32723
32916
|
const identity = `${spec.type}
|
|
32724
32917
|
${spec.title.trim()}
|
|
32725
32918
|
${spec.body ?? ""}`;
|
|
32726
|
-
const hash = (0,
|
|
32919
|
+
const hash = (0, import_node_crypto10.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
32727
32920
|
return `${batchKey}:${hash}`;
|
|
32728
32921
|
}
|
|
32729
32922
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -33085,7 +33278,10 @@ function extendCreateCommand(issue, batchAttach) {
|
|
|
33085
33278
|
try {
|
|
33086
33279
|
const raw = (0, import_node_fs31.readFileSync)(opts.batch, "utf8");
|
|
33087
33280
|
specs = JSON.parse(raw);
|
|
33088
|
-
if (!Array.isArray(specs))
|
|
33281
|
+
if (!Array.isArray(specs)) {
|
|
33282
|
+
const top = specs === null ? "null" : typeof specs === "object" ? `an object with keys: ${Object.keys(specs).join(", ") || "(none)"}` : `a ${typeof specs}`;
|
|
33283
|
+
throw new Error(`batch file must contain a JSON array, but its top level is ${top}`);
|
|
33284
|
+
}
|
|
33089
33285
|
} catch (e) {
|
|
33090
33286
|
return fail(`issue create --batch: cannot read/parse ${opts.batch}: ${e.message}`);
|
|
33091
33287
|
}
|
|
@@ -35768,14 +35964,14 @@ function registerStageCommands(program3) {
|
|
|
35768
35964
|
}
|
|
35769
35965
|
|
|
35770
35966
|
// src/tenant-artifact.ts
|
|
35771
|
-
var
|
|
35967
|
+
var import_node_crypto11 = require("node:crypto");
|
|
35772
35968
|
var import_node_fs35 = require("node:fs");
|
|
35773
35969
|
var import_promises6 = require("node:fs/promises");
|
|
35774
35970
|
var import_node_path32 = require("node:path");
|
|
35775
35971
|
var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
35776
35972
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
35777
35973
|
async function sha256File(path2) {
|
|
35778
|
-
const hash = (0,
|
|
35974
|
+
const hash = (0, import_node_crypto11.createHash)("sha256");
|
|
35779
35975
|
for await (const chunk of (0, import_node_fs35.createReadStream)(path2)) hash.update(chunk);
|
|
35780
35976
|
return hash.digest("hex");
|
|
35781
35977
|
}
|
|
@@ -35940,7 +36136,7 @@ function renderVerifySecrets(body) {
|
|
|
35940
36136
|
// src/command-register-collaboration.ts
|
|
35941
36137
|
var import_node_child_process16 = require("node:child_process");
|
|
35942
36138
|
var import_node_fs43 = require("node:fs");
|
|
35943
|
-
var
|
|
36139
|
+
var import_promises7 = require("node:fs/promises");
|
|
35944
36140
|
|
|
35945
36141
|
// src/session-runtime.ts
|
|
35946
36142
|
var WIN32_TRAMPOLINE = "const{spawn}=require('node:child_process');const a=JSON.parse(process.argv[1]);const c=spawn(a.cmd,a.args,{stdio:'ignore',windowsHide:true,cwd:a.cwd});c.on('exit',x=>process.exit(x??0));c.on('error',()=>process.exit(1));";
|
|
@@ -35959,7 +36155,7 @@ function spawnDetachedSelf(args, deps, opts = {}) {
|
|
|
35959
36155
|
}
|
|
35960
36156
|
|
|
35961
36157
|
// src/command-register-collaboration.ts
|
|
35962
|
-
var
|
|
36158
|
+
var import_node_path40 = require("node:path");
|
|
35963
36159
|
|
|
35964
36160
|
// src/attach-to-project.ts
|
|
35965
36161
|
function boardAttachRateLimitedReceipt(resetEpochSeconds) {
|
|
@@ -36043,10 +36239,18 @@ function prHeadBehindBase(input) {
|
|
|
36043
36239
|
function prHeadUpdateRemedy(head, base) {
|
|
36044
36240
|
return `git fetch origin ${base} && git merge origin/${base} (on ${head}), resolve the conflicts, push, then rerun \u2014 docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
36045
36241
|
}
|
|
36242
|
+
var PROTECTED_PR_HEAD_BRANCHES = ["main", "master", "development", "rc"];
|
|
36243
|
+
function isProtectedPrHead(head) {
|
|
36244
|
+
return PROTECTED_PR_HEAD_BRANCHES.includes(head);
|
|
36245
|
+
}
|
|
36246
|
+
function protectedPrHeadRemedy(head, base) {
|
|
36247
|
+
return `PR head \`${head}\` is a train branch \u2014 updating it from \`${base}\` would merge \`${base}\` into \`${head}\`, promoting every unreleased commit. Never do that and never hand-merge on \`${head}\`. This is the pre-#6288 alignment shape: close this PR and let the train rebuild the alignment on a \`${base}\`-based branch that merges \`${head}\` into it (\`mmi-cli devops release --resume\`), then land THAT with a true merge \u2014 docs/Guides/train-troubleshooting.md#head-behind-base`;
|
|
36248
|
+
}
|
|
36046
36249
|
var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
|
|
36047
36250
|
var PR_HEAD_UPDATE_API_READ_DELAY_MS = 2e3;
|
|
36048
36251
|
async function updatePrHeadFromBase(input) {
|
|
36049
36252
|
const remedy = prHeadUpdateRemedy(input.head, input.base);
|
|
36253
|
+
if (isProtectedPrHead(input.head)) throw new Error(protectedPrHeadRemedy(input.head, input.base));
|
|
36050
36254
|
if (input.localCheckedOut) {
|
|
36051
36255
|
const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
36052
36256
|
await input.git(["fetch", "origin", input.base]);
|
|
@@ -36145,6 +36349,14 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
36145
36349
|
};
|
|
36146
36350
|
}
|
|
36147
36351
|
}
|
|
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
|
+
};
|
|
36148
36360
|
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
36149
36361
|
base.ciPolicy = ciPolicy;
|
|
36150
36362
|
const checksWaitError = (checksWait) => {
|
|
@@ -36762,8 +36974,8 @@ function annotateChangeMeaning(changed, policy, read) {
|
|
|
36762
36974
|
function isMeaningfulRow(file) {
|
|
36763
36975
|
return file.meaningful !== false;
|
|
36764
36976
|
}
|
|
36765
|
-
function loadPolicy(root,
|
|
36766
|
-
const raw =
|
|
36977
|
+
function loadPolicy(root, readFile9 = readFileOrNull2) {
|
|
36978
|
+
const raw = readFile9((0, import_node_path33.join)(root, POLICY_FILE));
|
|
36767
36979
|
if (raw == null) return { mandatory: [], declared: false };
|
|
36768
36980
|
return parsePolicy(raw, POLICY_FILE);
|
|
36769
36981
|
}
|
|
@@ -37454,9 +37666,287 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37454
37666
|
};
|
|
37455
37667
|
}
|
|
37456
37668
|
|
|
37457
|
-
// src/
|
|
37669
|
+
// src/review-verdict.ts
|
|
37670
|
+
var import_node_child_process14 = require("node:child_process");
|
|
37458
37671
|
var import_node_fs38 = require("node:fs");
|
|
37672
|
+
var import_node_os19 = require("node:os");
|
|
37459
37673
|
var import_node_path35 = require("node:path");
|
|
37674
|
+
var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
|
|
37675
|
+
var REVIEW_VERDICTS = ["PROCEED", "CORRECT", "ESCALATE"];
|
|
37676
|
+
function isReviewVerdict(value) {
|
|
37677
|
+
return typeof value === "string" && REVIEW_VERDICTS.includes(value);
|
|
37678
|
+
}
|
|
37679
|
+
function renderReviewVerdictComment(input) {
|
|
37680
|
+
const payload = {
|
|
37681
|
+
v: 1,
|
|
37682
|
+
patch: input.patch,
|
|
37683
|
+
head: input.head,
|
|
37684
|
+
verdict: input.verdict,
|
|
37685
|
+
scope: input.scope,
|
|
37686
|
+
risk: input.risk,
|
|
37687
|
+
unverified: input.unverified,
|
|
37688
|
+
reviewer: input.reviewer
|
|
37689
|
+
};
|
|
37690
|
+
const findings = input.findings?.trim();
|
|
37691
|
+
return `${REVIEW_VERDICT_MARKER}
|
|
37692
|
+
\`\`\`json
|
|
37693
|
+
${JSON.stringify(payload, null, 2)}
|
|
37694
|
+
\`\`\`
|
|
37695
|
+
${findings ? `
|
|
37696
|
+
${findings}
|
|
37697
|
+
` : ""}`;
|
|
37698
|
+
}
|
|
37699
|
+
function isReviewVerdictComment(body) {
|
|
37700
|
+
return body.trimStart().startsWith(REVIEW_VERDICT_MARKER);
|
|
37701
|
+
}
|
|
37702
|
+
var FENCE_RE = /^(?:`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n(?:`{3,}|~{3,})\s*$/m;
|
|
37703
|
+
function parseReviewVerdictComment(body) {
|
|
37704
|
+
if (!isReviewVerdictComment(body)) return void 0;
|
|
37705
|
+
const rest = body.trimStart().slice(REVIEW_VERDICT_MARKER.length);
|
|
37706
|
+
const fence = FENCE_RE.exec(rest);
|
|
37707
|
+
if (!fence) return void 0;
|
|
37708
|
+
let parsed;
|
|
37709
|
+
try {
|
|
37710
|
+
parsed = JSON.parse(fence[1]);
|
|
37711
|
+
} catch {
|
|
37712
|
+
return void 0;
|
|
37713
|
+
}
|
|
37714
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
37715
|
+
const p = parsed;
|
|
37716
|
+
if (p.v !== 1 || !isReviewVerdict(p.verdict)) return void 0;
|
|
37717
|
+
if (typeof p.patch !== "string" || !/^[0-9a-f]{40}$/.test(p.patch)) return void 0;
|
|
37718
|
+
if (typeof p.head !== "string" || typeof p.scope !== "string" || typeof p.risk !== "string" || typeof p.reviewer !== "string") return void 0;
|
|
37719
|
+
const unverified = Array.isArray(p.unverified) ? p.unverified.filter((u) => typeof u === "string") : [];
|
|
37720
|
+
return { v: 1, patch: p.patch, head: p.head, verdict: p.verdict, scope: p.scope, risk: p.risk, unverified, reviewer: p.reviewer };
|
|
37721
|
+
}
|
|
37722
|
+
function latestReviewComment(comments) {
|
|
37723
|
+
let latest;
|
|
37724
|
+
for (const c of comments) {
|
|
37725
|
+
if (!isReviewVerdictComment(c.body)) continue;
|
|
37726
|
+
if (!latest || c.createdAt > latest.createdAt || c.createdAt === latest.createdAt && (c.id ?? 0) >= (latest.id ?? 0)) latest = c;
|
|
37727
|
+
}
|
|
37728
|
+
return latest;
|
|
37729
|
+
}
|
|
37730
|
+
function evaluateReviewVerdict(comments, currentPatchId) {
|
|
37731
|
+
const latest = latestReviewComment(comments);
|
|
37732
|
+
if (!latest) return { ok: false, reason: "none" };
|
|
37733
|
+
const payload = parseReviewVerdictComment(latest.body);
|
|
37734
|
+
if (!payload) return { ok: false, reason: "malformed" };
|
|
37735
|
+
if (payload.verdict !== "PROCEED") return { ok: false, reason: "not-proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37736
|
+
if (payload.patch !== currentPatchId) return { ok: false, reason: "stale", verdict: payload.verdict, patch: payload.patch };
|
|
37737
|
+
return { ok: true, reason: "proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37738
|
+
}
|
|
37739
|
+
function evaluateTrustedReviewVerdict(comments, patch, head) {
|
|
37740
|
+
const latest = latestReviewComment(comments);
|
|
37741
|
+
if (!latest) return { ok: false, reason: "none" };
|
|
37742
|
+
const identity = { commentId: latest.id, commentCreatedAt: latest.createdAt };
|
|
37743
|
+
if (latest.author?.toLowerCase() !== "jervaise") return { ...identity, ok: false, reason: "untrusted-author" };
|
|
37744
|
+
const evaluation = evaluateReviewVerdict([latest], patch);
|
|
37745
|
+
if (!evaluation.ok) return { ...identity, ok: false, reason: evaluation.reason };
|
|
37746
|
+
if (parseReviewVerdictComment(latest.body)?.head !== head) return { ...identity, ok: false, reason: "stale-head" };
|
|
37747
|
+
return { ...identity, ok: true, reason: "proceed" };
|
|
37748
|
+
}
|
|
37749
|
+
async function checkPrReview(number, repo, head, deps = {
|
|
37750
|
+
readHead: readPrHeadSha,
|
|
37751
|
+
readComments: readPrIssueComments,
|
|
37752
|
+
computePatch: computePrPatchId
|
|
37753
|
+
}) {
|
|
37754
|
+
const base = { repo, number, head };
|
|
37755
|
+
if (!/^[1-9][0-9]*$/.test(number) || !/^[\w.-]+\/[\w.-]+$/.test(repo) || !/^[0-9a-f]{40}$/.test(head)) {
|
|
37756
|
+
return { ...base, ok: false, reason: "invalid-input", detail: "expected a PR number, owner/repo and exact lowercase 40-character head SHA" };
|
|
37757
|
+
}
|
|
37758
|
+
try {
|
|
37759
|
+
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
37760
|
+
const [comments, patch] = await Promise.all([deps.readComments(number, repo), deps.computePatch(number, repo)]);
|
|
37761
|
+
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
37762
|
+
return { ...base, patch, ...evaluateTrustedReviewVerdict(comments, patch, head) };
|
|
37763
|
+
} catch (e) {
|
|
37764
|
+
return { ...base, ok: false, reason: "unreadable", detail: e.message };
|
|
37765
|
+
}
|
|
37766
|
+
}
|
|
37767
|
+
function computePrPatchId(number, repo) {
|
|
37768
|
+
return new Promise((resolve7, reject) => {
|
|
37769
|
+
const gh = (0, import_node_child_process14.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
37770
|
+
const git3 = (0, import_node_child_process14.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
37771
|
+
let out = "";
|
|
37772
|
+
let ghErr = "";
|
|
37773
|
+
let gitErr = "";
|
|
37774
|
+
const timer = setTimeout(() => {
|
|
37775
|
+
gh.kill();
|
|
37776
|
+
git3.kill();
|
|
37777
|
+
reject(new Error(`patch-id: timed out after ${GC_GH_TIMEOUT_MS4}ms`));
|
|
37778
|
+
}, GC_GH_TIMEOUT_MS4);
|
|
37779
|
+
gh.stdout.pipe(git3.stdin);
|
|
37780
|
+
gh.stderr.on("data", (d) => {
|
|
37781
|
+
ghErr += d.toString();
|
|
37782
|
+
});
|
|
37783
|
+
git3.stderr.on("data", (d) => {
|
|
37784
|
+
gitErr += d.toString();
|
|
37785
|
+
});
|
|
37786
|
+
git3.stdout.on("data", (d) => {
|
|
37787
|
+
out += d.toString();
|
|
37788
|
+
});
|
|
37789
|
+
gh.on("error", (e) => {
|
|
37790
|
+
clearTimeout(timer);
|
|
37791
|
+
reject(e);
|
|
37792
|
+
});
|
|
37793
|
+
git3.on("error", (e) => {
|
|
37794
|
+
clearTimeout(timer);
|
|
37795
|
+
reject(e);
|
|
37796
|
+
});
|
|
37797
|
+
let ghCode;
|
|
37798
|
+
let gitCode;
|
|
37799
|
+
const finish = () => {
|
|
37800
|
+
if (ghCode === void 0 || gitCode === void 0) return;
|
|
37801
|
+
clearTimeout(timer);
|
|
37802
|
+
if (ghCode !== 0) return reject(Object.assign(new Error(`gh pr diff ${number} --repo ${repo} exited ${ghCode}: ${ghErr.trim()}`), { stderr: ghErr }));
|
|
37803
|
+
if (gitCode !== 0) return reject(new Error(`git patch-id --stable exited ${gitCode}: ${gitErr.trim()}`));
|
|
37804
|
+
const id = out.trim().split(/\s+/)[0];
|
|
37805
|
+
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"})`));
|
|
37806
|
+
resolve7(id);
|
|
37807
|
+
};
|
|
37808
|
+
gh.on("close", (code) => {
|
|
37809
|
+
ghCode = code;
|
|
37810
|
+
finish();
|
|
37811
|
+
});
|
|
37812
|
+
git3.on("close", (code) => {
|
|
37813
|
+
gitCode = code;
|
|
37814
|
+
finish();
|
|
37815
|
+
});
|
|
37816
|
+
git3.stdin.on("error", (e) => {
|
|
37817
|
+
clearTimeout(timer);
|
|
37818
|
+
gh.kill();
|
|
37819
|
+
git3.kill();
|
|
37820
|
+
reject(e);
|
|
37821
|
+
});
|
|
37822
|
+
});
|
|
37823
|
+
}
|
|
37824
|
+
async function readPrHeadSha(number, repo) {
|
|
37825
|
+
const { stdout } = await execFileP("gh", ["api", `repos/${repo}/pulls/${number}`, "--jq", ".head.sha"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
37826
|
+
const sha = stdout.trim();
|
|
37827
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`could not read PR #${number} head sha`);
|
|
37828
|
+
return sha;
|
|
37829
|
+
}
|
|
37830
|
+
async function readPrIssueComments(number, repo) {
|
|
37831
|
+
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 });
|
|
37832
|
+
const comments = parseNdjsonLines(stdout);
|
|
37833
|
+
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");
|
|
37834
|
+
return comments;
|
|
37835
|
+
}
|
|
37836
|
+
async function postPrCommentFromFile(number, repo, body) {
|
|
37837
|
+
const dir = (0, import_node_fs38.mkdtempSync)((0, import_node_path35.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
|
|
37838
|
+
const path2 = (0, import_node_path35.join)(dir, "body.md");
|
|
37839
|
+
try {
|
|
37840
|
+
(0, import_node_fs38.writeFileSync)(path2, body, "utf8");
|
|
37841
|
+
const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
37842
|
+
return stdout.trim();
|
|
37843
|
+
} finally {
|
|
37844
|
+
try {
|
|
37845
|
+
(0, import_node_fs38.rmSync)(dir, { recursive: true, force: true });
|
|
37846
|
+
} catch {
|
|
37847
|
+
}
|
|
37848
|
+
}
|
|
37849
|
+
}
|
|
37850
|
+
|
|
37851
|
+
// src/pr-mergify-queue.ts
|
|
37852
|
+
var MERGIFY_PILOT_REPO = "mutmutco/Jerv-JervCode";
|
|
37853
|
+
var MERGIFY_PILOT_VARIABLE = "JERV_BATCH_QUEUE_PILOT";
|
|
37854
|
+
var MERGIFY_READY_LABEL = "ready-to-merge";
|
|
37855
|
+
async function ghJson(args) {
|
|
37856
|
+
return JSON.parse((await execFileP("gh", ["api", ...args], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
|
|
37857
|
+
}
|
|
37858
|
+
async function usesMergifyPilot(repo, base, readVariable = async () => ghJson([`repos/${repo}/actions/variables/${MERGIFY_PILOT_VARIABLE}`])) {
|
|
37859
|
+
if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase() || base !== "development") return false;
|
|
37860
|
+
const variable = await readVariable();
|
|
37861
|
+
if (variable?.value === "true") return true;
|
|
37862
|
+
if (variable?.value === "false") return false;
|
|
37863
|
+
throw new Error(`Mergify pilot activation is missing or malformed on ${repo}; refusing native merge`);
|
|
37864
|
+
}
|
|
37865
|
+
function queueCheckState(checks, head) {
|
|
37866
|
+
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];
|
|
37867
|
+
if (!check) return "refused";
|
|
37868
|
+
if (check.conclusion && !["success", "neutral"].includes(check.conclusion)) return "refused";
|
|
37869
|
+
if (["In merge queue", "Running merge queue checks"].includes(check.output?.title ?? "")) return "pending";
|
|
37870
|
+
return "requested";
|
|
37871
|
+
}
|
|
37872
|
+
async function requestMergifyMerge(number, repo, guardedText, expectedHead, deps = {
|
|
37873
|
+
readPull: () => ghJson([`repos/${repo}/pulls/${number}`]),
|
|
37874
|
+
readChecks: async (head) => parseNdjsonLines((await execFileP("gh", [
|
|
37875
|
+
"api",
|
|
37876
|
+
"--paginate",
|
|
37877
|
+
`repos/${repo}/commits/${head}/check-runs?per_page=100`,
|
|
37878
|
+
"--jq",
|
|
37879
|
+
".check_runs[]"
|
|
37880
|
+
], { timeout: GC_GH_TIMEOUT_MS4 })).stdout),
|
|
37881
|
+
addLabel: async () => {
|
|
37882
|
+
await ghJson([`repos/${repo}/issues/${number}/labels`, "--method", "POST", "-f", `labels[]=${MERGIFY_READY_LABEL}`]);
|
|
37883
|
+
},
|
|
37884
|
+
review: (head) => checkPrReview(number, repo, head),
|
|
37885
|
+
now: () => Date.now(),
|
|
37886
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
|
|
37887
|
+
}, timeoutMs = 6e5) {
|
|
37888
|
+
let head = expectedHead ?? "";
|
|
37889
|
+
let state = "refused";
|
|
37890
|
+
const receipt = (detail) => ({ provider: "mergify", state, head, detail });
|
|
37891
|
+
try {
|
|
37892
|
+
if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase()) return receipt("repository is outside the Mergify pilot");
|
|
37893
|
+
const initial = await deps.readPull();
|
|
37894
|
+
head = expectedHead ?? initial.head.sha;
|
|
37895
|
+
const sameTarget = (pr) => /^[0-9a-f]{40}$/.test(head) && pr.head.sha === head && pr.base.ref === "development";
|
|
37896
|
+
if (!sameTarget(initial)) return receipt("PR head or target base changed");
|
|
37897
|
+
if (initial.merged === true) {
|
|
37898
|
+
state = "merged";
|
|
37899
|
+
return receipt("GitHub confirms the PR merged");
|
|
37900
|
+
}
|
|
37901
|
+
const validate = (pr) => pr.state === "open" && sameTarget(pr) && `${pr.title}
|
|
37902
|
+
${pr.body ?? ""}` === guardedText;
|
|
37903
|
+
if (!validate(initial)) return receipt("PR state, head, base or guarded title/body changed");
|
|
37904
|
+
const initialState = queueCheckState(await deps.readChecks(head), head);
|
|
37905
|
+
if (initialState === "refused") return receipt("trusted current-head Mergify check is absent or failed");
|
|
37906
|
+
if (!(await deps.review(head)).ok) return receipt("current trusted review verdict does not permit queue admission");
|
|
37907
|
+
if (!validate(await deps.readPull())) return receipt("PR changed before queue request");
|
|
37908
|
+
let mutationDetail = "";
|
|
37909
|
+
if (!initial.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
|
|
37910
|
+
try {
|
|
37911
|
+
await deps.addLabel();
|
|
37912
|
+
} catch {
|
|
37913
|
+
mutationDetail = "Label response was ambiguous; no replay was attempted. ";
|
|
37914
|
+
}
|
|
37915
|
+
}
|
|
37916
|
+
state = "requested";
|
|
37917
|
+
const deadline = deps.now() + timeoutMs;
|
|
37918
|
+
while (true) {
|
|
37919
|
+
const pr = await deps.readPull();
|
|
37920
|
+
if (!sameTarget(pr)) {
|
|
37921
|
+
state = "refused";
|
|
37922
|
+
return receipt("PR head or target base changed while queued");
|
|
37923
|
+
}
|
|
37924
|
+
if (pr.merged === true) {
|
|
37925
|
+
state = "merged";
|
|
37926
|
+
return receipt("GitHub confirms the PR merged");
|
|
37927
|
+
}
|
|
37928
|
+
if (!validate(pr)) {
|
|
37929
|
+
state = "refused";
|
|
37930
|
+
return receipt("PR state, head, base or guarded title/body changed while queued");
|
|
37931
|
+
}
|
|
37932
|
+
state = queueCheckState(await deps.readChecks(head), head);
|
|
37933
|
+
if (state === "refused") return receipt("trusted current-head Mergify check is absent or failed");
|
|
37934
|
+
if (!pr.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
|
|
37935
|
+
state = "refused";
|
|
37936
|
+
return receipt(`${mutationDetail}Queue request label is absent`);
|
|
37937
|
+
}
|
|
37938
|
+
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`);
|
|
37939
|
+
await deps.sleep(3e4);
|
|
37940
|
+
}
|
|
37941
|
+
} catch {
|
|
37942
|
+
state = "refused";
|
|
37943
|
+
return receipt("Mergify or GitHub state is unreadable; no native merge was attempted");
|
|
37944
|
+
}
|
|
37945
|
+
}
|
|
37946
|
+
|
|
37947
|
+
// src/post-merge-recon.ts
|
|
37948
|
+
var import_node_fs39 = require("node:fs");
|
|
37949
|
+
var import_node_path36 = require("node:path");
|
|
37460
37950
|
|
|
37461
37951
|
// src/cross-repo-filing-issue.ts
|
|
37462
37952
|
function crossRepoFilingRetryCommand(prRepo, prNumber) {
|
|
@@ -37622,16 +38112,16 @@ function buildPostMergeReconRecovery(input) {
|
|
|
37622
38112
|
}
|
|
37623
38113
|
function writePostMergeReconRecovery(cwd, recovery) {
|
|
37624
38114
|
const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
|
|
37625
|
-
(0,
|
|
37626
|
-
(0,
|
|
38115
|
+
(0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(path2), { recursive: true });
|
|
38116
|
+
(0, import_node_fs39.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
|
|
37627
38117
|
`, "utf8");
|
|
37628
38118
|
return path2;
|
|
37629
38119
|
}
|
|
37630
38120
|
function clearPostMergeReconRecovery(cwd, repo, pr) {
|
|
37631
38121
|
const path2 = postMergeReconStatePath(cwd, repo, pr);
|
|
37632
|
-
if (!(0,
|
|
38122
|
+
if (!(0, import_node_fs39.existsSync)(path2)) return;
|
|
37633
38123
|
try {
|
|
37634
|
-
(0,
|
|
38124
|
+
(0, import_node_fs39.unlinkSync)(path2);
|
|
37635
38125
|
} catch {
|
|
37636
38126
|
}
|
|
37637
38127
|
}
|
|
@@ -37661,184 +38151,6 @@ function postMergeReconWarnings(input) {
|
|
|
37661
38151
|
return lines2;
|
|
37662
38152
|
}
|
|
37663
38153
|
|
|
37664
|
-
// src/review-verdict.ts
|
|
37665
|
-
var import_node_child_process14 = require("node:child_process");
|
|
37666
|
-
var import_node_fs39 = require("node:fs");
|
|
37667
|
-
var import_node_os19 = require("node:os");
|
|
37668
|
-
var import_node_path36 = require("node:path");
|
|
37669
|
-
var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
|
|
37670
|
-
var REVIEW_WAIVER_MARKER = "<!-- zeroci-review-waived v1 -->";
|
|
37671
|
-
var REVIEW_VERDICTS = ["PROCEED", "CORRECT", "ESCALATE"];
|
|
37672
|
-
function isReviewVerdict(value) {
|
|
37673
|
-
return typeof value === "string" && REVIEW_VERDICTS.includes(value);
|
|
37674
|
-
}
|
|
37675
|
-
function renderReviewVerdictComment(input) {
|
|
37676
|
-
const payload = {
|
|
37677
|
-
v: 1,
|
|
37678
|
-
patch: input.patch,
|
|
37679
|
-
head: input.head,
|
|
37680
|
-
verdict: input.verdict,
|
|
37681
|
-
scope: input.scope,
|
|
37682
|
-
risk: input.risk,
|
|
37683
|
-
unverified: input.unverified,
|
|
37684
|
-
reviewer: input.reviewer
|
|
37685
|
-
};
|
|
37686
|
-
const findings = input.findings?.trim();
|
|
37687
|
-
return `${REVIEW_VERDICT_MARKER}
|
|
37688
|
-
\`\`\`json
|
|
37689
|
-
${JSON.stringify(payload, null, 2)}
|
|
37690
|
-
\`\`\`
|
|
37691
|
-
${findings ? `
|
|
37692
|
-
${findings}
|
|
37693
|
-
` : ""}`;
|
|
37694
|
-
}
|
|
37695
|
-
function isReviewVerdictComment(body) {
|
|
37696
|
-
return body.trimStart().startsWith(REVIEW_VERDICT_MARKER);
|
|
37697
|
-
}
|
|
37698
|
-
var FENCE_RE = /^(?:`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n(?:`{3,}|~{3,})\s*$/m;
|
|
37699
|
-
function parseReviewVerdictComment(body) {
|
|
37700
|
-
if (!isReviewVerdictComment(body)) return void 0;
|
|
37701
|
-
const rest = body.trimStart().slice(REVIEW_VERDICT_MARKER.length);
|
|
37702
|
-
const fence = FENCE_RE.exec(rest);
|
|
37703
|
-
if (!fence) return void 0;
|
|
37704
|
-
let parsed;
|
|
37705
|
-
try {
|
|
37706
|
-
parsed = JSON.parse(fence[1]);
|
|
37707
|
-
} catch {
|
|
37708
|
-
return void 0;
|
|
37709
|
-
}
|
|
37710
|
-
if (!parsed || typeof parsed !== "object") return void 0;
|
|
37711
|
-
const p = parsed;
|
|
37712
|
-
if (p.v !== 1 || !isReviewVerdict(p.verdict)) return void 0;
|
|
37713
|
-
if (typeof p.patch !== "string" || !/^[0-9a-f]{40}$/.test(p.patch)) return void 0;
|
|
37714
|
-
if (typeof p.head !== "string" || typeof p.scope !== "string" || typeof p.risk !== "string" || typeof p.reviewer !== "string") return void 0;
|
|
37715
|
-
const unverified = Array.isArray(p.unverified) ? p.unverified.filter((u) => typeof u === "string") : [];
|
|
37716
|
-
return { v: 1, patch: p.patch, head: p.head, verdict: p.verdict, scope: p.scope, risk: p.risk, unverified, reviewer: p.reviewer };
|
|
37717
|
-
}
|
|
37718
|
-
function evaluateReviewVerdict(comments, currentPatchId) {
|
|
37719
|
-
let latest;
|
|
37720
|
-
for (const c of comments) {
|
|
37721
|
-
if (!isReviewVerdictComment(c.body)) continue;
|
|
37722
|
-
if (!latest || c.createdAt >= latest.createdAt) latest = c;
|
|
37723
|
-
}
|
|
37724
|
-
if (!latest) return { ok: false, reason: "none" };
|
|
37725
|
-
const payload = parseReviewVerdictComment(latest.body);
|
|
37726
|
-
if (!payload) return { ok: false, reason: "malformed" };
|
|
37727
|
-
if (payload.verdict !== "PROCEED") return { ok: false, reason: "not-proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37728
|
-
if (payload.patch !== currentPatchId) return { ok: false, reason: "stale", verdict: payload.verdict, patch: payload.patch };
|
|
37729
|
-
return { ok: true, reason: "proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37730
|
-
}
|
|
37731
|
-
function reviewVerdictRemedyCommand(number, repo) {
|
|
37732
|
-
return `mmi-cli devops pr review-verdict ${number} --repo ${repo} --verdict PROCEED --scope "<one line>" --risk "<one line>" --reviewer <seat-or-model-id>`;
|
|
37733
|
-
}
|
|
37734
|
-
function reviewVerdictRefusalMessage(context, number, repo, evaluation, currentPatchId) {
|
|
37735
|
-
const why = {
|
|
37736
|
-
none: "no review verdict comment on the PR",
|
|
37737
|
-
stale: `the latest verdict is bound to patch ${evaluation.patch} but the PR's current diff is ${currentPatchId} \u2014 the content changed since the review`,
|
|
37738
|
-
"not-proceed": `the latest verdict is ${evaluation.verdict}, not PROCEED`,
|
|
37739
|
-
malformed: "the latest verdict comment is malformed (marker present, JSON block unreadable or wrong shape)",
|
|
37740
|
-
proceed: "proceed"
|
|
37741
|
-
}[evaluation.reason];
|
|
37742
|
-
return `${context}: REVIEW VERDICT REQUIRED (#6255) \u2014 ${why}. Review PR #${number} and post the verdict, then rerun:
|
|
37743
|
-
${reviewVerdictRemedyCommand(number, repo)}
|
|
37744
|
-
To land without a review, pass --without-review "<reason>" \u2014 it posts a waiver comment on the PR.`;
|
|
37745
|
-
}
|
|
37746
|
-
function renderReviewWaiverComment(reason, actor) {
|
|
37747
|
-
return `${REVIEW_WAIVER_MARKER}
|
|
37748
|
-
Review gate waived by @${actor}: ${reason.trim()}
|
|
37749
|
-
`;
|
|
37750
|
-
}
|
|
37751
|
-
function computePrPatchId(number, repo) {
|
|
37752
|
-
return new Promise((resolve7, reject) => {
|
|
37753
|
-
const gh = (0, import_node_child_process14.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
37754
|
-
const git3 = (0, import_node_child_process14.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
37755
|
-
let out = "";
|
|
37756
|
-
let ghErr = "";
|
|
37757
|
-
let gitErr = "";
|
|
37758
|
-
const timer = setTimeout(() => {
|
|
37759
|
-
gh.kill();
|
|
37760
|
-
git3.kill();
|
|
37761
|
-
reject(new Error(`patch-id: timed out after ${GC_GH_TIMEOUT_MS4}ms`));
|
|
37762
|
-
}, GC_GH_TIMEOUT_MS4);
|
|
37763
|
-
gh.stdout.pipe(git3.stdin);
|
|
37764
|
-
gh.stderr.on("data", (d) => {
|
|
37765
|
-
ghErr += d.toString();
|
|
37766
|
-
});
|
|
37767
|
-
git3.stderr.on("data", (d) => {
|
|
37768
|
-
gitErr += d.toString();
|
|
37769
|
-
});
|
|
37770
|
-
git3.stdout.on("data", (d) => {
|
|
37771
|
-
out += d.toString();
|
|
37772
|
-
});
|
|
37773
|
-
gh.on("error", (e) => {
|
|
37774
|
-
clearTimeout(timer);
|
|
37775
|
-
reject(e);
|
|
37776
|
-
});
|
|
37777
|
-
git3.on("error", (e) => {
|
|
37778
|
-
clearTimeout(timer);
|
|
37779
|
-
reject(e);
|
|
37780
|
-
});
|
|
37781
|
-
let ghCode;
|
|
37782
|
-
gh.on("close", (code) => {
|
|
37783
|
-
ghCode = code;
|
|
37784
|
-
});
|
|
37785
|
-
git3.on("close", (code) => {
|
|
37786
|
-
clearTimeout(timer);
|
|
37787
|
-
if (ghCode) return reject(Object.assign(new Error(`gh pr diff ${number} --repo ${repo} exited ${ghCode}: ${ghErr.trim()}`), { stderr: ghErr }));
|
|
37788
|
-
if (code) return reject(new Error(`git patch-id --stable exited ${code}: ${gitErr.trim()}`));
|
|
37789
|
-
const id = out.trim().split(/\s+/)[0];
|
|
37790
|
-
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"})`));
|
|
37791
|
-
resolve7(id);
|
|
37792
|
-
});
|
|
37793
|
-
});
|
|
37794
|
-
}
|
|
37795
|
-
async function readPrHeadSha(number, repo) {
|
|
37796
|
-
const { stdout } = await execFileP("gh", ["api", `repos/${repo}/pulls/${number}`, "--jq", ".head.sha"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
37797
|
-
const sha = stdout.trim();
|
|
37798
|
-
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`could not read PR #${number} head sha`);
|
|
37799
|
-
return sha;
|
|
37800
|
-
}
|
|
37801
|
-
async function readPrIssueComments(number, repo) {
|
|
37802
|
-
const { stdout } = await execFileP("gh", ["api", "--paginate", `repos/${repo}/issues/${number}/comments?per_page=100`, "--jq", ".[] | {body, createdAt: .created_at}"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
37803
|
-
return parseNdjsonLines(stdout);
|
|
37804
|
-
}
|
|
37805
|
-
async function postPrCommentFromFile(number, repo, body) {
|
|
37806
|
-
const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path36.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
|
|
37807
|
-
const path2 = (0, import_node_path36.join)(dir, "body.md");
|
|
37808
|
-
try {
|
|
37809
|
-
(0, import_node_fs39.writeFileSync)(path2, body, "utf8");
|
|
37810
|
-
const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
37811
|
-
return stdout.trim();
|
|
37812
|
-
} finally {
|
|
37813
|
-
try {
|
|
37814
|
-
(0, import_node_fs39.rmSync)(dir, { recursive: true, force: true });
|
|
37815
|
-
} catch {
|
|
37816
|
-
}
|
|
37817
|
-
}
|
|
37818
|
-
}
|
|
37819
|
-
async function requireReviewVerdict(context, number, repo, opts) {
|
|
37820
|
-
if (opts.withoutReview !== void 0) {
|
|
37821
|
-
const reason = opts.withoutReview.trim();
|
|
37822
|
-
if (!reason) throw new Error(`${context}: --without-review needs a non-empty reason`);
|
|
37823
|
-
const actor = await execFileP("gh", ["api", "user", "--jq", ".login"], { timeout: GC_GH_TIMEOUT_MS4 }).then((r) => r.stdout.trim()).catch(() => "") || "unknown";
|
|
37824
|
-
await postPrCommentFromFile(number, repo, renderReviewWaiverComment(reason, actor));
|
|
37825
|
-
console.warn(`${context}: review gate WAIVED by @${actor} \u2014 ${reason} (waiver comment posted on PR #${number})`);
|
|
37826
|
-
return void 0;
|
|
37827
|
-
}
|
|
37828
|
-
let comments;
|
|
37829
|
-
let currentPatchId;
|
|
37830
|
-
try {
|
|
37831
|
-
[comments, currentPatchId] = await Promise.all([readPrIssueComments(number, repo), computePrPatchId(number, repo)]);
|
|
37832
|
-
} catch (e) {
|
|
37833
|
-
const detail = e.message || String(e);
|
|
37834
|
-
const limited = isGitHubRateLimitError(e) ? " (gh rate-limited \u2014 retry after the pool resets)" : "";
|
|
37835
|
-
throw new Error(`${context}: cannot read PR #${number}'s review verdict${limited} \u2014 ${detail}. Refusing to merge WITHOUT the review gate (#6255).`);
|
|
37836
|
-
}
|
|
37837
|
-
const evaluation = evaluateReviewVerdict(comments, currentPatchId);
|
|
37838
|
-
if (evaluation.ok) return void 0;
|
|
37839
|
-
return reviewVerdictRefusalMessage(context, number, repo, evaluation, currentPatchId);
|
|
37840
|
-
}
|
|
37841
|
-
|
|
37842
38154
|
// src/pr-create-docs-check.ts
|
|
37843
38155
|
var import_node_child_process15 = require("node:child_process");
|
|
37844
38156
|
var GIT_TIMEOUT_MS2 = 15e3;
|
|
@@ -37960,120 +38272,6 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
37960
38272
|
};
|
|
37961
38273
|
}
|
|
37962
38274
|
|
|
37963
|
-
// src/pr-create-zeroci.ts
|
|
37964
|
-
var import_promises7 = require("node:fs/promises");
|
|
37965
|
-
var import_node_path38 = require("node:path");
|
|
37966
|
-
|
|
37967
|
-
// src/jervcode-node-modules-cleanup.ts
|
|
37968
|
-
var import_node_fs40 = require("node:fs");
|
|
37969
|
-
var import_node_os20 = require("node:os");
|
|
37970
|
-
var import_node_path37 = require("node:path");
|
|
37971
|
-
var JERVCODE_PACKAGE_ENTRY = (0, import_node_path37.join)("node_modules", "@jervaise", "jervcode", "dist", "launcher-entry.js");
|
|
37972
|
-
var WIN_NAMES2 = ["jervcode.cmd", "jervcode"];
|
|
37973
|
-
var POSIX_NAMES2 = ["jervcode"];
|
|
37974
|
-
var NODE_MODULES_CLEANUP_TIMEOUT_MS = 3e5;
|
|
37975
|
-
function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os20.homedir)(), platform2 = process.platform) {
|
|
37976
|
-
const names = platform2 === "win32" ? WIN_NAMES2 : POSIX_NAMES2;
|
|
37977
|
-
const out = [];
|
|
37978
|
-
for (const dir of jervCliCandidateDirs(env, home, platform2)) {
|
|
37979
|
-
for (const name of names) out.push((0, import_node_path37.join)(dir, name));
|
|
37980
|
-
}
|
|
37981
|
-
return out;
|
|
37982
|
-
}
|
|
37983
|
-
function resolveJervcodePath(env = process.env, home = (0, import_node_os20.homedir)(), platform2 = process.platform, exists = import_node_fs40.existsSync) {
|
|
37984
|
-
for (const candidate of jervcodeCandidatePaths(env, home, platform2)) {
|
|
37985
|
-
if (exists(candidate)) return candidate;
|
|
37986
|
-
}
|
|
37987
|
-
return void 0;
|
|
37988
|
-
}
|
|
37989
|
-
function jervcodeExecFileArgs(args, opts = {}) {
|
|
37990
|
-
const platform2 = opts.platform ?? process.platform;
|
|
37991
|
-
const exists = opts.exists ?? import_node_fs40.existsSync;
|
|
37992
|
-
const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0, import_node_os20.homedir)(), platform2, exists);
|
|
37993
|
-
if (resolved) {
|
|
37994
|
-
const entry = (0, import_node_path37.join)((0, import_node_path37.join)(resolved, ".."), JERVCODE_PACKAGE_ENTRY);
|
|
37995
|
-
if (exists(entry)) {
|
|
37996
|
-
return { file: opts.execPath ?? process.execPath, args: [entry, ...args], via: "node-entry" };
|
|
37997
|
-
}
|
|
37998
|
-
}
|
|
37999
|
-
const bin = resolved ?? "jervcode";
|
|
38000
|
-
if (platform2 === "win32") {
|
|
38001
|
-
return { file: "cmd.exe", args: ["/c", bin, ...args], via: resolved ? "cmd-shim" : "bare" };
|
|
38002
|
-
}
|
|
38003
|
-
return { file: bin, args: [...args], via: resolved ? "posix" : "bare" };
|
|
38004
|
-
}
|
|
38005
|
-
async function removeWorktreeNodeModulesViaHelper(wtPath, opts = {}) {
|
|
38006
|
-
const { cwd, timeoutMs = NODE_MODULES_CLEANUP_TIMEOUT_MS } = opts;
|
|
38007
|
-
const env = opts.env ?? process.env;
|
|
38008
|
-
const exec = opts.exec ?? execFileP;
|
|
38009
|
-
const candidates = jervcodeCandidatePaths(env, opts.home, opts.platform ?? process.platform);
|
|
38010
|
-
const plan = jervcodeExecFileArgs(["worktree-node-modules-cleanup", "--worktree", wtPath], opts);
|
|
38011
|
-
const execOptions = { timeout: timeoutMs, ...cwd ? { cwd } : {} };
|
|
38012
|
-
try {
|
|
38013
|
-
await exec(plan.file, plan.args, execOptions);
|
|
38014
|
-
return { ok: true };
|
|
38015
|
-
} catch (e) {
|
|
38016
|
-
const err = e;
|
|
38017
|
-
const base = formatJervCliSpawnFailure(err, plan, candidates);
|
|
38018
|
-
const stderr = typeof err.stderr === "string" ? err.stderr.trim() : "";
|
|
38019
|
-
const detail = stderr.split("\n")[0] || base;
|
|
38020
|
-
return { ok: false, error: `jervcode worktree-node-modules-cleanup failed: ${detail}` };
|
|
38021
|
-
}
|
|
38022
|
-
}
|
|
38023
|
-
|
|
38024
|
-
// src/pr-create-zeroci.ts
|
|
38025
|
-
var GATE_WORKFLOW_PATH = ".github/workflows/gate.yml";
|
|
38026
|
-
var ZEROCI_MINT_MARKER = "zeroci-mint: seat";
|
|
38027
|
-
var ZEROCI_MINT_TIMEOUT_MS = 18e5;
|
|
38028
|
-
var ZEROCI_MINT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
38029
|
-
function createPrCreateZeroCiDeps() {
|
|
38030
|
-
const git3 = createPrCreateDocsIndexDeps();
|
|
38031
|
-
return {
|
|
38032
|
-
worktreeRoot: git3.worktreeRoot,
|
|
38033
|
-
originRepo: git3.originRepo,
|
|
38034
|
-
readGateWorkflow: async (root) => {
|
|
38035
|
-
try {
|
|
38036
|
-
return await (0, import_promises7.readFile)((0, import_node_path38.join)(root, GATE_WORKFLOW_PATH), "utf8");
|
|
38037
|
-
} catch {
|
|
38038
|
-
return void 0;
|
|
38039
|
-
}
|
|
38040
|
-
},
|
|
38041
|
-
mint: async (root) => {
|
|
38042
|
-
const plan = jervcodeExecFileArgs(["zeroci-receipt", "--from-gate"]);
|
|
38043
|
-
try {
|
|
38044
|
-
await execFileHard(plan.file, plan.args, {
|
|
38045
|
-
cwd: root,
|
|
38046
|
-
timeout: ZEROCI_MINT_TIMEOUT_MS,
|
|
38047
|
-
maxBuffer: ZEROCI_MINT_MAX_BUFFER,
|
|
38048
|
-
step: "jervcode zeroci-receipt --from-gate"
|
|
38049
|
-
});
|
|
38050
|
-
} catch (e) {
|
|
38051
|
-
const err = e;
|
|
38052
|
-
const stderr = typeof err.stderr === "string" ? err.stderr.trim() : "";
|
|
38053
|
-
throw new Error(stderr.split("\n").filter(Boolean).pop() || formatJervCliSpawnFailure(err, plan, jervcodeCandidatePaths()));
|
|
38054
|
-
}
|
|
38055
|
-
}
|
|
38056
|
-
};
|
|
38057
|
-
}
|
|
38058
|
-
async function mintZeroCiReceipt(opts, deps) {
|
|
38059
|
-
if (opts.zeroci === false) return void 0;
|
|
38060
|
-
const root = await deps.worktreeRoot();
|
|
38061
|
-
if (!root) return void 0;
|
|
38062
|
-
if (opts.repo) {
|
|
38063
|
-
const origin = await deps.originRepo(root);
|
|
38064
|
-
if (!origin || origin.toLowerCase() !== opts.repo.toLowerCase()) return void 0;
|
|
38065
|
-
}
|
|
38066
|
-
const gate = await deps.readGateWorkflow(root);
|
|
38067
|
-
if (!gate || !gate.includes("zeroci-verify")) return void 0;
|
|
38068
|
-
if (!gate.includes(ZEROCI_MINT_MARKER)) return void 0;
|
|
38069
|
-
try {
|
|
38070
|
-
await deps.mint(root);
|
|
38071
|
-
return void 0;
|
|
38072
|
-
} catch (e) {
|
|
38073
|
-
return `pr create: WARNING \u2014 ZeroCI receipt not minted, opening the PR anyway (the gate runs in full): ${e.message}`;
|
|
38074
|
-
}
|
|
38075
|
-
}
|
|
38076
|
-
|
|
38077
38275
|
// src/pr-create-claim-guard.ts
|
|
38078
38276
|
var CLAIM_GUARD_RATE_LIMIT_WAIT_CAP_MS = 3e4;
|
|
38079
38277
|
function withRateLimitRetry(client, seams = {}) {
|
|
@@ -38132,11 +38330,68 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
|
38132
38330
|
|
|
38133
38331
|
// src/worktree-merge-cleanup.ts
|
|
38134
38332
|
var import_node_fs42 = require("node:fs");
|
|
38135
|
-
var
|
|
38333
|
+
var import_node_path39 = require("node:path");
|
|
38334
|
+
|
|
38335
|
+
// src/jervcode-node-modules-cleanup.ts
|
|
38336
|
+
var import_node_fs40 = require("node:fs");
|
|
38337
|
+
var import_node_os20 = require("node:os");
|
|
38338
|
+
var import_node_path37 = require("node:path");
|
|
38339
|
+
var JERVCODE_PACKAGE_ENTRY = (0, import_node_path37.join)("node_modules", "@jervaise", "jervcode", "dist", "launcher-entry.js");
|
|
38340
|
+
var WIN_NAMES2 = ["jervcode.cmd", "jervcode"];
|
|
38341
|
+
var POSIX_NAMES2 = ["jervcode"];
|
|
38342
|
+
var NODE_MODULES_CLEANUP_TIMEOUT_MS = 3e5;
|
|
38343
|
+
function jervcodeCandidatePaths(env = process.env, home = (0, import_node_os20.homedir)(), platform2 = process.platform) {
|
|
38344
|
+
const names = platform2 === "win32" ? WIN_NAMES2 : POSIX_NAMES2;
|
|
38345
|
+
const out = [];
|
|
38346
|
+
for (const dir of jervCliCandidateDirs(env, home, platform2)) {
|
|
38347
|
+
for (const name of names) out.push((0, import_node_path37.join)(dir, name));
|
|
38348
|
+
}
|
|
38349
|
+
return out;
|
|
38350
|
+
}
|
|
38351
|
+
function resolveJervcodePath(env = process.env, home = (0, import_node_os20.homedir)(), platform2 = process.platform, exists = import_node_fs40.existsSync) {
|
|
38352
|
+
for (const candidate of jervcodeCandidatePaths(env, home, platform2)) {
|
|
38353
|
+
if (exists(candidate)) return candidate;
|
|
38354
|
+
}
|
|
38355
|
+
return void 0;
|
|
38356
|
+
}
|
|
38357
|
+
function jervcodeExecFileArgs(args, opts = {}) {
|
|
38358
|
+
const platform2 = opts.platform ?? process.platform;
|
|
38359
|
+
const exists = opts.exists ?? import_node_fs40.existsSync;
|
|
38360
|
+
const resolved = resolveJervcodePath(opts.env ?? process.env, opts.home ?? (0, import_node_os20.homedir)(), platform2, exists);
|
|
38361
|
+
if (resolved) {
|
|
38362
|
+
const entry = (0, import_node_path37.join)((0, import_node_path37.join)(resolved, ".."), JERVCODE_PACKAGE_ENTRY);
|
|
38363
|
+
if (exists(entry)) {
|
|
38364
|
+
return { file: opts.execPath ?? process.execPath, args: [entry, ...args], via: "node-entry" };
|
|
38365
|
+
}
|
|
38366
|
+
}
|
|
38367
|
+
const bin = resolved ?? "jervcode";
|
|
38368
|
+
if (platform2 === "win32") {
|
|
38369
|
+
return { file: "cmd.exe", args: ["/c", bin, ...args], via: resolved ? "cmd-shim" : "bare" };
|
|
38370
|
+
}
|
|
38371
|
+
return { file: bin, args: [...args], via: resolved ? "posix" : "bare" };
|
|
38372
|
+
}
|
|
38373
|
+
async function removeWorktreeNodeModulesViaHelper(wtPath, opts = {}) {
|
|
38374
|
+
const { cwd, timeoutMs = NODE_MODULES_CLEANUP_TIMEOUT_MS } = opts;
|
|
38375
|
+
const env = opts.env ?? process.env;
|
|
38376
|
+
const exec = opts.exec ?? execFileP;
|
|
38377
|
+
const candidates = jervcodeCandidatePaths(env, opts.home, opts.platform ?? process.platform);
|
|
38378
|
+
const plan = jervcodeExecFileArgs(["worktree-node-modules-cleanup", "--worktree", wtPath], opts);
|
|
38379
|
+
const execOptions = { timeout: timeoutMs, ...cwd ? { cwd } : {} };
|
|
38380
|
+
try {
|
|
38381
|
+
await exec(plan.file, plan.args, execOptions);
|
|
38382
|
+
return { ok: true };
|
|
38383
|
+
} catch (e) {
|
|
38384
|
+
const err = e;
|
|
38385
|
+
const base = formatJervCliSpawnFailure(err, plan, candidates);
|
|
38386
|
+
const stderr = typeof err.stderr === "string" ? err.stderr.trim() : "";
|
|
38387
|
+
const detail = stderr.split("\n")[0] || base;
|
|
38388
|
+
return { ok: false, error: `jervcode worktree-node-modules-cleanup failed: ${detail}` };
|
|
38389
|
+
}
|
|
38390
|
+
}
|
|
38136
38391
|
|
|
38137
38392
|
// src/worktree-evidence-archive.ts
|
|
38138
38393
|
var import_node_fs41 = require("node:fs");
|
|
38139
|
-
var
|
|
38394
|
+
var import_node_path38 = require("node:path");
|
|
38140
38395
|
var JERV_ARTIFACT_RUN_SCOPE_ENV_VARS = ["JERV_RUN_ID", ...SESSION_ID_ENV_VARS];
|
|
38141
38396
|
function sanitizeArchiveSegment(value, max = 80) {
|
|
38142
38397
|
const scrubbed = value.replace(/[^A-Za-z0-9._@+-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -38168,7 +38423,7 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
|
38168
38423
|
const resolveRoot = deps.resolveArchiveRoot ?? repoRuntimeStatePath;
|
|
38169
38424
|
if (!args.primaryRoot?.trim()) return { status: "skipped", reason: "missing-primary-root" };
|
|
38170
38425
|
if (!args.worktreePath?.trim()) return { status: "skipped", reason: "missing-worktree-path" };
|
|
38171
|
-
const source = (0,
|
|
38426
|
+
const source = (0, import_node_path38.join)(args.worktreePath, ".jerv");
|
|
38172
38427
|
if (!exists(source)) return { status: "absent" };
|
|
38173
38428
|
if (!isDirectory(source)) return { status: "skipped", reason: "jerv-not-a-directory" };
|
|
38174
38429
|
const runScope = resolveArtifactRunScope(env);
|
|
@@ -38176,7 +38431,7 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
|
|
|
38176
38431
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
38177
38432
|
const dest = resolveRoot(args.primaryRoot, "jerv-artifacts", runScope, branchSlug, stamp, ".jerv");
|
|
38178
38433
|
try {
|
|
38179
|
-
mkdirp((0,
|
|
38434
|
+
mkdirp((0, import_node_path38.dirname)(dest));
|
|
38180
38435
|
copyDir(source, dest);
|
|
38181
38436
|
if (!exists(dest)) return { status: "failed", error: `archive write left no directory at ${dest}` };
|
|
38182
38437
|
return { status: "archived", path: dest, runScope };
|
|
@@ -38192,7 +38447,7 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
|
|
|
38192
38447
|
const exists = deps.exists ?? import_node_fs41.existsSync;
|
|
38193
38448
|
const stat4 = deps.stat ?? defaultStat;
|
|
38194
38449
|
const readdir2 = deps.readdir ?? import_node_fs41.readdirSync;
|
|
38195
|
-
const tmpRoot = (0,
|
|
38450
|
+
const tmpRoot = (0, import_node_path38.join)(worktreePath, "tmp");
|
|
38196
38451
|
if (!exists(tmpRoot)) return [];
|
|
38197
38452
|
const entries = [];
|
|
38198
38453
|
const walk2 = (dir) => {
|
|
@@ -38203,14 +38458,14 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
|
|
|
38203
38458
|
return;
|
|
38204
38459
|
}
|
|
38205
38460
|
for (const name of names) {
|
|
38206
|
-
const full = (0,
|
|
38461
|
+
const full = (0, import_node_path38.join)(dir, name);
|
|
38207
38462
|
let st;
|
|
38208
38463
|
try {
|
|
38209
38464
|
st = stat4(full);
|
|
38210
38465
|
} catch {
|
|
38211
38466
|
continue;
|
|
38212
38467
|
}
|
|
38213
|
-
const relPath = (0,
|
|
38468
|
+
const relPath = (0, import_node_path38.relative)(worktreePath, full).replace(/\\/g, "/");
|
|
38214
38469
|
if (st.isDirectory()) {
|
|
38215
38470
|
if (st.mtimeMs > newerThanMs) entries.push({ relPath, bytes: 0, mtimeMs: st.mtimeMs });
|
|
38216
38471
|
walk2(full);
|
|
@@ -38235,7 +38490,7 @@ function archiveWorktreeTmpArtifacts(args, deps = {}) {
|
|
|
38235
38490
|
if (!args.primaryRoot?.trim()) return { status: "skipped", reason: "missing-primary-root" };
|
|
38236
38491
|
if (!args.worktreePath?.trim()) return { status: "skipped", reason: "missing-worktree-path" };
|
|
38237
38492
|
const scanned = scanWorktreeTmpEvidence(args.worktreePath, args.newerThanMs, deps);
|
|
38238
|
-
const source = (0,
|
|
38493
|
+
const source = (0, import_node_path38.join)(args.worktreePath, "tmp");
|
|
38239
38494
|
if (!scanned.length) return { status: "absent" };
|
|
38240
38495
|
if (!exists(source)) return { status: "absent" };
|
|
38241
38496
|
const runScope = resolveArtifactRunScope(env);
|
|
@@ -38243,7 +38498,7 @@ function archiveWorktreeTmpArtifacts(args, deps = {}) {
|
|
|
38243
38498
|
const stamp = now().toISOString().replace(/[:.]/g, "-");
|
|
38244
38499
|
const dest = resolveRoot(args.primaryRoot, "worktree-artifacts", runScope, branchSlug, stamp, "tmp");
|
|
38245
38500
|
try {
|
|
38246
|
-
mkdirp((0,
|
|
38501
|
+
mkdirp((0, import_node_path38.dirname)(dest));
|
|
38247
38502
|
copyDir(source, dest);
|
|
38248
38503
|
if (!exists(dest)) return { status: "failed", error: `archive write left no directory at ${dest}` };
|
|
38249
38504
|
const bytes = scanned.reduce((sum, e) => sum + e.bytes, 0);
|
|
@@ -38312,7 +38567,7 @@ function normPath2(p) {
|
|
|
38312
38567
|
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
38313
38568
|
}
|
|
38314
38569
|
function unlinkNodeModulesJunction(wtPath) {
|
|
38315
|
-
const nm = (0,
|
|
38570
|
+
const nm = (0, import_node_path39.join)(wtPath, "node_modules");
|
|
38316
38571
|
try {
|
|
38317
38572
|
if ((0, import_node_fs42.lstatSync)(nm).isSymbolicLink()) (0, import_node_fs42.rmdirSync)(nm);
|
|
38318
38573
|
return { ok: true };
|
|
@@ -38449,7 +38704,7 @@ async function preCleanWorktreeForRemoval(wtPath, execGit) {
|
|
|
38449
38704
|
}
|
|
38450
38705
|
async function listNestedIgnoredNodeModules(wtPath, execGit) {
|
|
38451
38706
|
const out = await execGit(["-C", wtPath, "ls-files", "--others", "--ignored", "--exclude-standard", "--directory"]).catch(() => "");
|
|
38452
|
-
return out.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.endsWith("node_modules/") && line !== "node_modules/").map((line) => (0,
|
|
38707
|
+
return out.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.endsWith("node_modules/") && line !== "node_modules/").map((line) => (0, import_node_path39.join)(wtPath, line.slice(0, -1)));
|
|
38453
38708
|
}
|
|
38454
38709
|
function safeRemoveTree(path2) {
|
|
38455
38710
|
const stat4 = (0, import_node_fs42.lstatSync)(path2);
|
|
@@ -38462,7 +38717,7 @@ function safeRemoveTree(path2) {
|
|
|
38462
38717
|
return;
|
|
38463
38718
|
}
|
|
38464
38719
|
if (stat4.isDirectory()) {
|
|
38465
|
-
for (const entry of (0, import_node_fs42.readdirSync)(path2)) safeRemoveTree((0,
|
|
38720
|
+
for (const entry of (0, import_node_fs42.readdirSync)(path2)) safeRemoveTree((0, import_node_path39.join)(path2, entry));
|
|
38466
38721
|
(0, import_node_fs42.rmdirSync)(path2);
|
|
38467
38722
|
return;
|
|
38468
38723
|
}
|
|
@@ -38505,7 +38760,7 @@ function unlinkEscapingReparsePoints(root, primaryRoot) {
|
|
|
38505
38760
|
return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
|
|
38506
38761
|
}
|
|
38507
38762
|
for (const entry of entries) {
|
|
38508
|
-
const child2 = (0,
|
|
38763
|
+
const child2 = (0, import_node_path39.join)(dir, entry.name);
|
|
38509
38764
|
if (entry.isSymbolicLink()) {
|
|
38510
38765
|
let target = "";
|
|
38511
38766
|
try {
|
|
@@ -38539,7 +38794,7 @@ async function describePreCleanFailure(wtPath, execGit, error) {
|
|
|
38539
38794
|
const more = remaining.length > 10 ? ` (+${remaining.length - 10} more)` : "";
|
|
38540
38795
|
const quote = (path2) => path2.replace(/'/g, "''");
|
|
38541
38796
|
const nested = remaining.find((path2) => path2.endsWith("node_modules/") && path2 !== "node_modules/");
|
|
38542
|
-
const remediation = remaining.includes("node_modules/") ? `jervcode worktree-node-modules-cleanup --worktree '${quote(wtPath)}'` : nested ? `Remove-Item -LiteralPath '${quote((0,
|
|
38797
|
+
const remediation = remaining.includes("node_modules/") ? `jervcode worktree-node-modules-cleanup --worktree '${quote(wtPath)}'` : nested ? `Remove-Item -LiteralPath '${quote((0, import_node_path39.join)(wtPath, nested.slice(0, -1)))}' -Recurse -Force` : void 0;
|
|
38543
38798
|
return { ok: false, error: `${error}; remaining ignored paths: ${shown}${more}`, ...remediation ? { remediation } : {} };
|
|
38544
38799
|
}
|
|
38545
38800
|
function formatWorktreeRemovalFailureDetail(options) {
|
|
@@ -38833,7 +39088,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
38833
39088
|
const preHelperGuard = unlinkEscapingReparsePoints(wtPath, options.primaryRoot);
|
|
38834
39089
|
if (!preHelperGuard.ok) return refuseReparseEscape(preHelperGuard.error);
|
|
38835
39090
|
unlinkedReparsePoints.push(...preHelperGuard.unlinked);
|
|
38836
|
-
if (pathExists((0,
|
|
39091
|
+
if (pathExists((0, import_node_path39.join)(wtPath, "node_modules"))) {
|
|
38837
39092
|
const nmRemoved = await (options.removeRealNodeModules ?? ((p) => removeWorktreeNodeModulesViaHelper(p, { cwd: mainWorktreePath })))(wtPath);
|
|
38838
39093
|
if (!nmRemoved.ok) {
|
|
38839
39094
|
report.worktree = {
|
|
@@ -39000,10 +39255,10 @@ function argvWantsJson2() {
|
|
|
39000
39255
|
return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
|
|
39001
39256
|
}
|
|
39002
39257
|
function hubRoot() {
|
|
39003
|
-
const fromPkg = (0,
|
|
39258
|
+
const fromPkg = (0, import_node_path40.join)(__dirname, "..", "..");
|
|
39004
39259
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
39005
|
-
if ((0, import_node_fs43.existsSync)((0,
|
|
39006
|
-
if ((0, import_node_fs43.existsSync)((0,
|
|
39260
|
+
if ((0, import_node_fs43.existsSync)((0, import_node_path40.join)(fromPkg, marker))) return fromPkg;
|
|
39261
|
+
if ((0, import_node_fs43.existsSync)((0, import_node_path40.join)(process.cwd(), marker))) return process.cwd();
|
|
39007
39262
|
return null;
|
|
39008
39263
|
}
|
|
39009
39264
|
function ciAuditDeps() {
|
|
@@ -39015,12 +39270,12 @@ function ciAuditDeps() {
|
|
|
39015
39270
|
getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise)),
|
|
39016
39271
|
readSeedFile: (path2) => {
|
|
39017
39272
|
if (!root) return null;
|
|
39018
|
-
const fullPath = (0,
|
|
39273
|
+
const fullPath = (0, import_node_path40.join)(root, path2);
|
|
39019
39274
|
return (0, import_node_fs43.existsSync)(fullPath) ? (0, import_node_fs43.readFileSync)(fullPath, "utf8") : null;
|
|
39020
39275
|
}
|
|
39021
39276
|
};
|
|
39022
39277
|
}
|
|
39023
|
-
async function
|
|
39278
|
+
async function ghJson2(args, timeout = 1e4) {
|
|
39024
39279
|
const { stdout } = await execFileP("gh", args, { timeout });
|
|
39025
39280
|
return JSON.parse(stdout);
|
|
39026
39281
|
}
|
|
@@ -39166,7 +39421,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39166
39421
|
try {
|
|
39167
39422
|
title = await resolveIssueTitle(
|
|
39168
39423
|
{ title: opts.title, titleFile: opts.titleFile },
|
|
39169
|
-
{ readFile:
|
|
39424
|
+
{ readFile: import_promises7.readFile, readStdin }
|
|
39170
39425
|
);
|
|
39171
39426
|
} catch (e) {
|
|
39172
39427
|
return fail(
|
|
@@ -39208,8 +39463,8 @@ function registerCollaborationCommands(program3) {
|
|
|
39208
39463
|
let surfaceFlagLabel;
|
|
39209
39464
|
try {
|
|
39210
39465
|
issueType = resolveCreateType(o.type, "issue create", o.label);
|
|
39211
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
39212
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
39466
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39467
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39213
39468
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
39214
39469
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
39215
39470
|
extraLabels = [...o.label ?? []];
|
|
@@ -39307,7 +39562,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39307
39562
|
async function readParentField(number, repo) {
|
|
39308
39563
|
let payload;
|
|
39309
39564
|
try {
|
|
39310
|
-
payload = await
|
|
39565
|
+
payload = await ghJson2(["api", `repos/${repo}/issues/${number}`]);
|
|
39311
39566
|
} catch (e) {
|
|
39312
39567
|
const err = e;
|
|
39313
39568
|
return { parentReadError: (err.stderr || err.message || String(e)).trim() };
|
|
@@ -39339,7 +39594,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39339
39594
|
emit(data2, await readParentField(n, repo));
|
|
39340
39595
|
return;
|
|
39341
39596
|
}
|
|
39342
|
-
const data = await
|
|
39597
|
+
const data = await ghJson2(["issue", "view", String(n), "--repo", repo, "--json", gh.ghFields]);
|
|
39343
39598
|
emit(data, await readParentField(n, repo));
|
|
39344
39599
|
} catch (e) {
|
|
39345
39600
|
const err = e;
|
|
@@ -39354,7 +39609,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39354
39609
|
const repo = await resolveRepo(o.repo);
|
|
39355
39610
|
if (!repo) return fail("issue discover-related: could not resolve repo");
|
|
39356
39611
|
try {
|
|
39357
|
-
const issues = await
|
|
39612
|
+
const issues = await ghJson2([
|
|
39358
39613
|
"issue",
|
|
39359
39614
|
"list",
|
|
39360
39615
|
"--repo",
|
|
@@ -39369,7 +39624,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39369
39624
|
const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
|
|
39370
39625
|
if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
|
|
39371
39626
|
if (!candidates.length) return;
|
|
39372
|
-
const viewed = await
|
|
39627
|
+
const viewed = await ghJson2([
|
|
39373
39628
|
"issue",
|
|
39374
39629
|
"view",
|
|
39375
39630
|
String(number),
|
|
@@ -39417,7 +39672,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39417
39672
|
}
|
|
39418
39673
|
let body;
|
|
39419
39674
|
try {
|
|
39420
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
39675
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39421
39676
|
} catch (e) {
|
|
39422
39677
|
return fail(`issue comment: ${e.message}`);
|
|
39423
39678
|
}
|
|
@@ -39443,7 +39698,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39443
39698
|
const checked = o.off !== true;
|
|
39444
39699
|
let body;
|
|
39445
39700
|
try {
|
|
39446
|
-
const viewed = await
|
|
39701
|
+
const viewed = await ghJson2(["issue", "view", String(parsed.number), "--repo", repo, "--json", "body"]);
|
|
39447
39702
|
body = viewed.body ?? "";
|
|
39448
39703
|
} catch (e) {
|
|
39449
39704
|
return fail(`issue check: could not read ${repo}#${parsed.number}: ${e.message}`);
|
|
@@ -39477,8 +39732,8 @@ ${list}`);
|
|
|
39477
39732
|
let title;
|
|
39478
39733
|
const sourceRepo = o.repo ?? await resolveRepo(void 0);
|
|
39479
39734
|
try {
|
|
39480
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
39481
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
39735
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39736
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39482
39737
|
priority = resolveCreatePriority(o.priority, "report");
|
|
39483
39738
|
if (!ISSUE_TYPES.includes(o.type)) {
|
|
39484
39739
|
throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
@@ -39533,8 +39788,8 @@ ${list}`);
|
|
|
39533
39788
|
let args;
|
|
39534
39789
|
try {
|
|
39535
39790
|
skill = assertSkillName(o.skill);
|
|
39536
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
39537
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
39791
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39792
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39538
39793
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
39539
39794
|
priority = resolveCreatePriority(o.priority, "skill-lesson");
|
|
39540
39795
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -39545,7 +39800,7 @@ ${list}`);
|
|
|
39545
39800
|
if (!o.force) {
|
|
39546
39801
|
let openLessons = [];
|
|
39547
39802
|
try {
|
|
39548
|
-
openLessons = await
|
|
39803
|
+
openLessons = await ghJson2([
|
|
39549
39804
|
"issue",
|
|
39550
39805
|
"list",
|
|
39551
39806
|
"--repo",
|
|
@@ -39591,12 +39846,12 @@ ${list}`);
|
|
|
39591
39846
|
console.log(JSON.stringify({ ...created, projectItemId, onBoard }));
|
|
39592
39847
|
});
|
|
39593
39848
|
const pr = program3.command("pr").description("pull requests \u2014 reliable create with structured output");
|
|
39594
|
-
withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--no-zeroci", "
|
|
39849
|
+
withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--no-zeroci", "compatibility option; pr create does not mint receipts").option("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
|
|
39595
39850
|
let body;
|
|
39596
39851
|
let title;
|
|
39597
39852
|
try {
|
|
39598
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
39599
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
39853
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39854
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
|
|
39600
39855
|
} catch (e) {
|
|
39601
39856
|
return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
39602
39857
|
}
|
|
@@ -39616,9 +39871,6 @@ ${list}`);
|
|
|
39616
39871
|
}
|
|
39617
39872
|
const claimRefusal = await prCreateClaimRefusal(body, o.repo);
|
|
39618
39873
|
if (claimRefusal) return fail(claimRefusal);
|
|
39619
|
-
const zeroCiWarn = await mintZeroCiReceipt({ repo: o.repo, zeroci: o.zeroci }, createPrCreateZeroCiDeps());
|
|
39620
|
-
if (zeroCiWarn) process.stderr.write(`${zeroCiWarn}
|
|
39621
|
-
`);
|
|
39622
39874
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
39623
39875
|
if (isGhCreateRateLimited(created)) {
|
|
39624
39876
|
console.log(JSON.stringify(created));
|
|
@@ -39654,7 +39906,7 @@ ${list}`);
|
|
|
39654
39906
|
console.log(JSON.stringify(data2));
|
|
39655
39907
|
return;
|
|
39656
39908
|
}
|
|
39657
|
-
const data = await
|
|
39909
|
+
const data = await ghJson2(["pr", "view", String(n), "--repo", repo, "--json", effective]);
|
|
39658
39910
|
console.log(JSON.stringify(data));
|
|
39659
39911
|
} catch (e) {
|
|
39660
39912
|
const err = e;
|
|
@@ -39662,11 +39914,11 @@ ${list}`);
|
|
|
39662
39914
|
}
|
|
39663
39915
|
});
|
|
39664
39916
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
39665
|
-
const wfDir = (0,
|
|
39917
|
+
const wfDir = (0, import_node_path40.join)(cwd, ".github", "workflows");
|
|
39666
39918
|
if (!(0, import_node_fs43.existsSync)(wfDir)) return [];
|
|
39667
39919
|
return (0, import_node_fs43.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
39668
39920
|
try {
|
|
39669
|
-
return workflowReportsPrChecks((0, import_node_fs43.readFileSync)((0,
|
|
39921
|
+
return workflowReportsPrChecks((0, import_node_fs43.readFileSync)((0, import_node_path40.join)(wfDir, name), "utf8"));
|
|
39670
39922
|
} catch {
|
|
39671
39923
|
return true;
|
|
39672
39924
|
}
|
|
@@ -39854,7 +40106,12 @@ ${list}`);
|
|
|
39854
40106
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
39855
40107
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
39856
40108
|
});
|
|
39857
|
-
|
|
40109
|
+
pr.command("review-check <number>").description("read-only trusted review verdict check for an exact PR head").requiredOption("--repo <owner/repo>", "target repository").requiredOption("--head <sha>", "exact expected head SHA").option("--json", "machine-readable output").action(async (number, o) => {
|
|
40110
|
+
const result = await checkPrReview(number, o.repo, o.head);
|
|
40111
|
+
console.log(o.json ? JSON.stringify(result) : `pr review-check: ${result.reason}`);
|
|
40112
|
+
if (!result.ok) process.exitCode = 1;
|
|
40113
|
+
});
|
|
40114
|
+
jsonParity(pr.command("review-verdict <number>").description("post an explicit review verdict comment: computes the PR diff patch-id and head sha, renders the v1 contract body, and posts it as a PR comment").requiredOption("--repo <owner/repo>", "target repo").requiredOption("--verdict <verdict>", `one of ${REVIEW_VERDICTS.join("|")}`).requiredOption("--scope <text>", "one line: what was reviewed").requiredOption("--risk <text>", "one line: the residual risk").option("--unverified <text>", "a claim the reviewer could not verify (repeatable)", (v, acc) => [...acc, v], []).requiredOption("--reviewer <id>", "reviewer seat or model id").option("--findings-file <path>", "Markdown findings appended after the JSON block")).action(async (number, o) => {
|
|
39858
40115
|
if (!isReviewVerdict(o.verdict)) return fail(`pr review-verdict: --verdict must be one of ${REVIEW_VERDICTS.join("|")} (got ${o.verdict})`);
|
|
39859
40116
|
const verdict = o.verdict;
|
|
39860
40117
|
const findings = o.findingsFile ? (0, import_node_fs43.readFileSync)(o.findingsFile, "utf8") : void 0;
|
|
@@ -39896,7 +40153,7 @@ ${list}`);
|
|
|
39896
40153
|
}
|
|
39897
40154
|
class PrHeadBehindBaseError extends Error {
|
|
39898
40155
|
}
|
|
39899
|
-
pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "
|
|
40156
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
|
|
39900
40157
|
if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
|
|
39901
40158
|
try {
|
|
39902
40159
|
const parsed = parseIssueRef(number, o.repo);
|
|
@@ -39914,7 +40171,7 @@ ${list}`);
|
|
|
39914
40171
|
await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
|
|
39915
40172
|
async (n) => {
|
|
39916
40173
|
if (!landRepoForGuard) return void 0;
|
|
39917
|
-
const viewed = await
|
|
40174
|
+
const viewed = await ghJson2(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
|
|
39918
40175
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
39919
40176
|
}
|
|
39920
40177
|
);
|
|
@@ -39931,16 +40188,15 @@ ${list}`);
|
|
|
39931
40188
|
return;
|
|
39932
40189
|
}
|
|
39933
40190
|
if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
|
|
39934
|
-
{
|
|
39935
|
-
const gateRepo = landRepoForGuard ?? await requireRepo(o.repo);
|
|
39936
|
-
const refusal = await requireReviewVerdict("pr land", number, gateRepo, { withoutReview: o.withoutReview });
|
|
39937
|
-
if (refusal) {
|
|
39938
|
-
console.error(refusal);
|
|
39939
|
-
process.exitCode = 1;
|
|
39940
|
-
return;
|
|
39941
|
-
}
|
|
39942
|
-
}
|
|
39943
40191
|
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
40192
|
+
queueMerge: async (prNumber, repo) => {
|
|
40193
|
+
const meta = await ghJson2(["pr", "view", prNumber, "--repo", repo, "--json", "baseRefName,headRefOid"]);
|
|
40194
|
+
if (!await usesMergifyPilot(repo, meta.baseRefName)) return void 0;
|
|
40195
|
+
if (!landClosingGuardInput) throw new Error("pr land: Mergify requires a readable closing-keyword guard");
|
|
40196
|
+
const guard = evaluateClosingGuard(landClosingGuardInput, { force: o.force, context: "pr land", squashBodyText: landClosingGuardInput.text });
|
|
40197
|
+
if (guard.blocked) throw new Error(guard.message);
|
|
40198
|
+
return requestMergifyMerge(prNumber, repo, landClosingGuardInput.text, meta.headRefOid);
|
|
40199
|
+
},
|
|
39944
40200
|
resolveRepo: async (prNumber, repoOpt) => {
|
|
39945
40201
|
const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
|
|
39946
40202
|
const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS4 })).stdout.trim();
|
|
@@ -40075,7 +40331,7 @@ ${list}`);
|
|
|
40075
40331
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
40076
40332
|
if (result.status === "failed") process.exitCode = 1;
|
|
40077
40333
|
});
|
|
40078
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "
|
|
40334
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict")).action(async (number, o) => {
|
|
40079
40335
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
40080
40336
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
40081
40337
|
if (o.disableAuto) {
|
|
@@ -40097,20 +40353,24 @@ ${list}`);
|
|
|
40097
40353
|
const headRef = prMeta.head;
|
|
40098
40354
|
const baseRef = prMeta.base;
|
|
40099
40355
|
const headRefOid = (prMeta.oid ?? "").trim() || void 0;
|
|
40356
|
+
if (!repoForPostCleanup) throw new Error("pr merge: cannot resolve target repository");
|
|
40357
|
+
const mergifyPilot = await usesMergifyPilot(repoForPostCleanup, baseRef);
|
|
40358
|
+
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");
|
|
40100
40359
|
const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
|
|
40101
40360
|
const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
|
|
40102
40361
|
const closingGuardInput = await withAlreadyClosedCommitTargets(
|
|
40103
40362
|
await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
|
|
40104
40363
|
async (n) => {
|
|
40105
40364
|
if (!repoForPostCleanup) return void 0;
|
|
40106
|
-
const viewed = await
|
|
40365
|
+
const viewed = await ghJson2(["issue", "view", String(n), "--repo", repoForPostCleanup, "--json", "state"]);
|
|
40107
40366
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
40108
40367
|
}
|
|
40109
40368
|
);
|
|
40110
40369
|
if (o.squashBodyFile && method !== "--squash") {
|
|
40111
40370
|
throw new Error("pr merge: --squash-body-file applies only to squash merges");
|
|
40112
40371
|
}
|
|
40113
|
-
|
|
40372
|
+
if (mergifyPilot && !closingGuardInput) throw new Error("pr merge: Mergify requires a readable closing-keyword guard");
|
|
40373
|
+
const mergeSquashBody = mergifyPilot ? closingGuardInput.text : squashBodyTextForMerge(
|
|
40114
40374
|
closingGuardInput,
|
|
40115
40375
|
method === "--squash",
|
|
40116
40376
|
o.squashBodyFile ? (0, import_node_fs43.readFileSync)(o.squashBodyFile, "utf8") : void 0
|
|
@@ -40127,15 +40387,6 @@ ${list}`);
|
|
|
40127
40387
|
return;
|
|
40128
40388
|
}
|
|
40129
40389
|
if (closingGuardVerdict.message) console.warn(closingGuardVerdict.message);
|
|
40130
|
-
if (prMeta.state !== "MERGED") {
|
|
40131
|
-
const gateRepo = repoForPostCleanup ?? await requireRepo(o.repo);
|
|
40132
|
-
const refusal = await requireReviewVerdict("pr merge", number, gateRepo, { withoutReview: o.withoutReview });
|
|
40133
|
-
if (refusal) {
|
|
40134
|
-
console.error(refusal);
|
|
40135
|
-
process.exitCode = 1;
|
|
40136
|
-
return;
|
|
40137
|
-
}
|
|
40138
|
-
}
|
|
40139
40390
|
if (prMeta.state !== "MERGED" && !o.preserveWorktree) {
|
|
40140
40391
|
if (!repoForPostCleanup) throw new Error("pr merge: repository is unreadable; cannot prove remote branch preservation");
|
|
40141
40392
|
const repoSettings = await fetchRestRepoMergeSettings(repoForPostCleanup);
|
|
@@ -40154,7 +40405,7 @@ ${list}`);
|
|
|
40154
40405
|
const remote = foreignCwd ? `https://github.com/${targetRepo2}.git` : "origin";
|
|
40155
40406
|
let foreignCheckout;
|
|
40156
40407
|
if (foreignCwd) {
|
|
40157
|
-
const sibling = (0,
|
|
40408
|
+
const sibling = (0, import_node_path40.join)((0, import_node_path40.dirname)(beforeWorktrees[0]?.path || startingPath || process.cwd()), targetRepo2.split("/")[1]);
|
|
40158
40409
|
const siblingRepo = repoFromRemoteUrl(await gitOut(["-C", sibling, "remote", "get-url", "origin"]).catch(() => ""));
|
|
40159
40410
|
if (siblingRepo?.toLowerCase() === targetRepo2.toLowerCase()) {
|
|
40160
40411
|
const siblingWorktrees = await execFileP("git", ["-C", sibling, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseGitWorktreePorcelain(r.stdout)).catch(() => void 0);
|
|
@@ -40217,7 +40468,7 @@ ${list}`);
|
|
|
40217
40468
|
}
|
|
40218
40469
|
return true;
|
|
40219
40470
|
};
|
|
40220
|
-
if (o.wait && !await runWaitGate()) return;
|
|
40471
|
+
if (!mergifyPilot && o.wait && !await runWaitGate()) return;
|
|
40221
40472
|
if (ciPolicy.policy === "no-ci") {
|
|
40222
40473
|
const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
|
|
40223
40474
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
@@ -40226,7 +40477,7 @@ ${list}`);
|
|
|
40226
40477
|
const remoteBefore = await remoteBranchExists2(headRef, { remote });
|
|
40227
40478
|
let upgradedToAuto = false;
|
|
40228
40479
|
let remoteNotAttemptedReason = "preserved-delayed-cleanup";
|
|
40229
|
-
const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
40480
|
+
const overrideBody = mergifyPilot ? void 0 : mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
40230
40481
|
number,
|
|
40231
40482
|
repoArgs,
|
|
40232
40483
|
async (a, t) => (await execFileP("gh", a, { timeout: t })).stdout,
|
|
@@ -40306,7 +40557,15 @@ ${list}`);
|
|
|
40306
40557
|
});
|
|
40307
40558
|
try {
|
|
40308
40559
|
try {
|
|
40309
|
-
|
|
40560
|
+
if (mergifyPilot) {
|
|
40561
|
+
const queue = await requestMergifyMerge(number, repoForPostCleanup, closingGuardInput.text, headRefOid);
|
|
40562
|
+
if (queue.state !== "merged") {
|
|
40563
|
+
console.log(JSON.stringify({ mergeStatus: "failed", pr: number, repo: repoForPostCleanup, queue }));
|
|
40564
|
+
process.exitCode = queue.state === "refused" ? 1 : PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
40565
|
+
return;
|
|
40566
|
+
}
|
|
40567
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
40568
|
+
} else await mergeOnce();
|
|
40310
40569
|
} catch (e) {
|
|
40311
40570
|
if (!(e instanceof PrHeadBehindBaseError)) throw e;
|
|
40312
40571
|
const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
|
|
@@ -40524,7 +40783,7 @@ async function resolveWhoami(deps) {
|
|
|
40524
40783
|
}
|
|
40525
40784
|
|
|
40526
40785
|
// src/command-register-developer.ts
|
|
40527
|
-
var
|
|
40786
|
+
var import_node_path45 = require("node:path");
|
|
40528
40787
|
|
|
40529
40788
|
// src/wave-land.ts
|
|
40530
40789
|
function planWaveLand(prs) {
|
|
@@ -40778,26 +41037,26 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
40778
41037
|
|
|
40779
41038
|
// src/dist-drift.ts
|
|
40780
41039
|
var import_node_child_process17 = require("node:child_process");
|
|
40781
|
-
var
|
|
41040
|
+
var import_node_crypto13 = require("node:crypto");
|
|
40782
41041
|
var import_node_fs46 = require("node:fs");
|
|
40783
41042
|
var import_node_os21 = require("node:os");
|
|
40784
|
-
var
|
|
41043
|
+
var import_node_path42 = require("node:path");
|
|
40785
41044
|
|
|
40786
41045
|
// ../scripts/distribution-digest.mjs
|
|
40787
|
-
var
|
|
41046
|
+
var import_node_crypto12 = require("node:crypto");
|
|
40788
41047
|
var import_node_fs45 = require("node:fs");
|
|
40789
|
-
var
|
|
41048
|
+
var import_node_path41 = require("node:path");
|
|
40790
41049
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
40791
41050
|
function repoPath(root, declaredPath, label) {
|
|
40792
|
-
const absoluteRoot = (0,
|
|
40793
|
-
const target = (0,
|
|
40794
|
-
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${
|
|
41051
|
+
const absoluteRoot = (0, import_node_path41.resolve)(root);
|
|
41052
|
+
const target = (0, import_node_path41.resolve)(root, declaredPath);
|
|
41053
|
+
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${import_node_path41.sep}`)) {
|
|
40795
41054
|
throw new Error(`${label} ${declaredPath} escapes the repository root`);
|
|
40796
41055
|
}
|
|
40797
41056
|
return target;
|
|
40798
41057
|
}
|
|
40799
41058
|
function digestFiles(files) {
|
|
40800
|
-
const hash = (0,
|
|
41059
|
+
const hash = (0, import_node_crypto12.createHash)("sha256");
|
|
40801
41060
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
40802
41061
|
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs45.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs45.readFileSync)(file.absolute);
|
|
40803
41062
|
hash.update(file.relative, "utf8");
|
|
@@ -40827,7 +41086,7 @@ var DIST_ARTIFACTS = [
|
|
|
40827
41086
|
];
|
|
40828
41087
|
var BOM_DIST_TREE_ID = "mmi-cli-dist";
|
|
40829
41088
|
var ABSENT = "absent";
|
|
40830
|
-
var sha256 = (bytes) => `sha256:${(0,
|
|
41089
|
+
var sha256 = (bytes) => `sha256:${(0, import_node_crypto13.createHash)("sha256").update(bytes).digest("hex")}`;
|
|
40831
41090
|
function artifactDrift(path2, committedBytes, rebuiltBytes) {
|
|
40832
41091
|
const committed = committedBytes ? sha256(committedBytes) : ABSENT;
|
|
40833
41092
|
const rebuiltExpected = rebuiltBytes ? sha256(rebuiltBytes) : ABSENT;
|
|
@@ -40905,7 +41164,7 @@ function walkFiles(root) {
|
|
|
40905
41164
|
const files = [];
|
|
40906
41165
|
const walk2 = (directory) => {
|
|
40907
41166
|
for (const entry of (0, import_node_fs46.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
40908
|
-
const child2 = (0,
|
|
41167
|
+
const child2 = (0, import_node_path42.join)(directory, entry.name);
|
|
40909
41168
|
if (entry.isDirectory()) walk2(child2);
|
|
40910
41169
|
else files.push(child2);
|
|
40911
41170
|
}
|
|
@@ -40915,10 +41174,10 @@ function walkFiles(root) {
|
|
|
40915
41174
|
}
|
|
40916
41175
|
function bomPathFor(root) {
|
|
40917
41176
|
try {
|
|
40918
|
-
const registry2 = JSON.parse((0, import_node_fs46.readFileSync)((0,
|
|
40919
|
-
return (0,
|
|
41177
|
+
const registry2 = JSON.parse((0, import_node_fs46.readFileSync)((0, import_node_path42.join)(root, "surfaces.json"), "utf8"));
|
|
41178
|
+
return (0, import_node_path42.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
|
|
40920
41179
|
} catch {
|
|
40921
|
-
return (0,
|
|
41180
|
+
return (0, import_node_path42.join)(root, "distribution-bom.json");
|
|
40922
41181
|
}
|
|
40923
41182
|
}
|
|
40924
41183
|
function rebuildTo(packageRoot, outDir) {
|
|
@@ -40931,28 +41190,28 @@ function rebuildTo(packageRoot, outDir) {
|
|
|
40931
41190
|
});
|
|
40932
41191
|
}
|
|
40933
41192
|
function runDistStatus(root) {
|
|
40934
|
-
const stage = (0, import_node_fs46.mkdtempSync)((0,
|
|
41193
|
+
const stage = (0, import_node_fs46.mkdtempSync)((0, import_node_path42.join)((0, import_node_os21.tmpdir)(), "mmi-dist-drift-"));
|
|
40935
41194
|
let overlayCount = 0;
|
|
40936
41195
|
try {
|
|
40937
|
-
const cliOut = (0,
|
|
40938
|
-
const hubOut = (0,
|
|
40939
|
-
rebuildTo((0,
|
|
40940
|
-
rebuildTo((0,
|
|
41196
|
+
const cliOut = (0, import_node_path42.join)(stage, "cli-dist");
|
|
41197
|
+
const hubOut = (0, import_node_path42.join)(stage, "hub-dist");
|
|
41198
|
+
rebuildTo((0, import_node_path42.join)(root, "cli"), cliOut);
|
|
41199
|
+
rebuildTo((0, import_node_path42.join)(root, "updater"), hubOut);
|
|
40941
41200
|
const outDirFor = (packageDir) => packageDir === "cli" ? cliOut : hubOut;
|
|
40942
41201
|
const rebuilt = (path2) => {
|
|
40943
41202
|
const spec = DIST_ARTIFACTS.find((entry) => entry.path === path2);
|
|
40944
|
-
return spec ? readOrNull((0,
|
|
41203
|
+
return spec ? readOrNull((0, import_node_path42.join)(outDirFor(spec.packageDir), spec.output)) : null;
|
|
40945
41204
|
};
|
|
40946
|
-
const committed = (path2) => readOrNull((0,
|
|
40947
|
-
const tree = (path2) => readOrNull((0,
|
|
40948
|
-
const distRoot = (0,
|
|
40949
|
-
const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0,
|
|
41205
|
+
const committed = (path2) => readOrNull((0, import_node_path42.join)(root, path2));
|
|
41206
|
+
const tree = (path2) => readOrNull((0, import_node_path42.join)(root, path2));
|
|
41207
|
+
const distRoot = (0, import_node_path42.join)(root, "cli", "dist");
|
|
41208
|
+
const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path42.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
|
|
40950
41209
|
const bom = JSON.parse((0, import_node_fs46.readFileSync)(bomPathFor(root), "utf8"));
|
|
40951
41210
|
const digest = (entries) => {
|
|
40952
|
-
const overlay = (0,
|
|
41211
|
+
const overlay = (0, import_node_path42.join)(stage, `overlay-${overlayCount++}`);
|
|
40953
41212
|
for (const entry of entries) {
|
|
40954
|
-
const target = (0,
|
|
40955
|
-
(0, import_node_fs46.mkdirSync)((0,
|
|
41213
|
+
const target = (0, import_node_path42.join)(overlay, entry.path);
|
|
41214
|
+
(0, import_node_fs46.mkdirSync)((0, import_node_path42.dirname)(target), { recursive: true });
|
|
40956
41215
|
(0, import_node_fs46.writeFileSync)(target, entry.bytes);
|
|
40957
41216
|
}
|
|
40958
41217
|
return digestPackedFiles(overlay, entries.map((entry) => entry.path));
|
|
@@ -41030,8 +41289,8 @@ function registerEdgeCommands(program3) {
|
|
|
41030
41289
|
}
|
|
41031
41290
|
|
|
41032
41291
|
// src/schedules-lift-command.ts
|
|
41033
|
-
var
|
|
41034
|
-
var
|
|
41292
|
+
var import_promises8 = require("node:fs/promises");
|
|
41293
|
+
var import_node_path43 = require("node:path");
|
|
41035
41294
|
var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
|
|
41036
41295
|
var SCHEDULE_REPO_RE = /^[A-Za-z0-9_.-]+$/;
|
|
41037
41296
|
var SchedulesLiftUsageError = class extends Error {
|
|
@@ -41051,14 +41310,14 @@ var RegistryUnreachableError = class extends Error {
|
|
|
41051
41310
|
async function readWorkflowFiles(dir) {
|
|
41052
41311
|
let names;
|
|
41053
41312
|
try {
|
|
41054
|
-
names = await (0,
|
|
41313
|
+
names = await (0, import_promises8.readdir)(dir);
|
|
41055
41314
|
} catch {
|
|
41056
41315
|
return [];
|
|
41057
41316
|
}
|
|
41058
41317
|
const files = [];
|
|
41059
41318
|
for (const name of names.sort()) {
|
|
41060
41319
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
41061
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0,
|
|
41320
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises8.readFile)((0, import_node_path43.join)(dir, name), "utf8") });
|
|
41062
41321
|
}
|
|
41063
41322
|
return files;
|
|
41064
41323
|
}
|
|
@@ -41142,7 +41401,7 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
|
41142
41401
|
// src/spawn-policy-core.ts
|
|
41143
41402
|
var import_node_child_process18 = require("node:child_process");
|
|
41144
41403
|
var import_node_fs47 = require("node:fs");
|
|
41145
|
-
var
|
|
41404
|
+
var import_node_path44 = require("node:path");
|
|
41146
41405
|
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
41147
41406
|
var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
|
|
41148
41407
|
var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
@@ -41228,7 +41487,7 @@ function runSpawnPolicy(root) {
|
|
|
41228
41487
|
for (const file of files) {
|
|
41229
41488
|
let raw;
|
|
41230
41489
|
try {
|
|
41231
|
-
raw = (0, import_node_fs47.readFileSync)((0,
|
|
41490
|
+
raw = (0, import_node_fs47.readFileSync)((0, import_node_path44.join)(root, file), "utf8");
|
|
41232
41491
|
} catch {
|
|
41233
41492
|
continue;
|
|
41234
41493
|
}
|
|
@@ -41248,7 +41507,7 @@ function runSpawnPolicy(root) {
|
|
|
41248
41507
|
function registerDeveloperCommands(program3) {
|
|
41249
41508
|
const rules = program3.command("rules").description("org-managed .gitignore delivery");
|
|
41250
41509
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
41251
|
-
const path2 = (0,
|
|
41510
|
+
const path2 = (0, import_node_path45.join)(process.cwd(), ".gitignore");
|
|
41252
41511
|
const current = (0, import_node_fs48.existsSync)(path2) ? (0, import_node_fs48.readFileSync)(path2, "utf8") : null;
|
|
41253
41512
|
const plan = planManagedGitignore(current);
|
|
41254
41513
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
@@ -41508,10 +41767,10 @@ function registerDeveloperCommands(program3) {
|
|
|
41508
41767
|
}
|
|
41509
41768
|
|
|
41510
41769
|
// src/command-register-train-operations.ts
|
|
41511
|
-
var
|
|
41770
|
+
var import_promises10 = require("node:fs/promises");
|
|
41512
41771
|
|
|
41513
41772
|
// src/hotfix-apply.ts
|
|
41514
|
-
var
|
|
41773
|
+
var import_promises9 = require("node:fs/promises");
|
|
41515
41774
|
|
|
41516
41775
|
// src/slack-alert.ts
|
|
41517
41776
|
var SSM_REGION = "eu-central-1";
|
|
@@ -42295,7 +42554,7 @@ async function runHotfixRelease(deps, versionInput, options = {}, doctor = runTr
|
|
|
42295
42554
|
}
|
|
42296
42555
|
let lines2;
|
|
42297
42556
|
try {
|
|
42298
|
-
lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0,
|
|
42557
|
+
lines2 = summaryFileLines(await (deps.readFile ?? ((p) => (0, import_promises9.readFile)(p, "utf8")))(options.announceSummaryFile));
|
|
42299
42558
|
} catch (e) {
|
|
42300
42559
|
throw new Error(`could not read --announce-summary-file ${options.announceSummaryFile}: ${e.message} \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
42301
42560
|
}
|
|
@@ -42854,7 +43113,7 @@ function checkHotfixCarries(options) {
|
|
|
42854
43113
|
|
|
42855
43114
|
// src/train-commands.ts
|
|
42856
43115
|
var import_node_fs49 = require("node:fs");
|
|
42857
|
-
var
|
|
43116
|
+
var import_node_path46 = require("node:path");
|
|
42858
43117
|
var INVOKED_ARGV = process.argv.slice(2);
|
|
42859
43118
|
var RELEASE_BUMP_INTENTS = ["major", "minor", "patch"];
|
|
42860
43119
|
function resolveReleaseBumpIntent(raw) {
|
|
@@ -42866,7 +43125,7 @@ function resolveReleaseBumpIntent(raw) {
|
|
|
42866
43125
|
}
|
|
42867
43126
|
function readRepoVersion() {
|
|
42868
43127
|
try {
|
|
42869
|
-
return JSON.parse((0, import_node_fs49.readFileSync)((0,
|
|
43128
|
+
return JSON.parse((0, import_node_fs49.readFileSync)((0, import_node_path46.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
42870
43129
|
} catch {
|
|
42871
43130
|
return void 0;
|
|
42872
43131
|
}
|
|
@@ -43029,8 +43288,8 @@ function trainApplyDeps() {
|
|
|
43029
43288
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
43030
43289
|
announce: (args) => announceRelease({
|
|
43031
43290
|
run: async (file, cmdArgs) => (await execFileP(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
43032
|
-
readFile: (path2) => (0,
|
|
43033
|
-
removeFile: (path2) => (0,
|
|
43291
|
+
readFile: (path2) => (0, import_promises10.readFile)(path2, "utf8"),
|
|
43292
|
+
removeFile: (path2) => (0, import_promises10.unlink)(path2)
|
|
43034
43293
|
}, args),
|
|
43035
43294
|
// #4713 (I/O-boundary census): `null` used to mean BOTH "this project configures no edge domains"
|
|
43036
43295
|
// (a real answer) and "the registry read missed" — so a release verdict printed an environments block
|
|
@@ -43416,7 +43675,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
43416
43675
|
}
|
|
43417
43676
|
let summaryLines;
|
|
43418
43677
|
try {
|
|
43419
|
-
summaryLines = summaryFileLines(await (0,
|
|
43678
|
+
summaryLines = summaryFileLines(await (0, import_promises10.readFile)(o.announceSummaryFile, "utf8"));
|
|
43420
43679
|
} catch (e) {
|
|
43421
43680
|
return fail(`release: could not read --announce-summary-file ${o.announceSummaryFile}: ${e.message} \u2014 see docs/Guides/train-troubleshooting.md#announce-summary-missing`);
|
|
43422
43681
|
}
|
|
@@ -43610,7 +43869,7 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
43610
43869
|
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
43611
43870
|
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
43612
43871
|
function envHealLockPath(home) {
|
|
43613
|
-
return (0,
|
|
43872
|
+
return (0, import_node_path47.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
43614
43873
|
}
|
|
43615
43874
|
async function withEnvHealLock(what, run) {
|
|
43616
43875
|
try {
|
|
@@ -43781,7 +44040,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
43781
44040
|
// has no generated routing index would
|
|
43782
44041
|
// get a permanent — demanding an artifact it never asked for.
|
|
43783
44042
|
docsIndexState: (root) => {
|
|
43784
|
-
if (!(0, import_node_fs50.existsSync)((0,
|
|
44043
|
+
if (!(0, import_node_fs50.existsSync)((0, import_node_path47.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
43785
44044
|
const real = createDocsIndexDeps(root);
|
|
43786
44045
|
let docs;
|
|
43787
44046
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -43790,7 +44049,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
43790
44049
|
},
|
|
43791
44050
|
// #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
|
|
43792
44051
|
healDocsIndex: (root) => {
|
|
43793
|
-
if (!(0, import_node_fs50.existsSync)((0,
|
|
44052
|
+
if (!(0, import_node_fs50.existsSync)((0, import_node_path47.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
|
|
43794
44053
|
const real = createDocsIndexDeps(root);
|
|
43795
44054
|
let docs;
|
|
43796
44055
|
const listDocs = () => docs ??= real.listDocs();
|
|
@@ -44669,16 +44928,16 @@ function ciAuditDeps2() {
|
|
|
44669
44928
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
44670
44929
|
readSeedFile: (path2) => {
|
|
44671
44930
|
if (!root) return null;
|
|
44672
|
-
const fullPath = (0,
|
|
44931
|
+
const fullPath = (0, import_node_path47.join)(root, path2);
|
|
44673
44932
|
return (0, import_node_fs50.existsSync)(fullPath) ? (0, import_node_fs50.readFileSync)(fullPath, "utf8") : null;
|
|
44674
44933
|
}
|
|
44675
44934
|
};
|
|
44676
44935
|
}
|
|
44677
44936
|
function hubRoot2() {
|
|
44678
|
-
const fromPkg = (0,
|
|
44937
|
+
const fromPkg = (0, import_node_path47.join)(__dirname, "..", "..");
|
|
44679
44938
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
44680
|
-
if ((0, import_node_fs50.existsSync)((0,
|
|
44681
|
-
if ((0, import_node_fs50.existsSync)((0,
|
|
44939
|
+
if ((0, import_node_fs50.existsSync)((0, import_node_path47.join)(fromPkg, marker))) return fromPkg;
|
|
44940
|
+
if ((0, import_node_fs50.existsSync)((0, import_node_path47.join)(process.cwd(), marker))) return process.cwd();
|
|
44682
44941
|
return null;
|
|
44683
44942
|
}
|
|
44684
44943
|
registerQueryCommands(program2);
|
|
@@ -44817,7 +45076,7 @@ function directoryBytes(path2) {
|
|
|
44817
45076
|
return 0;
|
|
44818
45077
|
}
|
|
44819
45078
|
for (const entry of entries) {
|
|
44820
|
-
const child2 = (0,
|
|
45079
|
+
const child2 = (0, import_node_path47.join)(path2, entry.name);
|
|
44821
45080
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
44822
45081
|
else {
|
|
44823
45082
|
try {
|
|
@@ -44847,7 +45106,7 @@ function pluginCacheFsDeps(configRoot, dirBytes) {
|
|
|
44847
45106
|
dirBytes,
|
|
44848
45107
|
listStagingDirs: (root) => (0, import_node_fs50.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
44849
45108
|
try {
|
|
44850
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
45109
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path47.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs50.statSync)(p).mtimeMs) };
|
|
44851
45110
|
} catch {
|
|
44852
45111
|
return { name: d.name, mtimeMs: Date.now() };
|
|
44853
45112
|
}
|
|
@@ -44861,7 +45120,7 @@ function stagingApplyFsGuard(configRoot) {
|
|
|
44861
45120
|
return {
|
|
44862
45121
|
referencedPaths: () => readInstalledPluginRefs(configRoot),
|
|
44863
45122
|
mtimeMs: (name) => {
|
|
44864
|
-
const p = (0,
|
|
45123
|
+
const p = (0, import_node_path47.join)(stagingRoot, name);
|
|
44865
45124
|
if (!(0, import_node_fs50.existsSync)(p)) return null;
|
|
44866
45125
|
try {
|
|
44867
45126
|
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs50.statSync)(q).mtimeMs);
|