Skip to content

Commit a3baba2

Browse files
thePunderWomanjosephperrott
authored andcommitted
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 a3baba2

File tree

2 files changed

+144
-27
lines changed

2 files changed

+144
-27
lines changed

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

Lines changed: 67 additions & 14 deletions
Large diffs are not rendered by default.

ng-dev/utils/git/github.ts

Lines changed: 77 additions & 13 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,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. */
3294
export 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

Comments
 (0)