Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2045,6 +2045,9 @@ export function derivePublicCommentMergeFacts(args: {
unifiedFiles: Awaited<ReturnType<typeof listPullRequestFiles>>;
repoFullName: string;
prLabels: readonly string[];
// Optional so a self-hoster's PROTECTED_AUTOCLOSE_AUTHORS_EXTRA allowlist extension reaches neverClosed
// (#8645). Callers that already hold `env` MUST pass it; omitted only keeps the built-in bot set.
env?: Env;
}): PublicCommentMergeFacts {
const mergeStateLabel = args.liveMergeState ?? args.mergeableState ?? undefined; // fail-safe to the stored value
const ciState: MergeReadiness["ciState"] =
Expand All @@ -2069,7 +2072,8 @@ export function derivePublicCommentMergeFacts(args: {
const repoOwner = args.repoFullName.includes("/") ? args.repoFullName.slice(0, args.repoFullName.indexOf("/")) : "";
const authorLogin = args.authorLogin ?? "";
const neverClosed =
(authorLogin.length > 0 && authorLogin.toLowerCase() === repoOwner.toLowerCase()) || isProtectedAutomationAuthor(args.authorLogin);
(authorLogin.length > 0 && authorLogin.toLowerCase() === repoOwner.toLowerCase()) ||
isProtectedAutomationAuthor(args.authorLogin, args.env);
return { ciState, mergeStateLabel, mergeReadiness, heldForReview, neverClosed };
}

Expand Down Expand Up @@ -2721,7 +2725,7 @@ async function maybeCloseForContributorCapOnOpen(
const authorIsOwner = pr.authorLogin.toLowerCase() === repoOwner.toLowerCase();
// #4889: per-repo admin mode swaps the global-allowlist grant for the live per-repo permission.
const authorIsAdmin = await isPerTenantAdmin(env, installationId, repoFullName, pr.authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin, env);
if (authorIsOwner || authorIsAdmin || authorIsAutomationBot) return false;
// #ignore-authors-parity: a manifest ignore_authors match (e.g. "release-please*") means the bot treats
// this author as entirely invisible -- maybePublishPrPublicSurface's own reviewEligibility check (deep
Expand Down Expand Up @@ -2975,7 +2979,7 @@ async function runAgentMaintenancePlanAndExecute(
authorLogin.length > 0 &&
// #4889: per-repo admin mode swaps the global-allowlist grant for the live per-repo permission.
(await isPerTenantAdmin(env, installationId, repoFullName, authorLogin));
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin, env);

// Linked-issue HARD-RULE close (#linked-issue-hard-rules): when the repo enabled any rule, a body that links
// MORE closing references than we can safely verify (overflow) is itself a violation; otherwise evaluate the
Expand Down Expand Up @@ -3550,7 +3554,7 @@ export async function reReviewStoredPullRequest(
// this resync exists to recover), fail open into the full review/gate instead of trusting the immutable author.
if (
automationBotSkipEnabled &&
isTrustedAutomationBotAuthor(pr.authorLogin) &&
isTrustedAutomationBotAuthor(pr.authorLogin, env) &&
live?.head?.sha === storedHeadShaBeforeResync
)
return false;
Expand Down Expand Up @@ -5476,7 +5480,7 @@ async function maybeCloseIssueOverContributorCap(
const authorIsOwner = authorLogin.toLowerCase() === repoOwner.toLowerCase();
// #4889: per-repo admin mode swaps the global-allowlist grant for the live per-repo permission.
const authorIsAdmin = await isPerTenantAdmin(env, args.installationId, repoFullName, authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(authorLogin, env);
if (authorIsOwner || authorIsAdmin || authorIsAutomationBot) return;

// Account-age throttle (#2561): mirror the PR-path cap tightening — a below-threshold author gets half
Expand Down Expand Up @@ -6231,7 +6235,7 @@ async function handlePullRequestWebhookEvent(
// bot PR's branch still gets full review of their own commits.
if (
resolveSkipAutomationBotPullRequests(isSkipAutomationBotPullRequestsEnabledGlobally(env), settings.skipAutomationBotAuthors) &&
isTrustedAutomationBotWebhookActor(payload.sender, pr.authorLogin)
isTrustedAutomationBotWebhookActor(payload.sender, pr.authorLogin, env)
) {
await recordAuditEvent(env, {
eventType: "github_app.automation_bot_pr_skipped",
Expand Down Expand Up @@ -6365,7 +6369,7 @@ async function handlePullRequestWebhookEvent(
pr.headSha &&
pr.state === "open" &&
isAgentConfigured(settings.autonomy) &&
!isProtectedAutomationAuthor(pr.authorLogin)
!isProtectedAutomationAuthor(pr.authorLogin, env)
) {
// Deliberately UNCAUGHT here: closeDraftDodgeAttemptIfBlocked catches every operation that should
// fail safely, but leaves the write-permission-readiness getInstallation read (#2134) uncaught on
Expand Down Expand Up @@ -6732,7 +6736,7 @@ async function handleIssueWebhookEvent(
const authorIsOwner = authorLogin.toLowerCase() === repoOwner.toLowerCase();
// #4889: per-repo admin mode swaps the global-allowlist grant for the live per-repo permission.
const authorIsAdmin = await isPerTenantAdmin(env, installationId, payload.repository.full_name, authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(authorLogin, env);
const accountAgeThresholdDays = issueSettings.accountAgeThresholdDays;
if (
!authorIsOwner &&
Expand Down Expand Up @@ -9727,7 +9731,7 @@ async function maybePublishPrPublicSurface(
(author.toLowerCase() === repoOwnerLoginFromFullName(repoFullName).toLowerCase() ||
// #4889: per-repo admin mode swaps the global-allowlist grant for the live per-repo permission.
(await isPerTenantAdmin(env, installationId, repoFullName, author)) ||
isProtectedAutomationAuthor(author));
isProtectedAutomationAuthor(author, env));
const isFrozenForManualReview =
webhook.forceAiReview !== true &&
!authorIsExemptFromFreeze &&
Expand Down Expand Up @@ -11154,6 +11158,7 @@ async function maybePublishPrPublicSurface(
unifiedFiles,
repoFullName,
prLabels: pr.labels,
env,
});
// The public comment must match the authoritative Gate check-run conclusion.
const commentGate = commentGateEvaluation;
Expand Down
10 changes: 5 additions & 5 deletions src/queue/review-evasion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ async function closeReviewEvasionSelfCloseIfReviewed(
// Only the PR's OWN author closing their OWN PR is a self-close-evasion candidate -- a third party (e.g. a
// maintainer) closing someone else's PR is an ordinary maintainer action, not evasion.
if (!closer || !authorLogin || closer !== authorLogin) return;
if (isProtectedAutomationAuthor(pr.authorLogin)) return;
if (isProtectedAutomationAuthor(pr.authorLogin, env)) return;
if (isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)) return;
if (!pr.headSha) return;
const headSha = pr.headSha; // captured so the allowStaleIf closure below keeps the narrowed non-null type
Expand Down Expand Up @@ -828,7 +828,7 @@ async function closeReviewEvasionDraftConversionIfReviewed(
// not evasion, and must never be enforced against the AUTHOR who didn't do it (mirrors the self-close
// sibling's identical actor check).
if (!converter || !authorLogin || converter !== authorLogin) return;
if (isProtectedAutomationAuthor(pr.authorLogin)) return;
if (isProtectedAutomationAuthor(pr.authorLogin, env)) return;
if (isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)) return;
if (!pr.headSha) return;
const headSha = pr.headSha;
Expand Down Expand Up @@ -994,7 +994,7 @@ async function closeRepeatedDraftCyclingIfDetected(
const converter = (payload.sender?.login ?? "").toLowerCase();
/* v8 ignore next -- unreachable given the call site's own author-only increment guarantee; see comment above. */
if (!converter || !authorLogin || converter !== authorLogin) return;
if (isProtectedAutomationAuthor(pr.authorLogin)) return;
if (isProtectedAutomationAuthor(pr.authorLogin, env)) return;
// Honor the maintainer's trusted-contributor allowlist, same as the two sibling review-evasion guards
// (closeReviewEvasionSelfCloseIfReviewed / closeReviewEvasionDraftConversionIfReviewed) already do (#6165).
if (isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)) return;
Expand Down Expand Up @@ -1155,7 +1155,7 @@ async function closeDraftPrIfPolicyEnabled(
const actorLogin = (payload.sender?.login ?? "").toLowerCase();
const authorLogin = (pr.authorLogin ?? "").toLowerCase();
if (!actorLogin || !authorLogin || actorLogin !== authorLogin) return;
if (isProtectedAutomationAuthor(pr.authorLogin)) return;
if (isProtectedAutomationAuthor(pr.authorLogin, env)) return;
if (isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)) return;
if (!pr.headSha) return;
const headSha = pr.headSha;
Expand Down Expand Up @@ -1289,7 +1289,7 @@ async function closeSynchronizeAmendmentIfPolicyEnabled(
const actorLogin = (payload.sender?.login ?? "").toLowerCase();
const authorLogin = (pr.authorLogin ?? "").toLowerCase();
if (!actorLogin || !authorLogin || actorLogin !== authorLogin) return;
if (isProtectedAutomationAuthor(pr.authorLogin)) return;
if (isProtectedAutomationAuthor(pr.authorLogin, env)) return;
if (isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)) return;
if (!pr.headSha) return;
const headSha = pr.headSha;
Expand Down
4 changes: 2 additions & 2 deletions src/services/agent-approval-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export async function decidePendingAgentAction(env: Env, input: { id: string; de
const repoOwner = pending.repoFullName.includes("/") ? pending.repoFullName.slice(0, pending.repoFullName.indexOf("/")) : "";
const authorLogin = pr.authorLogin ?? "";
const authorIsOwner = authorLogin.length > 0 && authorLogin.toLowerCase() === repoOwner.toLowerCase();
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin, env);
const closeEligible = (!authorIsOwner && !authorIsAutomationBot) || (authorIsOwner && settings.closeOwnerAuthors === true);
let stillJustified = closeEligible;
if (closeEligible) {
Expand Down Expand Up @@ -358,7 +358,7 @@ export async function decidePendingAgentAction(env: Env, input: { id: string; de
const repoOwner = pending.repoFullName.includes("/") ? pending.repoFullName.slice(0, pending.repoFullName.indexOf("/")) : "";
const authorLogin = pr.authorLogin ?? "";
const authorIsOwner = authorLogin.length > 0 && authorLogin.toLowerCase() === repoOwner.toLowerCase();
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin);
const authorIsAutomationBot = isProtectedAutomationAuthor(pr.authorLogin, env);
const closeEligible = (!authorIsOwner && !authorIsAutomationBot) || (authorIsOwner && settings.closeOwnerAuthors === true);
if (closeEligible) {
const linkedIssueRulesConfig = await loadLinkedIssueHardRules(env, pending.repoFullName);
Expand Down
9 changes: 5 additions & 4 deletions src/settings/automation-bot-skip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ export function resolveSkipAutomationBotPullRequests(globalDefault: boolean, mod
export function isTrustedAutomationBotWebhookActor(
sender: { login?: string | null | undefined; type?: string | null | undefined } | null | undefined,
prAuthorLogin: string | null | undefined,
env?: Env,
): boolean {
return (
sender?.type === "Bot" &&
isProtectedAutomationAuthor(sender.login) &&
isProtectedAutomationAuthor(prAuthorLogin)
isProtectedAutomationAuthor(sender.login, env) &&
isProtectedAutomationAuthor(prAuthorLogin, env)
);
}

Expand All @@ -67,6 +68,6 @@ export function isTrustedAutomationBotWebhookActor(
* live PR head still matches the stored head) before using this result to bypass review. Original PR authorship
* alone does not prove that later commits on the branch were still produced by the trusted bot.
*/
export function isTrustedAutomationBotAuthor(prAuthorLogin: string | null | undefined): boolean {
return isProtectedAutomationAuthor(prAuthorLogin);
export function isTrustedAutomationBotAuthor(prAuthorLogin: string | null | undefined, env?: Env): boolean {
return isProtectedAutomationAuthor(prAuthorLogin, env);
}
6 changes: 6 additions & 0 deletions test/unit/processors-public-comment-merge-facts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ describe("derivePublicCommentMergeFacts() — neverClosed (#8/#9, #4607)", () =>
expect(facts({ authorLogin: "github-actions[bot]" }).neverClosed).toBe(true);
});

it("is true for a PROTECTED_AUTOCLOSE_AUTHORS_EXTRA bot only when env is passed (#8645)", () => {
const env = { PROTECTED_AUTOCLOSE_AUTHORS_EXTRA: "mergify[bot]" } as Env;
expect(facts({ authorLogin: "mergify[bot]" }).neverClosed).toBe(false);
expect(facts({ authorLogin: "mergify[bot]", env }).neverClosed).toBe(true);
});

it("is false for an ordinary contributor", () => {
expect(facts({ authorLogin: "contributor" }).neverClosed).toBe(false);
});
Expand Down
Loading