Skip to content

Commit 5e95d1e

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 5e95d1e

File tree

2 files changed

+116
-25
lines changed

2 files changed

+116
-25
lines changed

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

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

ng-dev/utils/git/github.ts

Lines changed: 61 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,52 @@ 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 GraphQL NOT_FOUND errors
50+
if (e instanceof GraphqlResponseError && e.errors?.every((err) => err.type === 'NOT_FOUND')) {
51+
throw e;
52+
}
53+
54+
Log.warn(`GitHub API call failed (attempt ${attempt}/${retries}). Retrying in ${delay}ms...`);
55+
await new Promise((resolve) => setTimeout(resolve, delay));
56+
}
57+
}
58+
throw new Error('Unreachable');
59+
}
60+
61+
/** Creates a proxy that intercepts function calls and applies retries. */
62+
function createRetryProxy<T extends object>(target: T): T {
63+
return new Proxy(target, {
64+
get(targetObj, prop, receiver) {
65+
const value = Reflect.get(targetObj, prop, receiver);
66+
if (typeof value === 'function') {
67+
return new Proxy(value, {
68+
apply(targetFn, thisArg, argArray) {
69+
return invokeWithRetry(() => (targetFn as Function).apply(targetObj, argArray));
70+
},
71+
});
72+
}
73+
return value;
74+
},
75+
});
76+
}
77+
3178
/** A Github client for interacting with the Github APIs. */
3279
export class GithubClient {
3380
/** The octokit instance actually performing API requests. */
@@ -43,18 +90,18 @@ export class GithubClient {
4390
...this._octokitOptions,
4491
});
4592

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;
93+
readonly pulls: Octokit['pulls'] = createRetryProxy(this._octokit.pulls);
94+
readonly orgs: Octokit['orgs'] = createRetryProxy(this._octokit.orgs);
95+
readonly repos: Octokit['repos'] = createRetryProxy(this._octokit.repos);
96+
readonly issues: Octokit['issues'] = createRetryProxy(this._octokit.issues);
97+
readonly git: Octokit['git'] = createRetryProxy(this._octokit.git);
98+
readonly rateLimit: Octokit['rateLimit'] = createRetryProxy(this._octokit.rateLimit);
99+
readonly teams: Octokit['teams'] = createRetryProxy(this._octokit.teams);
100+
readonly search: Octokit['search'] = createRetryProxy(this._octokit.search);
101+
readonly rest: Octokit['rest'] = createRetryProxy(this._octokit.rest);
55102
readonly paginate: Octokit['paginate'] = this._octokit.paginate;
56-
readonly checks: Octokit['checks'] = this._octokit.checks;
57-
readonly users: Octokit['users'] = this._octokit.users;
103+
readonly checks: Octokit['checks'] = createRetryProxy(this._octokit.checks);
104+
readonly users: Octokit['users'] = createRetryProxy(this._octokit.users);
58105

59106
constructor(private _octokitOptions?: OctokitOptions) {}
60107
}
@@ -76,7 +123,9 @@ export class AuthenticatedGithubClient extends GithubClient {
76123

77124
/** Perform a query using Github's Graphql API. */
78125
async graphql<T extends GraphqlQueryObject>(queryObject: T, params: RequestParameters = {}) {
79-
return (await this._graphql(query(queryObject).toString(), params)) as T;
126+
return invokeWithRetry(async () => {
127+
return (await this._graphql(query(queryObject).toString(), params)) as T;
128+
});
80129
}
81130
}
82131

0 commit comments

Comments
 (0)