fix(github): preserve outage attribution edge cases

This commit is contained in:
Brennan Benson
2026-07-16 18:32:50 -07:00
parent 6085cb9c62
commit 9b82183d4c
4 changed files with 36 additions and 4 deletions
@@ -6322,6 +6322,32 @@ describe('createGitHubSlice.fetchWorkItems source/error envelope', () => {
}
})
it('ignores an ineligible SSH repo when every GitHub source is unavailable', async () => {
const store = createTestStore()
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
mockApi.gh.listWorkItems
.mockRejectedValueOnce(new Error(GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE))
.mockRejectedValueOnce(new Error('HTTP 503: Service Unavailable'))
try {
const result = await store.getState().fetchWorkItemsAcrossRepos(
[
{ repoId: 'ssh-repo', path: '/server/ssh-repo' },
{ repoId: 'github-repo', path: '/server/github-repo' }
],
24,
100,
''
)
expect(result.items).toEqual([])
expect(result.failedCount).toBe(1)
expect(result.githubUnavailable).toBe(true)
} finally {
consoleWarn.mockRestore()
}
})
it('keeps the partial-failure count when another GitHub repo still loads', async () => {
const store = createTestStore()
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+7 -3
View File
@@ -2866,6 +2866,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
const state = get()
let failedCount = 0
let unavailableFailureCount = 0
let skippedSourceCount = 0
const perProjectResults = await Promise.all(
repos.map(async (r) => {
try {
@@ -2881,6 +2882,7 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
// Why: must use perRepoLimit (not displayLimit) so the cache key
// matches what fetchWorkItems wrote.
if (isGitHubWorkItemsSshRemoteRequiredError(err)) {
skippedSourceCount += 1
return [] as GitHubWorkItem[]
}
const key =
@@ -2911,10 +2913,12 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
const merged = sortWorkItemsByNumber(perProjectResults.flat()).slice(0, displayLimit)
// Why: one repo can fail with a 5xx while another succeeds (or fails for a
// permission reason). Only claim a global availability failure when every
// selected repo failed for a GitHub reachability reason; otherwise keep the
// exact count.
// eligible source failed for a GitHub reachability reason; intentionally
// skipped SSH repos are not GitHub sources for this calculation.
const githubUnavailable =
failedCount > 0 && failedCount === repos.length && unavailableFailureCount === failedCount
failedCount > 0 &&
failedCount === repos.length - skippedSourceCount &&
unavailableFailureCount === failedCount
return { items: merged, failedCount, githubUnavailable }
},
@@ -18,6 +18,8 @@ describe('classifyGitHubUnavailable', () => {
'request to https://api.github.com failed, reason: getaddrinfo ENOTFOUND api.github.com',
'dial tcp: lookup api.github.com: no such host',
'connect ETIMEDOUT 140.82.112.5:443',
'TimeoutError: request aborted',
'NetworkError when attempting to fetch resource',
'fetch failed',
'socket hang up',
'could not resolve host: api.github.com',
+1 -1
View File
@@ -23,7 +23,7 @@ const SERVER_ERROR_PATTERN =
// both Node (ENOTFOUND/ECONNRESET) and the gh Go client ("dial tcp", "i/o
// timeout", "no such host") shapes.
const NETWORK_PATTERN =
/\btimeout\b|\btimed out\b|\bno such host\b|could not resolve host|could not resolve to a host|\bnetwork\b|\bconnection (?:refused|reset)\b|\bdial tcp\b|\bi\/o timeout\b|\bfetch failed\b|\bsocket hang up\b|ENOTFOUND|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i
/timeout|\btimed out\b|\bno such host\b|could not resolve host|could not resolve to a host|network|\bconnection (?:refused|reset)\b|\bdial tcp\b|\bi\/o timeout\b|\bfetch failed\b|\bsocket hang up\b|ENOTFOUND|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i
/**
* Classify a gh/Octokit error message as a GitHub-reachability problem, or