@mutmutco/cli 4.3.19 → 4.3.21
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 +528 -271
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -12902,6 +12902,29 @@ async function waitForPrChecks(deps) {
|
|
|
12902
12902
|
|
|
12903
12903
|
// src/bootstrap-ruleset.ts
|
|
12904
12904
|
var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
|
|
12905
|
+
var PRODUCT_RULESET_PATH = ".github/rulesets/mmi-product-required-checks.json";
|
|
12906
|
+
function reseedProductRulesetStrictness(current, seed) {
|
|
12907
|
+
const parse = (raw) => {
|
|
12908
|
+
const policy2 = JSON.parse(raw);
|
|
12909
|
+
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")) {
|
|
12910
|
+
throw new Error("product ruleset must be a branch ruleset object with well-formed rules");
|
|
12911
|
+
}
|
|
12912
|
+
const required = policy2.rules.filter((r) => r.type === "required_status_checks");
|
|
12913
|
+
const parameters2 = required[0]?.parameters;
|
|
12914
|
+
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") {
|
|
12915
|
+
throw new Error("product ruleset must carry exactly one well-formed required_status_checks rule");
|
|
12916
|
+
}
|
|
12917
|
+
return { policy: policy2, parameters: parameters2 };
|
|
12918
|
+
};
|
|
12919
|
+
const strict = parse(seed).parameters.strict_required_status_checks_policy;
|
|
12920
|
+
if (typeof strict !== "boolean") throw new Error("canonical product ruleset must declare boolean strictness");
|
|
12921
|
+
if (current === null) return seed;
|
|
12922
|
+
const { policy, parameters } = parse(current);
|
|
12923
|
+
if (parameters.strict_required_status_checks_policy === strict) return current;
|
|
12924
|
+
parameters.strict_required_status_checks_policy = strict;
|
|
12925
|
+
return `${JSON.stringify(policy, null, 2)}
|
|
12926
|
+
`;
|
|
12927
|
+
}
|
|
12905
12928
|
function stripRulesetComment(raw) {
|
|
12906
12929
|
const parsed = JSON.parse(raw);
|
|
12907
12930
|
delete parsed._comment;
|
|
@@ -12921,14 +12944,6 @@ function rulesetStrictPolicy(ruleset) {
|
|
|
12921
12944
|
const rules = (ruleset.rules ?? []).filter((rule) => rule.type === "required_status_checks");
|
|
12922
12945
|
return rules.length > 0 && rules.every((rule) => rule.parameters?.strict_required_status_checks_policy === true);
|
|
12923
12946
|
}
|
|
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
12947
|
function rulesetBranchIncludes(ruleset) {
|
|
12933
12948
|
const raw = ruleset.conditions?.ref_name?.include;
|
|
12934
12949
|
return Array.isArray(raw) ? [...new Set(raw.filter((ref) => typeof ref === "string" && ref.length > 0))].sort((a, b) => a.localeCompare(b)) : [];
|
|
@@ -12948,8 +12963,7 @@ function patchRulesetRequiredContexts(body, contexts) {
|
|
|
12948
12963
|
...r,
|
|
12949
12964
|
parameters: {
|
|
12950
12965
|
...r.parameters,
|
|
12951
|
-
strict_required_status_checks_policy:
|
|
12952
|
-
// #6263: org policy — see patchRulesetStrictPolicy
|
|
12966
|
+
strict_required_status_checks_policy: r.parameters?.strict_required_status_checks_policy ?? false,
|
|
12953
12967
|
required_status_checks: sorted.map((context) => ({ context }))
|
|
12954
12968
|
}
|
|
12955
12969
|
};
|
|
@@ -15502,10 +15516,10 @@ var rollout_plan_default = {
|
|
|
15502
15516
|
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
15517
|
},
|
|
15504
15518
|
baseline: {
|
|
15505
|
-
version: "4.3.
|
|
15506
|
-
tag: "v4.3.
|
|
15507
|
-
commit: "
|
|
15508
|
-
npm: "@mutmutco/cli@4.3.
|
|
15519
|
+
version: "4.3.21",
|
|
15520
|
+
tag: "v4.3.21",
|
|
15521
|
+
commit: "f55fc2217cd3",
|
|
15522
|
+
npm: "@mutmutco/cli@4.3.21"
|
|
15509
15523
|
},
|
|
15510
15524
|
exitCriterion: "fleet-n-of-n",
|
|
15511
15525
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15522,14 +15536,14 @@ var rollout_plan_default = {
|
|
|
15522
15536
|
repo: "mutmutco/mmi-hub",
|
|
15523
15537
|
role: "canary",
|
|
15524
15538
|
schedule: "train",
|
|
15525
|
-
v3Target: "v4.3.
|
|
15539
|
+
v3Target: "v4.3.21"
|
|
15526
15540
|
}
|
|
15527
15541
|
],
|
|
15528
15542
|
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
15543
|
rollback: {
|
|
15530
15544
|
independent: true,
|
|
15531
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15532
|
-
v3Target: "v4.3.
|
|
15545
|
+
mechanism: "npm dist-tag latest -> 4.3.21 and redeploy the Hub Lambda from tag v4.3.21 (f55fc2217cd3); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15546
|
+
v3Target: "v4.3.21 (@mutmutco/cli@4.3.21, tag commit f55fc2217cd3 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15533
15547
|
}
|
|
15534
15548
|
},
|
|
15535
15549
|
{
|
|
@@ -23527,12 +23541,12 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
|
|
|
23527
23541
|
const contexts = sortedUnique(registryContexts ?? committedContexts);
|
|
23528
23542
|
const explicitBranches = Array.isArray(meta?.requiredCheckBranches) && meta.requiredCheckBranches.length > 0 ? resolveRequiredCheckBranches(meta, repo) : null;
|
|
23529
23543
|
const committedStrict = rulesetStrictPolicy(committedPayload);
|
|
23530
|
-
let apiPayload =
|
|
23544
|
+
let apiPayload = registryContexts == null ? committedPayload : patchRulesetRequiredContexts(committedPayload, contexts);
|
|
23531
23545
|
if (explicitBranches) apiPayload = patchRulesetBranchIncludes(apiPayload, explicitBranches);
|
|
23532
23546
|
const branchIncludes = rulesetBranchIncludes(apiPayload);
|
|
23533
23547
|
const authoritativeFilePayload = {
|
|
23534
23548
|
...filePayload,
|
|
23535
|
-
...registryContexts == null
|
|
23549
|
+
...registryContexts == null ? {} : { rules: apiPayload.rules },
|
|
23536
23550
|
...explicitBranches == null ? {} : { conditions: apiPayload.conditions }
|
|
23537
23551
|
};
|
|
23538
23552
|
return {
|
|
@@ -23554,7 +23568,7 @@ function sameContexts(left, right) {
|
|
|
23554
23568
|
function resolveProductRulesetReconcilePlan(input) {
|
|
23555
23569
|
const liveNeedsContextConvergence = !sameContexts(input.liveContexts, input.authorityContexts);
|
|
23556
23570
|
const liveNeedsBranchConvergence = input.liveBranchIncludes !== void 0 && input.authorityBranchIncludes !== void 0 && !sameContexts(input.liveBranchIncludes, input.authorityBranchIncludes);
|
|
23557
|
-
const liveNeedsStrictConvergence = input.liveStrict
|
|
23571
|
+
const liveNeedsStrictConvergence = input.liveStrict !== void 0 && input.authorityStrict !== void 0 && input.liveStrict !== input.authorityStrict;
|
|
23558
23572
|
const liveIsActive = input.liveEnforcement === "active";
|
|
23559
23573
|
if (liveIsActive && !liveNeedsContextConvergence && !liveNeedsBranchConvergence && !liveNeedsStrictConvergence) {
|
|
23560
23574
|
return { shouldActivate: false, targetEnforcement: "active" };
|
|
@@ -23765,12 +23779,12 @@ async function auditRepoCi(repo, deps) {
|
|
|
23765
23779
|
const liveContextsAligned = sameContexts(liveContexts, authoritativeRuleset.contexts);
|
|
23766
23780
|
const fileBranchesAligned = sameContexts(authoritativeRuleset.committedBranchIncludes, authoritativeRuleset.branchIncludes);
|
|
23767
23781
|
const liveBranchesAligned = sameContexts(liveBranchIncludes, authoritativeRuleset.branchIncludes);
|
|
23768
|
-
const strictAligned = authoritativeRuleset.committedStrict
|
|
23782
|
+
const strictAligned = authoritativeRuleset.committedStrict === liveStrict;
|
|
23769
23783
|
const aligned = fileContextsAligned && liveContextsAligned && fileBranchesAligned && liveBranchesAligned && strictAligned;
|
|
23770
23784
|
checks.push({
|
|
23771
23785
|
ok: aligned,
|
|
23772
23786
|
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
|
|
23787
|
+
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
23788
|
remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
|
|
23775
23789
|
});
|
|
23776
23790
|
}
|
|
@@ -24360,6 +24374,7 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
24360
24374
|
liveBranchIncludes,
|
|
24361
24375
|
authorityBranchIncludes: authority.branchIncludes,
|
|
24362
24376
|
liveStrict: live == null ? void 0 : rulesetStrictPolicy(live),
|
|
24377
|
+
authorityStrict: authority.committedStrict,
|
|
24363
24378
|
gateProvenGreen: await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles),
|
|
24364
24379
|
unsafeContexts: unsafe
|
|
24365
24380
|
});
|
|
@@ -27167,11 +27182,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
27167
27182
|
label: "product required-check ruleset enforcement active",
|
|
27168
27183
|
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
27184
|
});
|
|
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
27185
|
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
27176
27186
|
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
27177
27187
|
checks.push({
|
|
@@ -27225,6 +27235,100 @@ function renderBootstrapVerifyReport(report) {
|
|
|
27225
27235
|
return lines2.join("\n");
|
|
27226
27236
|
}
|
|
27227
27237
|
|
|
27238
|
+
// src/bootstrap-seed-delivery.ts
|
|
27239
|
+
var import_node_crypto9 = require("node:crypto");
|
|
27240
|
+
var SHA = /^[a-f0-9]{40}$/;
|
|
27241
|
+
async function readSeedFile(repo, target, ref, client) {
|
|
27242
|
+
try {
|
|
27243
|
+
const file = await client.rest(
|
|
27244
|
+
"GET",
|
|
27245
|
+
`repos/${repo}/contents/${target.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`
|
|
27246
|
+
);
|
|
27247
|
+
if (!file?.sha || !SHA.test(file.sha) || file.encoding !== "base64" || typeof file.content !== "string") {
|
|
27248
|
+
throw new Error("bootstrap apply: scoped seed file is unreadable");
|
|
27249
|
+
}
|
|
27250
|
+
return { sha: file.sha, content: decodeGitHubContents(file.content) };
|
|
27251
|
+
} catch (e) {
|
|
27252
|
+
if (e instanceof GitHubApiError && e.status === 404) return null;
|
|
27253
|
+
throw e;
|
|
27254
|
+
}
|
|
27255
|
+
}
|
|
27256
|
+
async function readTargetedSeedFiles(repo, plan, target, ref, client) {
|
|
27257
|
+
const base = await readSeedFile(repo, target, plan.baseSha, client);
|
|
27258
|
+
const candidate = ref === plan.baseSha ? base : await readSeedFile(repo, target, ref, client);
|
|
27259
|
+
return { base, candidate };
|
|
27260
|
+
}
|
|
27261
|
+
async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, client) {
|
|
27262
|
+
const [rules, base] = await Promise.all([
|
|
27263
|
+
client.rest("GET", `repos/${repo}/rules/branches/${encodeURIComponent(baseBranch)}`),
|
|
27264
|
+
client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(baseBranch)}`)
|
|
27265
|
+
]);
|
|
27266
|
+
if (!Array.isArray(rules) || rules.some((rule) => !rule || typeof rule.type !== "string" || !rule.type)) {
|
|
27267
|
+
throw new Error("bootstrap apply: base branch protection is unreadable");
|
|
27268
|
+
}
|
|
27269
|
+
const baseSha = base?.object?.sha;
|
|
27270
|
+
if (!baseSha || !SHA.test(baseSha) || !SHA.test(sourceSha)) throw new Error("bootstrap apply: seed identity is unreadable");
|
|
27271
|
+
const identity = (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify([repo.toLowerCase(), target, baseSha, sourceSha])).digest("hex").slice(0, 24);
|
|
27272
|
+
return { ...planSeedDelivery(rules, slug, baseBranch, `bootstrap-seed-${identity}`), baseSha, baseBranch };
|
|
27273
|
+
}
|
|
27274
|
+
async function verifyTargetedSeedBranch(repo, plan, target, client, allowMissing = false) {
|
|
27275
|
+
let ref;
|
|
27276
|
+
try {
|
|
27277
|
+
ref = await client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(plan.ref)}`);
|
|
27278
|
+
} catch (e) {
|
|
27279
|
+
if (allowMissing && e instanceof GitHubApiError && e.status === 404) return null;
|
|
27280
|
+
throw e;
|
|
27281
|
+
}
|
|
27282
|
+
const head = ref?.object?.sha;
|
|
27283
|
+
if (!head || !SHA.test(head)) throw new Error("bootstrap apply: scoped seed head is unreadable");
|
|
27284
|
+
if (head === plan.baseSha) return head;
|
|
27285
|
+
const diff = await client.rest("GET", `repos/${repo}/compare/${plan.baseSha}...${head}`);
|
|
27286
|
+
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 ?? "")) {
|
|
27287
|
+
throw new Error("bootstrap apply: scoped seed branch contains changes outside the requested target or base");
|
|
27288
|
+
}
|
|
27289
|
+
return head;
|
|
27290
|
+
}
|
|
27291
|
+
async function prepareTargetedSeedBranch(repo, plan, target, client, gh) {
|
|
27292
|
+
const candidates = await client.rest(
|
|
27293
|
+
"GET",
|
|
27294
|
+
`repos/${repo}/pulls?state=open&head=${encodeURIComponent(`${repo.split("/")[0]}:${plan.branch}`)}&base=${encodeURIComponent(plan.baseBranch)}&per_page=2`
|
|
27295
|
+
);
|
|
27296
|
+
if (!Array.isArray(candidates) || candidates.length > 1) throw new Error("bootstrap apply: scoped seed PR identity is unreadable");
|
|
27297
|
+
for (const pr of candidates) {
|
|
27298
|
+
if (!Number.isInteger(pr?.number) || pr.number <= 0 || pr.head?.ref !== plan.branch || pr.base?.ref !== plan.baseBranch) {
|
|
27299
|
+
throw new Error("bootstrap apply: scoped seed PR identity is unreadable");
|
|
27300
|
+
}
|
|
27301
|
+
await gh(["pr", "merge", String(pr.number), "--repo", repo, "--disable-auto"]);
|
|
27302
|
+
const after = await client.rest("GET", `repos/${repo}/pulls/${pr.number}`);
|
|
27303
|
+
if (after?.auto_merge !== null) throw new Error("bootstrap apply: scoped seed PR auto-merge was not disabled");
|
|
27304
|
+
}
|
|
27305
|
+
const existing = await verifyTargetedSeedBranch(repo, plan, target, client, true);
|
|
27306
|
+
if (existing) return;
|
|
27307
|
+
try {
|
|
27308
|
+
await client.rest("POST", `repos/${repo}/git/refs`, { body: { ref: `refs/heads/${plan.branch}`, sha: plan.baseSha } });
|
|
27309
|
+
} catch (e) {
|
|
27310
|
+
if (!(e instanceof GitHubApiError) || e.status !== 422) throw e;
|
|
27311
|
+
}
|
|
27312
|
+
await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
27313
|
+
}
|
|
27314
|
+
async function enableTargetedSeedAutoMerge(repo, number, plan, target, expected, client, gh) {
|
|
27315
|
+
const head = await verifyTargetedSeedBranch(repo, plan, target, client);
|
|
27316
|
+
const pr = await client.rest("GET", `repos/${repo}/pulls/${number}`);
|
|
27317
|
+
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");
|
|
27318
|
+
const files = await client.rest(
|
|
27319
|
+
"GET",
|
|
27320
|
+
`repos/${repo}/pulls/${number}/files?per_page=2`
|
|
27321
|
+
);
|
|
27322
|
+
if (!Array.isArray(files) || files.length !== 1 || files[0]?.filename !== target || files[0].previous_filename !== void 0) {
|
|
27323
|
+
throw new Error("bootstrap apply: scoped seed PR contains changes outside the requested target");
|
|
27324
|
+
}
|
|
27325
|
+
const file = await readSeedFile(repo, target, head, client);
|
|
27326
|
+
if (file?.content !== expected) {
|
|
27327
|
+
throw new Error("bootstrap apply: scoped seed content differs from this invocation");
|
|
27328
|
+
}
|
|
27329
|
+
await gh(["pr", "merge", String(number), "--repo", repo, "--auto", "--squash", "--match-head-commit", head]);
|
|
27330
|
+
}
|
|
27331
|
+
|
|
27228
27332
|
// src/bootstrap-commands.ts
|
|
27229
27333
|
var execGitForSeedSource = async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
27230
27334
|
function bootstrapTrainDoctorDeps() {
|
|
@@ -27632,14 +27736,23 @@ function registerBootstrapCommands(program3) {
|
|
|
27632
27736
|
});
|
|
27633
27737
|
}
|
|
27634
27738
|
let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
|
|
27739
|
+
let scopedPlan;
|
|
27740
|
+
let scopedContent;
|
|
27635
27741
|
let seededToBranch = 0;
|
|
27636
|
-
if (
|
|
27742
|
+
if (onlyTarget) {
|
|
27743
|
+
scopedPlan = await targetedSeedPlan(repo, slug, onlyTarget, baseBranch, seedSource.sha, controlClient);
|
|
27744
|
+
seedPlan = scopedPlan;
|
|
27745
|
+
if (o.execute && scopedPlan.branch) {
|
|
27746
|
+
await prepareTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient, gh);
|
|
27747
|
+
}
|
|
27748
|
+
} else if (o.execute) {
|
|
27637
27749
|
let branchRules = [];
|
|
27638
27750
|
try {
|
|
27639
27751
|
branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
|
|
27640
27752
|
} catch {
|
|
27641
|
-
|
|
27753
|
+
throw new Error("bootstrap apply: base branch protection is unreadable");
|
|
27642
27754
|
}
|
|
27755
|
+
if (!Array.isArray(branchRules)) throw new Error("bootstrap apply: base branch protection is unreadable");
|
|
27643
27756
|
seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
|
|
27644
27757
|
if (seedPlan.branch) {
|
|
27645
27758
|
const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
|
|
@@ -27651,6 +27764,7 @@ function registerBootstrapCommands(program3) {
|
|
|
27651
27764
|
}
|
|
27652
27765
|
}
|
|
27653
27766
|
const docsForIndex = [];
|
|
27767
|
+
const seedReadRef = !o.execute && scopedPlan ? scopedPlan.baseSha : seedPlan.ref;
|
|
27654
27768
|
for (const seed of seedsToApply) {
|
|
27655
27769
|
if (!seed.classes.includes(o.class)) continue;
|
|
27656
27770
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
@@ -27664,8 +27778,20 @@ function registerBootstrapCommands(program3) {
|
|
|
27664
27778
|
let exists = false;
|
|
27665
27779
|
let sha;
|
|
27666
27780
|
let remoteContent = null;
|
|
27667
|
-
|
|
27668
|
-
|
|
27781
|
+
let baseContent = null;
|
|
27782
|
+
const preserveRuleset = onlyTarget === PRODUCT_RULESET_PATH;
|
|
27783
|
+
if (scopedPlan) {
|
|
27784
|
+
try {
|
|
27785
|
+
const files = await readTargetedSeedFiles(repo, scopedPlan, resolved.target, seedReadRef, controlClient);
|
|
27786
|
+
baseContent = files.base?.content ?? null;
|
|
27787
|
+
exists = files.candidate !== null;
|
|
27788
|
+
sha = files.candidate?.sha;
|
|
27789
|
+
remoteContent = files.candidate?.content ?? null;
|
|
27790
|
+
} catch (e) {
|
|
27791
|
+
return failGraceful(`bootstrap apply: cannot read scoped seed file; no replacement made: ${e.message}`);
|
|
27792
|
+
}
|
|
27793
|
+
} else try {
|
|
27794
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedReadRef}`]);
|
|
27669
27795
|
exists = true;
|
|
27670
27796
|
try {
|
|
27671
27797
|
const parsed = JSON.parse(r.stdout);
|
|
@@ -27678,23 +27804,34 @@ function registerBootstrapCommands(program3) {
|
|
|
27678
27804
|
} catch {
|
|
27679
27805
|
exists = false;
|
|
27680
27806
|
}
|
|
27681
|
-
const planned = planSeedAction(resolved, exists);
|
|
27807
|
+
const planned = planSeedAction(resolved, scopedPlan ? baseContent !== null : exists);
|
|
27808
|
+
if (scopedPlan && planned.action !== "skip") planned.action = exists ? "update" : "create";
|
|
27682
27809
|
const isLegacyBlock = resolved.source === "managed-block";
|
|
27683
27810
|
let content = null;
|
|
27684
27811
|
let isManaged = isLegacyBlock || resolved.managedBlock != null;
|
|
27812
|
+
const preservedContent = scopedPlan ? baseContent : remoteContent;
|
|
27685
27813
|
if (planned.action === "create" || planned.action === "update") {
|
|
27686
27814
|
if (isLegacyBlock) {
|
|
27687
|
-
content = upsertManagedGitignoreBlock(
|
|
27815
|
+
content = upsertManagedGitignoreBlock(preservedContent).content;
|
|
27688
27816
|
} else {
|
|
27689
|
-
const writeContent = resolveSeedWriteContent(resolved, vars, readFile9,
|
|
27817
|
+
const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, preservedContent);
|
|
27690
27818
|
if (!writeContent.ok) {
|
|
27691
27819
|
return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
27692
27820
|
}
|
|
27693
27821
|
content = writeContent.content;
|
|
27822
|
+
if (preserveRuleset) {
|
|
27823
|
+
if (content === null) return fail("bootstrap apply: canonical product ruleset seed is unreadable");
|
|
27824
|
+
try {
|
|
27825
|
+
content = reseedProductRulesetStrictness(baseContent, content);
|
|
27826
|
+
} catch (e) {
|
|
27827
|
+
return fail(`bootstrap apply: product ruleset preservation refused: ${e.message}`);
|
|
27828
|
+
}
|
|
27829
|
+
}
|
|
27694
27830
|
isManaged = writeContent.managed;
|
|
27695
27831
|
}
|
|
27696
27832
|
}
|
|
27697
27833
|
const action = reconcileSeedAction(planned, content, isManaged, remoteContent);
|
|
27834
|
+
if (onlyTarget && content !== null) scopedContent = content;
|
|
27698
27835
|
actions.push(action);
|
|
27699
27836
|
const docBody = content ?? remoteContent;
|
|
27700
27837
|
if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
|
|
@@ -27729,7 +27866,8 @@ function registerBootstrapCommands(program3) {
|
|
|
27729
27866
|
}
|
|
27730
27867
|
}
|
|
27731
27868
|
let seedPrUrl;
|
|
27732
|
-
|
|
27869
|
+
const scopedHead = o.execute && scopedPlan?.branch ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
|
|
27870
|
+
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
|
|
27733
27871
|
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
27872
|
});
|
|
27735
27873
|
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
@@ -27758,7 +27896,12 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
27758
27896
|
seedPrUrl = created.url;
|
|
27759
27897
|
}
|
|
27760
27898
|
let autoMergeEnabled = true;
|
|
27761
|
-
|
|
27899
|
+
if (scopedPlan) {
|
|
27900
|
+
if (scopedContent === void 0) throw new Error("bootstrap apply: scoped seed content is unresolved");
|
|
27901
|
+
const number = Number(seedPrUrl.split("/").pop());
|
|
27902
|
+
if (!Number.isInteger(number) || number <= 0) throw new Error("bootstrap apply: scoped seed PR number is unreadable");
|
|
27903
|
+
await enableTargetedSeedAutoMerge(repo, number, scopedPlan, onlyTarget, scopedContent, controlClient, gh);
|
|
27904
|
+
} else await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
|
|
27762
27905
|
const message2 = String(e.message ?? "");
|
|
27763
27906
|
if (/already/i.test(message2)) return;
|
|
27764
27907
|
if (/clean status|enablePullRequestAutoMerge/i.test(message2)) {
|
|
@@ -27941,7 +28084,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
27941
28084
|
deployFactsRead: controlDeployFacts !== null
|
|
27942
28085
|
});
|
|
27943
28086
|
}
|
|
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));
|
|
28087
|
+
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
28088
|
else {
|
|
27946
28089
|
console.log(renderSeedPlan(actions));
|
|
27947
28090
|
if (controlRows.length) console.log(`
|
|
@@ -27968,7 +28111,7 @@ LIVE apply to ${repo}:
|
|
|
27968
28111
|
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
27969
28112
|
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
28113
|
const hubContent = seed.managedBlock ? null : (0, import_node_fs26.readFileSync)(seed.target, "utf8");
|
|
27971
|
-
const
|
|
28114
|
+
const readSeedFile2 = (path2) => (0, import_node_fs26.existsSync)(path2) ? (0, import_node_fs26.readFileSync)(path2, "utf8") : null;
|
|
27972
28115
|
const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
|
|
27973
28116
|
const cfg = await loadConfig();
|
|
27974
28117
|
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
@@ -28045,14 +28188,14 @@ LIVE apply to ${repo}:
|
|
|
28045
28188
|
const project2 = projects.find((p) => (p.repos ?? []).some((repo) => repo.toLowerCase() === r.repo.toLowerCase()));
|
|
28046
28189
|
const track = resolveReleaseTrack(project2, void 0, r.repo);
|
|
28047
28190
|
const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass, track, project2?.requiredCheckBranches);
|
|
28048
|
-
const resolved = resolveSeedWriteContent(seed, vars,
|
|
28191
|
+
const resolved = resolveSeedWriteContent(seed, vars, readSeedFile2, content);
|
|
28049
28192
|
if (!resolved.ok || resolved.content == null) {
|
|
28050
28193
|
return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.ok ? "rendered no content" : resolved.reason} \u2014 refusing an incomplete per-repo render`);
|
|
28051
28194
|
}
|
|
28052
28195
|
desired = resolved.content;
|
|
28053
28196
|
} else if (seed.managedBlock) {
|
|
28054
28197
|
const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass);
|
|
28055
|
-
const resolved = resolveSeedWriteContent(seed, vars,
|
|
28198
|
+
const resolved = resolveSeedWriteContent(seed, vars, readSeedFile2, content);
|
|
28056
28199
|
if (!resolved.ok) {
|
|
28057
28200
|
return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
28058
28201
|
}
|
|
@@ -31891,8 +32034,7 @@ var LOOP_PLAYBOOKS = {
|
|
|
31891
32034
|
{ label: "Apply the repository test policy, then build the touched package", command: "mmi-cli tests policy --base origin/development && npm run build" },
|
|
31892
32035
|
{ label: "Publish the branch", command: "git push origin <branch>:<branch>" },
|
|
31893
32036
|
{ 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>" },
|
|
32037
|
+
{ label: "Land to development (waits for checks itself)", command: "mmi-cli devops pr land <PR-number>" },
|
|
31896
32038
|
{ label: "Release only after the gated train is authorized", command: "mmi-cli devops release --apply" },
|
|
31897
32039
|
// #5552: learning-tagged filings are cloud-agent owned — file and return to the current task.
|
|
31898
32040
|
{ 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 +32055,14 @@ var LOOP_PLAYBOOKS = {
|
|
|
31913
32055
|
steps: [
|
|
31914
32056
|
{ label: "Publish the branch", command: "git push origin <branch>:<branch>" },
|
|
31915
32057
|
{ 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
|
|
32058
|
+
{ label: "Land the PR (merge to development \u2014 waits for checks itself)", command: "mmi-cli devops pr land <PR-number>" }
|
|
31917
32059
|
]
|
|
31918
32060
|
},
|
|
31919
32061
|
"hotfix": {
|
|
31920
32062
|
title: "Hotfix",
|
|
31921
32063
|
steps: [
|
|
31922
32064
|
{ 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
|
|
32065
|
+
{ label: "Merge the main-base PR (waits for checks itself)", command: "mmi-cli devops pr merge <PR-number> --squash" },
|
|
31924
32066
|
{ 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
32067
|
]
|
|
31926
32068
|
}
|
|
@@ -32193,7 +32335,7 @@ function parseOriginRepo(remoteUrl) {
|
|
|
32193
32335
|
|
|
32194
32336
|
// src/issue-commands.ts
|
|
32195
32337
|
var import_node_fs31 = require("node:fs");
|
|
32196
|
-
var
|
|
32338
|
+
var import_node_crypto10 = require("node:crypto");
|
|
32197
32339
|
|
|
32198
32340
|
// src/issue-body.ts
|
|
32199
32341
|
var import_node_os16 = require("node:os");
|
|
@@ -32723,7 +32865,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
32723
32865
|
const identity = `${spec.type}
|
|
32724
32866
|
${spec.title.trim()}
|
|
32725
32867
|
${spec.body ?? ""}`;
|
|
32726
|
-
const hash = (0,
|
|
32868
|
+
const hash = (0, import_node_crypto10.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
32727
32869
|
return `${batchKey}:${hash}`;
|
|
32728
32870
|
}
|
|
32729
32871
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -35768,14 +35910,14 @@ function registerStageCommands(program3) {
|
|
|
35768
35910
|
}
|
|
35769
35911
|
|
|
35770
35912
|
// src/tenant-artifact.ts
|
|
35771
|
-
var
|
|
35913
|
+
var import_node_crypto11 = require("node:crypto");
|
|
35772
35914
|
var import_node_fs35 = require("node:fs");
|
|
35773
35915
|
var import_promises6 = require("node:fs/promises");
|
|
35774
35916
|
var import_node_path32 = require("node:path");
|
|
35775
35917
|
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
35918
|
var MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
35777
35919
|
async function sha256File(path2) {
|
|
35778
|
-
const hash = (0,
|
|
35920
|
+
const hash = (0, import_node_crypto11.createHash)("sha256");
|
|
35779
35921
|
for await (const chunk of (0, import_node_fs35.createReadStream)(path2)) hash.update(chunk);
|
|
35780
35922
|
return hash.digest("hex");
|
|
35781
35923
|
}
|
|
@@ -36145,6 +36287,14 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
36145
36287
|
};
|
|
36146
36288
|
}
|
|
36147
36289
|
}
|
|
36290
|
+
const queue = await deps.queueMerge?.(prNumber, repo);
|
|
36291
|
+
if (queue) return {
|
|
36292
|
+
...base,
|
|
36293
|
+
queue,
|
|
36294
|
+
status: queue.state === "merged" ? "merged" : "failed",
|
|
36295
|
+
mergeStatus: queue.state === "merged" ? "merged" : "failed",
|
|
36296
|
+
...queue.state === "merged" ? {} : { error: queue.detail }
|
|
36297
|
+
};
|
|
36148
36298
|
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
36149
36299
|
base.ciPolicy = ciPolicy;
|
|
36150
36300
|
const checksWaitError = (checksWait) => {
|
|
@@ -37454,9 +37604,287 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
37454
37604
|
};
|
|
37455
37605
|
}
|
|
37456
37606
|
|
|
37457
|
-
// src/
|
|
37607
|
+
// src/review-verdict.ts
|
|
37608
|
+
var import_node_child_process14 = require("node:child_process");
|
|
37458
37609
|
var import_node_fs38 = require("node:fs");
|
|
37610
|
+
var import_node_os19 = require("node:os");
|
|
37459
37611
|
var import_node_path35 = require("node:path");
|
|
37612
|
+
var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
|
|
37613
|
+
var REVIEW_VERDICTS = ["PROCEED", "CORRECT", "ESCALATE"];
|
|
37614
|
+
function isReviewVerdict(value) {
|
|
37615
|
+
return typeof value === "string" && REVIEW_VERDICTS.includes(value);
|
|
37616
|
+
}
|
|
37617
|
+
function renderReviewVerdictComment(input) {
|
|
37618
|
+
const payload = {
|
|
37619
|
+
v: 1,
|
|
37620
|
+
patch: input.patch,
|
|
37621
|
+
head: input.head,
|
|
37622
|
+
verdict: input.verdict,
|
|
37623
|
+
scope: input.scope,
|
|
37624
|
+
risk: input.risk,
|
|
37625
|
+
unverified: input.unverified,
|
|
37626
|
+
reviewer: input.reviewer
|
|
37627
|
+
};
|
|
37628
|
+
const findings = input.findings?.trim();
|
|
37629
|
+
return `${REVIEW_VERDICT_MARKER}
|
|
37630
|
+
\`\`\`json
|
|
37631
|
+
${JSON.stringify(payload, null, 2)}
|
|
37632
|
+
\`\`\`
|
|
37633
|
+
${findings ? `
|
|
37634
|
+
${findings}
|
|
37635
|
+
` : ""}`;
|
|
37636
|
+
}
|
|
37637
|
+
function isReviewVerdictComment(body) {
|
|
37638
|
+
return body.trimStart().startsWith(REVIEW_VERDICT_MARKER);
|
|
37639
|
+
}
|
|
37640
|
+
var FENCE_RE = /^(?:`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n(?:`{3,}|~{3,})\s*$/m;
|
|
37641
|
+
function parseReviewVerdictComment(body) {
|
|
37642
|
+
if (!isReviewVerdictComment(body)) return void 0;
|
|
37643
|
+
const rest = body.trimStart().slice(REVIEW_VERDICT_MARKER.length);
|
|
37644
|
+
const fence = FENCE_RE.exec(rest);
|
|
37645
|
+
if (!fence) return void 0;
|
|
37646
|
+
let parsed;
|
|
37647
|
+
try {
|
|
37648
|
+
parsed = JSON.parse(fence[1]);
|
|
37649
|
+
} catch {
|
|
37650
|
+
return void 0;
|
|
37651
|
+
}
|
|
37652
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
37653
|
+
const p = parsed;
|
|
37654
|
+
if (p.v !== 1 || !isReviewVerdict(p.verdict)) return void 0;
|
|
37655
|
+
if (typeof p.patch !== "string" || !/^[0-9a-f]{40}$/.test(p.patch)) return void 0;
|
|
37656
|
+
if (typeof p.head !== "string" || typeof p.scope !== "string" || typeof p.risk !== "string" || typeof p.reviewer !== "string") return void 0;
|
|
37657
|
+
const unverified = Array.isArray(p.unverified) ? p.unverified.filter((u) => typeof u === "string") : [];
|
|
37658
|
+
return { v: 1, patch: p.patch, head: p.head, verdict: p.verdict, scope: p.scope, risk: p.risk, unverified, reviewer: p.reviewer };
|
|
37659
|
+
}
|
|
37660
|
+
function latestReviewComment(comments) {
|
|
37661
|
+
let latest;
|
|
37662
|
+
for (const c of comments) {
|
|
37663
|
+
if (!isReviewVerdictComment(c.body)) continue;
|
|
37664
|
+
if (!latest || c.createdAt > latest.createdAt || c.createdAt === latest.createdAt && (c.id ?? 0) >= (latest.id ?? 0)) latest = c;
|
|
37665
|
+
}
|
|
37666
|
+
return latest;
|
|
37667
|
+
}
|
|
37668
|
+
function evaluateReviewVerdict(comments, currentPatchId) {
|
|
37669
|
+
const latest = latestReviewComment(comments);
|
|
37670
|
+
if (!latest) return { ok: false, reason: "none" };
|
|
37671
|
+
const payload = parseReviewVerdictComment(latest.body);
|
|
37672
|
+
if (!payload) return { ok: false, reason: "malformed" };
|
|
37673
|
+
if (payload.verdict !== "PROCEED") return { ok: false, reason: "not-proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37674
|
+
if (payload.patch !== currentPatchId) return { ok: false, reason: "stale", verdict: payload.verdict, patch: payload.patch };
|
|
37675
|
+
return { ok: true, reason: "proceed", verdict: payload.verdict, patch: payload.patch };
|
|
37676
|
+
}
|
|
37677
|
+
function evaluateTrustedReviewVerdict(comments, patch, head) {
|
|
37678
|
+
const latest = latestReviewComment(comments);
|
|
37679
|
+
if (!latest) return { ok: false, reason: "none" };
|
|
37680
|
+
const identity = { commentId: latest.id, commentCreatedAt: latest.createdAt };
|
|
37681
|
+
if (latest.author?.toLowerCase() !== "jervaise") return { ...identity, ok: false, reason: "untrusted-author" };
|
|
37682
|
+
const evaluation = evaluateReviewVerdict([latest], patch);
|
|
37683
|
+
if (!evaluation.ok) return { ...identity, ok: false, reason: evaluation.reason };
|
|
37684
|
+
if (parseReviewVerdictComment(latest.body)?.head !== head) return { ...identity, ok: false, reason: "stale-head" };
|
|
37685
|
+
return { ...identity, ok: true, reason: "proceed" };
|
|
37686
|
+
}
|
|
37687
|
+
async function checkPrReview(number, repo, head, deps = {
|
|
37688
|
+
readHead: readPrHeadSha,
|
|
37689
|
+
readComments: readPrIssueComments,
|
|
37690
|
+
computePatch: computePrPatchId
|
|
37691
|
+
}) {
|
|
37692
|
+
const base = { repo, number, head };
|
|
37693
|
+
if (!/^[1-9][0-9]*$/.test(number) || !/^[\w.-]+\/[\w.-]+$/.test(repo) || !/^[0-9a-f]{40}$/.test(head)) {
|
|
37694
|
+
return { ...base, ok: false, reason: "invalid-input", detail: "expected a PR number, owner/repo and exact lowercase 40-character head SHA" };
|
|
37695
|
+
}
|
|
37696
|
+
try {
|
|
37697
|
+
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
37698
|
+
const [comments, patch] = await Promise.all([deps.readComments(number, repo), deps.computePatch(number, repo)]);
|
|
37699
|
+
if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
|
|
37700
|
+
return { ...base, patch, ...evaluateTrustedReviewVerdict(comments, patch, head) };
|
|
37701
|
+
} catch (e) {
|
|
37702
|
+
return { ...base, ok: false, reason: "unreadable", detail: e.message };
|
|
37703
|
+
}
|
|
37704
|
+
}
|
|
37705
|
+
function computePrPatchId(number, repo) {
|
|
37706
|
+
return new Promise((resolve7, reject) => {
|
|
37707
|
+
const gh = (0, import_node_child_process14.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
37708
|
+
const git3 = (0, import_node_child_process14.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
37709
|
+
let out = "";
|
|
37710
|
+
let ghErr = "";
|
|
37711
|
+
let gitErr = "";
|
|
37712
|
+
const timer = setTimeout(() => {
|
|
37713
|
+
gh.kill();
|
|
37714
|
+
git3.kill();
|
|
37715
|
+
reject(new Error(`patch-id: timed out after ${GC_GH_TIMEOUT_MS4}ms`));
|
|
37716
|
+
}, GC_GH_TIMEOUT_MS4);
|
|
37717
|
+
gh.stdout.pipe(git3.stdin);
|
|
37718
|
+
gh.stderr.on("data", (d) => {
|
|
37719
|
+
ghErr += d.toString();
|
|
37720
|
+
});
|
|
37721
|
+
git3.stderr.on("data", (d) => {
|
|
37722
|
+
gitErr += d.toString();
|
|
37723
|
+
});
|
|
37724
|
+
git3.stdout.on("data", (d) => {
|
|
37725
|
+
out += d.toString();
|
|
37726
|
+
});
|
|
37727
|
+
gh.on("error", (e) => {
|
|
37728
|
+
clearTimeout(timer);
|
|
37729
|
+
reject(e);
|
|
37730
|
+
});
|
|
37731
|
+
git3.on("error", (e) => {
|
|
37732
|
+
clearTimeout(timer);
|
|
37733
|
+
reject(e);
|
|
37734
|
+
});
|
|
37735
|
+
let ghCode;
|
|
37736
|
+
let gitCode;
|
|
37737
|
+
const finish = () => {
|
|
37738
|
+
if (ghCode === void 0 || gitCode === void 0) return;
|
|
37739
|
+
clearTimeout(timer);
|
|
37740
|
+
if (ghCode !== 0) return reject(Object.assign(new Error(`gh pr diff ${number} --repo ${repo} exited ${ghCode}: ${ghErr.trim()}`), { stderr: ghErr }));
|
|
37741
|
+
if (gitCode !== 0) return reject(new Error(`git patch-id --stable exited ${gitCode}: ${gitErr.trim()}`));
|
|
37742
|
+
const id = out.trim().split(/\s+/)[0];
|
|
37743
|
+
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"})`));
|
|
37744
|
+
resolve7(id);
|
|
37745
|
+
};
|
|
37746
|
+
gh.on("close", (code) => {
|
|
37747
|
+
ghCode = code;
|
|
37748
|
+
finish();
|
|
37749
|
+
});
|
|
37750
|
+
git3.on("close", (code) => {
|
|
37751
|
+
gitCode = code;
|
|
37752
|
+
finish();
|
|
37753
|
+
});
|
|
37754
|
+
git3.stdin.on("error", (e) => {
|
|
37755
|
+
clearTimeout(timer);
|
|
37756
|
+
gh.kill();
|
|
37757
|
+
git3.kill();
|
|
37758
|
+
reject(e);
|
|
37759
|
+
});
|
|
37760
|
+
});
|
|
37761
|
+
}
|
|
37762
|
+
async function readPrHeadSha(number, repo) {
|
|
37763
|
+
const { stdout } = await execFileP("gh", ["api", `repos/${repo}/pulls/${number}`, "--jq", ".head.sha"], { timeout: GC_GH_TIMEOUT_MS4 });
|
|
37764
|
+
const sha = stdout.trim();
|
|
37765
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`could not read PR #${number} head sha`);
|
|
37766
|
+
return sha;
|
|
37767
|
+
}
|
|
37768
|
+
async function readPrIssueComments(number, repo) {
|
|
37769
|
+
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 });
|
|
37770
|
+
const comments = parseNdjsonLines(stdout);
|
|
37771
|
+
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");
|
|
37772
|
+
return comments;
|
|
37773
|
+
}
|
|
37774
|
+
async function postPrCommentFromFile(number, repo, body) {
|
|
37775
|
+
const dir = (0, import_node_fs38.mkdtempSync)((0, import_node_path35.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
|
|
37776
|
+
const path2 = (0, import_node_path35.join)(dir, "body.md");
|
|
37777
|
+
try {
|
|
37778
|
+
(0, import_node_fs38.writeFileSync)(path2, body, "utf8");
|
|
37779
|
+
const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
37780
|
+
return stdout.trim();
|
|
37781
|
+
} finally {
|
|
37782
|
+
try {
|
|
37783
|
+
(0, import_node_fs38.rmSync)(dir, { recursive: true, force: true });
|
|
37784
|
+
} catch {
|
|
37785
|
+
}
|
|
37786
|
+
}
|
|
37787
|
+
}
|
|
37788
|
+
|
|
37789
|
+
// src/pr-mergify-queue.ts
|
|
37790
|
+
var MERGIFY_PILOT_REPO = "mutmutco/Jerv-JervCode";
|
|
37791
|
+
var MERGIFY_PILOT_VARIABLE = "JERV_BATCH_QUEUE_PILOT";
|
|
37792
|
+
var MERGIFY_READY_LABEL = "ready-to-merge";
|
|
37793
|
+
async function ghJson(args) {
|
|
37794
|
+
return JSON.parse((await execFileP("gh", ["api", ...args], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
|
|
37795
|
+
}
|
|
37796
|
+
async function usesMergifyPilot(repo, base, readVariable = async () => ghJson([`repos/${repo}/actions/variables/${MERGIFY_PILOT_VARIABLE}`])) {
|
|
37797
|
+
if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase() || base !== "development") return false;
|
|
37798
|
+
const variable = await readVariable();
|
|
37799
|
+
if (variable?.value === "true") return true;
|
|
37800
|
+
if (variable?.value === "false") return false;
|
|
37801
|
+
throw new Error(`Mergify pilot activation is missing or malformed on ${repo}; refusing native merge`);
|
|
37802
|
+
}
|
|
37803
|
+
function queueCheckState(checks, head) {
|
|
37804
|
+
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];
|
|
37805
|
+
if (!check) return "refused";
|
|
37806
|
+
if (check.conclusion && !["success", "neutral"].includes(check.conclusion)) return "refused";
|
|
37807
|
+
if (["In merge queue", "Running merge queue checks"].includes(check.output?.title ?? "")) return "pending";
|
|
37808
|
+
return "requested";
|
|
37809
|
+
}
|
|
37810
|
+
async function requestMergifyMerge(number, repo, guardedText, expectedHead, deps = {
|
|
37811
|
+
readPull: () => ghJson([`repos/${repo}/pulls/${number}`]),
|
|
37812
|
+
readChecks: async (head) => parseNdjsonLines((await execFileP("gh", [
|
|
37813
|
+
"api",
|
|
37814
|
+
"--paginate",
|
|
37815
|
+
`repos/${repo}/commits/${head}/check-runs?per_page=100`,
|
|
37816
|
+
"--jq",
|
|
37817
|
+
".check_runs[]"
|
|
37818
|
+
], { timeout: GC_GH_TIMEOUT_MS4 })).stdout),
|
|
37819
|
+
addLabel: async () => {
|
|
37820
|
+
await ghJson([`repos/${repo}/issues/${number}/labels`, "--method", "POST", "-f", `labels[]=${MERGIFY_READY_LABEL}`]);
|
|
37821
|
+
},
|
|
37822
|
+
review: (head) => checkPrReview(number, repo, head),
|
|
37823
|
+
now: () => Date.now(),
|
|
37824
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
|
|
37825
|
+
}, timeoutMs = 6e5) {
|
|
37826
|
+
let head = expectedHead ?? "";
|
|
37827
|
+
let state = "refused";
|
|
37828
|
+
const receipt = (detail) => ({ provider: "mergify", state, head, detail });
|
|
37829
|
+
try {
|
|
37830
|
+
if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase()) return receipt("repository is outside the Mergify pilot");
|
|
37831
|
+
const initial = await deps.readPull();
|
|
37832
|
+
head = expectedHead ?? initial.head.sha;
|
|
37833
|
+
const sameTarget = (pr) => /^[0-9a-f]{40}$/.test(head) && pr.head.sha === head && pr.base.ref === "development";
|
|
37834
|
+
if (!sameTarget(initial)) return receipt("PR head or target base changed");
|
|
37835
|
+
if (initial.merged === true) {
|
|
37836
|
+
state = "merged";
|
|
37837
|
+
return receipt("GitHub confirms the PR merged");
|
|
37838
|
+
}
|
|
37839
|
+
const validate = (pr) => pr.state === "open" && sameTarget(pr) && `${pr.title}
|
|
37840
|
+
${pr.body ?? ""}` === guardedText;
|
|
37841
|
+
if (!validate(initial)) return receipt("PR state, head, base or guarded title/body changed");
|
|
37842
|
+
const initialState = queueCheckState(await deps.readChecks(head), head);
|
|
37843
|
+
if (initialState === "refused") return receipt("trusted current-head Mergify check is absent or failed");
|
|
37844
|
+
if (!(await deps.review(head)).ok) return receipt("current trusted review verdict does not permit queue admission");
|
|
37845
|
+
if (!validate(await deps.readPull())) return receipt("PR changed before queue request");
|
|
37846
|
+
let mutationDetail = "";
|
|
37847
|
+
if (!initial.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
|
|
37848
|
+
try {
|
|
37849
|
+
await deps.addLabel();
|
|
37850
|
+
} catch {
|
|
37851
|
+
mutationDetail = "Label response was ambiguous; no replay was attempted. ";
|
|
37852
|
+
}
|
|
37853
|
+
}
|
|
37854
|
+
state = "requested";
|
|
37855
|
+
const deadline = deps.now() + timeoutMs;
|
|
37856
|
+
while (true) {
|
|
37857
|
+
const pr = await deps.readPull();
|
|
37858
|
+
if (!sameTarget(pr)) {
|
|
37859
|
+
state = "refused";
|
|
37860
|
+
return receipt("PR head or target base changed while queued");
|
|
37861
|
+
}
|
|
37862
|
+
if (pr.merged === true) {
|
|
37863
|
+
state = "merged";
|
|
37864
|
+
return receipt("GitHub confirms the PR merged");
|
|
37865
|
+
}
|
|
37866
|
+
if (!validate(pr)) {
|
|
37867
|
+
state = "refused";
|
|
37868
|
+
return receipt("PR state, head, base or guarded title/body changed while queued");
|
|
37869
|
+
}
|
|
37870
|
+
state = queueCheckState(await deps.readChecks(head), head);
|
|
37871
|
+
if (state === "refused") return receipt("trusted current-head Mergify check is absent or failed");
|
|
37872
|
+
if (!pr.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
|
|
37873
|
+
state = "refused";
|
|
37874
|
+
return receipt(`${mutationDetail}Queue request label is absent`);
|
|
37875
|
+
}
|
|
37876
|
+
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`);
|
|
37877
|
+
await deps.sleep(3e4);
|
|
37878
|
+
}
|
|
37879
|
+
} catch {
|
|
37880
|
+
state = "refused";
|
|
37881
|
+
return receipt("Mergify or GitHub state is unreadable; no native merge was attempted");
|
|
37882
|
+
}
|
|
37883
|
+
}
|
|
37884
|
+
|
|
37885
|
+
// src/post-merge-recon.ts
|
|
37886
|
+
var import_node_fs39 = require("node:fs");
|
|
37887
|
+
var import_node_path36 = require("node:path");
|
|
37460
37888
|
|
|
37461
37889
|
// src/cross-repo-filing-issue.ts
|
|
37462
37890
|
function crossRepoFilingRetryCommand(prRepo, prNumber) {
|
|
@@ -37622,16 +38050,16 @@ function buildPostMergeReconRecovery(input) {
|
|
|
37622
38050
|
}
|
|
37623
38051
|
function writePostMergeReconRecovery(cwd, recovery) {
|
|
37624
38052
|
const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
|
|
37625
|
-
(0,
|
|
37626
|
-
(0,
|
|
38053
|
+
(0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(path2), { recursive: true });
|
|
38054
|
+
(0, import_node_fs39.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
|
|
37627
38055
|
`, "utf8");
|
|
37628
38056
|
return path2;
|
|
37629
38057
|
}
|
|
37630
38058
|
function clearPostMergeReconRecovery(cwd, repo, pr) {
|
|
37631
38059
|
const path2 = postMergeReconStatePath(cwd, repo, pr);
|
|
37632
|
-
if (!(0,
|
|
38060
|
+
if (!(0, import_node_fs39.existsSync)(path2)) return;
|
|
37633
38061
|
try {
|
|
37634
|
-
(0,
|
|
38062
|
+
(0, import_node_fs39.unlinkSync)(path2);
|
|
37635
38063
|
} catch {
|
|
37636
38064
|
}
|
|
37637
38065
|
}
|
|
@@ -37661,184 +38089,6 @@ function postMergeReconWarnings(input) {
|
|
|
37661
38089
|
return lines2;
|
|
37662
38090
|
}
|
|
37663
38091
|
|
|
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
38092
|
// src/pr-create-docs-check.ts
|
|
37843
38093
|
var import_node_child_process15 = require("node:child_process");
|
|
37844
38094
|
var GIT_TIMEOUT_MS2 = 15e3;
|
|
@@ -38963,7 +39213,7 @@ function ciAuditDeps() {
|
|
|
38963
39213
|
}
|
|
38964
39214
|
};
|
|
38965
39215
|
}
|
|
38966
|
-
async function
|
|
39216
|
+
async function ghJson2(args, timeout = 1e4) {
|
|
38967
39217
|
const { stdout } = await execFileP("gh", args, { timeout });
|
|
38968
39218
|
return JSON.parse(stdout);
|
|
38969
39219
|
}
|
|
@@ -39250,7 +39500,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39250
39500
|
async function readParentField(number, repo) {
|
|
39251
39501
|
let payload;
|
|
39252
39502
|
try {
|
|
39253
|
-
payload = await
|
|
39503
|
+
payload = await ghJson2(["api", `repos/${repo}/issues/${number}`]);
|
|
39254
39504
|
} catch (e) {
|
|
39255
39505
|
const err = e;
|
|
39256
39506
|
return { parentReadError: (err.stderr || err.message || String(e)).trim() };
|
|
@@ -39282,7 +39532,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39282
39532
|
emit(data2, await readParentField(n, repo));
|
|
39283
39533
|
return;
|
|
39284
39534
|
}
|
|
39285
|
-
const data = await
|
|
39535
|
+
const data = await ghJson2(["issue", "view", String(n), "--repo", repo, "--json", gh.ghFields]);
|
|
39286
39536
|
emit(data, await readParentField(n, repo));
|
|
39287
39537
|
} catch (e) {
|
|
39288
39538
|
const err = e;
|
|
@@ -39297,7 +39547,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39297
39547
|
const repo = await resolveRepo(o.repo);
|
|
39298
39548
|
if (!repo) return fail("issue discover-related: could not resolve repo");
|
|
39299
39549
|
try {
|
|
39300
|
-
const issues = await
|
|
39550
|
+
const issues = await ghJson2([
|
|
39301
39551
|
"issue",
|
|
39302
39552
|
"list",
|
|
39303
39553
|
"--repo",
|
|
@@ -39312,7 +39562,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39312
39562
|
const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
|
|
39313
39563
|
if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
|
|
39314
39564
|
if (!candidates.length) return;
|
|
39315
|
-
const viewed = await
|
|
39565
|
+
const viewed = await ghJson2([
|
|
39316
39566
|
"issue",
|
|
39317
39567
|
"view",
|
|
39318
39568
|
String(number),
|
|
@@ -39386,7 +39636,7 @@ function registerCollaborationCommands(program3) {
|
|
|
39386
39636
|
const checked = o.off !== true;
|
|
39387
39637
|
let body;
|
|
39388
39638
|
try {
|
|
39389
|
-
const viewed = await
|
|
39639
|
+
const viewed = await ghJson2(["issue", "view", String(parsed.number), "--repo", repo, "--json", "body"]);
|
|
39390
39640
|
body = viewed.body ?? "";
|
|
39391
39641
|
} catch (e) {
|
|
39392
39642
|
return fail(`issue check: could not read ${repo}#${parsed.number}: ${e.message}`);
|
|
@@ -39488,7 +39738,7 @@ ${list}`);
|
|
|
39488
39738
|
if (!o.force) {
|
|
39489
39739
|
let openLessons = [];
|
|
39490
39740
|
try {
|
|
39491
|
-
openLessons = await
|
|
39741
|
+
openLessons = await ghJson2([
|
|
39492
39742
|
"issue",
|
|
39493
39743
|
"list",
|
|
39494
39744
|
"--repo",
|
|
@@ -39534,7 +39784,7 @@ ${list}`);
|
|
|
39534
39784
|
console.log(JSON.stringify({ ...created, projectItemId, onBoard }));
|
|
39535
39785
|
});
|
|
39536
39786
|
const pr = program3.command("pr").description("pull requests \u2014 reliable create with structured output");
|
|
39537
|
-
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("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
|
|
39787
|
+
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) => {
|
|
39538
39788
|
let body;
|
|
39539
39789
|
let title;
|
|
39540
39790
|
try {
|
|
@@ -39594,7 +39844,7 @@ ${list}`);
|
|
|
39594
39844
|
console.log(JSON.stringify(data2));
|
|
39595
39845
|
return;
|
|
39596
39846
|
}
|
|
39597
|
-
const data = await
|
|
39847
|
+
const data = await ghJson2(["pr", "view", String(n), "--repo", repo, "--json", effective]);
|
|
39598
39848
|
console.log(JSON.stringify(data));
|
|
39599
39849
|
} catch (e) {
|
|
39600
39850
|
const err = e;
|
|
@@ -39794,7 +40044,12 @@ ${list}`);
|
|
|
39794
40044
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
39795
40045
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
39796
40046
|
});
|
|
39797
|
-
|
|
40047
|
+
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) => {
|
|
40048
|
+
const result = await checkPrReview(number, o.repo, o.head);
|
|
40049
|
+
console.log(o.json ? JSON.stringify(result) : `pr review-check: ${result.reason}`);
|
|
40050
|
+
if (!result.ok) process.exitCode = 1;
|
|
40051
|
+
});
|
|
40052
|
+
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) => {
|
|
39798
40053
|
if (!isReviewVerdict(o.verdict)) return fail(`pr review-verdict: --verdict must be one of ${REVIEW_VERDICTS.join("|")} (got ${o.verdict})`);
|
|
39799
40054
|
const verdict = o.verdict;
|
|
39800
40055
|
const findings = o.findingsFile ? (0, import_node_fs43.readFileSync)(o.findingsFile, "utf8") : void 0;
|
|
@@ -39836,7 +40091,7 @@ ${list}`);
|
|
|
39836
40091
|
}
|
|
39837
40092
|
class PrHeadBehindBaseError extends Error {
|
|
39838
40093
|
}
|
|
39839
|
-
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>", "
|
|
40094
|
+
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) => {
|
|
39840
40095
|
if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
|
|
39841
40096
|
try {
|
|
39842
40097
|
const parsed = parseIssueRef(number, o.repo);
|
|
@@ -39854,7 +40109,7 @@ ${list}`);
|
|
|
39854
40109
|
await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
|
|
39855
40110
|
async (n) => {
|
|
39856
40111
|
if (!landRepoForGuard) return void 0;
|
|
39857
|
-
const viewed = await
|
|
40112
|
+
const viewed = await ghJson2(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
|
|
39858
40113
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
39859
40114
|
}
|
|
39860
40115
|
);
|
|
@@ -39871,16 +40126,15 @@ ${list}`);
|
|
|
39871
40126
|
return;
|
|
39872
40127
|
}
|
|
39873
40128
|
if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
|
|
39874
|
-
{
|
|
39875
|
-
const gateRepo = landRepoForGuard ?? await requireRepo(o.repo);
|
|
39876
|
-
const refusal = await requireReviewVerdict("pr land", number, gateRepo, { withoutReview: o.withoutReview });
|
|
39877
|
-
if (refusal) {
|
|
39878
|
-
console.error(refusal);
|
|
39879
|
-
process.exitCode = 1;
|
|
39880
|
-
return;
|
|
39881
|
-
}
|
|
39882
|
-
}
|
|
39883
40129
|
const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
|
|
40130
|
+
queueMerge: async (prNumber, repo) => {
|
|
40131
|
+
const meta = await ghJson2(["pr", "view", prNumber, "--repo", repo, "--json", "baseRefName,headRefOid"]);
|
|
40132
|
+
if (!await usesMergifyPilot(repo, meta.baseRefName)) return void 0;
|
|
40133
|
+
if (!landClosingGuardInput) throw new Error("pr land: Mergify requires a readable closing-keyword guard");
|
|
40134
|
+
const guard = evaluateClosingGuard(landClosingGuardInput, { force: o.force, context: "pr land", squashBodyText: landClosingGuardInput.text });
|
|
40135
|
+
if (guard.blocked) throw new Error(guard.message);
|
|
40136
|
+
return requestMergifyMerge(prNumber, repo, landClosingGuardInput.text, meta.headRefOid);
|
|
40137
|
+
},
|
|
39884
40138
|
resolveRepo: async (prNumber, repoOpt) => {
|
|
39885
40139
|
const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
|
|
39886
40140
|
const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS4 })).stdout.trim();
|
|
@@ -40015,7 +40269,7 @@ ${list}`);
|
|
|
40015
40269
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
40016
40270
|
if (result.status === "failed") process.exitCode = 1;
|
|
40017
40271
|
});
|
|
40018
|
-
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>", "
|
|
40272
|
+
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) => {
|
|
40019
40273
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
40020
40274
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
40021
40275
|
if (o.disableAuto) {
|
|
@@ -40037,20 +40291,24 @@ ${list}`);
|
|
|
40037
40291
|
const headRef = prMeta.head;
|
|
40038
40292
|
const baseRef = prMeta.base;
|
|
40039
40293
|
const headRefOid = (prMeta.oid ?? "").trim() || void 0;
|
|
40294
|
+
if (!repoForPostCleanup) throw new Error("pr merge: cannot resolve target repository");
|
|
40295
|
+
const mergifyPilot = await usesMergifyPilot(repoForPostCleanup, baseRef);
|
|
40296
|
+
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");
|
|
40040
40297
|
const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
|
|
40041
40298
|
const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
|
|
40042
40299
|
const closingGuardInput = await withAlreadyClosedCommitTargets(
|
|
40043
40300
|
await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
|
|
40044
40301
|
async (n) => {
|
|
40045
40302
|
if (!repoForPostCleanup) return void 0;
|
|
40046
|
-
const viewed = await
|
|
40303
|
+
const viewed = await ghJson2(["issue", "view", String(n), "--repo", repoForPostCleanup, "--json", "state"]);
|
|
40047
40304
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
40048
40305
|
}
|
|
40049
40306
|
);
|
|
40050
40307
|
if (o.squashBodyFile && method !== "--squash") {
|
|
40051
40308
|
throw new Error("pr merge: --squash-body-file applies only to squash merges");
|
|
40052
40309
|
}
|
|
40053
|
-
|
|
40310
|
+
if (mergifyPilot && !closingGuardInput) throw new Error("pr merge: Mergify requires a readable closing-keyword guard");
|
|
40311
|
+
const mergeSquashBody = mergifyPilot ? closingGuardInput.text : squashBodyTextForMerge(
|
|
40054
40312
|
closingGuardInput,
|
|
40055
40313
|
method === "--squash",
|
|
40056
40314
|
o.squashBodyFile ? (0, import_node_fs43.readFileSync)(o.squashBodyFile, "utf8") : void 0
|
|
@@ -40067,15 +40325,6 @@ ${list}`);
|
|
|
40067
40325
|
return;
|
|
40068
40326
|
}
|
|
40069
40327
|
if (closingGuardVerdict.message) console.warn(closingGuardVerdict.message);
|
|
40070
|
-
if (prMeta.state !== "MERGED") {
|
|
40071
|
-
const gateRepo = repoForPostCleanup ?? await requireRepo(o.repo);
|
|
40072
|
-
const refusal = await requireReviewVerdict("pr merge", number, gateRepo, { withoutReview: o.withoutReview });
|
|
40073
|
-
if (refusal) {
|
|
40074
|
-
console.error(refusal);
|
|
40075
|
-
process.exitCode = 1;
|
|
40076
|
-
return;
|
|
40077
|
-
}
|
|
40078
|
-
}
|
|
40079
40328
|
if (prMeta.state !== "MERGED" && !o.preserveWorktree) {
|
|
40080
40329
|
if (!repoForPostCleanup) throw new Error("pr merge: repository is unreadable; cannot prove remote branch preservation");
|
|
40081
40330
|
const repoSettings = await fetchRestRepoMergeSettings(repoForPostCleanup);
|
|
@@ -40157,7 +40406,7 @@ ${list}`);
|
|
|
40157
40406
|
}
|
|
40158
40407
|
return true;
|
|
40159
40408
|
};
|
|
40160
|
-
if (o.wait && !await runWaitGate()) return;
|
|
40409
|
+
if (!mergifyPilot && o.wait && !await runWaitGate()) return;
|
|
40161
40410
|
if (ciPolicy.policy === "no-ci") {
|
|
40162
40411
|
const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
|
|
40163
40412
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
@@ -40166,7 +40415,7 @@ ${list}`);
|
|
|
40166
40415
|
const remoteBefore = await remoteBranchExists2(headRef, { remote });
|
|
40167
40416
|
let upgradedToAuto = false;
|
|
40168
40417
|
let remoteNotAttemptedReason = "preserved-delayed-cleanup";
|
|
40169
|
-
const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
40418
|
+
const overrideBody = mergifyPilot ? void 0 : mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
40170
40419
|
number,
|
|
40171
40420
|
repoArgs,
|
|
40172
40421
|
async (a, t) => (await execFileP("gh", a, { timeout: t })).stdout,
|
|
@@ -40246,7 +40495,15 @@ ${list}`);
|
|
|
40246
40495
|
});
|
|
40247
40496
|
try {
|
|
40248
40497
|
try {
|
|
40249
|
-
|
|
40498
|
+
if (mergifyPilot) {
|
|
40499
|
+
const queue = await requestMergifyMerge(number, repoForPostCleanup, closingGuardInput.text, headRefOid);
|
|
40500
|
+
if (queue.state !== "merged") {
|
|
40501
|
+
console.log(JSON.stringify({ mergeStatus: "failed", pr: number, repo: repoForPostCleanup, queue }));
|
|
40502
|
+
process.exitCode = queue.state === "refused" ? 1 : PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
40503
|
+
return;
|
|
40504
|
+
}
|
|
40505
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
40506
|
+
} else await mergeOnce();
|
|
40250
40507
|
} catch (e) {
|
|
40251
40508
|
if (!(e instanceof PrHeadBehindBaseError)) throw e;
|
|
40252
40509
|
const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
|
|
@@ -40718,13 +40975,13 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
40718
40975
|
|
|
40719
40976
|
// src/dist-drift.ts
|
|
40720
40977
|
var import_node_child_process17 = require("node:child_process");
|
|
40721
|
-
var
|
|
40978
|
+
var import_node_crypto13 = require("node:crypto");
|
|
40722
40979
|
var import_node_fs46 = require("node:fs");
|
|
40723
40980
|
var import_node_os21 = require("node:os");
|
|
40724
40981
|
var import_node_path42 = require("node:path");
|
|
40725
40982
|
|
|
40726
40983
|
// ../scripts/distribution-digest.mjs
|
|
40727
|
-
var
|
|
40984
|
+
var import_node_crypto12 = require("node:crypto");
|
|
40728
40985
|
var import_node_fs45 = require("node:fs");
|
|
40729
40986
|
var import_node_path41 = require("node:path");
|
|
40730
40987
|
var slash = (value) => value.replaceAll("\\", "/");
|
|
@@ -40737,7 +40994,7 @@ function repoPath(root, declaredPath, label) {
|
|
|
40737
40994
|
return target;
|
|
40738
40995
|
}
|
|
40739
40996
|
function digestFiles(files) {
|
|
40740
|
-
const hash = (0,
|
|
40997
|
+
const hash = (0, import_node_crypto12.createHash)("sha256");
|
|
40741
40998
|
for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
|
|
40742
40999
|
const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs45.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs45.readFileSync)(file.absolute);
|
|
40743
41000
|
hash.update(file.relative, "utf8");
|
|
@@ -40767,7 +41024,7 @@ var DIST_ARTIFACTS = [
|
|
|
40767
41024
|
];
|
|
40768
41025
|
var BOM_DIST_TREE_ID = "mmi-cli-dist";
|
|
40769
41026
|
var ABSENT = "absent";
|
|
40770
|
-
var sha256 = (bytes) => `sha256:${(0,
|
|
41027
|
+
var sha256 = (bytes) => `sha256:${(0, import_node_crypto13.createHash)("sha256").update(bytes).digest("hex")}`;
|
|
40771
41028
|
function artifactDrift(path2, committedBytes, rebuiltBytes) {
|
|
40772
41029
|
const committed = committedBytes ? sha256(committedBytes) : ABSENT;
|
|
40773
41030
|
const rebuiltExpected = rebuiltBytes ? sha256(rebuiltBytes) : ABSENT;
|
package/package.json
CHANGED