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.
This commit is contained in:
Salman Chishti 2026-04-08 20:41:28 +00:00 committed by GitHub
parent 744020488d
commit 95933befc0
4 changed files with 282 additions and 2 deletions

View file

@ -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)
})
})

42
dist/index.js vendored
View file

@ -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,

View file

@ -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<string, unknown>,
// 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<string, unknown> | undefined) ?? {}
const userRequest =
(userOpts.request as Record<string, unknown> | 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
}

View file

@ -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<void> {
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<void> {
__original_require__: __non_webpack_require__,
github,
octokit: github,
getOctokit,
getOctokit: configuredGetOctokit,
context,
core,
exec,