Skip to content

Commit 84cf524

Browse files
Add GHES support to the review agent (#611)
* add support for GHES to the review agent * fix throttling types --------- Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
1 parent 7c72578 commit 84cf524

File tree

2 files changed

+65
-12
lines changed

2 files changed

+65
-12
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- Fixed review agent so that it works with GHES instances [#611](https://github.com/sourcebot-dev/sourcebot/pull/611)
12+
1013
## [4.10.2] - 2025-12-04
1114

1215
### Fixed

packages/web/src/app/api/(server)/webhook/route.ts

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,88 @@ import { WebhookEventDefinition} from "@octokit/webhooks/types";
66
import { EndpointDefaults } from "@octokit/types";
77
import { env } from "@sourcebot/shared";
88
import { processGitHubPullRequest } from "@/features/agents/review-agent/app";
9-
import { throttling } from "@octokit/plugin-throttling";
9+
import { throttling, type ThrottlingOptions } from "@octokit/plugin-throttling";
1010
import fs from "fs";
1111
import { GitHubPullRequest } from "@/features/agents/review-agent/types";
1212
import { createLogger } from "@sourcebot/shared";
1313

1414
const logger = createLogger('github-webhook');
1515

16-
let githubApp: App | undefined;
16+
const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
17+
type GitHubAppBaseOptions = Omit<ConstructorParameters<typeof App>[0], "Octokit"> & { throttle: ThrottlingOptions };
18+
19+
let githubAppBaseOptions: GitHubAppBaseOptions | undefined;
20+
const githubAppCache = new Map<string, App>();
21+
1722
if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET && env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH) {
1823
try {
1924
const privateKey = fs.readFileSync(env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH, "utf8");
2025

21-
const throttledOctokit = Octokit.plugin(throttling);
22-
githubApp = new App({
26+
githubAppBaseOptions = {
2327
appId: env.GITHUB_REVIEW_AGENT_APP_ID,
24-
privateKey: privateKey,
28+
privateKey,
2529
webhooks: {
2630
secret: env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET,
2731
},
28-
Octokit: throttledOctokit,
2932
throttle: {
30-
onRateLimit: (retryAfter: number, options: Required<EndpointDefaults>, octokit: Octokit, retryCount: number) => {
33+
enabled: true,
34+
onRateLimit: (retryAfter, _options, _octokit, retryCount) => {
3135
if (retryCount > 3) {
3236
logger.warn(`Rate limit exceeded: ${retryAfter} seconds`);
3337
return false;
3438
}
3539

3640
return true;
3741
},
38-
}
39-
});
42+
onSecondaryRateLimit: (_retryAfter, options) => {
43+
// no retries on secondary rate limits
44+
logger.warn(`SecondaryRateLimit detected for ${options.method} ${options.url}`);
45+
}
46+
},
47+
};
4048
} catch (error) {
4149
logger.error(`Error initializing GitHub app: ${error}`);
4250
}
4351
}
4452

53+
const normalizeGithubApiBaseUrl = (baseUrl?: string) => {
54+
if (!baseUrl) {
55+
return DEFAULT_GITHUB_API_BASE_URL;
56+
}
57+
58+
return baseUrl.replace(/\/+$/, "");
59+
};
60+
61+
const resolveGithubApiBaseUrl = (headers: Record<string, string>) => {
62+
const enterpriseHost = headers["x-github-enterprise-host"];
63+
if (enterpriseHost) {
64+
return normalizeGithubApiBaseUrl(`https://${enterpriseHost}/api/v3`);
65+
}
66+
67+
return DEFAULT_GITHUB_API_BASE_URL;
68+
};
69+
70+
const getGithubAppForBaseUrl = (baseUrl: string) => {
71+
if (!githubAppBaseOptions) {
72+
return undefined;
73+
}
74+
75+
const normalizedBaseUrl = normalizeGithubApiBaseUrl(baseUrl);
76+
const cachedApp = githubAppCache.get(normalizedBaseUrl);
77+
if (cachedApp) {
78+
return cachedApp;
79+
}
80+
81+
const OctokitWithBaseUrl = Octokit.plugin(throttling).defaults({ baseUrl: normalizedBaseUrl });
82+
const app = new App({
83+
...githubAppBaseOptions,
84+
Octokit: OctokitWithBaseUrl,
85+
});
86+
87+
githubAppCache.set(normalizedBaseUrl, app);
88+
return app;
89+
};
90+
4591
function isPullRequestEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize"> {
4692
return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize");
4793
}
@@ -52,12 +98,16 @@ function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is
5298

5399
export const POST = async (request: NextRequest) => {
54100
const body = await request.json();
55-
const headers = Object.fromEntries(request.headers.entries());
101+
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
56102

57-
const githubEvent = headers['x-github-event'] || headers['X-GitHub-Event'];
103+
const githubEvent = headers['x-github-event'];
58104
if (githubEvent) {
59105
logger.info('GitHub event received:', githubEvent);
60106

107+
const githubApiBaseUrl = resolveGithubApiBaseUrl(headers);
108+
logger.debug('Using GitHub API base URL for event', { githubApiBaseUrl });
109+
const githubApp = getGithubAppForBaseUrl(githubApiBaseUrl);
110+
61111
if (!githubApp) {
62112
logger.warn('Received GitHub webhook event but GitHub app env vars are not set');
63113
return Response.json({ status: 'ok' });
@@ -113,4 +163,4 @@ export const POST = async (request: NextRequest) => {
113163
}
114164

115165
return Response.json({ status: 'ok' });
116-
}
166+
}

0 commit comments

Comments
 (0)