Files
orca/src/main/github/issue-create.ts
T
Wooseong KimandNeil 6ad0a42425 fix(github): send issue bodies to gh over stdin instead of argv (#25215)
Issue create (including the empty-body fallback and follow-up PATCH),
issue body edits, and issue comments passed the body as
`--raw-field body=...` on argv. Windows caps the command line at
32,767 chars, so bodies with pasted inline images failed with
`spawn ENAMETOOLONG` before reaching GitHub.

These calls now use `gh api -X <METHOD> <endpoint> --input -` with a
JSON payload on stdin. ghExecFileWithScopeAsync also now forwards
`options.stdin` to the child process; it was previously dropped.

Fixes #15832

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-10-26 03:26:35 -07:00

115 lines
3.8 KiB
TypeScript

import type {
GitHubCreateIssueFields,
GitHubCreateIssueResult
} from '../../shared/issue-mutation-types'
import type { IssueSourcePreference } from '../../shared/repo-types'
import type { LocalGitExecOptions } from './gh-utils'
import {
resolveGitHubRepoExecution,
resolveIssueGitHubApiRepositorySource
} from './github-api-repository'
import { acquire, extractExecError, ghExecFileAsync, release } from './gh-utils'
function githubIssueErrorMessage(error: unknown): string {
const { stderr, stdout } = extractExecError(error)
return stderr.trim() || stdout.trim()
}
/** Explicit owner/repo preserves the Tasks-selected source independently of cwd remotes. */
export async function createIssue(
repoPath: string,
title: string,
body: string,
preference?: IssueSourcePreference,
connectionId?: string | null,
fields?: GitHubCreateIssueFields,
localGitOptions: LocalGitExecOptions = {}
): Promise<GitHubCreateIssueResult> {
const trimmedTitle = title.trim()
if (!trimmedTitle) {
return { ok: false, error: 'Title is required' }
}
const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution(
repoPath,
async () =>
(
await resolveIssueGitHubApiRepositorySource(
repoPath,
preference,
connectionId,
localGitOptions
)
).source,
connectionId,
localGitOptions
)
if (!ownerRepo) {
return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' }
}
await acquire()
try {
const issuesEndpoint = `repos/${ownerRepo.owner}/${ownerRepo.repo}/issues`
// Why: stdin keeps large inline-image bodies off argv, which Windows caps at 32K chars.
const createIssueRequest = (issueBody: string) =>
ghExecFileAsync(['api', '-X', 'POST', issuesEndpoint, '--input', '-'], {
...ghOptions,
stdin: JSON.stringify({
title: trimmedTitle,
body: issueBody,
...(fields?.labels?.length ? { labels: fields.labels } : {}),
...(fields?.assignees?.length ? { assignees: fields.assignees } : {})
})
})
const parseIssue = (stdout: string) =>
JSON.parse(stdout) as { number?: number; html_url?: string; url?: string }
let data: { number?: number; html_url?: string; url?: string }
try {
const { stdout } = await createIssueRequest(body)
data = parseIssue(stdout)
} catch (err) {
const message = githubIssueErrorMessage(err)
if (!/body is too long \(maximum is \d+ characters\)/i.test(message)) {
return { ok: false, error: message }
}
// GitHub can accept a body on update that it rejects during creation; establish the issue first.
const { stdout } = await createIssueRequest('')
data = parseIssue(stdout)
if (typeof data.number !== 'number') {
return { ok: false, error: 'Unexpected response from GitHub' }
}
try {
await ghExecFileAsync(
['api', '-X', 'PATCH', `${issuesEndpoint}/${data.number}`, '--input', '-'],
{ ...ghOptions, stdin: JSON.stringify({ body }) }
)
} catch (patchErr) {
const patchMessage = githubIssueErrorMessage(patchErr)
const identity = data.html_url ?? data.url ?? `#${data.number}`
return {
ok: true,
number: data.number,
url: String(data.html_url ?? data.url ?? ''),
bodySaveWarning: `Issue ${identity} was created, but saving its body failed: ${patchMessage}`
}
}
}
if (typeof data.number !== 'number') {
return { ok: false, error: 'Unexpected response from GitHub' }
}
return {
ok: true,
number: data.number,
url: String(data.html_url ?? data.url ?? '')
}
} catch (err) {
return { ok: false, error: githubIssueErrorMessage(err) }
} finally {
release()
}
}