@@ -6,42 +6,88 @@ import { WebhookEventDefinition} from "@octokit/webhooks/types";
66import { EndpointDefaults } from "@octokit/types" ;
77import { env } from "@sourcebot/shared" ;
88import { processGitHubPullRequest } from "@/features/agents/review-agent/app" ;
9- import { throttling } from "@octokit/plugin-throttling" ;
9+ import { throttling , type ThrottlingOptions } from "@octokit/plugin-throttling" ;
1010import fs from "fs" ;
1111import { GitHubPullRequest } from "@/features/agents/review-agent/types" ;
1212import { createLogger } from "@sourcebot/shared" ;
1313
1414const 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+
1722if ( 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+
4591function 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
5399export 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