Files
orca/src/main/github/pr-refresh-error-classification.ts
T
OrcaWinandOrcaWin 4687e90a16 [comment audit] Continue main-process comment cleanup (#32946)
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci]

* [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci]

* [comment audit] Tighten Git execution and source-control comment contracts [skip ci]

* [comment audit] Clarify repository probes and maintenance comments [skip ci]

* [comment audit] Shorten status, diff-cache, and checkout comments [skip ci]

* [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci]

* [comment audit] Shorten forge identity and review-refresh comments [skip ci]

* [comment audit] Tighten hosted-review client comment contracts [skip ci]

* [comment audit] Clarify GitLab compatibility and review comments [skip ci]

* [comment audit] Tighten hook ownership and watchdog rationale [skip ci]

* [comment audit] Condense host and integration compatibility comments [skip ci]

* [comment audit] Clarify IPC authority and remote-routing rationale [skip ci]

* [comment audit] Finalize file-import and containment comment cleanup

* [comment audit] Fix stale Jira type imports blocking full CI

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 16:27:37 -07:00

116 lines
4.2 KiB
TypeScript

import type { PRRefreshErrorType } from '../../shared/github/pull-request-refresh-types'
import { classifyGitHubUnavailable } from '../../shared/github/api-availability'
import { extractExecError } from '../git/exec-error'
/** Classify runner failures before UI rendering; rate-limit 403s precede permissions, and repo 404s precede network heuristics. */
export function classifyPRRefreshError(err: unknown): PRRefreshErrorType {
// Runner diagnostics may live only on stdout/stderr; retain structured spawn codes too.
const { stderr, stdout } = extractExecError(err)
const message = err instanceof Error ? err.message : String(err)
const lower = `${message}\n${stderr}\n${stdout}`.toLowerCase()
const code =
err && typeof err === 'object' && 'code' in err
? String((err as { code?: unknown }).code ?? '').toLowerCase()
: ''
// Secondary limits may mention abuse or Retry-After without the phrase rate limit.
const isHttp429 = lower.includes('http 429') || lower.includes('429 too many requests')
const isHttp403 = lower.includes('http 403')
const hasRetryAfter = lower.includes('retry-after')
if (
isHttp429 ||
lower.includes('secondary rate limit') ||
lower.includes('abuse detection') ||
lower.includes('abuse-rate-limits') ||
lower.includes('you have triggered an abuse') ||
((isHttp403 || isHttp429) && hasRetryAfter) ||
lower.includes('api rate limit exceeded') ||
lower.includes('rate limit')
) {
return 'rate_limited'
}
// Match repository failures before incidental connectivity wording.
if (lower.includes('http 404') || lower.includes('could not resolve to a repository')) {
return 'repo_unavailable'
}
const unavailable = classifyGitHubUnavailable(lower)
if (unavailable) {
return unavailable
}
// Require structured codes or full phrases; a repository named network-* is not a network failure.
const networkCodes = new Set([
'etimedout',
'econnreset',
'econnrefused',
'enotfound',
'eai_again',
'enetunreach',
'enetdown'
])
if (
networkCodes.has(code) ||
lower.includes('etimedout') ||
lower.includes('econnreset') ||
lower.includes('econnrefused') ||
lower.includes('enotfound') ||
lower.includes('eai_again') ||
lower.includes('timed out') ||
lower.includes('timeout') ||
lower.includes('could not resolve host') ||
lower.includes('no such host') ||
lower.includes('network is unreachable') ||
lower.includes('network is down') ||
lower.includes('network error') ||
lower.includes('connection refused') ||
lower.includes('connection reset')
) {
return 'network'
}
if (isHttp403 || lower.includes('resource not accessible')) {
return 'permission'
}
// Prefer structured launch codes so paths containing gh cannot imply a missing executable.
if (
code === 'enoent' ||
lower.includes('spawn gh enoent') ||
lower.includes('gh: command not found') ||
lower.includes("'gh' is not recognized")
) {
return 'gh_unavailable'
}
// Full auth phrases avoid matching author/authored.
if (
lower.includes('http 401') ||
lower.includes('unauthorized') ||
lower.includes('authorization') ||
lower.includes('authentication') ||
lower.includes('bad credentials') ||
lower.includes('gh auth') ||
/\b(login|credentials?)\b/i.test(lower)
) {
return 'auth'
}
return 'unknown'
}
export function safePRRefreshErrorMessage(errorType: PRRefreshErrorType): string {
switch (errorType) {
case 'rate_limited':
return 'GitHub rate limit is low. Try again after the limit resets.'
case 'auth':
return 'GitHub authentication is unavailable. Check your gh login.'
case 'network':
return 'GitHub is unreachable right now. Check your network and try again.'
case 'server_error':
return "GitHub's API is temporarily unavailable (server error). This is a GitHub-side issue."
case 'permission':
return 'GitHub did not allow access to this pull request.'
case 'repo_unavailable':
return 'The GitHub repository is unavailable or cannot be resolved.'
case 'gh_unavailable':
return 'GitHub CLI is unavailable.'
case 'unknown':
return 'GitHub pull request refresh failed.'
}
}