Files
orca/src/main/github/gh-error-classification.ts
T
Jinjing 63dbf12d14 Split github client (#15214)
* refactor(github-client): reorganize client into lifecycle folders

* refactor(github-client): extract PR refresh data and outcome assembly

Separate the derived data calculation and outcome assembly logic from
branch-lookup-resolution into dedicated modules for better separation of
concerns. Modernize type import syntax and format exports consistently.

* refactor(github-client): improve error handling and resilience

Defensive GraphQL parsing prevents partial responses from breaking REST fallbacks.
Cache failures now use shorter TTLs for faster recovery. PR operations have
dedicated error classification. GraphQL mutations track rate limit usage to prevent
quota exhaustion. Data validation improved to reject spurious values.

* Extract check rerun error classification with operation context

Create classifyRerunChecksError() to provide operation-specific error
messages when check reruns fail. This replaces generic GitHub error
copy with context appropriate to what the user attempted (rerun
checks). Follows the pattern of classifyListPrsError and improves
error handling by delegating extraction to extractExecError.

* Make check-rerun not-found error message resource-neutral

Error handling for failed check reruns now covers both workflow-run
reruns and standalone check-run rerequests. Tests verify the neutral
message works for both scenarios.
2026-08-17 21:18:44 -07:00

102 lines
4.5 KiB
TypeScript

import type { ClassifiedError } from '../../shared/classified-error'
// Why: gh CLI surfaces API errors as unstructured stderr. Map known
// patterns to typed errors so callers can show user-friendly messages.
export function classifyGhError(stderr: string): ClassifiedError {
const s = stderr.toLowerCase()
// Why: primary rate-limit errors also carry "HTTP 403" — check rate limit
// first so they don't misclassify as a token-scope problem.
if (s.includes('rate limit')) {
return {
type: 'rate_limited',
message: 'GitHub rate limit hit. Try again in a few minutes.'
}
}
if (s.includes('http 403') || s.includes('resource not accessible')) {
return {
type: 'permission_denied',
message: "You don't have permission to edit this issue. Check your GitHub token scopes."
}
}
if (s.includes('http 404') || s.includes('could not resolve to a repository')) {
return { type: 'not_found', message: 'Issue not found — it may have been deleted.' }
}
if (s.includes('has disabled issues')) {
return { type: 'issues_disabled', message: 'Issues are disabled on this repository.' }
}
if (s.includes('http 422') || s.includes('validation failed')) {
return { type: 'validation_error', message: `Invalid update — ${stderr.trim()}` }
}
if (
s.includes('timeout') ||
s.includes('no such host') ||
s.includes('network') ||
s.includes('could not resolve host')
) {
return { type: 'network_error', message: 'Network error — check your connection.' }
}
return { type: 'unknown', message: `Failed to update issue: ${stderr.trim()}` }
}
// Why: classifyGhError's copy is phrased for edit/update operations, but
// listIssues is a read op and renderer banners interpolate the message.
export function classifyListIssuesError(stderr: string): ClassifiedError {
const c = classifyGhError(stderr)
const trimmed = stderr.trim()
const readMessages: Record<ClassifiedError['type'], string> = {
permission_denied:
"You don't have permission to read issues for this repository. Check your GitHub token scopes.",
not_found: 'Repository not found.',
issues_disabled: 'Issues are disabled on this repository.',
validation_error: `Invalid request — ${trimmed}`,
rate_limited: 'GitHub rate limit hit. Try again in a few minutes.',
network_error: 'Network error — check your connection.',
unknown: `Failed to load issues: ${trimmed}`
}
return { type: c.type, message: readMessages[c.type] }
}
// Why: classifyGhError's copy names an "issue"; PR mutations reuse the same
// classification but must not tell the user their pull request is an issue.
export function classifyPullRequestUpdateError(stderr: string): ClassifiedError {
const c = classifyGhError(stderr)
const trimmed = stderr.trim()
const pullRequestMessages: Record<ClassifiedError['type'], string> = {
permission_denied:
"You don't have permission to edit this pull request. Check your GitHub token scopes.",
not_found: 'Pull request not found — it may have been deleted.',
issues_disabled: c.message,
validation_error: `Invalid update — ${trimmed}`,
rate_limited: c.message,
network_error: c.message,
unknown: `Failed to update pull request: ${trimmed}`
}
return { type: c.type, message: pullRequestMessages[c.type] }
}
// Why: PR-side list failures need the same read-op classification — pagination
// decisions key on the type, and swallowing them made failures look like
// end-of-data (#11485).
export function classifyListPrsError(stderr: string): ClassifiedError {
const c = classifyGhError(stderr)
return { type: c.type, message: `Failed to load pull requests: ${stderr.trim()}` }
}
// Why: classifyGhError's copy names an "issue"; a failed check rerun must name
// the operation the user actually triggered.
export function classifyRerunChecksError(stderr: string): ClassifiedError {
const c = classifyGhError(stderr)
const trimmed = stderr.trim()
const rerunMessages: Record<ClassifiedError['type'], string> = {
permission_denied:
"You don't have permission to rerun checks on this repository. Check your GitHub token scopes.",
not_found: 'GitHub resource to rerun was not found — it may have expired or been deleted.',
issues_disabled: `Failed to rerun checks: ${trimmed}`,
validation_error: `Could not rerun checks — ${trimmed}`,
rate_limited: c.message,
network_error: c.message,
unknown: `Failed to rerun checks: ${trimmed}`
}
return { type: c.type, message: rerunMessages[c.type] }
}