From c84d1e3ab579a0e40a4525934e143797363219d2 Mon Sep 17 00:00:00 2001 From: Brian Phillips <28457+brianphillips@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:17:27 -0600 Subject: [PATCH 1/2] add support for GHES to the review agent --- CHANGELOG.md | 1 + .../web/src/app/api/(server)/webhook/route.ts | 65 ++++++++++++++++--- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52207d31..4b2ea48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed incorrect shutdown of PostHog SDK in the worker. [#609](https://github.com/sourcebot-dev/sourcebot/pull/609) - Fixed race condition in job schedulers. [#607](https://github.com/sourcebot-dev/sourcebot/pull/607) +- Fixed review agent so that it works with GHES instances [#611](https://github.com/sourcebot-dev/sourcebot/pull/611) ## [4.9.1] - 2025-11-07 diff --git a/packages/web/src/app/api/(server)/webhook/route.ts b/packages/web/src/app/api/(server)/webhook/route.ts index cfd01a25..c7eda966 100644 --- a/packages/web/src/app/api/(server)/webhook/route.ts +++ b/packages/web/src/app/api/(server)/webhook/route.ts @@ -13,19 +13,22 @@ import { createLogger } from "@sourcebot/shared"; const logger = createLogger('github-webhook'); -let githubApp: App | undefined; +const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com"; +type GitHubAppBaseOptions = Omit[0], "Octokit">; + +let githubAppBaseOptions: GitHubAppBaseOptions | undefined; +const githubAppCache = new Map(); + if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET && env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH) { try { const privateKey = fs.readFileSync(env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH, "utf8"); - const throttledOctokit = Octokit.plugin(throttling); - githubApp = new App({ + githubAppBaseOptions = { appId: env.GITHUB_REVIEW_AGENT_APP_ID, - privateKey: privateKey, + privateKey, webhooks: { secret: env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET, }, - Octokit: throttledOctokit, throttle: { onRateLimit: (retryAfter: number, options: Required, octokit: Octokit, retryCount: number) => { if (retryCount > 3) { @@ -35,13 +38,51 @@ if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET return true; }, - } - }); + }, + }; } catch (error) { logger.error(`Error initializing GitHub app: ${error}`); } } +const normalizeGithubApiBaseUrl = (baseUrl?: string) => { + if (!baseUrl) { + return DEFAULT_GITHUB_API_BASE_URL; + } + + return baseUrl.replace(/\/+$/, ""); +}; + +const resolveGithubApiBaseUrl = (headers: Record) => { + const enterpriseHost = headers["x-github-enterprise-host"]; + if (enterpriseHost) { + return normalizeGithubApiBaseUrl(`https://${enterpriseHost}/api/v3`); + } + + return DEFAULT_GITHUB_API_BASE_URL; +}; + +const getGithubAppForBaseUrl = (baseUrl: string) => { + if (!githubAppBaseOptions) { + return undefined; + } + + const normalizedBaseUrl = normalizeGithubApiBaseUrl(baseUrl); + const cachedApp = githubAppCache.get(normalizedBaseUrl); + if (cachedApp) { + return cachedApp; + } + + const OctokitWithBaseUrl = Octokit.plugin(throttling).defaults({ baseUrl: normalizedBaseUrl }); + const app = new App({ + ...githubAppBaseOptions, + Octokit: OctokitWithBaseUrl, + }); + + githubAppCache.set(normalizedBaseUrl, app); + return app; +}; + function isPullRequestEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize"> { return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize"); } @@ -52,12 +93,16 @@ function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is export const POST = async (request: NextRequest) => { const body = await request.json(); - const headers = Object.fromEntries(request.headers.entries()); + const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value])); - const githubEvent = headers['x-github-event'] || headers['X-GitHub-Event']; + const githubEvent = headers['x-github-event']; if (githubEvent) { logger.info('GitHub event received:', githubEvent); + const githubApiBaseUrl = resolveGithubApiBaseUrl(headers); + logger.debug('Using GitHub API base URL for event', { githubApiBaseUrl }); + const githubApp = getGithubAppForBaseUrl(githubApiBaseUrl); + if (!githubApp) { logger.warn('Received GitHub webhook event but GitHub app env vars are not set'); return Response.json({ status: 'ok' }); @@ -113,4 +158,4 @@ export const POST = async (request: NextRequest) => { } return Response.json({ status: 'ok' }); -} \ No newline at end of file +} From c4c4d100f0baa49930cbdd3eebd459449e49e06e Mon Sep 17 00:00:00 2001 From: Brian Phillips <28457+brianphillips@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:16:53 -0600 Subject: [PATCH 2/2] fix throttling types --- packages/web/src/app/api/(server)/webhook/route.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/web/src/app/api/(server)/webhook/route.ts b/packages/web/src/app/api/(server)/webhook/route.ts index c7eda966..ebf3469c 100644 --- a/packages/web/src/app/api/(server)/webhook/route.ts +++ b/packages/web/src/app/api/(server)/webhook/route.ts @@ -6,7 +6,7 @@ import { WebhookEventDefinition} from "@octokit/webhooks/types"; import { EndpointDefaults } from "@octokit/types"; import { env } from "@sourcebot/shared"; import { processGitHubPullRequest } from "@/features/agents/review-agent/app"; -import { throttling } from "@octokit/plugin-throttling"; +import { throttling, type ThrottlingOptions } from "@octokit/plugin-throttling"; import fs from "fs"; import { GitHubPullRequest } from "@/features/agents/review-agent/types"; import { createLogger } from "@sourcebot/shared"; @@ -14,7 +14,7 @@ import { createLogger } from "@sourcebot/shared"; const logger = createLogger('github-webhook'); const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com"; -type GitHubAppBaseOptions = Omit[0], "Octokit">; +type GitHubAppBaseOptions = Omit[0], "Octokit"> & { throttle: ThrottlingOptions }; let githubAppBaseOptions: GitHubAppBaseOptions | undefined; const githubAppCache = new Map(); @@ -30,7 +30,8 @@ if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET secret: env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET, }, throttle: { - onRateLimit: (retryAfter: number, options: Required, octokit: Octokit, retryCount: number) => { + enabled: true, + onRateLimit: (retryAfter, _options, _octokit, retryCount) => { if (retryCount > 3) { logger.warn(`Rate limit exceeded: ${retryAfter} seconds`); return false; @@ -38,6 +39,10 @@ if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET return true; }, + onSecondaryRateLimit: (_retryAfter, options) => { + // no retries on secondary rate limits + logger.warn(`SecondaryRateLimit detected for ${options.method} ${options.url}`); + } }, }; } catch (error) {