mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
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.
134 lines
4.1 KiB
TypeScript
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()
|
|
}
|
|
}
|