Files
orca/src/main/github/issue-create.ts
Neil 83117f2860 refactor(integrations): split issue-tracker clients under the max-lines budget (#14704)
The GitLab, GitHub, Jira and Linear integration modules, their two IPC
registrars, and the shared GitHub project types each carried a file-level
`eslint-disable max-lines` and ran 351-614 counted lines against a 300-line
budget. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all
eight suppressions and prunes their entries (341 -> 333).

Pure move, no behavior change. Each client is cut along the seam it already
had: per-operation modules for the issue APIs (create / update / comment /
field options), and for Jira the request queue, site credential store,
authenticated request, and site identity. The two IPC registrars keep their own
handlers and delegate the rest to per-domain sub-registrars, so they remain
real entry points rather than re-export shims.

The IPC surface is proved intact rather than assumed: comparing (method,
channel) multisets between HEAD and the split gives 52 registrations across 52
distinct channels on both sides.

Provider-neutrality is preserved -- GitLab and GitHub keep separate, parallel
module layouts rather than being merged behind a shared abstraction.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the one remaining failure is a pre-existing load flake in an untouched file,
green when re-run serially), no new runtime import cycles among 744 modules,
and no lint suppression added anywhere.
2026-08-15 18:17:20 -07:00

134 lines
4.1 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()
}
/**
* Create a new GitHub issue. Uses `gh api` with explicit owner/repo so the
* call does not depend on the current working directory having a remote that
* matches the repo the user picked in the tasks page.
*/
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 createArgs = (issueBody: string) => {
const args = [
'api',
'-X',
'POST',
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues`,
'--raw-field',
`title=${trimmedTitle}`,
'--raw-field',
`body=${issueBody}`
]
for (const label of fields?.labels ?? []) {
args.push('--raw-field', `labels[]=${label}`)
}
for (const assignee of fields?.assignees ?? []) {
args.push('--raw-field', `assignees[]=${assignee}`)
}
return args
}
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 ghExecFileAsync(createArgs(body), ghOptions)
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 }
}
// Why: GitHub rejects oversized bodies on create but accepts the same body
// on update, so establish the issue before attaching its body.
const { stdout } = await ghExecFileAsync(createArgs(''), ghOptions)
data = parseIssue(stdout)
if (typeof data.number !== 'number') {
return { ok: false, error: 'Unexpected response from GitHub' }
}
try {
await ghExecFileAsync(
[
'api',
'-X',
'PATCH',
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues/${data.number}`,
'--raw-field',
`body=${body}`
],
ghOptions
)
} 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()
}
}