chore: add helper to get an octokit client

* add unit tests

Signed-off-by: Jan Michael Ong <adobejmong@gmail.com>
This commit is contained in:
Jan Michael Ong
2026-04-22 08:44:52 -07:00
parent 19a4d08bd2
commit e351c07daa
2 changed files with 235 additions and 0 deletions
@@ -0,0 +1,154 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Octokit } from 'octokit';
import { retry } from '@octokit/plugin-retry';
import { mockServices } from '@backstage/backend-test-utils';
import { isRetryEnabled, getOctokitClient } from './util';
jest.mock('octokit', () => ({
Octokit: Object.assign(jest.fn(), {
plugin: jest.fn(),
}),
}));
jest.mock('@octokit/plugin-retry', () => ({
retry: jest.fn(),
}));
const mockOctokitInstance = { request: jest.fn() };
const MockOctokit = Octokit as unknown as jest.MockedClass<typeof Octokit> & {
plugin: jest.Mock;
};
describe('isRetryEnabled', () => {
it('returns true when both params are undefined', () => {
expect(isRetryEnabled()).toBe(true);
});
it('returns true when retries is positive and retryDelay is undefined', () => {
expect(isRetryEnabled(3)).toBe(true);
});
it('returns true when retries is undefined and retryDelay is positive', () => {
expect(isRetryEnabled(undefined, 1000)).toBe(true);
});
it('returns true when both retries and retryDelay are positive', () => {
expect(isRetryEnabled(5, 2000)).toBe(true);
});
it('returns false when retries is 0', () => {
expect(isRetryEnabled(0)).toBe(false);
});
it('returns false when retryDelay is 0', () => {
expect(isRetryEnabled(undefined, 0)).toBe(false);
});
it('returns false when retries is 0 even if retryDelay is positive', () => {
expect(isRetryEnabled(0, 1000)).toBe(false);
});
it('returns false when retryDelay is 0 even if retries is positive', () => {
expect(isRetryEnabled(3, 0)).toBe(false);
});
});
describe('getOctokitClient', () => {
const logger = mockServices.logger.mock();
const octokitOptions = {
auth: 'test-token',
baseUrl: 'https://api.github.com',
};
beforeEach(() => {
jest.clearAllMocks();
MockOctokit.mockReturnValue(mockOctokitInstance as any);
MockOctokit.plugin.mockReturnValue(MockOctokit);
});
it('returns a plain Octokit client when retries is 0', () => {
const client = getOctokitClient(octokitOptions, logger, 0);
expect(MockOctokit.plugin).not.toHaveBeenCalled();
expect(MockOctokit).toHaveBeenCalledWith({
...octokitOptions,
log: logger,
});
expect(client).toBe(mockOctokitInstance);
});
it('returns a plain Octokit client when retryDelay is 0', () => {
const client = getOctokitClient(octokitOptions, logger, 3, 0);
expect(MockOctokit.plugin).not.toHaveBeenCalled();
expect(MockOctokit).toHaveBeenCalledWith({
...octokitOptions,
log: logger,
});
expect(client).toBe(mockOctokitInstance);
});
it('returns a retry-enabled Octokit client with default retries and delay', () => {
const client = getOctokitClient(octokitOptions, logger);
expect(MockOctokit.plugin).toHaveBeenCalledWith(retry);
expect(MockOctokit).toHaveBeenCalledWith({
...octokitOptions,
request: {
retries: 3,
retryAfter: 1000,
},
log: logger,
});
expect(client).toBe(mockOctokitInstance);
});
it('returns a retry-enabled Octokit client with custom retries and delay', () => {
const client = getOctokitClient(octokitOptions, logger, 5, 2000);
expect(MockOctokit.plugin).toHaveBeenCalledWith(retry);
expect(MockOctokit).toHaveBeenCalledWith({
...octokitOptions,
request: {
retries: 5,
retryAfter: 2000,
},
log: logger,
});
expect(client).toBe(mockOctokitInstance);
});
it('merges existing request options when building retry client', () => {
const optionsWithRequest = {
...octokitOptions,
request: { timeout: 60_000 },
};
getOctokitClient(optionsWithRequest, logger, 3, 1000);
expect(MockOctokit).toHaveBeenCalledWith({
...optionsWithRequest,
request: {
timeout: 60_000,
retries: 3,
retryAfter: 1000,
},
log: logger,
});
});
});
@@ -22,9 +22,90 @@ import {
} from '@backstage/integration';
import { parseRepoUrl } from '@backstage/plugin-scaffolder-node';
import { OctokitOptions } from '@octokit/core/dist-types/types';
import { Octokit } from 'octokit';
import { retry } from '@octokit/plugin-retry';
import { LoggerService } from '@backstage/backend-plugin-api';
const DEFAULT_TIMEOUT_MS = 60_000;
// By default, octokit/plugin-retry will retry 3 times with a 1 second delay
// between retries.
const DEFAULT_RETRY_ATTEMPTS = 3;
const DEFAULT_RETRY_DELAY_MS = 1000;
/**
* Helper function to determine if retries are enabled based on the provided
* options.
*
* Retries are enabled by default, but can be disabled by setting either `retries`
* or `retryDelay` to 0.
*
* @param retries - The number of retry attempts for failed requests. Default is 3.
* Setting to 0 will disable retries.
* @param retryDelay - The delay in milliseconds between retry attempts.
* Default is 1000ms. Setting to 0 will disable retries.
*
* @returns A boolean indicating whether retries are enabled or not.
*/
export function isRetryEnabled(retries?: number, retryDelay?: number): boolean {
if (retries === 0) {
return false;
}
if (retryDelay === 0) {
return false;
}
return true;
}
/**
* Helper for generating an authenticated Octokit client with (or without)
* retry capabilities.
*
* If retries are enabled (default), the client will retry failed requests up
* to the specified number of retries and delay.
* To disable retries, set either `retries` or `retryDelay` to 0 in the options.
*
* @param octokitOptions - The options for configuring the Octokit client.
* Generally provided by the `getOctokitOptions` helper.
* @param logger - LoggerService instance for logging retry attempts and
* failures.
* @param retries - The number of retry attempts for failed requests.
* Default is 3. Setting to 0 will disable retries.
* @param retryDelay - The delay in milliseconds between retry attempts.
* Default is 1000ms. Setting to 0 will disable retries.
* @returns An authenticated Octokit client instance based on the provided
* options.
*/
export function getOctokitClient(
octokitOptions: OctokitOptions,
logger: LoggerService,
retries: number = DEFAULT_RETRY_ATTEMPTS,
retryDelay: number = DEFAULT_RETRY_DELAY_MS,
): Octokit {
// Default behavior is to enable retries, but allow callers to disable by
// explicitly setting retries or retryDelay to 0
if (!isRetryEnabled(retries, retryDelay)) {
return new Octokit({
...octokitOptions,
log: logger,
});
}
// Update the octokit options to include retry configuration with logging
const MyOctokit = Octokit.plugin(retry);
return new MyOctokit({
...octokitOptions,
request: {
...octokitOptions.request,
retries,
retryAfter: retryDelay,
},
log: logger,
});
}
/**
* Helper for generating octokit configuration options.
* If no token is provided, it will attempt to get a token from the credentials provider.