Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions .github/local-actions/branch-manager/main.js

Large diffs are not rendered by default.

90 changes: 77 additions & 13 deletions ng-dev/utils/git/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {OctokitOptions} from '@octokit/core';
import {Octokit} from '@octokit/rest';
import {RequestParameters} from '@octokit/types';
import {RequestError} from '@octokit/request-error';
import {GraphqlResponseError} from '@octokit/graphql';
import {query} from 'typed-graphqlify';
import {Log} from '../logging';

Expand All @@ -28,6 +29,67 @@ export interface GithubRepo {
name: string;
}

/** Helper to invoke an async function with retries. */
async function invokeWithRetry<T>(fn: () => Promise<T>, retries = 3, delay = 1000): Promise<T> {
let attempt = 0;
while (attempt < retries) {
try {
return await fn();
} catch (e) {
attempt++;
if (attempt >= retries) {
throw e;
}

// Do not retry valid 4xx Client Errors (especially 404 Not Found)
if (isGithubApiError(e) && e.status < 500) {
throw e;
}

// Do not retry permanent GraphQL errors
if (e instanceof GraphqlResponseError) {
if (!e.errors) {
throw e; // Missing errors, assume permanent or unknown
}
if (
e.errors.every((err) =>
['NOT_FOUND', 'FORBIDDEN', 'BAD_USER_INPUT', 'UNAUTHENTICATED'].includes(err.type!),
)
) {
throw e;
}
}

Log.warn(`GitHub API call failed (attempt ${attempt}/${retries}). Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error('Unreachable');
}

/** Creates a proxy that intercepts function calls and applies retries. */
function createRetryProxy<T extends object>(target: T): T {
return new Proxy(target, {
get(targetObj, prop, receiver) {
const value = Reflect.get(targetObj, prop, receiver);
if (typeof value === 'function') {
return new Proxy(value, {
apply(targetFn, thisArg, argArray) {
return invokeWithRetry(() => (targetFn as Function).apply(targetObj, argArray));
},
});
}
if (typeof value === 'object' && value !== null) {
return createRetryProxy(value);
}
return value;
},
apply(targetFn, thisArg, argArray) {
return invokeWithRetry(() => (targetFn as Function).apply(thisArg, argArray));
},
});
}

/** A Github client for interacting with the Github APIs. */
export class GithubClient {
/** The octokit instance actually performing API requests. */
Expand All @@ -43,18 +105,18 @@ export class GithubClient {
...this._octokitOptions,
});

readonly pulls: Octokit['pulls'] = this._octokit.pulls;
readonly orgs: Octokit['orgs'] = this._octokit.orgs;
readonly repos: Octokit['repos'] = this._octokit.repos;
readonly issues: Octokit['issues'] = this._octokit.issues;
readonly git: Octokit['git'] = this._octokit.git;
readonly rateLimit: Octokit['rateLimit'] = this._octokit.rateLimit;
readonly teams: Octokit['teams'] = this._octokit.teams;
readonly search: Octokit['search'] = this._octokit.search;
readonly rest: Octokit['rest'] = this._octokit.rest;
readonly paginate: Octokit['paginate'] = this._octokit.paginate;
readonly checks: Octokit['checks'] = this._octokit.checks;
readonly users: Octokit['users'] = this._octokit.users;
readonly pulls: Octokit['pulls'] = createRetryProxy(this._octokit.pulls);
readonly orgs: Octokit['orgs'] = createRetryProxy(this._octokit.orgs);
readonly repos: Octokit['repos'] = createRetryProxy(this._octokit.repos);
readonly issues: Octokit['issues'] = createRetryProxy(this._octokit.issues);
readonly git: Octokit['git'] = createRetryProxy(this._octokit.git);
readonly rateLimit: Octokit['rateLimit'] = createRetryProxy(this._octokit.rateLimit);
readonly teams: Octokit['teams'] = createRetryProxy(this._octokit.teams);
readonly search: Octokit['search'] = createRetryProxy(this._octokit.search);
readonly rest: Octokit['rest'] = createRetryProxy(this._octokit.rest);
readonly paginate: Octokit['paginate'] = createRetryProxy(this._octokit.paginate);
readonly checks: Octokit['checks'] = createRetryProxy(this._octokit.checks);
readonly users: Octokit['users'] = createRetryProxy(this._octokit.users);

constructor(private _octokitOptions?: OctokitOptions) {}
}
Expand All @@ -76,7 +138,9 @@ export class AuthenticatedGithubClient extends GithubClient {

/** Perform a query using Github's Graphql API. */
async graphql<T extends GraphqlQueryObject>(queryObject: T, params: RequestParameters = {}) {
return (await this._graphql(query(queryObject).toString(), params)) as T;
return invokeWithRetry(async () => {
return (await this._graphql(query(queryObject).toString(), params)) as T;
});
}
}

Expand Down
Loading