Files
orca/src/main/github/issue-update.ts
T
OrcaWinandOrcaWin 3b9c1d356d fix(i18n): code GitHub/GitLab operation failures for the renderer (#35689)
* fix(i18n): code GitHub/GitLab operation failures for the renderer

* fix(i18n): show coded failures in comment composers; exhaustive classification switches

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 22:18:41 -07:00

132 lines
4.0 KiB
TypeScript

import type { GitHubIssueUpdate } from '../../shared/issue-mutation-types'
import type { IssueSourcePreference } from '../../shared/repo-types'
import type { LocalGitExecOptions } from './gh-utils'
import {
resolveGitHubRepoExecution,
resolveIssueGitHubApiRepositorySource,
type GitHubApiRepository
} from './github-api-repository'
import { acquire, classifyGhError, ghExecFileAsync, release } from './gh-utils'
import { codeHostFailure, combinedClassifiedFailure } from '../../shared/code-host-failure-copy'
import type { ClassifiedError } from '../../shared/classified-error'
export async function updateIssue(
repoPath: string,
issueNumber: number,
updates: GitHubIssueUpdate,
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {},
preference?: IssueSourcePreference,
ownerRepoOverride?: GitHubApiRepository | null
): Promise<{ ok: true } | { ok: false; error: string }> {
const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution(
repoPath,
ownerRepoOverride ??
(async () =>
(
await resolveIssueGitHubApiRepositorySource(
repoPath,
preference,
connectionId,
localGitOptions
)
).source),
connectionId,
localGitOptions
)
if (!ownerRepo) {
return codeHostFailure('github.repoUnresolved')
}
const repo = `${ownerRepo.owner}/${ownerRepo.repo}`
const errors: ClassifiedError[] = []
if (updates.state) {
await acquire()
try {
if (updates.state === 'closed') {
const closeArgs = ['issue', 'close', String(issueNumber), '--repo', repo]
if (updates.stateReason === 'completed') {
closeArgs.push('--reason', 'completed')
} else if (updates.stateReason === 'not_planned') {
closeArgs.push('--reason', 'not planned')
} else if (updates.stateReason === 'duplicate' && updates.duplicateOf) {
closeArgs.push('--duplicate-of', String(updates.duplicateOf))
}
await ghExecFileAsync(closeArgs, ghOptions)
} else {
await ghExecFileAsync(['issue', 'reopen', String(issueNumber), '--repo', repo], ghOptions)
}
} catch (err) {
// Why: gh exits 0 when the issue is already in the target state, so any failure is real.
const stderr = err instanceof Error ? err.message : String(err)
errors.push(classifyGhError(stderr))
} finally {
release()
}
}
if (updates.body !== undefined) {
await acquire()
try {
await ghExecFileAsync(
[
'api',
'-X',
'PATCH',
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues/${issueNumber}`,
'--input',
'-'
],
{ ...ghOptions, stdin: JSON.stringify({ body: updates.body }) }
)
} catch (err) {
const stderr = err instanceof Error ? err.message : String(err)
errors.push(classifyGhError(stderr))
} finally {
release()
}
}
const editArgs: string[] = ['issue', 'edit', String(issueNumber), '--repo', repo]
let hasEditArgs = false
if (updates.title) {
editArgs.push('--title', updates.title)
hasEditArgs = true
}
for (const label of updates.addLabels ?? []) {
editArgs.push('--add-label', label)
hasEditArgs = true
}
for (const label of updates.removeLabels ?? []) {
editArgs.push('--remove-label', label)
hasEditArgs = true
}
for (const assignee of updates.addAssignees ?? []) {
editArgs.push('--add-assignee', assignee)
hasEditArgs = true
}
for (const assignee of updates.removeAssignees ?? []) {
editArgs.push('--remove-assignee', assignee)
hasEditArgs = true
}
if (hasEditArgs) {
await acquire()
try {
await ghExecFileAsync(editArgs, ghOptions)
} catch (err) {
const stderr = err instanceof Error ? err.message : String(err)
errors.push(classifyGhError(stderr))
} finally {
release()
}
}
if (errors.length > 0) {
return combinedClassifiedFailure(errors)
}
return { ok: true }
}