mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 00:02:05 +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.
156 lines
4.0 KiB
TypeScript
156 lines
4.0 KiB
TypeScript
import type { GitLabIssueUpdate } from '../../shared/gitlab-types'
|
|
import type { IssueSourcePreference } from '../../shared/repo-types'
|
|
import {
|
|
acquire,
|
|
classifyGlabError,
|
|
getGlabKnownHosts,
|
|
glabExecFileAsync,
|
|
glabHostnameArgs,
|
|
glabRepoExecOptions,
|
|
release,
|
|
resolveIssueSource,
|
|
type LocalGitExecOptions,
|
|
type ProjectRef
|
|
} from './gl-utils'
|
|
import { encodedProject } from './project-path-encoding'
|
|
|
|
/**
|
|
* Update an existing GitLab issue.
|
|
*
|
|
* Why: callers that list through a per-repo issue source preference must
|
|
* mutate the same GitLab project, or identical IIDs on origin/upstream can
|
|
* silently edit the wrong issue.
|
|
*/
|
|
export async function updateIssue(
|
|
repoPath: string,
|
|
issueNumber: number,
|
|
updates: GitLabIssueUpdate,
|
|
preference?: IssueSourcePreference,
|
|
connectionId?: string | null,
|
|
projectRefOverride?: ProjectRef | null,
|
|
localGitOptions: LocalGitExecOptions = {}
|
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
const projectRef =
|
|
projectRefOverride ??
|
|
(
|
|
await resolveIssueSource(
|
|
repoPath,
|
|
preference,
|
|
await getGlabKnownHosts(connectionId, localGitOptions),
|
|
connectionId,
|
|
localGitOptions
|
|
)
|
|
).source
|
|
if (!projectRef) {
|
|
return {
|
|
ok: false,
|
|
error: 'Could not resolve GitLab project for this repository'
|
|
}
|
|
}
|
|
|
|
const repoFlag = projectRef.path
|
|
const errors: string[] = []
|
|
|
|
// State change requires a separate command (parallel to github's split).
|
|
if (updates.state) {
|
|
await acquire()
|
|
try {
|
|
const cmd = updates.state === 'closed' ? 'close' : 'reopen'
|
|
await glabExecFileAsync(
|
|
[
|
|
'issue',
|
|
cmd,
|
|
String(issueNumber),
|
|
'-R',
|
|
repoFlag,
|
|
...glabHostnameArgs(projectRef, connectionId)
|
|
],
|
|
glabRepoExecOptions(repoPath, connectionId, localGitOptions)
|
|
)
|
|
} catch (err) {
|
|
const stderr = err instanceof Error ? err.message : String(err)
|
|
// Treat "already closed/reopened" as a no-op (matches gh path).
|
|
if (!stderr.toLowerCase().includes('already')) {
|
|
errors.push(classifyGlabError(stderr).message)
|
|
}
|
|
} finally {
|
|
release()
|
|
}
|
|
}
|
|
|
|
if (updates.body !== undefined) {
|
|
await acquire()
|
|
try {
|
|
await glabExecFileAsync(
|
|
[
|
|
'api',
|
|
...glabHostnameArgs(projectRef, connectionId),
|
|
'-X',
|
|
'PUT',
|
|
`projects/${encodedProject(repoFlag)}/issues/${issueNumber}`,
|
|
'-f',
|
|
`description=${updates.body}`
|
|
],
|
|
glabRepoExecOptions(repoPath, connectionId, localGitOptions)
|
|
)
|
|
} catch (err) {
|
|
const stderr = err instanceof Error ? err.message : String(err)
|
|
errors.push(classifyGlabError(stderr).message)
|
|
} finally {
|
|
release()
|
|
}
|
|
}
|
|
|
|
// Field edits via `glab issue update`.
|
|
const editArgs: string[] = [
|
|
'issue',
|
|
'update',
|
|
String(issueNumber),
|
|
'-R',
|
|
repoFlag,
|
|
...glabHostnameArgs(projectRef, connectionId)
|
|
]
|
|
let hasEditArgs = false
|
|
|
|
if (updates.title) {
|
|
editArgs.push('--title', updates.title)
|
|
hasEditArgs = true
|
|
}
|
|
for (const label of updates.addLabels ?? []) {
|
|
editArgs.push('--label', label)
|
|
hasEditArgs = true
|
|
}
|
|
for (const label of updates.removeLabels ?? []) {
|
|
editArgs.push('--unlabel', label)
|
|
hasEditArgs = true
|
|
}
|
|
for (const assignee of updates.addAssignees ?? []) {
|
|
editArgs.push('--assignee', assignee)
|
|
hasEditArgs = true
|
|
}
|
|
for (const assignee of updates.removeAssignees ?? []) {
|
|
editArgs.push('--unassignee', assignee)
|
|
hasEditArgs = true
|
|
}
|
|
|
|
if (hasEditArgs) {
|
|
await acquire()
|
|
try {
|
|
await glabExecFileAsync(
|
|
editArgs,
|
|
glabRepoExecOptions(repoPath, connectionId, localGitOptions)
|
|
)
|
|
} catch (err) {
|
|
const stderr = err instanceof Error ? err.message : String(err)
|
|
errors.push(classifyGlabError(stderr).message)
|
|
} finally {
|
|
release()
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return { ok: false, error: errors.join('; ') }
|
|
}
|
|
return { ok: true }
|
|
}
|