@@ -100,60 +100,60 @@ const isHttpError = (error: unknown, status: number): boolean => {
100100 && error . status === status ;
101101}
102102
103- export const createOctokitFromToken = async ( { token, url } : { token ?: string , url ?: string } ) : Promise < { octokit : Octokit , isAuthenticated : boolean } > => {
103+ /**
104+ * Creates an Octokit client using a GitHub App installation when one is
105+ * configured for the requested owner, or falls back to the provided token.
106+ */
107+ export const createOctokit = async ( {
108+ token,
109+ url,
110+ owner,
111+ context,
112+ } : {
113+ token ?: string ,
114+ url ?: string ,
115+ owner ?: string ,
116+ context ?: string ,
117+ } ) : Promise < { octokit : Octokit , isAuthenticated : boolean } > => {
118+ let resolvedToken = token ;
119+
120+ if ( owner ) {
121+ const githubAppManager = GithubAppManager . getInstance ( ) ;
122+ await githubAppManager . ensureInitialized ( ) ;
123+
124+ if ( githubAppManager . appsConfigured ( ) ) {
125+ if ( ! await hasEntitlement ( 'github-app' ) ) {
126+ throw new Error ( `GitHub App authentication is not currently licensed for ${ context ?? owner } .` ) ;
127+ }
128+
129+ try {
130+ const hostname = url ? new URL ( url ) . hostname : GITHUB_CLOUD_HOSTNAME ;
131+ resolvedToken = await githubAppManager . getInstallationToken ( owner , hostname ) ;
132+ } catch ( error ) {
133+ if ( error instanceof GithubAppInstallationNotFoundError ) {
134+ throw error ;
135+ }
136+
137+ logger . error ( `Error getting GitHub App token for ${ context ?? owner } .` , error ) ;
138+ throw error ;
139+ }
140+ }
141+ }
142+
104143 const isGitHubCloud = url ? new URL ( url ) . hostname === GITHUB_CLOUD_HOSTNAME : true ;
105144 const octokit = new Octokit ( {
106- auth : token ,
145+ auth : resolvedToken ,
107146 ...( url && ! isGitHubCloud ? {
108147 baseUrl : `${ url } /api/v3`
109148 } : { } ) ,
110149 } ) ;
111150
112151 return {
113152 octokit,
114- isAuthenticated : ! ! token ,
153+ isAuthenticated : ! ! resolvedToken ,
115154 } ;
116155}
117156
118- /**
119- * Uses GitHub App authentication when an app is configured. App initialization
120- * and token failures are propagated so callers cannot mistake a partial,
121- * unauthenticated response for an authoritative repository list.
122- */
123- export const getOctokitWithGithubApp = async (
124- octokit : Octokit ,
125- owner : string ,
126- url : string | undefined ,
127- context : string
128- ) : Promise < Octokit > => {
129- const githubAppManager = GithubAppManager . getInstance ( ) ;
130- await githubAppManager . ensureInitialized ( ) ;
131- if ( ! githubAppManager . appsConfigured ( ) ) {
132- return octokit ;
133- }
134-
135- if ( ! await hasEntitlement ( 'github-app' ) ) {
136- throw new Error ( `GitHub App authentication is not currently licensed for ${ context } .` ) ;
137- }
138-
139- try {
140- const hostname = url ? new URL ( url ) . hostname : GITHUB_CLOUD_HOSTNAME ;
141- const token = await githubAppManager . getInstallationToken ( owner , hostname ) ;
142- const { octokit : octokitFromToken } = await createOctokitFromToken ( {
143- token,
144- url,
145- } ) ;
146- return octokitFromToken ;
147- } catch ( error ) {
148- if ( error instanceof GithubAppInstallationNotFoundError ) {
149- throw error ;
150- }
151-
152- logger . error ( `Error getting GitHub App token for ${ context } .` , error ) ;
153- throw error ;
154- }
155- }
156-
157157export const getGitHubReposFromConfig = async ( config : GithubConnectionConfig , signal : AbortSignal ) : Promise < { repos : OctokitRepository [ ] , warnings : string [ ] } > => {
158158 const hostname = config . url ?
159159 new URL ( config . url ) . hostname :
@@ -165,7 +165,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, s
165165 env . FALLBACK_GITHUB_CLOUD_TOKEN :
166166 undefined ;
167167
168- const { octokit, isAuthenticated } = await createOctokitFromToken ( {
168+ const { octokit, isAuthenticated } = await createOctokit ( {
169169 token,
170170 url : config . url ,
171171 } ) ;
@@ -185,19 +185,19 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, s
185185 let allWarnings : string [ ] = [ ] ;
186186
187187 if ( config . orgs ) {
188- const { repos, warnings } = await getReposForOrgs ( config . orgs , octokit , signal , config . url ) ;
188+ const { repos, warnings } = await getReposForOrgs ( config . orgs , token , signal , config . url ) ;
189189 allRepos = allRepos . concat ( repos ) ;
190190 allWarnings = allWarnings . concat ( warnings ) ;
191191 }
192192
193193 if ( config . repos ) {
194- const { repos, warnings } = await getRepos ( config . repos , octokit , signal , config . url ) ;
194+ const { repos, warnings } = await getRepos ( config . repos , token , signal , config . url ) ;
195195 allRepos = allRepos . concat ( repos ) ;
196196 allWarnings = allWarnings . concat ( warnings ) ;
197197 }
198198
199199 if ( config . users ) {
200- const { repos, warnings } = await getReposOwnedByUsers ( config . users , octokit , signal , config . url ) ;
200+ const { repos, warnings } = await getReposOwnedByUsers ( config . users , token , signal , config . url ) ;
201201 allRepos = allRepos . concat ( repos ) ;
202202 allWarnings = allWarnings . concat ( warnings ) ;
203203 }
@@ -295,12 +295,17 @@ export const getOAuthScopesForAuthenticatedUser = async (octokit: Octokit, token
295295 }
296296}
297297
298- const getReposOwnedByUsers = async ( users : string [ ] , octokit : Octokit , signal : AbortSignal , url ?: string ) => {
298+ const getReposOwnedByUsers = async ( users : string [ ] , token : string | undefined , signal : AbortSignal , url ?: string ) => {
299299 const results = await Promise . allSettled ( users . map ( ( user ) => githubQueryLimit ( async ( ) => {
300300 try {
301301 logger . debug ( `Fetching repository info for user ${ user } ...` ) ;
302302
303- const octokitToUse = await getOctokitWithGithubApp ( octokit , user , url , `user ${ user } ` ) ;
303+ const { octokit } = await createOctokit ( {
304+ token,
305+ url,
306+ owner : user ,
307+ context : `user ${ user } ` ,
308+ } ) ;
304309 const { durationMs, data } = await measure ( async ( ) => {
305310 const fetchFn = async ( ) => {
306311 let query = `user:${ user } ` ;
@@ -317,7 +322,7 @@ const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: A
317322 // signal.aborted between pages. paginate() only passes the signal to
318323 // individual fetch requests but doesn't check abort state between pages.
319324 const allRepos : OctokitRepository [ ] = [ ] ;
320- const iterator = octokitToUse . paginate . iterator ( octokitToUse . rest . search . repos , {
325+ const iterator = octokit . paginate . iterator ( octokit . rest . search . repos , {
321326 q : query ,
322327 per_page : 100 ,
323328 request : {
@@ -368,19 +373,24 @@ const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: A
368373 } ;
369374}
370375
371- const getReposForOrgs = async ( orgs : string [ ] , octokit : Octokit , signal : AbortSignal , url ?: string ) => {
376+ const getReposForOrgs = async ( orgs : string [ ] , token : string | undefined , signal : AbortSignal , url ?: string ) => {
372377 const results = await Promise . allSettled ( orgs . map ( ( org ) => githubQueryLimit ( async ( ) => {
373378 try {
374379 logger . debug ( `Fetching repository info for org ${ org } ...` ) ;
375380
376- const octokitToUse = await getOctokitWithGithubApp ( octokit , org , url , `org ${ org } ` ) ;
381+ const { octokit } = await createOctokit ( {
382+ token,
383+ url,
384+ owner : org ,
385+ context : `org ${ org } ` ,
386+ } ) ;
377387 const { durationMs, data } = await measure ( async ( ) => {
378388 // @note : We use paginate.iterator() instead of paginate() to check
379389 // signal.aborted between pages. paginate() only passes the signal to
380390 // individual fetch requests but doesn't check abort state between pages.
381391 const fetchFn = async ( ) => {
382392 const allRepos : OctokitRepository [ ] = [ ] ;
383- const iterator = octokitToUse . paginate . iterator ( octokitToUse . repos . listForOrg , {
393+ const iterator = octokit . paginate . iterator ( octokit . repos . listForOrg , {
384394 org : org ,
385395 per_page : 100 ,
386396 request : {
@@ -440,15 +450,20 @@ const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSi
440450 } ;
441451}
442452
443- const getRepos = async ( repoList : string [ ] , octokit : Octokit , signal : AbortSignal , url ?: string ) => {
453+ const getRepos = async ( repoList : string [ ] , token : string | undefined , signal : AbortSignal , url ?: string ) => {
444454 const results = await Promise . allSettled ( repoList . map ( ( repo ) => githubQueryLimit ( async ( ) => {
445455 try {
446456 const [ owner , repoName ] = repo . split ( '/' ) ;
447457 logger . debug ( `Fetching repository info for ${ repo } ...` ) ;
448458
449- const octokitToUse = await getOctokitWithGithubApp ( octokit , owner , url , `repo ${ repo } ` ) ;
459+ const { octokit } = await createOctokit ( {
460+ token,
461+ url,
462+ owner,
463+ context : `repo ${ repo } ` ,
464+ } ) ;
450465 const { durationMs, data : result } = await measure ( async ( ) => {
451- const fetchFn = ( ) => octokitToUse . repos . get ( {
466+ const fetchFn = ( ) => octokit . repos . get ( {
452467 owner,
453468 repo : repoName ,
454469 request : {
0 commit comments