Skip to content

Commit c82f7d2

Browse files
heiskrCopilot
andauthored
Handle top-level array REST request bodies in gh CLI code samples (#62191)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0fcb228 commit c82f7d2

4 files changed

Lines changed: 115 additions & 11 deletions

File tree

src/rest/components/get-rest-code-samples.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -200,18 +200,16 @@ export function getGHExample(
200200
// and the type is a string.
201201
const { bodyParameters } = codeSample.request
202202
if (bodyParameters) {
203-
// There should not be a case in a REST API where only an array is sent
204-
if (Array.isArray(bodyParameters)) {
205-
throw new Error('Array of arrays found in body parameters')
206-
}
207-
208203
if (typeof bodyParameters === 'object') {
209204
// Special handling for gist endpoints - use --input for nested file structures
210205
const isGistEndpoint =
206+
!Array.isArray(bodyParameters) &&
211207
operation.requestPath.includes('/gists') &&
212208
(operation.title === 'Create a gist' || operation.title === 'Update a gist')
213209

214-
// For complex objects with arrays, use --input with JSON
210+
// For top-level arrays or complex objects with arrays, use --input with JSON.
211+
// The gh CLI -f/-F flags can't represent a request body that is itself an array,
212+
// so we fall back to piping the JSON body via --input.
215213
const hasArrays = hasNestedArrays(bodyParameters as NestedObjectParameter)
216214
if (hasArrays || isGistEndpoint) {
217215
const jsonBody = JSON.stringify(
@@ -233,7 +231,7 @@ export function getGHExample(
233231
requestBodyParams += handleObjectParameter(bodyParameters as NestedObjectParameter)
234232
}
235233
} else {
236-
requestBodyParams += handleSingleParameter('', bodyParameters)
234+
requestBodyParams += handleSingleParameter('', bodyParameters as NestedObjectParameter)
237235
}
238236
}
239237

@@ -388,11 +386,13 @@ export function getJSExample(
388386
if (codeSample.request) {
389387
Object.assign(parameters, codeSample.request.parameters)
390388
// Most of the time the example body parameters have a name and value
391-
// and are included in an object. But, some cases are a single value
392-
// and the type is a string.
389+
// and are included in an object. But some cases are a single scalar value
390+
// or a top-level JSON array, both of which Octokit sends as the raw
391+
// request body via the `data` option.
393392
if (
394393
codeSample.request.bodyParameters &&
395-
typeof codeSample.request.bodyParameters !== 'object'
394+
(typeof codeSample.request.bodyParameters !== 'object' ||
395+
Array.isArray(codeSample.request.bodyParameters))
396396
) {
397397
parameters.data = codeSample.request.bodyParameters
398398
} else {

src/rest/components/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ export interface CodeSample {
5151
request: {
5252
contentType: string
5353
acceptHeader: string
54-
bodyParameters: Record<string, string | Array<string | { [key: string]: string }>>
54+
// Most request bodies are an object of named parameters, but some REST
55+
// operations take a single scalar value or a top-level JSON array.
56+
bodyParameters:
57+
| Record<string, string | Array<string | { [key: string]: string }>>
58+
| Array<unknown>
59+
| string
5560
parameters: Record<string, string>
5661
description: string
5762
}

src/rest/tests/get-rest-code-samples-2.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,36 @@ describe('REST code samples authentication header handling', () => {
447447
expect(result).toContain("await octokit.request('POST /credentials/revo")
448448
expect(result).toContain("'X-GitHub-Api-Version': '2022-11-28'")
449449
})
450+
451+
test('sends a top-level array body via the data option', () => {
452+
const arrayBodyCodeSample: CodeSample = {
453+
request: {
454+
contentType: 'application/json',
455+
description: 'Example',
456+
acceptHeader: 'application/vnd.github+json',
457+
bodyParameters: [{ id: 'MVS-2026-001', summary: 'Example summary' }],
458+
parameters: {},
459+
},
460+
response: {
461+
statusCode: '200',
462+
contentType: 'application/json',
463+
description: 'Response',
464+
example: {},
465+
},
466+
}
467+
468+
const result = getJSExample(
469+
standardOperation,
470+
arrayBodyCodeSample,
471+
'free-pro-team@latest',
472+
mockVersions,
473+
)
474+
475+
// The array must be nested under `data`, not spread as numeric keys ("0", "1").
476+
expect(result).toContain('data: [')
477+
expect(result).toContain("id: 'MVS-2026-001'")
478+
expect(result).not.toMatch(/["']0["']\s*:/)
479+
})
450480
})
451481

452482
describe('edge cases and special scenarios', () => {

src/rest/tests/get-rest-code-samples.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,73 @@ describe('getGHExample - GitHub CLI code generation', () => {
229229
expect(result).toContain('"tag2"')
230230
expect(result).toContain('"tag3"')
231231
})
232+
233+
test('handles a top-level array body via --input', () => {
234+
const operation: Operation = {
235+
serverUrl: 'https://api.github.com',
236+
verb: 'post',
237+
requestPath: '/enterprises/{enterprise}/innersource-vulnerabilities/sync',
238+
title: 'Sync InnerSource vulnerabilities',
239+
descriptionHTML: '<p>Sync vulnerabilities</p>',
240+
previews: [],
241+
statusCodes: [],
242+
bodyParameters: [],
243+
category: 'enterprise-admin',
244+
subcategory: 'security-advisories',
245+
parameters: [
246+
{
247+
name: 'enterprise',
248+
in: 'path',
249+
required: true,
250+
description: 'The enterprise slug',
251+
schema: { type: 'string' },
252+
},
253+
],
254+
codeExamples: [],
255+
progAccess: {
256+
permissions: [],
257+
userToServerRest: true,
258+
serverToServer: true,
259+
fineGrainedPat: true,
260+
},
261+
}
262+
263+
const codeSample: CodeSample = {
264+
request: {
265+
contentType: 'application/json',
266+
description: 'Example',
267+
acceptHeader: 'application/vnd.github+json',
268+
bodyParameters: [
269+
{
270+
id: 'MVS-2026-001',
271+
summary: 'Example vulnerability summary',
272+
aliases: ['GHSA-xxxx-xxxx-xxxx'],
273+
},
274+
],
275+
parameters: { enterprise: 'octo-enterprise' },
276+
},
277+
response: {
278+
statusCode: '200',
279+
contentType: 'application/json',
280+
description: 'Response',
281+
example: {},
282+
},
283+
}
284+
285+
const currentVersion = 'enterprise-cloud@latest'
286+
const allVersions: Record<string, VersionItem> = {
287+
'enterprise-cloud@latest': {
288+
version: 'enterprise-cloud@latest',
289+
versionTitle: 'Enterprise Cloud',
290+
apiVersions: ['2022-11-28'],
291+
latestApiVersion: '2022-11-28',
292+
},
293+
}
294+
295+
const result = getGHExample(operation, codeSample, currentVersion, allVersions)
296+
297+
expect(result).toContain('--input - <<<')
298+
expect(result).toContain('"MVS-2026-001"')
299+
expect(result).toContain('"GHSA-xxxx-xxxx-xxxx"')
300+
})
232301
})

0 commit comments

Comments
 (0)