@@ -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,60 @@ 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 (
51+ e instanceof GraphqlResponseError &&
52+ e . errors ?. every ( ( err ) =>
53+ [ 'NOT_FOUND' , 'FORBIDDEN' , 'BAD_USER_INPUT' , 'UNAUTHENTICATED' ] . includes ( err . type ! ) ,
54+ )
55+ ) {
56+ throw e ;
57+ }
58+
59+ Log . warn ( `GitHub API call failed (attempt ${ attempt } /${ retries } ). Retrying in ${ delay } ms...` ) ;
60+ await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) ) ;
61+ }
62+ }
63+ throw new Error ( 'Unreachable' ) ;
64+ }
65+
66+ /** Creates a proxy that intercepts function calls and applies retries. */
67+ function createRetryProxy < T extends object > ( target : T ) : T {
68+ return new Proxy ( target , {
69+ get ( targetObj , prop , receiver ) {
70+ const value = Reflect . get ( targetObj , prop , receiver ) ;
71+ if ( typeof value === 'function' ) {
72+ return new Proxy ( value , {
73+ apply ( targetFn , thisArg , argArray ) {
74+ return invokeWithRetry ( ( ) => ( targetFn as Function ) . apply ( targetObj , argArray ) ) ;
75+ } ,
76+ } ) ;
77+ }
78+ if ( typeof value === 'object' && value !== null ) {
79+ return createRetryProxy ( value ) ;
80+ }
81+ return value ;
82+ } ,
83+ } ) ;
84+ }
85+
3186/** A Github client for interacting with the Github APIs. */
3287export class GithubClient {
3388 /** The octokit instance actually performing API requests. */
@@ -43,18 +98,18 @@ export class GithubClient {
4398 ...this . _octokitOptions ,
4499 } ) ;
45100
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 ;
101+ readonly pulls : Octokit [ 'pulls' ] = createRetryProxy ( this . _octokit . pulls ) ;
102+ readonly orgs : Octokit [ 'orgs' ] = createRetryProxy ( this . _octokit . orgs ) ;
103+ readonly repos : Octokit [ 'repos' ] = createRetryProxy ( this . _octokit . repos ) ;
104+ readonly issues : Octokit [ 'issues' ] = createRetryProxy ( this . _octokit . issues ) ;
105+ readonly git : Octokit [ 'git' ] = createRetryProxy ( this . _octokit . git ) ;
106+ readonly rateLimit : Octokit [ 'rateLimit' ] = createRetryProxy ( this . _octokit . rateLimit ) ;
107+ readonly teams : Octokit [ 'teams' ] = createRetryProxy ( this . _octokit . teams ) ;
108+ readonly search : Octokit [ 'search' ] = createRetryProxy ( this . _octokit . search ) ;
109+ readonly rest : Octokit [ 'rest' ] = createRetryProxy ( this . _octokit . rest ) ;
55110 readonly paginate : Octokit [ 'paginate' ] = this . _octokit . paginate ;
56- readonly checks : Octokit [ 'checks' ] = this . _octokit . checks ;
57- readonly users : Octokit [ 'users' ] = this . _octokit . users ;
111+ readonly checks : Octokit [ 'checks' ] = createRetryProxy ( this . _octokit . checks ) ;
112+ readonly users : Octokit [ 'users' ] = createRetryProxy ( this . _octokit . users ) ;
58113
59114 constructor ( private _octokitOptions ?: OctokitOptions ) { }
60115}
@@ -76,7 +131,9 @@ export class AuthenticatedGithubClient extends GithubClient {
76131
77132 /** Perform a query using Github's Graphql API. */
78133 async graphql < T extends GraphqlQueryObject > ( queryObject : T , params : RequestParameters = { } ) {
79- return ( await this . _graphql ( query ( queryObject ) . toString ( ) , params ) ) as T ;
134+ return invokeWithRetry ( async ( ) => {
135+ return ( await this . _graphql ( query ( queryObject ) . toString ( ) , params ) ) as T ;
136+ } ) ;
80137 }
81138}
82139
0 commit comments