Skip to content

Commit 3fbd2d0

Browse files
refactor(ng-dev): Add github api retries for increased reliability
Github has been increasingly flakey lately resulting in partial merges and all sorts of issues with failures in the tooling. This adds a retry system to ensure github API calls retry up to 3 times if they fail.
1 parent 616a50d commit 3fbd2d0

File tree

2 files changed

+127
-25
lines changed

2 files changed

+127
-25
lines changed

.github/local-actions/branch-manager/main.js

Lines changed: 58 additions & 13 deletions
Large diffs are not rendered by default.

ng-dev/utils/git/github.ts

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {OctokitOptions} from '@octokit/core';
1111
import {Octokit} from '@octokit/rest';
1212
import {RequestParameters} from '@octokit/types';
1313
import {RequestError} from '@octokit/request-error';
14+
import {GraphqlResponseError} from '@octokit/graphql';
1415
import {query} from 'typed-graphqlify';
1516
import {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. */
3287
export 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

Comments
 (0)