@@ -11,6 +11,7 @@ import type {OctokitOptions} from '@octokit/core';
1111import { Octokit } from '@octokit/rest' ;
1212import { RequestParameters } from '@octokit/types' ;
1313import { RequestError } from '@octokit/request-error' ;
14+ import { GraphqlResponseError } from '@octokit/graphql' ;
1415import { query } from 'typed-graphqlify' ;
1516import { Log } from '../logging' ;
1617
@@ -28,6 +29,67 @@ export interface GithubRepo {
2829 name : string ;
2930}
3031
32+ /** Helper to invoke an async function with retries. */
33+ async function invokeWithRetry < T > ( fn : ( ) => Promise < T > , retries = 3 , delay = 1000 ) : Promise < T > {
34+ let attempt = 0 ;
35+ while ( attempt < retries ) {
36+ try {
37+ return await fn ( ) ;
38+ } catch ( e ) {
39+ attempt ++ ;
40+ if ( attempt >= retries ) {
41+ throw e ;
42+ }
43+
44+ // Do not retry valid 4xx Client Errors (especially 404 Not Found)
45+ if ( isGithubApiError ( e ) && e . status < 500 ) {
46+ throw e ;
47+ }
48+
49+ // Do not retry permanent GraphQL errors
50+ if ( e instanceof GraphqlResponseError ) {
51+ if ( ! e . errors ) {
52+ throw e ; // Missing errors, assume permanent or unknown
53+ }
54+ if (
55+ e . errors . every ( ( err ) =>
56+ [ 'NOT_FOUND' , 'FORBIDDEN' , 'BAD_USER_INPUT' , 'UNAUTHENTICATED' ] . includes ( err . type ! ) ,
57+ )
58+ ) {
59+ throw e ;
60+ }
61+ }
62+
63+ Log . warn ( `GitHub API call failed (attempt ${ attempt } /${ retries } ). Retrying in ${ delay } ms...` ) ;
64+ await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) ) ;
65+ }
66+ }
67+ throw new Error ( 'Unreachable' ) ;
68+ }
69+
70+ /** Creates a proxy that intercepts function calls and applies retries. */
71+ function createRetryProxy < T extends object > ( target : T ) : T {
72+ return new Proxy ( target , {
73+ get ( targetObj , prop , receiver ) {
74+ const value = Reflect . get ( targetObj , prop , receiver ) ;
75+ if ( typeof value === 'function' ) {
76+ return new Proxy ( value , {
77+ apply ( targetFn , thisArg , argArray ) {
78+ return invokeWithRetry ( ( ) => ( targetFn as Function ) . apply ( targetObj , argArray ) ) ;
79+ } ,
80+ } ) ;
81+ }
82+ if ( typeof value === 'object' && value !== null ) {
83+ return createRetryProxy ( value ) ;
84+ }
85+ return value ;
86+ } ,
87+ apply ( targetFn , thisArg , argArray ) {
88+ return invokeWithRetry ( ( ) => ( targetFn as Function ) . apply ( thisArg , argArray ) ) ;
89+ } ,
90+ } ) ;
91+ }
92+
3193/** A Github client for interacting with the Github APIs. */
3294export class GithubClient {
3395 /** The octokit instance actually performing API requests. */
@@ -43,18 +105,18 @@ export class GithubClient {
43105 ...this . _octokitOptions ,
44106 } ) ;
45107
46- readonly pulls : Octokit [ 'pulls' ] = this . _octokit . pulls ;
47- readonly orgs : Octokit [ 'orgs' ] = this . _octokit . orgs ;
48- readonly repos : Octokit [ 'repos' ] = this . _octokit . repos ;
49- readonly issues : Octokit [ 'issues' ] = this . _octokit . issues ;
50- readonly git : Octokit [ 'git' ] = this . _octokit . git ;
51- readonly rateLimit : Octokit [ 'rateLimit' ] = this . _octokit . rateLimit ;
52- readonly teams : Octokit [ 'teams' ] = this . _octokit . teams ;
53- readonly search : Octokit [ 'search' ] = this . _octokit . search ;
54- readonly rest : Octokit [ 'rest' ] = this . _octokit . rest ;
55- readonly paginate : Octokit [ 'paginate' ] = this . _octokit . paginate ;
56- readonly checks : Octokit [ 'checks' ] = this . _octokit . checks ;
57- readonly users : Octokit [ 'users' ] = this . _octokit . users ;
108+ readonly pulls : Octokit [ 'pulls' ] = createRetryProxy ( this . _octokit . pulls ) ;
109+ readonly orgs : Octokit [ 'orgs' ] = createRetryProxy ( this . _octokit . orgs ) ;
110+ readonly repos : Octokit [ 'repos' ] = createRetryProxy ( this . _octokit . repos ) ;
111+ readonly issues : Octokit [ 'issues' ] = createRetryProxy ( this . _octokit . issues ) ;
112+ readonly git : Octokit [ 'git' ] = createRetryProxy ( this . _octokit . git ) ;
113+ readonly rateLimit : Octokit [ 'rateLimit' ] = createRetryProxy ( this . _octokit . rateLimit ) ;
114+ readonly teams : Octokit [ 'teams' ] = createRetryProxy ( this . _octokit . teams ) ;
115+ readonly search : Octokit [ 'search' ] = createRetryProxy ( this . _octokit . search ) ;
116+ readonly rest : Octokit [ 'rest' ] = createRetryProxy ( this . _octokit . rest ) ;
117+ readonly paginate : Octokit [ 'paginate' ] = createRetryProxy ( this . _octokit . paginate ) ;
118+ readonly checks : Octokit [ 'checks' ] = createRetryProxy ( this . _octokit . checks ) ;
119+ readonly users : Octokit [ 'users' ] = createRetryProxy ( this . _octokit . users ) ;
58120
59121 constructor ( private _octokitOptions ?: OctokitOptions ) { }
60122}
@@ -76,7 +138,9 @@ export class AuthenticatedGithubClient extends GithubClient {
76138
77139 /** Perform a query using Github's Graphql API. */
78140 async graphql < T extends GraphqlQueryObject > ( queryObject : T , params : RequestParameters = { } ) {
79- return ( await this . _graphql ( query ( queryObject ) . toString ( ) , params ) ) as T ;
141+ return invokeWithRetry ( async ( ) => {
142+ return ( await this . _graphql ( query ( queryObject ) . toString ( ) , params ) ) as T ;
143+ } ) ;
80144 }
81145}
82146
0 commit comments