mirror of
https://github.com/actions/github-script.git
synced 2026-04-09 15:40:08 +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));
|
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
|
;// CONCATENATED MODULE: ./src/retry-options.ts
|
||||||
|
|
||||||
function getRetryOptions(retries, exemptStatusCodes, defaultOptions) {
|
function getRetryOptions(retries, exemptStatusCodes, defaultOptions) {
|
||||||
|
|
@ -36256,6 +36292,7 @@ const wrapRequire = new Proxy(require, {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
process.on('unhandledRejection', handleError);
|
process.on('unhandledRejection', handleError);
|
||||||
main().catch(handleError);
|
main().catch(handleError);
|
||||||
async function main() {
|
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 github = (0,lib_github.getOctokit)(token, opts, plugin_retry_dist_node.retry, dist_node.requestLog);
|
||||||
const script = core.getInput('script', { required: true });
|
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.
|
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
|
||||||
const result = await callAsyncFunction({
|
const result = await callAsyncFunction({
|
||||||
require: wrapRequire,
|
require: wrapRequire,
|
||||||
__original_require__: require,
|
__original_require__: require,
|
||||||
github,
|
github,
|
||||||
octokit: github,
|
octokit: github,
|
||||||
getOctokit: lib_github.getOctokit,
|
getOctokit: configuredGetOctokit,
|
||||||
context: lib_github.context,
|
context: lib_github.context,
|
||||||
core: core,
|
core: core,
|
||||||
exec: exec,
|
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 {retry} from '@octokit/plugin-retry'
|
||||||
import {RequestRequestOptions} from '@octokit/types'
|
import {RequestRequestOptions} from '@octokit/types'
|
||||||
import {callAsyncFunction} from './async-function'
|
import {callAsyncFunction} from './async-function'
|
||||||
|
import {createConfiguredGetOctokit} from './create-configured-getoctokit'
|
||||||
import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options'
|
import {RetryOptions, getRetryOptions, parseNumberArray} from './retry-options'
|
||||||
import {wrapRequire} from './wrap-require'
|
import {wrapRequire} from './wrap-require'
|
||||||
|
|
||||||
|
|
@ -59,6 +60,15 @@ async function main(): Promise<void> {
|
||||||
const github = getOctokit(token, opts, retry, requestLog)
|
const github = getOctokit(token, opts, retry, requestLog)
|
||||||
const script = core.getInput('script', {required: true})
|
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.
|
// Using property/value shorthand on `require` (e.g. `{require}`) causes compilation errors.
|
||||||
const result = await callAsyncFunction(
|
const result = await callAsyncFunction(
|
||||||
{
|
{
|
||||||
|
|
@ -66,7 +76,7 @@ async function main(): Promise<void> {
|
||||||
__original_require__: __non_webpack_require__,
|
__original_require__: __non_webpack_require__,
|
||||||
github,
|
github,
|
||||||
octokit: github,
|
octokit: github,
|
||||||
getOctokit,
|
getOctokit: configuredGetOctokit,
|
||||||
context,
|
context,
|
||||||
core,
|
core,
|
||||||
exec,
|
exec,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue