mirror of
https://github.com/actions/github-script.git
synced 2026-04-09 07:30:05 +00:00
fix: rename binding to createOctokit and harden option merging
- Rename context binding from getOctokit to createOctokit to avoid
SyntaxError when users write const { getOctokit } = require(...)
in their scripts (~10 public workflows affected)
- Strip undefined values from user options to prevent clobbering
defaults (e.g. GHES baseUrl)
- Deep-merge retry options alongside request options
- Use nullish coalescing (??) instead of logical OR (||)
- Shallow-copy opts to prevent shared reference mutation
- Add tests: undefined stripping, retry merge, falsy value preservation,
no mutation of defaults
- 32 tests passing, lint clean, dist rebuilt
This commit is contained in:
parent
95933befc0
commit
f9d72d3f45
8 changed files with 169 additions and 63 deletions
|
|
@ -8,26 +8,28 @@ describe('callAsyncFunction', () => {
|
|||
expect(result).toEqual('bar')
|
||||
})
|
||||
|
||||
test('passes getOctokit through the script context', async () => {
|
||||
const getOctokit = jest.fn().mockReturnValue('secondary-client')
|
||||
test('passes createOctokit through the script context', async () => {
|
||||
const createOctokit = jest.fn().mockReturnValue('secondary-client')
|
||||
|
||||
const result = await callAsyncFunction(
|
||||
{getOctokit} as any,
|
||||
"return getOctokit('token')"
|
||||
{createOctokit} as any,
|
||||
"return createOctokit('token')"
|
||||
)
|
||||
|
||||
expect(getOctokit).toHaveBeenCalledWith('token')
|
||||
expect(createOctokit).toHaveBeenCalledWith('token')
|
||||
expect(result).toEqual('secondary-client')
|
||||
})
|
||||
|
||||
test('getOctokit creates client independent from github', async () => {
|
||||
test('createOctokit creates client independent from github', async () => {
|
||||
const github = {rest: {issues: 'primary'}}
|
||||
const getOctokit = jest.fn().mockReturnValue({rest: {issues: 'secondary'}})
|
||||
const createOctokit = jest
|
||||
.fn()
|
||||
.mockReturnValue({rest: {issues: 'secondary'}})
|
||||
|
||||
const result = await callAsyncFunction(
|
||||
{github, getOctokit} as any,
|
||||
{github, createOctokit} as any,
|
||||
`
|
||||
const secondary = getOctokit('other-token')
|
||||
const secondary = createOctokit('other-token')
|
||||
return {
|
||||
primary: github.rest.issues,
|
||||
secondary: secondary.rest.issues,
|
||||
|
|
@ -41,32 +43,32 @@ describe('callAsyncFunction', () => {
|
|||
secondary: 'secondary',
|
||||
different: true
|
||||
})
|
||||
expect(getOctokit).toHaveBeenCalledWith('other-token')
|
||||
expect(createOctokit).toHaveBeenCalledWith('other-token')
|
||||
})
|
||||
|
||||
test('getOctokit passes options through', async () => {
|
||||
const getOctokit = jest.fn().mockReturnValue('client-with-opts')
|
||||
test('createOctokit passes options through', async () => {
|
||||
const createOctokit = jest.fn().mockReturnValue('client-with-opts')
|
||||
|
||||
const result = await callAsyncFunction(
|
||||
{getOctokit} as any,
|
||||
`return getOctokit('my-token', { baseUrl: 'https://ghes.example.com/api/v3' })`
|
||||
{createOctokit} as any,
|
||||
`return createOctokit('my-token', { baseUrl: 'https://ghes.example.com/api/v3' })`
|
||||
)
|
||||
|
||||
expect(getOctokit).toHaveBeenCalledWith('my-token', {
|
||||
expect(createOctokit).toHaveBeenCalledWith('my-token', {
|
||||
baseUrl: 'https://ghes.example.com/api/v3'
|
||||
})
|
||||
expect(result).toEqual('client-with-opts')
|
||||
})
|
||||
|
||||
test('getOctokit supports plugins', async () => {
|
||||
const getOctokit = jest.fn().mockReturnValue('client-with-plugins')
|
||||
test('createOctokit supports plugins', async () => {
|
||||
const createOctokit = jest.fn().mockReturnValue('client-with-plugins')
|
||||
|
||||
const result = await callAsyncFunction(
|
||||
{getOctokit} as any,
|
||||
`return getOctokit('my-token', { previews: ['v3'] }, 'pluginA', 'pluginB')`
|
||||
{createOctokit} as any,
|
||||
`return createOctokit('my-token', { previews: ['v3'] }, 'pluginA', 'pluginB')`
|
||||
)
|
||||
|
||||
expect(getOctokit).toHaveBeenCalledWith(
|
||||
expect(createOctokit).toHaveBeenCalledWith(
|
||||
'my-token',
|
||||
{previews: ['v3']},
|
||||
'pluginA',
|
||||
|
|
@ -75,24 +77,24 @@ describe('callAsyncFunction', () => {
|
|||
expect(result).toEqual('client-with-plugins')
|
||||
})
|
||||
|
||||
test('multiple getOctokit calls produce independent clients', async () => {
|
||||
const getOctokit = jest
|
||||
test('multiple createOctokit calls produce independent clients', async () => {
|
||||
const createOctokit = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({id: 'client-a'})
|
||||
.mockReturnValueOnce({id: 'client-b'})
|
||||
|
||||
const result = await callAsyncFunction(
|
||||
{getOctokit} as any,
|
||||
{createOctokit} as any,
|
||||
`
|
||||
const a = getOctokit('token-a')
|
||||
const b = getOctokit('token-b')
|
||||
const a = createOctokit('token-a')
|
||||
const b = createOctokit('token-b')
|
||||
return { a: a.id, b: b.id, different: a !== b }
|
||||
`
|
||||
)
|
||||
|
||||
expect(getOctokit).toHaveBeenCalledTimes(2)
|
||||
expect(getOctokit).toHaveBeenNthCalledWith(1, 'token-a')
|
||||
expect(getOctokit).toHaveBeenNthCalledWith(2, 'token-b')
|
||||
expect(createOctokit).toHaveBeenCalledTimes(2)
|
||||
expect(createOctokit).toHaveBeenNthCalledWith(1, 'token-a')
|
||||
expect(createOctokit).toHaveBeenNthCalledWith(2, 'token-b')
|
||||
expect(result).toEqual({a: 'client-a', b: 'client-b', different: true})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,23 @@ describe('createConfiguredGetOctokit', () => {
|
|||
)
|
||||
})
|
||||
|
||||
test('deep-merges retry so partial overrides preserve existing settings', () => {
|
||||
const raw = makeMockGetOctokit()
|
||||
const defaults = {
|
||||
retry: {enabled: true, retries: 3}
|
||||
}
|
||||
|
||||
const wrapped = createConfiguredGetOctokit(raw as any, defaults)
|
||||
wrapped('tok' as any, {retry: {retries: 5}} as any)
|
||||
|
||||
expect(raw).toHaveBeenCalledWith(
|
||||
'tok',
|
||||
expect.objectContaining({
|
||||
retry: {enabled: true, retries: 5}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('user can override request.retries explicitly', () => {
|
||||
const raw = makeMockGetOctokit()
|
||||
const defaults = {request: {retries: 3}}
|
||||
|
|
@ -158,15 +175,35 @@ describe('createConfiguredGetOctokit', () => {
|
|||
|
||||
test('baseUrl: undefined from user does not clobber default', () => {
|
||||
const raw = makeMockGetOctokit()
|
||||
const defaults = {baseUrl: 'https://api.github.com'}
|
||||
const defaults = {baseUrl: 'https://ghes.example.com/api/v3'}
|
||||
|
||||
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')
|
||||
expect(calledOpts.baseUrl).toBe('https://ghes.example.com/api/v3')
|
||||
})
|
||||
|
||||
test('undefined values in nested request are stripped', () => {
|
||||
const raw = makeMockGetOctokit()
|
||||
const defaults = {request: {retries: 3, agent: 'proxy'}}
|
||||
|
||||
const wrapped = createConfiguredGetOctokit(raw as any, defaults)
|
||||
wrapped('tok' as any, {request: {retries: undefined, timeout: 5000}} as any)
|
||||
|
||||
const calledOpts = raw.mock.calls[0][1]
|
||||
expect(calledOpts.request).toEqual({retries: 3, agent: 'proxy', timeout: 5000})
|
||||
})
|
||||
|
||||
test('undefined values in nested retry are stripped', () => {
|
||||
const raw = makeMockGetOctokit()
|
||||
const defaults = {retry: {enabled: true, retries: 3}}
|
||||
|
||||
const wrapped = createConfiguredGetOctokit(raw as any, defaults)
|
||||
wrapped('tok' as any, {retry: {enabled: undefined, retries: 5}} as any)
|
||||
|
||||
const calledOpts = raw.mock.calls[0][1]
|
||||
expect(calledOpts.retry).toEqual({enabled: true, retries: 5})
|
||||
})
|
||||
|
||||
test('each call creates an independent client', () => {
|
||||
|
|
@ -183,4 +220,39 @@ describe('createConfiguredGetOctokit', () => {
|
|||
expect(b).toBe('client-b')
|
||||
expect(raw).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('does not mutate defaultOptions between calls', () => {
|
||||
const raw = makeMockGetOctokit()
|
||||
const defaults = {
|
||||
request: {retries: 3},
|
||||
retry: {enabled: true}
|
||||
}
|
||||
const originalDefaults = JSON.parse(JSON.stringify(defaults))
|
||||
|
||||
const wrapped = createConfiguredGetOctokit(raw as any, defaults)
|
||||
wrapped('tok' as any, {request: {timeout: 5000}, retry: {retries: 10}} as any)
|
||||
wrapped('tok' as any, {request: {timeout: 9000}} as any)
|
||||
|
||||
expect(defaults).toEqual(originalDefaults)
|
||||
})
|
||||
|
||||
test('falsy-but-valid values are preserved, only undefined is stripped', () => {
|
||||
const raw = makeMockGetOctokit()
|
||||
const defaults = {baseUrl: 'https://ghes.example.com/api/v3'}
|
||||
|
||||
const wrapped = createConfiguredGetOctokit(raw as any, defaults)
|
||||
wrapped('tok' as any, {
|
||||
log: null,
|
||||
retries: 0,
|
||||
debug: false,
|
||||
userAgent: ''
|
||||
} as any)
|
||||
|
||||
const calledOpts = raw.mock.calls[0][1]
|
||||
expect(calledOpts.log).toBeNull()
|
||||
expect(calledOpts.retries).toBe(0)
|
||||
expect(calledOpts.debug).toBe(false)
|
||||
expect(calledOpts.userAgent).toBe('')
|
||||
expect(calledOpts.baseUrl).toBe('https://ghes.example.com/api/v3')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
import {getOctokit} from '@actions/github'
|
||||
import {callAsyncFunction} from '../src/async-function'
|
||||
|
||||
describe('getOctokit integration via callAsyncFunction', () => {
|
||||
describe('createOctokit integration via callAsyncFunction', () => {
|
||||
test('real getOctokit creates a functional Octokit client in script scope', async () => {
|
||||
const result = await callAsyncFunction(
|
||||
{getOctokit} as any,
|
||||
{createOctokit: getOctokit} as any,
|
||||
`
|
||||
const client = getOctokit('fake-token-for-test')
|
||||
const client = createOctokit('fake-token-for-test')
|
||||
return {
|
||||
hasRest: typeof client.rest === 'object',
|
||||
hasGraphql: typeof client.graphql === 'function',
|
||||
|
|
@ -32,9 +32,9 @@ describe('getOctokit integration via callAsyncFunction', () => {
|
|||
const primary = getOctokit('primary-token')
|
||||
|
||||
const result = await callAsyncFunction(
|
||||
{github: primary, getOctokit} as any,
|
||||
{github: primary, createOctokit: getOctokit} as any,
|
||||
`
|
||||
const secondary = getOctokit('secondary-token')
|
||||
const secondary = createOctokit('secondary-token')
|
||||
return {
|
||||
bothHaveRest: typeof github.rest === 'object' && typeof secondary.rest === 'object',
|
||||
areDistinct: github !== secondary
|
||||
|
|
@ -48,11 +48,11 @@ describe('getOctokit integration via callAsyncFunction', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('getOctokit accepts options for GHES base URL', async () => {
|
||||
test('createOctokit accepts options for GHES base URL', async () => {
|
||||
const result = await callAsyncFunction(
|
||||
{getOctokit} as any,
|
||||
{createOctokit: getOctokit} as any,
|
||||
`
|
||||
const client = getOctokit('fake-token', {
|
||||
const client = createOctokit('fake-token', {
|
||||
baseUrl: 'https://ghes.example.com/api/v3'
|
||||
})
|
||||
return typeof client.rest === 'object'
|
||||
|
|
@ -62,12 +62,12 @@ describe('getOctokit integration via callAsyncFunction', () => {
|
|||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test('multiple getOctokit calls produce independent clients with different tokens', async () => {
|
||||
test('multiple createOctokit calls produce independent clients with different tokens', async () => {
|
||||
const result = await callAsyncFunction(
|
||||
{getOctokit} as any,
|
||||
{createOctokit: getOctokit} as any,
|
||||
`
|
||||
const clientA = getOctokit('token-a')
|
||||
const clientB = getOctokit('token-b')
|
||||
const clientA = createOctokit('token-a')
|
||||
const clientB = createOctokit('token-b')
|
||||
return {
|
||||
aHasRest: typeof clientA.rest === 'object',
|
||||
bHasRest: typeof clientB.rest === 'object',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue