Repository navigation
[P1] GitHub API fetchers in src/lib/github-{issues,prs,ci,code-search}.ts do not retry transient 429/5xx responses #917
Description
Ask: Add a single shared withGitHubRetry wrapper around the fetch calls in src/lib/github-issues.ts, src/lib/github-prs.ts, src/lib/github-ci.ts, and src/lib/github-code-search.ts so transient 429 / 5xx responses back off and retry once or twice before throwing — matching the per-call retry already in src/lib/issue-reconciliation.ts.
Expected files: src/lib/github-issues.ts, src/lib/github-prs.ts, src/lib/github-ci.ts, src/lib/github-code-search.ts, src/lib/github-auth.ts (shared helper), and a single new test file pinning 429→retry, 5xx→retry, and 4xx-other→no-retry.
Problem: #795 ("Add retry/backoff on GitHub API fetch calls for transient 429/5xx") was filed at P1 and closed, but its coverage was bounded to src/lib/issue-reconciliation.ts. The remaining GitHub fetchers — github-issues.ts, github-prs.ts, github-ci.ts, github-code-search.ts — still call raw fetch and throw on the first non-2xx response. There are ~20 raw fetch call sites across these four files (fetchIssue, updateIssueLabels, fetchIssueComments, addIssueComment, updateIssueTitleAndBody, removeIssueLabel, syncStatusLabels, closeIssue, fetchPullRequests, fetchPullRequestHealthSignals, fetchPullRequestMergeState, fetchPullRequestCheckFailures, fetchWorkflows, fetchWorkflowRuns, fetchRunJobs, searchRepositoryCode, fetchRepositoryFileText, etc.). Any transient 429 from GitHub aborts the operation; the dispatching agent or scheduled job records a one-shot failure even though the underlying request was recoverable. The retry-on-429 path lives in the GitHub API client spec already (Retry-After header on 429), but the wrapper has not been applied across the surface. fetchPaginated is also not retried — a paginated loop that hits 429 mid-iteration throws and loses the partial results.
Evidence:
- src/lib/github-issues.ts:209-211 (
syncStatusLabels) and the surrounding helpers do not retry; lines 11-209 perform raw fetch + 2xx check. - src/lib/github-prs.ts (158 lines) — every export is a raw fetch + throw-on-!ok.
- src/lib/github-ci.ts (239 lines) — same pattern.
- src/lib/github-code-search.ts (91 lines) — same pattern.
- src/lib/issue-reconciliation.ts:370-380 shows the retry pattern already in use (exponential backoff with jitter, ~3 attempts, 429 + 5xx as the retry set).
- SECURITY-ACCEPTED-RISKS.md and .env.example both call out GitHub rate-limit exposure as a known risk class; not retrying the fetchers is the gap.
Acceptance: Introduce a withGitHubRetry(fn, options) helper in src/lib/github-auth.ts (or a new src/lib/github-retry.ts) that wraps a single fetch call with 2 retries, exponential backoff with jitter, and honours Retry-After on 429 responses. Apply it to every raw await fetch(...) in the four github-*.ts files (excluding fetchPaginated, which is a higher-level loop). Existing tests must remain green. New tests pin: (a) 429 response with Retry-After→retried and succeeds on second try, (b) 503 response → retried, (c) 404 response → no retry, (d) syncStatusLabels loops 5 labels with one transient 429 mid-loop → all 5 still attempted.
Activity
itsmiso-ai commentedon Sep 2, 2026
Not worth doing yet
Items considered during this audit but deliberately not filed as their own issues:
issues/label/route.ts(idempotent add path). The next sync reconciles cached labels against GitHub's actual label set, so any phantom casing is transient and self-heals. Folded into the test fixture pinned by [P3] New POST /api/issues/label route writes GitHub labels with no route.test.ts coverage #803/830, not a standalone fix.mysql2 <3.22.0advisory in npm audit (transitive via prisma 7). Production runtime uses PostgreSQL; the vulnerable mysql2 code path is never executed. The advisory is in the dependency chain so it shows up innpm audit, but it is not exploitable in this deployment. Adding an override would mask the advisory without removing it; pinning a non-vulnerable mysql2 transitively would require a Prisma major bump. Track with Renovate.body as Record<string, unknown>validation across ~20 route handlers. Each route does its own field-by-field validation with explicit type checks. It is verbose but consistent and tested per-route. Replacing it with Zod across the surface is a large mechanical refactor with no behavioural gain — the explicit checks already fail closed on bad input. Revisit only if Zod schemas become useful for shared OpenAPI-style documentation.fetchPullRequestscaps open-PR scans at 200. Only matters for tracked repos with >200 open bot PRs; current tracked repos (misospace/, joryirving/) are far smaller. Revisit if a tracked repo grows past that cap.as anycasts onLESSON_SCHEMAin lesson-feed.ts:122 and on test-fixture mocks. The schema cast pins the OpenAIjson_schemacontract; the test casts are isolated to vi.hoisted blocks. Tightening the production cast requires a generic over the schema type, which is not justified for one call site.fix(github): paginate code-search results and add a co-located client…