@mutmutco/cli 4.3.16 → 4.3.17
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 +220 -101
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -12917,6 +12917,18 @@ function rulesetRequiredContexts(ruleset) {
|
|
|
12917
12917
|
}
|
|
12918
12918
|
return contexts;
|
|
12919
12919
|
}
|
|
12920
|
+
function rulesetStrictPolicy(ruleset) {
|
|
12921
|
+
const rules = (ruleset.rules ?? []).filter((rule) => rule.type === "required_status_checks");
|
|
12922
|
+
return rules.length > 0 && rules.every((rule) => rule.parameters?.strict_required_status_checks_policy === true);
|
|
12923
|
+
}
|
|
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
|
+
}
|
|
12920
12932
|
function rulesetBranchIncludes(ruleset) {
|
|
12921
12933
|
const raw = ruleset.conditions?.ref_name?.include;
|
|
12922
12934
|
return Array.isArray(raw) ? [...new Set(raw.filter((ref) => typeof ref === "string" && ref.length > 0))].sort((a, b) => a.localeCompare(b)) : [];
|
|
@@ -12936,7 +12948,8 @@ function patchRulesetRequiredContexts(body, contexts) {
|
|
|
12936
12948
|
...r,
|
|
12937
12949
|
parameters: {
|
|
12938
12950
|
...r.parameters,
|
|
12939
|
-
strict_required_status_checks_policy:
|
|
12951
|
+
strict_required_status_checks_policy: true,
|
|
12952
|
+
// #6263: org policy — see patchRulesetStrictPolicy
|
|
12940
12953
|
required_status_checks: sorted.map((context) => ({ context }))
|
|
12941
12954
|
}
|
|
12942
12955
|
};
|
|
@@ -13023,6 +13036,7 @@ async function gateIsProvenGreen(repo, client, baseBranch, gateFiles = DEFAULT_G
|
|
|
13023
13036
|
}
|
|
13024
13037
|
async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
|
|
13025
13038
|
const want = new Set(rulesetRequiredContexts({ rules: rulesetBody.rules }));
|
|
13039
|
+
const wantStrict = rulesetStrictPolicy({ rules: rulesetBody.rules });
|
|
13026
13040
|
const wantBranches = rulesetBranchIncludes(rulesetBody);
|
|
13027
13041
|
const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
|
|
13028
13042
|
const existing = findProductRuleset(list ?? []);
|
|
@@ -13034,8 +13048,9 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
|
|
|
13034
13048
|
const have = new Set(rulesetRequiredContexts(detail));
|
|
13035
13049
|
const haveBranches = rulesetBranchIncludes(detail);
|
|
13036
13050
|
const branchesMatch = haveBranches.length === wantBranches.length && wantBranches.every((ref, index) => ref === haveBranches[index]);
|
|
13037
|
-
|
|
13038
|
-
|
|
13051
|
+
const strictMatch = rulesetStrictPolicy(detail) === wantStrict;
|
|
13052
|
+
if (detail.enforcement === effective && branchesMatch && strictMatch && have.size === want.size && [...want].every((c) => have.has(c))) {
|
|
13053
|
+
return { action: "skipped", enforcement: effective, detail: `${effective} ruleset already matches required contexts, branches and up-to-date policy${kept}` };
|
|
13039
13054
|
}
|
|
13040
13055
|
await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
|
|
13041
13056
|
return { action: "updated", enforcement: effective, detail: `ruleset ${existing.id}${kept}` };
|
|
@@ -15487,10 +15502,10 @@ var rollout_plan_default = {
|
|
|
15487
15502
|
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)."
|
|
15488
15503
|
},
|
|
15489
15504
|
baseline: {
|
|
15490
|
-
version: "4.3.
|
|
15491
|
-
tag: "v4.3.
|
|
15492
|
-
commit: "
|
|
15493
|
-
npm: "@mutmutco/cli@4.3.
|
|
15505
|
+
version: "4.3.17",
|
|
15506
|
+
tag: "v4.3.17",
|
|
15507
|
+
commit: "3b07c941910b",
|
|
15508
|
+
npm: "@mutmutco/cli@4.3.17"
|
|
15494
15509
|
},
|
|
15495
15510
|
exitCriterion: "fleet-n-of-n",
|
|
15496
15511
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15507,14 +15522,14 @@ var rollout_plan_default = {
|
|
|
15507
15522
|
repo: "mutmutco/mmi-hub",
|
|
15508
15523
|
role: "canary",
|
|
15509
15524
|
schedule: "train",
|
|
15510
|
-
v3Target: "v4.3.
|
|
15525
|
+
v3Target: "v4.3.17"
|
|
15511
15526
|
}
|
|
15512
15527
|
],
|
|
15513
15528
|
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.",
|
|
15514
15529
|
rollback: {
|
|
15515
15530
|
independent: true,
|
|
15516
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15517
|
-
v3Target: "v4.3.
|
|
15531
|
+
mechanism: "npm dist-tag latest -> 4.3.17 and redeploy the Hub Lambda from tag v4.3.17 (3b07c941910b); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15532
|
+
v3Target: "v4.3.17 (@mutmutco/cli@4.3.17, tag commit 3b07c941910b \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15518
15533
|
}
|
|
15519
15534
|
},
|
|
15520
15535
|
{
|
|
@@ -23511,12 +23526,13 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
|
|
|
23511
23526
|
const registryContexts = registryRequiredContexts(meta);
|
|
23512
23527
|
const contexts = sortedUnique(registryContexts ?? committedContexts);
|
|
23513
23528
|
const explicitBranches = Array.isArray(meta?.requiredCheckBranches) && meta.requiredCheckBranches.length > 0 ? resolveRequiredCheckBranches(meta, repo) : null;
|
|
23514
|
-
|
|
23529
|
+
const committedStrict = rulesetStrictPolicy(committedPayload);
|
|
23530
|
+
let apiPayload = patchRulesetStrictPolicy(registryContexts == null ? committedPayload : patchRulesetRequiredContexts(committedPayload, contexts));
|
|
23515
23531
|
if (explicitBranches) apiPayload = patchRulesetBranchIncludes(apiPayload, explicitBranches);
|
|
23516
23532
|
const branchIncludes = rulesetBranchIncludes(apiPayload);
|
|
23517
23533
|
const authoritativeFilePayload = {
|
|
23518
23534
|
...filePayload,
|
|
23519
|
-
...registryContexts == null ? {} : { rules: apiPayload.rules },
|
|
23535
|
+
...registryContexts == null && committedStrict ? {} : { rules: apiPayload.rules },
|
|
23520
23536
|
...explicitBranches == null ? {} : { conditions: apiPayload.conditions }
|
|
23521
23537
|
};
|
|
23522
23538
|
return {
|
|
@@ -23526,6 +23542,7 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
|
|
|
23526
23542
|
contexts,
|
|
23527
23543
|
branchIncludes,
|
|
23528
23544
|
committedBranchIncludes,
|
|
23545
|
+
committedStrict,
|
|
23529
23546
|
branchSource: explicitBranches ? "registry META requiredCheckBranches" : "committed ruleset reference",
|
|
23530
23547
|
source: registryContexts == null ? "committed ruleset reference" : "registry META requiredChecks",
|
|
23531
23548
|
coversReleaseBranches: rulesetCoversReleaseBranches(apiPayload, resolveReleaseTrack(meta, void 0, repo))
|
|
@@ -23537,8 +23554,9 @@ function sameContexts(left, right) {
|
|
|
23537
23554
|
function resolveProductRulesetReconcilePlan(input) {
|
|
23538
23555
|
const liveNeedsContextConvergence = !sameContexts(input.liveContexts, input.authorityContexts);
|
|
23539
23556
|
const liveNeedsBranchConvergence = input.liveBranchIncludes !== void 0 && input.authorityBranchIncludes !== void 0 && !sameContexts(input.liveBranchIncludes, input.authorityBranchIncludes);
|
|
23557
|
+
const liveNeedsStrictConvergence = input.liveStrict === false;
|
|
23540
23558
|
const liveIsActive = input.liveEnforcement === "active";
|
|
23541
|
-
if (liveIsActive && !liveNeedsContextConvergence && !liveNeedsBranchConvergence) {
|
|
23559
|
+
if (liveIsActive && !liveNeedsContextConvergence && !liveNeedsBranchConvergence && !liveNeedsStrictConvergence) {
|
|
23542
23560
|
return { shouldActivate: false, targetEnforcement: "active" };
|
|
23543
23561
|
}
|
|
23544
23562
|
if (input.unsafeContexts.length > 0) {
|
|
@@ -23726,6 +23744,7 @@ async function auditRepoCi(repo, deps) {
|
|
|
23726
23744
|
const productRuleset = rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
|
|
23727
23745
|
const liveContexts = productRuleset == null ? [] : sortedUnique(rulesetRequiredContexts(productRuleset));
|
|
23728
23746
|
const liveBranchIncludes = productRuleset == null ? [] : rulesetBranchIncludes(productRuleset);
|
|
23747
|
+
const liveStrict = productRuleset != null && rulesetStrictPolicy(productRuleset);
|
|
23729
23748
|
const hasRequiredChecks = productRuleset?.enforcement === "active" && liveContexts.length > 0;
|
|
23730
23749
|
checks.push({
|
|
23731
23750
|
ok: hasRequiredChecks,
|
|
@@ -23746,11 +23765,12 @@ async function auditRepoCi(repo, deps) {
|
|
|
23746
23765
|
const liveContextsAligned = sameContexts(liveContexts, authoritativeRuleset.contexts);
|
|
23747
23766
|
const fileBranchesAligned = sameContexts(authoritativeRuleset.committedBranchIncludes, authoritativeRuleset.branchIncludes);
|
|
23748
23767
|
const liveBranchesAligned = sameContexts(liveBranchIncludes, authoritativeRuleset.branchIncludes);
|
|
23749
|
-
const
|
|
23768
|
+
const strictAligned = authoritativeRuleset.committedStrict && liveStrict;
|
|
23769
|
+
const aligned = fileContextsAligned && liveContextsAligned && fileBranchesAligned && liveBranchesAligned && strictAligned;
|
|
23750
23770
|
checks.push({
|
|
23751
23771
|
ok: aligned,
|
|
23752
23772
|
label: RULESET_REFERENCE_MATCH_LABEL,
|
|
23753
|
-
detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}]
|
|
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 must be true (require branches to be up to date before merging, #6263) \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`),
|
|
23754
23774
|
remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
|
|
23755
23775
|
});
|
|
23756
23776
|
}
|
|
@@ -24284,7 +24304,7 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
24284
24304
|
result.errors.push(`${authority.source} declares no required contexts`);
|
|
24285
24305
|
return finalizeCiReconcile(repo, deps, result, report);
|
|
24286
24306
|
}
|
|
24287
|
-
const sourceNeedsConvergence = !sameContexts(authority.committedContexts, authority.contexts) || !sameContexts(authority.committedBranchIncludes, authority.branchIncludes);
|
|
24307
|
+
const sourceNeedsConvergence = !sameContexts(authority.committedContexts, authority.contexts) || !sameContexts(authority.committedBranchIncludes, authority.branchIncludes) || !authority.committedStrict;
|
|
24288
24308
|
if (sourceNeedsConvergence) {
|
|
24289
24309
|
try {
|
|
24290
24310
|
const delivery = await deliverSeedFile(
|
|
@@ -24339,6 +24359,7 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
24339
24359
|
authorityContexts: authority.contexts,
|
|
24340
24360
|
liveBranchIncludes,
|
|
24341
24361
|
authorityBranchIncludes: authority.branchIncludes,
|
|
24362
|
+
liveStrict: live == null ? void 0 : rulesetStrictPolicy(live),
|
|
24342
24363
|
gateProvenGreen: await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles),
|
|
24343
24364
|
unsafeContexts: unsafe
|
|
24344
24365
|
});
|
|
@@ -24347,7 +24368,7 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
24347
24368
|
result.skipped.push(plan.holdReason);
|
|
24348
24369
|
return finalizeCiReconcile(repo, deps, result, report, plan.holdReason);
|
|
24349
24370
|
}
|
|
24350
|
-
result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts and
|
|
24371
|
+
result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts, branches and up-to-date policy already match declared authority`);
|
|
24351
24372
|
return finalizeCiReconcile(repo, deps, result, report);
|
|
24352
24373
|
}
|
|
24353
24374
|
try {
|
|
@@ -27146,6 +27167,11 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
27146
27167
|
label: "product required-check ruleset enforcement active",
|
|
27147
27168
|
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
|
|
27148
27169
|
});
|
|
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
|
+
});
|
|
27149
27175
|
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
27150
27176
|
const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
|
|
27151
27177
|
checks.push({
|
|
@@ -36010,6 +36036,47 @@ function applyChecklistCheck(body, query, checked) {
|
|
|
36010
36036
|
}
|
|
36011
36037
|
|
|
36012
36038
|
// src/pr-land.ts
|
|
36039
|
+
function prHeadBehindBase(input) {
|
|
36040
|
+
if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
|
|
36041
|
+
return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
|
|
36042
|
+
}
|
|
36043
|
+
function prHeadUpdateRemedy(head, base) {
|
|
36044
|
+
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
|
+
}
|
|
36046
|
+
var PR_HEAD_UPDATE_API_READ_RETRIES = 5;
|
|
36047
|
+
var PR_HEAD_UPDATE_API_READ_DELAY_MS = 2e3;
|
|
36048
|
+
async function updatePrHeadFromBase(input) {
|
|
36049
|
+
const remedy = prHeadUpdateRemedy(input.head, input.base);
|
|
36050
|
+
if (input.localCheckedOut) {
|
|
36051
|
+
const from2 = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
36052
|
+
await input.git(["fetch", "origin", input.base]);
|
|
36053
|
+
try {
|
|
36054
|
+
await input.git(["merge", "--no-ff", "--no-edit", `origin/${input.base}`]);
|
|
36055
|
+
} catch (e) {
|
|
36056
|
+
await input.git(["merge", "--abort"]).catch(() => {
|
|
36057
|
+
});
|
|
36058
|
+
throw new Error(`updating ${input.head} from ${input.base} conflicted (${e.message.trim()}) \u2014 ${remedy}`);
|
|
36059
|
+
}
|
|
36060
|
+
await input.git(["push", "origin", input.head]);
|
|
36061
|
+
const to = (await input.git(["rev-parse", "HEAD"])).trim();
|
|
36062
|
+
return { from: from2, to, base: input.base, via: "local-merge" };
|
|
36063
|
+
}
|
|
36064
|
+
const pullPath = `repos/${input.repo}/pulls/${input.prNumber}`;
|
|
36065
|
+
const readHead2 = async () => (await input.rest("GET", pullPath)).head?.sha ?? "";
|
|
36066
|
+
const from = await readHead2();
|
|
36067
|
+
try {
|
|
36068
|
+
await input.rest("PUT", `${pullPath}/update-branch`, from ? { expected_head_sha: from } : void 0);
|
|
36069
|
+
} catch (e) {
|
|
36070
|
+
throw new Error(`GitHub could not update ${input.head} from ${input.base} (${e.message.trim()}) \u2014 ${remedy}`);
|
|
36071
|
+
}
|
|
36072
|
+
const sleep2 = input.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
36073
|
+
for (let attempt = 0; attempt < PR_HEAD_UPDATE_API_READ_RETRIES; attempt++) {
|
|
36074
|
+
const to = await readHead2();
|
|
36075
|
+
if (to && to !== from) return { from, to, base: input.base, via: "update-branch-api" };
|
|
36076
|
+
await sleep2(PR_HEAD_UPDATE_API_READ_DELAY_MS);
|
|
36077
|
+
}
|
|
36078
|
+
throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
|
|
36079
|
+
}
|
|
36013
36080
|
var PR_LAND_POLL_MS = 3e4;
|
|
36014
36081
|
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
36015
36082
|
var PR_LAND_STATE_READ_RETRIES = 3;
|
|
@@ -36080,21 +36147,30 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
36080
36147
|
}
|
|
36081
36148
|
const ciPolicy = await deps.resolveCiPolicy(repo);
|
|
36082
36149
|
base.ciPolicy = ciPolicy;
|
|
36083
|
-
const
|
|
36084
|
-
|
|
36085
|
-
|
|
36086
|
-
|
|
36087
|
-
|
|
36088
|
-
|
|
36089
|
-
}
|
|
36090
|
-
|
|
36091
|
-
|
|
36092
|
-
|
|
36093
|
-
|
|
36094
|
-
|
|
36095
|
-
|
|
36150
|
+
const checksWaitError = (checksWait) => {
|
|
36151
|
+
if (checksWait.status === "conflicting") {
|
|
36152
|
+
return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
|
|
36153
|
+
}
|
|
36154
|
+
if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
|
|
36155
|
+
return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
|
|
36156
|
+
}
|
|
36157
|
+
return void 0;
|
|
36158
|
+
};
|
|
36159
|
+
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
36160
|
+
const waitError = checksWaitError(base.checksWait);
|
|
36161
|
+
if (waitError) return { ...base, error: waitError };
|
|
36162
|
+
let prState = await deps.readPrState(prNumber, repo);
|
|
36163
|
+
if (prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
|
|
36164
|
+
try {
|
|
36165
|
+
base.branchUpdated = await deps.updateBranch(prNumber, repo);
|
|
36166
|
+
} catch (e) {
|
|
36167
|
+
return { ...base, error: `pr land #${prNumber}: ${e.message}` };
|
|
36168
|
+
}
|
|
36169
|
+
base.checksWait = await deps.waitForChecks(prNumber, repo);
|
|
36170
|
+
const reWaitError = checksWaitError(base.checksWait);
|
|
36171
|
+
if (reWaitError) return { ...base, error: reWaitError };
|
|
36172
|
+
prState = await deps.readPrState(prNumber, repo);
|
|
36096
36173
|
}
|
|
36097
|
-
const prState = await deps.readPrState(prNumber, repo);
|
|
36098
36174
|
if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
|
|
36099
36175
|
return {
|
|
36100
36176
|
...base,
|
|
@@ -39739,6 +39815,27 @@ ${list}`);
|
|
|
39739
39815
|
}
|
|
39740
39816
|
printLine(JSON.stringify({ number: Number(number), repo: o.repo, verdict, patch, head, commentUrl }));
|
|
39741
39817
|
});
|
|
39818
|
+
async function updatePrHeadForMerge(input) {
|
|
39819
|
+
return updatePrHeadFromBase({
|
|
39820
|
+
...input,
|
|
39821
|
+
git: async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
39822
|
+
rest: (method, path2, body) => defaultGitHubClient().rest(method, path2, { ...body ? { body } : {}, timeoutMs: GH_MUTATION_TIMEOUT_MS })
|
|
39823
|
+
});
|
|
39824
|
+
}
|
|
39825
|
+
async function prHeadCheckedOutHere(head, repo, explicitRepo) {
|
|
39826
|
+
if (await gitOut(["branch", "--show-current"]) !== head) return false;
|
|
39827
|
+
if (!explicitRepo || !repo) return true;
|
|
39828
|
+
const cwdRepo = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
|
|
39829
|
+
return cwdRepo?.toLowerCase() === repo.split("/").slice(-2).join("/").toLowerCase();
|
|
39830
|
+
}
|
|
39831
|
+
async function prLandUpdateBranch(prNumber, repo, explicitRepo) {
|
|
39832
|
+
const viewed = JSON.parse((await execFileP("gh", ["pr", "view", prNumber, "--repo", repo, "--json", "headRefName,baseRefName"], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
|
|
39833
|
+
const localCheckedOut = await prHeadCheckedOutHere(viewed.headRefName, repo, explicitRepo);
|
|
39834
|
+
console.warn(`pr land: PR #${prNumber} is BEHIND ${viewed.baseRefName} \u2014 updating ${viewed.headRefName} from the base (${localCheckedOut ? "local merge commit + push" : "GitHub update-branch"}) and re-waiting the checks once (#6263).`);
|
|
39835
|
+
return updatePrHeadForMerge({ prNumber, repo, head: viewed.headRefName, base: viewed.baseRefName, localCheckedOut });
|
|
39836
|
+
}
|
|
39837
|
+
class PrHeadBehindBaseError extends Error {
|
|
39838
|
+
}
|
|
39742
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>", "land without a review verdict (#6255): posts a `zeroci-review-waived` comment carrying the reason and actor, then proceeds").action(async (number, o) => {
|
|
39743
39840
|
if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
|
|
39744
39841
|
try {
|
|
@@ -39857,6 +39954,7 @@ ${list}`);
|
|
|
39857
39954
|
allowedClosing: landClosingGuardInput?.closing,
|
|
39858
39955
|
bodyText: landSquashBody
|
|
39859
39956
|
}),
|
|
39957
|
+
updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
|
|
39860
39958
|
pollMerged: async (prNumber, repo, deadlineMs) => {
|
|
39861
39959
|
let lastFailure;
|
|
39862
39960
|
while (Date.now() < deadlineMs) {
|
|
@@ -40008,7 +40106,7 @@ ${list}`);
|
|
|
40008
40106
|
}
|
|
40009
40107
|
const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
|
|
40010
40108
|
const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
|
|
40011
|
-
|
|
40109
|
+
const runWaitGate = async () => {
|
|
40012
40110
|
console.warn(`pr merge: --wait can hold the shell up to the full ${PR_CHECKS_TIMEOUT_MS / 6e4}m budget plus the merge \u2014 run under a shell timeout above that budget or as a background/monitor task (#6027)`);
|
|
40013
40111
|
const repo = await requireRepo(o.repo);
|
|
40014
40112
|
const budgetMs = PR_CHECKS_TIMEOUT_MS;
|
|
@@ -40020,7 +40118,7 @@ ${list}`);
|
|
|
40020
40118
|
if (snapshotRead.state === "failed") {
|
|
40021
40119
|
console.error(`pr merge: cannot resolve PR #${number}'s base branch \u2014 ${snapshotRead.error}. Refusing to wait against an assumed base; retry when the API answers.`);
|
|
40022
40120
|
process.exitCode = 1;
|
|
40023
|
-
return;
|
|
40121
|
+
return false;
|
|
40024
40122
|
}
|
|
40025
40123
|
const baseBranch = snapshotRead.snapshot.baseRef;
|
|
40026
40124
|
const requiredContexts = await fetchRequiredCheckContexts(repo, baseBranch).catch(() => null);
|
|
@@ -40055,9 +40153,11 @@ ${list}`);
|
|
|
40055
40153
|
console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
|
|
40056
40154
|
for (const line of failedChecksReceiptLines("pr merge", wait)) console.warn(line);
|
|
40057
40155
|
process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
|
|
40058
|
-
return;
|
|
40156
|
+
return false;
|
|
40059
40157
|
}
|
|
40060
|
-
|
|
40158
|
+
return true;
|
|
40159
|
+
};
|
|
40160
|
+
if (o.wait && !await runWaitGate()) return;
|
|
40061
40161
|
if (ciPolicy.policy === "no-ci") {
|
|
40062
40162
|
const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
|
|
40063
40163
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
@@ -40073,73 +40173,90 @@ ${list}`);
|
|
|
40073
40173
|
{ allowedClosing: method === "--squash" ? closingGuardInput?.closing : void 0 }
|
|
40074
40174
|
);
|
|
40075
40175
|
const bodyFile = overrideBody?.path;
|
|
40076
|
-
|
|
40077
|
-
|
|
40078
|
-
|
|
40079
|
-
|
|
40080
|
-
|
|
40081
|
-
|
|
40176
|
+
let branchUpdated;
|
|
40177
|
+
const mergeOnce = () => execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
|
|
40178
|
+
const message2 = String(e.message || "");
|
|
40179
|
+
if (/already been merged/i.test(message2)) {
|
|
40180
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
40181
|
+
return;
|
|
40182
|
+
}
|
|
40183
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
40184
|
+
if (note) throw new Error(`gh pr merge ${number}: ${note}`);
|
|
40185
|
+
if (!o.auto && !branchUpdated) {
|
|
40186
|
+
const mergeState = prHeadBehindBase({ message: message2 }) ? "BEHIND" : (await execFileP("gh", ["pr", "view", number, ...repoArgs, "--json", "mergeStateStatus", "--jq", ".mergeStateStatus"], { timeout: GC_GH_TIMEOUT_MS4 }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
40187
|
+
if (prHeadBehindBase({ mergeStateStatus: mergeState })) throw new PrHeadBehindBaseError(message2);
|
|
40188
|
+
}
|
|
40189
|
+
if (isGitHubRateLimitError(e)) {
|
|
40190
|
+
if (o.auto) {
|
|
40191
|
+
throw new Error(`gh pr merge ${number}: GraphQL rate limit exhausted, and enabling auto-merge has no REST equivalent \u2014 retry without --auto to merge now via REST, or retry --auto after the pool resets. (${message2.trim()})`);
|
|
40082
40192
|
}
|
|
40083
|
-
|
|
40084
|
-
|
|
40085
|
-
|
|
40086
|
-
|
|
40087
|
-
|
|
40193
|
+
if (!repoForPostCleanup) throw e;
|
|
40194
|
+
console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
|
|
40195
|
+
const commitMessage = bodyFile ? (0, import_node_fs43.readFileSync)(bodyFile, "utf8") : void 0;
|
|
40196
|
+
await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
|
|
40197
|
+
body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
|
|
40198
|
+
timeoutMs: GH_MUTATION_TIMEOUT_MS
|
|
40199
|
+
});
|
|
40200
|
+
return;
|
|
40201
|
+
}
|
|
40202
|
+
if (!o.auto && mergeRejectedBaseBranchModified(message2)) {
|
|
40203
|
+
console.warn(`pr merge: the base branch was modified by a concurrent merge \u2014 waiting ${PR_MERGE_BASE_RACE_RETRY_DELAY_MS / 1e3}s and retrying PR #${number}'s merge once (#6052).`);
|
|
40204
|
+
await new Promise((resolve7) => setTimeout(resolve7, PR_MERGE_BASE_RACE_RETRY_DELAY_MS));
|
|
40205
|
+
await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
40206
|
+
const m2 = String(e2.message || "");
|
|
40207
|
+
if (/already been merged/i.test(m2)) {
|
|
40208
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
40209
|
+
return;
|
|
40088
40210
|
}
|
|
40089
|
-
|
|
40090
|
-
|
|
40091
|
-
|
|
40092
|
-
|
|
40093
|
-
|
|
40094
|
-
|
|
40095
|
-
|
|
40096
|
-
|
|
40097
|
-
|
|
40098
|
-
|
|
40099
|
-
|
|
40100
|
-
|
|
40101
|
-
|
|
40102
|
-
|
|
40103
|
-
|
|
40104
|
-
|
|
40105
|
-
|
|
40106
|
-
|
|
40107
|
-
|
|
40108
|
-
|
|
40109
|
-
|
|
40110
|
-
|
|
40111
|
-
|
|
40112
|
-
|
|
40113
|
-
|
|
40114
|
-
|
|
40115
|
-
|
|
40116
|
-
|
|
40117
|
-
|
|
40118
|
-
|
|
40119
|
-
|
|
40120
|
-
|
|
40121
|
-
|
|
40122
|
-
|
|
40123
|
-
|
|
40124
|
-
|
|
40125
|
-
|
|
40126
|
-
|
|
40127
|
-
|
|
40128
|
-
|
|
40129
|
-
|
|
40130
|
-
|
|
40131
|
-
|
|
40132
|
-
|
|
40133
|
-
|
|
40134
|
-
|
|
40135
|
-
|
|
40136
|
-
|
|
40137
|
-
|
|
40138
|
-
});
|
|
40139
|
-
return;
|
|
40140
|
-
}
|
|
40141
|
-
if (!ghPrMergeLocalBranchDeleteWarning(message2)) throw e;
|
|
40142
|
-
});
|
|
40211
|
+
const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
|
|
40212
|
+
if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
|
|
40213
|
+
if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
|
|
40214
|
+
});
|
|
40215
|
+
return;
|
|
40216
|
+
}
|
|
40217
|
+
if (o.auto && mergeAutoRejectedPrAlreadyClean(message2)) {
|
|
40218
|
+
await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
40219
|
+
const m2 = String(e2.message || "");
|
|
40220
|
+
if (/already been merged/i.test(m2)) {
|
|
40221
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
40222
|
+
return;
|
|
40223
|
+
}
|
|
40224
|
+
const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
|
|
40225
|
+
if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
|
|
40226
|
+
if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
|
|
40227
|
+
});
|
|
40228
|
+
return;
|
|
40229
|
+
}
|
|
40230
|
+
if (!o.auto && basePolicyBlocksImmediateMerge(message2)) {
|
|
40231
|
+
console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
|
|
40232
|
+
upgradedToAuto = true;
|
|
40233
|
+
await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
40234
|
+
const m2 = String(e2.message || "");
|
|
40235
|
+
if (/already been merged/i.test(m2)) {
|
|
40236
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
40237
|
+
return;
|
|
40238
|
+
}
|
|
40239
|
+
const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
|
|
40240
|
+
if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
|
|
40241
|
+
if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
|
|
40242
|
+
});
|
|
40243
|
+
return;
|
|
40244
|
+
}
|
|
40245
|
+
if (!ghPrMergeLocalBranchDeleteWarning(message2)) throw e;
|
|
40246
|
+
});
|
|
40247
|
+
try {
|
|
40248
|
+
try {
|
|
40249
|
+
await mergeOnce();
|
|
40250
|
+
} catch (e) {
|
|
40251
|
+
if (!(e instanceof PrHeadBehindBaseError)) throw e;
|
|
40252
|
+
const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
|
|
40253
|
+
console.warn(`pr merge: GitHub refused the merge \u2014 PR #${number}'s head is BEHIND ${baseRef} \u2014 updating ${headRef} from the base (${localCheckedOut ? "local merge commit + push" : "GitHub update-branch"}) and retrying once (#6263).`);
|
|
40254
|
+
branchUpdated = await updatePrHeadForMerge({ prNumber: number, repo: await requireRepo(o.repo), head: headRef, base: baseRef, localCheckedOut }).catch((e2) => {
|
|
40255
|
+
throw new Error(`pr merge: ${e2.message}`);
|
|
40256
|
+
});
|
|
40257
|
+
if (o.wait && !await runWaitGate()) return;
|
|
40258
|
+
await mergeOnce();
|
|
40259
|
+
}
|
|
40143
40260
|
} finally {
|
|
40144
40261
|
overrideBody?.cleanup();
|
|
40145
40262
|
}
|
|
@@ -40174,7 +40291,8 @@ ${list}`);
|
|
|
40174
40291
|
pr: number,
|
|
40175
40292
|
branch: headRef,
|
|
40176
40293
|
state: stateRead.ok ? stateRead.state : "unknown",
|
|
40177
|
-
cleanupStatus: "skipped"
|
|
40294
|
+
cleanupStatus: "skipped",
|
|
40295
|
+
...branchUpdated ? { branchUpdated } : {}
|
|
40178
40296
|
}));
|
|
40179
40297
|
console.error(`pr merge: ${gate.message}. Nothing was deleted \u2014 re-check the PR and retry.`);
|
|
40180
40298
|
process.exitCode = 1;
|
|
@@ -40285,6 +40403,7 @@ ${list}`);
|
|
|
40285
40403
|
...methodField ? { method: methodField } : {},
|
|
40286
40404
|
remoteBranch,
|
|
40287
40405
|
housekeeping,
|
|
40406
|
+
...branchUpdated ? { branchUpdated } : {},
|
|
40288
40407
|
...foreignCwd ? { foreignCwd: true, ...foreignCheckout ? { foreignCheckout } : {} } : {},
|
|
40289
40408
|
...partialCleanup.length ? { cleanupStatus: "partial", partialCleanup } : {},
|
|
40290
40409
|
...localCleanup?.worktree ? { worktree: localCleanup.worktree } : {},
|
package/package.json
CHANGED