mirror of
https://github.com/actions/github-script.git
synced 2026-04-09 07:30:05 +00:00
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:
parent
744020488d
commit
95933befc0
4 changed files with 282 additions and 2 deletions
186
__test__/create-configured-getoctokit.test.ts
Normal file
186
__test__/create-configured-getoctokit.test.ts
Normal 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
42
dist/index.js
vendored
|
|
@ -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,
|
||||
|
|
|
|||
44
src/create-configured-getoctokit.ts
Normal file
44
src/create-configured-getoctokit.ts
Normal 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
|
||||
}
|
||||
12
src/main.ts
12
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<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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue