From 19a4d08bd2121fa4e7632ecbe7a3a5f6d623cb7d Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 08:44:11 -0700 Subject: [PATCH 01/13] chore: add octokit/plugin-retry Signed-off-by: Jan Michael Ong --- plugins/scaffolder-backend-module-github/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 982f5692f4..830f5615ff 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -51,6 +51,7 @@ "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "@octokit/core": "^5.0.0", + "@octokit/plugin-retry": "^6.0.0", "@octokit/webhooks": "^10.9.2", "libsodium-wrappers": "^0.8.0", "octokit": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 8e6e449ac4..8be93f0881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6731,6 +6731,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" "@octokit/core": "npm:^5.0.0" + "@octokit/plugin-retry": "npm:^6.0.0" "@octokit/webhooks": "npm:^10.9.2" "@types/libsodium-wrappers": "npm:^0.8.0" fs-extra: "npm:^11.2.0" From e351c07daa672211ffc10d98c6d7c611717c684b Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 08:44:52 -0700 Subject: [PATCH 02/13] chore: add helper to get an octokit client * add unit tests Signed-off-by: Jan Michael Ong --- .../src/util.test.ts | 154 ++++++++++++++++++ .../src/util.ts | 81 +++++++++ 2 files changed, 235 insertions(+) create mode 100644 plugins/scaffolder-backend-module-github/src/util.test.ts diff --git a/plugins/scaffolder-backend-module-github/src/util.test.ts b/plugins/scaffolder-backend-module-github/src/util.test.ts new file mode 100644 index 0000000000..9cb3035f21 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/util.test.ts @@ -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 & { + 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, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index 260c93993f..0d50b7c797 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -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. From a2ee9609001214d72054f5d8393d0301e6868fcf Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 08:53:29 -0700 Subject: [PATCH 03/13] chore: add changeset Signed-off-by: Jan Michael Ong --- .changeset/fine-cameras-deny.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fine-cameras-deny.md diff --git a/.changeset/fine-cameras-deny.md b/.changeset/fine-cameras-deny.md new file mode 100644 index 0000000000..9ca36ba0b0 --- /dev/null +++ b/.changeset/fine-cameras-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +add helper to return an Octokit client that supports retries (via @octokit/plugin-retry) From d5e2dee26a522948c9d49cc05ccc38ca776606e4 Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 09:18:54 -0700 Subject: [PATCH 04/13] chore: integrate copilot feedback Signed-off-by: Jan Michael Ong --- .changeset/fine-cameras-deny.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fine-cameras-deny.md b/.changeset/fine-cameras-deny.md index 9ca36ba0b0..e078b3605e 100644 --- a/.changeset/fine-cameras-deny.md +++ b/.changeset/fine-cameras-deny.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-github': patch --- -add helper to return an Octokit client that supports retries (via @octokit/plugin-retry) +improve Octokit client creation to support retries via @octokit/plugin-retry From 3b460c2e22c02fc5043c0b482f040832019f4c96 Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 09:19:53 -0700 Subject: [PATCH 05/13] fix: add missing export Signed-off-by: Jan Michael Ong --- plugins/scaffolder-backend-module-github/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-github/src/index.ts b/plugins/scaffolder-backend-module-github/src/index.ts index cf0bc003cb..8b4276eb78 100644 --- a/plugins/scaffolder-backend-module-github/src/index.ts +++ b/plugins/scaffolder-backend-module-github/src/index.ts @@ -22,4 +22,4 @@ export * from './actions'; export { githubModule as default } from './module'; -export { getOctokitOptions } from './util'; +export { getOctokitClient, getOctokitOptions } from './util'; From 9032ec7f1e0cd2306ce80f84fbe7966890ce9dfb Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 09:53:01 -0700 Subject: [PATCH 06/13] chore: integrate copilot / awanlin feedback Signed-off-by: Jan Michael Ong --- .../src/util.test.ts | 16 ++++++------ .../src/util.ts | 25 ++++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/util.test.ts b/plugins/scaffolder-backend-module-github/src/util.test.ts index 9cb3035f21..9bbaceee8a 100644 --- a/plugins/scaffolder-backend-module-github/src/util.test.ts +++ b/plugins/scaffolder-backend-module-github/src/util.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 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. @@ -39,15 +39,15 @@ describe('isRetryEnabled', () => { expect(isRetryEnabled()).toBe(true); }); - it('returns true when retries is positive and retryDelay is undefined', () => { + it('returns true when retries is positive and retryAfter is undefined', () => { expect(isRetryEnabled(3)).toBe(true); }); - it('returns true when retries is undefined and retryDelay is positive', () => { + it('returns true when retries is undefined and retryAfter is positive', () => { expect(isRetryEnabled(undefined, 1000)).toBe(true); }); - it('returns true when both retries and retryDelay are positive', () => { + it('returns true when both retries and retryAfter are positive', () => { expect(isRetryEnabled(5, 2000)).toBe(true); }); @@ -55,15 +55,15 @@ describe('isRetryEnabled', () => { expect(isRetryEnabled(0)).toBe(false); }); - it('returns false when retryDelay is 0', () => { + it('returns false when retryAfter is 0', () => { expect(isRetryEnabled(undefined, 0)).toBe(false); }); - it('returns false when retries is 0 even if retryDelay is positive', () => { + it('returns false when retries is 0 even if retryAfter is positive', () => { expect(isRetryEnabled(0, 1000)).toBe(false); }); - it('returns false when retryDelay is 0 even if retries is positive', () => { + it('returns false when retryAfter is 0 even if retries is positive', () => { expect(isRetryEnabled(3, 0)).toBe(false); }); }); @@ -92,7 +92,7 @@ describe('getOctokitClient', () => { expect(client).toBe(mockOctokitInstance); }); - it('returns a plain Octokit client when retryDelay is 0', () => { + it('returns a plain Octokit client when retryAfter is 0', () => { const client = getOctokitClient(octokitOptions, logger, 3, 0); expect(MockOctokit.plugin).not.toHaveBeenCalled(); diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index 0d50b7c797..aa0bea521d 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -38,21 +38,22 @@ const DEFAULT_RETRY_DELAY_MS = 1000; * options. * * Retries are enabled by default, but can be disabled by setting either `retries` - * or `retryDelay` to 0. + * or `retryAfter` to 0. + * @public * * @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. + * @param retryAfter - 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 { +export function isRetryEnabled(retries?: number, retryAfter?: number): boolean { if (retries === 0) { return false; } - if (retryDelay === 0) { + if (retryAfter === 0) { return false; } @@ -65,7 +66,7 @@ export function isRetryEnabled(retries?: number, retryDelay?: number): boolean { * * 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. + * To disable retries, set either `retries` or `retryAfter` to 0 in the options. * * @param octokitOptions - The options for configuring the Octokit client. * Generally provided by the `getOctokitOptions` helper. @@ -73,7 +74,7 @@ export function isRetryEnabled(retries?: number, retryDelay?: number): boolean { * 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. + * @param retryAfter - 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. @@ -82,11 +83,11 @@ export function getOctokitClient( octokitOptions: OctokitOptions, logger: LoggerService, retries: number = DEFAULT_RETRY_ATTEMPTS, - retryDelay: number = DEFAULT_RETRY_DELAY_MS, + retryAfter: 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)) { + // explicitly setting retries or retryAfter to 0 + if (!isRetryEnabled(retries, retryAfter)) { return new Octokit({ ...octokitOptions, log: logger, @@ -94,13 +95,13 @@ export function getOctokitClient( } // Update the octokit options to include retry configuration with logging - const MyOctokit = Octokit.plugin(retry); - return new MyOctokit({ + const OctokitClient = Octokit.plugin(retry); + return new OctokitClient({ ...octokitOptions, request: { ...octokitOptions.request, retries, - retryAfter: retryDelay, + retryAfter, }, log: logger, }); From ad3e56f4acda46f06ab836f5efe88d84495958b8 Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 10:02:24 -0700 Subject: [PATCH 07/13] chore: fix incorrect placement Signed-off-by: Jan Michael Ong --- plugins/scaffolder-backend-module-github/src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index aa0bea521d..6b9769e675 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -39,7 +39,6 @@ const DEFAULT_RETRY_DELAY_MS = 1000; * * Retries are enabled by default, but can be disabled by setting either `retries` * or `retryAfter` to 0. - * @public * * @param retries - The number of retry attempts for failed requests. Default is 3. * Setting to 0 will disable retries. @@ -67,6 +66,7 @@ export function isRetryEnabled(retries?: number, retryAfter?: number): boolean { * 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 `retryAfter` to 0 in the options. + * @public * * @param octokitOptions - The options for configuring the Octokit client. * Generally provided by the `getOctokitOptions` helper. From 5a048ed0425e95bdecd49b80de79d4ebbedbb8b8 Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 11:13:51 -0700 Subject: [PATCH 08/13] chore: integrate copilot suggestion Signed-off-by: Jan Michael Ong --- .changeset/fine-cameras-deny.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fine-cameras-deny.md b/.changeset/fine-cameras-deny.md index e078b3605e..f5493c2fa3 100644 --- a/.changeset/fine-cameras-deny.md +++ b/.changeset/fine-cameras-deny.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-github': patch --- -improve Octokit client creation to support retries via @octokit/plugin-retry +Improved Octokit client creation to support retries via @octokit/plugin-retry From 547b91de035413a7bb1d0a3b5aeb7fa23973e67e Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 11:14:47 -0700 Subject: [PATCH 09/13] chore: switch retryOptions to an options object Signed-off-by: Jan Michael Ong --- .../src/util.test.ts | 77 ++++++++++--------- .../src/util.ts | 51 +++++++----- 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/util.test.ts b/plugins/scaffolder-backend-module-github/src/util.test.ts index 9bbaceee8a..ed215b87d0 100644 --- a/plugins/scaffolder-backend-module-github/src/util.test.ts +++ b/plugins/scaffolder-backend-module-github/src/util.test.ts @@ -35,36 +35,39 @@ const MockOctokit = Octokit as unknown as jest.MockedClass & { }; describe('isRetryEnabled', () => { - it('returns true when both params are undefined', () => { - expect(isRetryEnabled()).toBe(true); - }); - - it('returns true when retries is positive and retryAfter is undefined', () => { - expect(isRetryEnabled(3)).toBe(true); - }); - - it('returns true when retries is undefined and retryAfter is positive', () => { - expect(isRetryEnabled(undefined, 1000)).toBe(true); - }); - - it('returns true when both retries and retryAfter are positive', () => { - expect(isRetryEnabled(5, 2000)).toBe(true); - }); - - it('returns false when retries is 0', () => { - expect(isRetryEnabled(0)).toBe(false); - }); - - it('returns false when retryAfter is 0', () => { - expect(isRetryEnabled(undefined, 0)).toBe(false); - }); - - it('returns false when retries is 0 even if retryAfter is positive', () => { - expect(isRetryEnabled(0, 1000)).toBe(false); - }); - - it('returns false when retryAfter is 0 even if retries is positive', () => { - expect(isRetryEnabled(3, 0)).toBe(false); + it.each([ + { + description: 'returns false when retries is == 0', + options: { retries: 0 }, + expected: false, + }, + { + description: 'returns false when retries is <= 0', + options: { retries: -10 }, + expected: false, + }, + { + description: 'returns false when retryAfter is == 0', + options: { retries: 1, retryAfter: 0 }, + expected: false, + }, + { + description: 'returns false when retryAfter is <= 0', + options: { retries: 1, retryAfter: -10 }, + expected: false, + }, + { + description: 'returns true when retryOptions is undefined', + options: undefined, + expected: true, + }, + { + description: 'returns true when retries is > 0 and retryAfter is > 0', + options: { retries: 5, retryAfter: 5 }, + expected: true, + }, + ])('$description', ({ options, expected }) => { + expect(isRetryEnabled(options)).toBe(expected); }); }); @@ -82,7 +85,8 @@ describe('getOctokitClient', () => { }); it('returns a plain Octokit client when retries is 0', () => { - const client = getOctokitClient(octokitOptions, logger, 0); + const retryOptions = { retries: 0 }; + const client = getOctokitClient(octokitOptions, logger, retryOptions); expect(MockOctokit.plugin).not.toHaveBeenCalled(); expect(MockOctokit).toHaveBeenCalledWith({ @@ -93,7 +97,8 @@ describe('getOctokitClient', () => { }); it('returns a plain Octokit client when retryAfter is 0', () => { - const client = getOctokitClient(octokitOptions, logger, 3, 0); + const retryOptions = { retries: 3, retryAfter: 0 }; + const client = getOctokitClient(octokitOptions, logger, retryOptions); expect(MockOctokit.plugin).not.toHaveBeenCalled(); expect(MockOctokit).toHaveBeenCalledWith({ @@ -103,7 +108,7 @@ describe('getOctokitClient', () => { expect(client).toBe(mockOctokitInstance); }); - it('returns a retry-enabled Octokit client with default retries and delay', () => { + it('returns a retry-enabled Octokit client with default retries and delay when retryOptions is undefined', () => { const client = getOctokitClient(octokitOptions, logger); expect(MockOctokit.plugin).toHaveBeenCalledWith(retry); @@ -119,7 +124,8 @@ describe('getOctokitClient', () => { }); it('returns a retry-enabled Octokit client with custom retries and delay', () => { - const client = getOctokitClient(octokitOptions, logger, 5, 2000); + const retryOptions = { retries: 5, retryAfter: 2000 }; + const client = getOctokitClient(octokitOptions, logger, retryOptions); expect(MockOctokit.plugin).toHaveBeenCalledWith(retry); expect(MockOctokit).toHaveBeenCalledWith({ @@ -139,7 +145,8 @@ describe('getOctokitClient', () => { request: { timeout: 60_000 }, }; - getOctokitClient(optionsWithRequest, logger, 3, 1000); + const retryOptions = { retries: 3, retryAfter: 1000 }; + getOctokitClient(optionsWithRequest, logger, retryOptions); expect(MockOctokit).toHaveBeenCalledWith({ ...optionsWithRequest, diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index 6b9769e675..e4965bff5b 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -33,26 +33,33 @@ const DEFAULT_TIMEOUT_MS = 60_000; const DEFAULT_RETRY_ATTEMPTS = 3; const DEFAULT_RETRY_DELAY_MS = 1000; +type RetryOptions = { + // The number of retry attempts for failed requests + retries?: number; + // The delay in milliseconds between retry attempts + retryAfter?: number; +}; + /** * 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 `retryAfter` to 0. + * @param retryOptions - Optional retry configuration options, including the number + * of retries and the delay between retries. * - * @param retries - The number of retry attempts for failed requests. Default is 3. - * Setting to 0 will disable retries. - * @param retryAfter - 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. + * @returns False if retries/retryAfter are explicitly set to 0 or less. + * Returns true otherwise. */ -export function isRetryEnabled(retries?: number, retryAfter?: number): boolean { - if (retries === 0) { +export function isRetryEnabled(retryOptions?: RetryOptions): boolean { + if (retryOptions === undefined) { + return true; + } + + if (retryOptions.retries !== undefined && retryOptions.retries <= 0) { return false; } - if (retryAfter === 0) { + if (retryOptions.retryAfter !== undefined && retryOptions.retryAfter <= 0) { return false; } @@ -72,22 +79,20 @@ export function isRetryEnabled(retries?: number, retryAfter?: number): boolean { * 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 retryAfter - The delay in milliseconds between retry attempts. - * Default is 1000ms. Setting to 0 will disable retries. + * @param retryOptions - Optional retry configuration options, including the + * number of retries and the delay between retries. + * * @returns An authenticated Octokit client instance based on the provided * options. */ export function getOctokitClient( octokitOptions: OctokitOptions, logger: LoggerService, - retries: number = DEFAULT_RETRY_ATTEMPTS, - retryAfter: number = DEFAULT_RETRY_DELAY_MS, + retryOptions?: RetryOptions, ): Octokit { // Default behavior is to enable retries, but allow callers to disable by // explicitly setting retries or retryAfter to 0 - if (!isRetryEnabled(retries, retryAfter)) { + if (!isRetryEnabled(retryOptions)) { return new Octokit({ ...octokitOptions, log: logger, @@ -96,6 +101,16 @@ export function getOctokitClient( // Update the octokit options to include retry configuration with logging const OctokitClient = Octokit.plugin(retry); + + // From the octokit/plugin-retry documentation, specifying these values will + // always retry regardless of the response code + const retries = retryOptions?.retries + ? retryOptions?.retries + : DEFAULT_RETRY_ATTEMPTS; + const retryAfter = retryOptions?.retryAfter + ? retryOptions?.retryAfter + : DEFAULT_RETRY_DELAY_MS; + return new OctokitClient({ ...octokitOptions, request: { From a50f4ec8189e78181a543a5319c5bb70e3993bbd Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 11:35:23 -0700 Subject: [PATCH 10/13] chore: incorporate additional copilot suggestions Signed-off-by: Jan Michael Ong --- plugins/scaffolder-backend-module-github/src/index.ts | 2 +- plugins/scaffolder-backend-module-github/src/util.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/index.ts b/plugins/scaffolder-backend-module-github/src/index.ts index 8b4276eb78..27e6ae60a5 100644 --- a/plugins/scaffolder-backend-module-github/src/index.ts +++ b/plugins/scaffolder-backend-module-github/src/index.ts @@ -22,4 +22,4 @@ export * from './actions'; export { githubModule as default } from './module'; -export { getOctokitClient, getOctokitOptions } from './util'; +export { getOctokitClient, getOctokitOptions, type RetryOptions } from './util'; diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index e4965bff5b..f9cd695b83 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -31,9 +31,9 @@ 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; +const DEFAULT_RETRY_DELAY_MS = 1_000; -type RetryOptions = { +export type RetryOptions = { // The number of retry attempts for failed requests retries?: number; // The delay in milliseconds between retry attempts From e72787948cf6fdc4b6aef5d629678d01d70a3431 Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 12:01:06 -0700 Subject: [PATCH 11/13] chore: fix Warning: (ae-forgotten-export) Signed-off-by: Jan Michael Ong --- .../report.api.md | 18 ++++++++++++++++-- .../src/util.ts | 4 ++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 9e930a7866..84e71c712a 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -8,6 +8,7 @@ import { CatalogService } from '@backstage/plugin-catalog-node'; import { Config } from '@backstage/config'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { GithubCredentialsProvider } from '@backstage/integration'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Octokit } from 'octokit'; import { OctokitOptions } from '@octokit/core/dist-types/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -253,8 +254,8 @@ export function createGithubRepoCreateAction(options: { access: string; } | { - team: string; access: string; + team: string; } )[] | undefined; @@ -444,8 +445,8 @@ export function createPublishGithubAction(options: { access: string; } | { - team: string; access: string; + team: string; } )[] | undefined; @@ -509,6 +510,13 @@ export const createPublishGithubPullRequestAction: ( 'v2' >; +// @public +export function getOctokitClient( + octokitOptions: OctokitOptions, + logger: LoggerService, + retryOptions?: RetryOptions, +): Octokit; + // @public export function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; @@ -530,4 +538,10 @@ export function getOctokitOptions(options: { // @public const githubModule: BackendFeature; export default githubModule; + +// @public +export type RetryOptions = { + retries?: number; + retryAfter?: number; +}; ``` diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index f9cd695b83..5e537582ca 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -33,6 +33,10 @@ const DEFAULT_TIMEOUT_MS = 60_000; const DEFAULT_RETRY_ATTEMPTS = 3; const DEFAULT_RETRY_DELAY_MS = 1_000; +/** + * Options used to override the default retry behavior of @octokit/plugin-retry. + * @public + */ export type RetryOptions = { // The number of retry attempts for failed requests retries?: number; From f38885379116558f73ae600ad12b8b28a0f1eebd Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 12:14:41 -0700 Subject: [PATCH 12/13] chore: fix tsdoc-characters-after-block-tag warning Signed-off-by: Jan Michael Ong --- plugins/scaffolder-backend-module-github/src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index 5e537582ca..09a8b0b6dd 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -34,7 +34,7 @@ const DEFAULT_RETRY_ATTEMPTS = 3; const DEFAULT_RETRY_DELAY_MS = 1_000; /** - * Options used to override the default retry behavior of @octokit/plugin-retry. + * Options used to override plugin-retry defaults * @public */ export type RetryOptions = { From 8d60c70e5d1057467d0ff3becb07a1c3c42f5d86 Mon Sep 17 00:00:00 2001 From: Jan Michael Ong Date: Wed, 22 Apr 2026 12:38:37 -0700 Subject: [PATCH 13/13] chore: fix uncommitted changes to the public API or reports of a package error Signed-off-by: Jan Michael Ong --- plugins/scaffolder-backend-module-github/report.api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 84e71c712a..76faa3582c 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -254,8 +254,8 @@ export function createGithubRepoCreateAction(options: { access: string; } | { - access: string; team: string; + access: string; } )[] | undefined; @@ -445,8 +445,8 @@ export function createPublishGithubAction(options: { access: string; } | { - access: string; team: string; + access: string; } )[] | undefined;