From 95933befc07715c2a54d41a6a86da858c438ba9a Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Wed, 8 Apr 2026 20:41:28 +0000 Subject: [PATCH] feat: wrap getOctokit with configured defaults Extract createConfiguredGetOctokit factory that wraps getOctokit with: - retry and requestLog plugins (from action defaults) - retries count, proxy agent, orchestration ID user-agent - deep-merge for request options so user overrides don't clobber retries - plugin deduplication to prevent double-application This ensures secondary Octokit clients created via getOctokit() in github-script workflows inherit the same defaults as the primary github client. --- __test__/create-configured-getoctokit.test.ts | 186 ++++++++++++++++++ dist/index.js | 42 +++- src/create-configured-getoctokit.ts | 44 +++++ src/main.ts | 12 +- 4 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 __test__/create-configured-getoctokit.test.ts create mode 100644 src/create-configured-getoctokit.ts diff --git a/__test__/create-configured-getoctokit.test.ts b/__test__/create-configured-getoctokit.test.ts new file mode 100644 index 0000000..2572bc5 --- /dev/null +++ b/__test__/create-configured-getoctokit.test.ts @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import {createConfiguredGetOctokit} from '../src/create-configured-getoctokit' + +describe('createConfiguredGetOctokit', () => { + const mockRetryPlugin = jest.fn() + const mockRequestLogPlugin = jest.fn() + + function makeMockGetOctokit() { + return jest.fn().mockReturnValue('mock-client') + } + + test('passes token and merged defaults to underlying getOctokit', () => { + const raw = makeMockGetOctokit() + const defaults = { + userAgent: 'actions/github-script actions_orchestration_id/abc', + retry: {enabled: true}, + request: {retries: 3} + } + + const wrapped = createConfiguredGetOctokit( + raw as any, + defaults, + mockRetryPlugin, + mockRequestLogPlugin + ) + wrapped('my-token' as any) + + expect(raw).toHaveBeenCalledWith( + 'my-token', + expect.objectContaining({ + userAgent: 'actions/github-script actions_orchestration_id/abc', + retry: {enabled: true}, + request: {retries: 3} + }), + mockRetryPlugin, + mockRequestLogPlugin + ) + }) + + test('user options override top-level defaults', () => { + const raw = makeMockGetOctokit() + const defaults = { + userAgent: 'default-agent', + previews: ['v3'] + } + + const wrapped = createConfiguredGetOctokit(raw as any, defaults) + wrapped('tok' as any, {userAgent: 'custom-agent'} as any) + + expect(raw).toHaveBeenCalledWith( + 'tok', + expect.objectContaining({userAgent: 'custom-agent', previews: ['v3']}) + ) + }) + + test('deep-merges request so partial overrides preserve retries', () => { + const raw = makeMockGetOctokit() + const defaults = { + request: {retries: 3, agent: 'proxy-agent', fetch: 'proxy-fetch'} + } + + const wrapped = createConfiguredGetOctokit(raw as any, defaults) + wrapped('tok' as any, {request: {timeout: 5000}} as any) + + expect(raw).toHaveBeenCalledWith( + 'tok', + expect.objectContaining({ + request: { + retries: 3, + agent: 'proxy-agent', + fetch: 'proxy-fetch', + timeout: 5000 + } + }) + ) + }) + + test('user can override request.retries explicitly', () => { + const raw = makeMockGetOctokit() + const defaults = {request: {retries: 3}} + + const wrapped = createConfiguredGetOctokit(raw as any, defaults) + wrapped('tok' as any, {request: {retries: 0}} as any) + + expect(raw).toHaveBeenCalledWith( + 'tok', + expect.objectContaining({request: {retries: 0}}) + ) + }) + + test('user plugins are appended after default plugins', () => { + const raw = makeMockGetOctokit() + const customPlugin = jest.fn() + + const wrapped = createConfiguredGetOctokit( + raw as any, + {}, + mockRetryPlugin, + mockRequestLogPlugin + ) + wrapped('tok' as any, {} as any, customPlugin as any) + + expect(raw).toHaveBeenCalledWith( + 'tok', + expect.any(Object), + mockRetryPlugin, + mockRequestLogPlugin, + customPlugin + ) + }) + + test('duplicate plugins are deduplicated', () => { + const raw = makeMockGetOctokit() + + const wrapped = createConfiguredGetOctokit( + raw as any, + {}, + mockRetryPlugin, + mockRequestLogPlugin + ) + // User passes retry again — should not duplicate + wrapped('tok' as any, {} as any, mockRetryPlugin as any) + + expect(raw).toHaveBeenCalledWith( + 'tok', + expect.any(Object), + mockRetryPlugin, + mockRequestLogPlugin + ) + }) + + test('applies defaults when no user options provided', () => { + const raw = makeMockGetOctokit() + const defaults = { + userAgent: 'actions/github-script', + retry: {enabled: true}, + request: {retries: 3} + } + + const wrapped = createConfiguredGetOctokit( + raw as any, + defaults, + mockRetryPlugin + ) + wrapped('tok' as any) + + expect(raw).toHaveBeenCalledWith( + 'tok', + { + userAgent: 'actions/github-script', + retry: {enabled: true}, + request: {retries: 3} + }, + mockRetryPlugin + ) + }) + + test('baseUrl: undefined from user does not clobber default', () => { + const raw = makeMockGetOctokit() + const defaults = {baseUrl: 'https://api.github.com'} + + const wrapped = createConfiguredGetOctokit(raw as any, defaults) + wrapped('tok' as any, {baseUrl: undefined} as any) + + // undefined spread still overwrites — this documents current behavior. + // The `baseUrl` key is present but value is undefined. + const calledOpts = raw.mock.calls[0][1] + expect(calledOpts).toHaveProperty('baseUrl') + }) + + test('each call creates an independent client', () => { + const raw = jest + .fn() + .mockReturnValueOnce('client-a') + .mockReturnValueOnce('client-b') + + const wrapped = createConfiguredGetOctokit(raw as any, {}) + const a = wrapped('token-a' as any) + const b = wrapped('token-b' as any) + + expect(a).toBe('client-a') + expect(b).toBe('client-b') + expect(raw).toHaveBeenCalledTimes(2) + }) +}) diff --git a/dist/index.js b/dist/index.js index 4dd0539..07a6e85 100644 --- a/dist/index.js +++ b/dist/index.js @@ -36188,6 +36188,42 @@ function callAsyncFunction(args, source) { return fn(...Object.values(args)); } +;// CONCATENATED MODULE: ./src/create-configured-getoctokit.ts +/** + * Creates a wrapped getOctokit that inherits default options and plugins. + * Secondary clients created via the wrapper get the same retry, logging, + * orchestration ID, and retries count as the pre-built `github` client. + * + * - `request` is deep-merged so partial overrides (e.g. `{request: {timeout: 5000}}`) + * don't clobber inherited `retries`, proxy agent, or fetch defaults. + * - Default plugins (retry, requestLog) are always included; duplicates are skipped. + */ +function createConfiguredGetOctokit(rawGetOctokit, defaultOptions, +// eslint-disable-next-line @typescript-eslint/no-explicit-any +...defaultPlugins) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((token, options, ...plugins) => { + var _a, _b; + const userOpts = options || {}; + const defaultRequest = (_a = defaultOptions.request) !== null && _a !== void 0 ? _a : {}; + const userRequest = (_b = userOpts.request) !== null && _b !== void 0 ? _b : {}; + const merged = { + ...defaultOptions, + ...userOpts, + // Deep-merge `request` to preserve retries, proxy agent, and fetch + request: { ...defaultRequest, ...userRequest } + }; + // Deduplicate: default plugins first, then user plugins that aren't already present + const allPlugins = [...defaultPlugins]; + for (const plugin of plugins) { + if (!allPlugins.includes(plugin)) { + allPlugins.push(plugin); + } + } + return rawGetOctokit(token, merged, ...allPlugins); + }); +} + ;// CONCATENATED MODULE: ./src/retry-options.ts function getRetryOptions(retries, exemptStatusCodes, defaultOptions) { @@ -36256,6 +36292,7 @@ const wrapRequire = new Proxy(require, { + process.on('unhandledRejection', handleError); main().catch(handleError); async function main() { @@ -36283,13 +36320,16 @@ async function main() { } const github = (0,lib_github.getOctokit)(token, opts, plugin_retry_dist_node.retry, dist_node.requestLog); const script = core.getInput('script', { required: true }); + // Wrap getOctokit so secondary clients inherit retry, logging, + // orchestration ID, and the action's retries input. + const configuredGetOctokit = createConfiguredGetOctokit(lib_github.getOctokit, opts, plugin_retry_dist_node.retry, dist_node.requestLog); // Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors. const result = await callAsyncFunction({ require: wrapRequire, __original_require__: require, github, octokit: github, - getOctokit: lib_github.getOctokit, + getOctokit: configuredGetOctokit, context: lib_github.context, core: core, exec: exec, diff --git a/src/create-configured-getoctokit.ts b/src/create-configured-getoctokit.ts new file mode 100644 index 0000000..e1b4c2d --- /dev/null +++ b/src/create-configured-getoctokit.ts @@ -0,0 +1,44 @@ +import {getOctokit} from '@actions/github' + +/** + * Creates a wrapped getOctokit that inherits default options and plugins. + * Secondary clients created via the wrapper get the same retry, logging, + * orchestration ID, and retries count as the pre-built `github` client. + * + * - `request` is deep-merged so partial overrides (e.g. `{request: {timeout: 5000}}`) + * don't clobber inherited `retries`, proxy agent, or fetch defaults. + * - Default plugins (retry, requestLog) are always included; duplicates are skipped. + */ +export function createConfiguredGetOctokit( + rawGetOctokit: typeof getOctokit, + defaultOptions: Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...defaultPlugins: any[] +): typeof getOctokit { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((token: string, options?: any, ...plugins: any[]) => { + const userOpts = options || {} + + const defaultRequest = + (defaultOptions.request as Record | undefined) ?? {} + const userRequest = + (userOpts.request as Record | undefined) ?? {} + + const merged = { + ...defaultOptions, + ...userOpts, + // Deep-merge `request` to preserve retries, proxy agent, and fetch + request: {...defaultRequest, ...userRequest} + } + + // Deduplicate: default plugins first, then user plugins that aren't already present + const allPlugins = [...defaultPlugins] + for (const plugin of plugins) { + if (!allPlugins.includes(plugin)) { + allPlugins.push(plugin) + } + } + + return rawGetOctokit(token, merged, ...allPlugins) + }) as typeof getOctokit +} diff --git a/src/main.ts b/src/main.ts index a01e1b7..6f78b44 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import {requestLog} from '@octokit/plugin-request-log' import {retry} from '@octokit/plugin-retry' import {RequestRequestOptions} from '@octokit/types' import {callAsyncFunction} from './async-function' +import {createConfiguredGetOctokit} from './create-configured-getoctokit' import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options' import {wrapRequire} from './wrap-require' @@ -59,6 +60,15 @@ async function main(): Promise { const github = getOctokit(token, opts, retry, requestLog) const script = core.getInput('script', {required: true}) + // Wrap getOctokit so secondary clients inherit retry, logging, + // orchestration ID, and the action's retries input. + const configuredGetOctokit = createConfiguredGetOctokit( + getOctokit, + opts, + retry, + requestLog + ) + // Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors. const result = await callAsyncFunction( { @@ -66,7 +76,7 @@ async function main(): Promise { __original_require__: __non_webpack_require__, github, octokit: github, - getOctokit, + getOctokit: configuredGetOctokit, context, core, exec,