Description
src/lib/github/app.ts:12 states the invariant: "Hard rule: no raw fetch to GitHub anywhere in the codebase." The Octokit install factory wraps every request in a hook that parses x-ratelimit-remaining / x-ratelimit-reset and persists them via updateRateBudget (app.ts:71-98), which feeds checkRateBudget used by all background jobs. The user-sync path never uses Octokit:
src/app/actions/github-sync.ts:49-74 builds headers manually, treats the install token as optional (catch → "proceed without auth — public PRs still visible", lines 54-61), then calls fetch(url, { headers }) (line 67) against the search API (author:<handle>, per_page=100, line 64). If the request fails it silently return [] (lines 72-74).
src/app/actions/github-sync-helpers.ts:50 (fetchMergedCount, search) and :90 (fetchContributionCalendar, GraphQL) also use raw fetch and throw on non-OK (lines 60, 102).
src/inngest/functions/github-stats-sync.ts:39-44 re-uses the same raw helpers in the background job.
src/app/actions/github-sync.ts:156-164 runs these three raw helpers in a Promise.all, so one search-API failure kills the entire sync.
Expected Behavior
All GitHub traffic goes through getInstallOctokit() so tokens, rate-budget, and secondary-rate-limit backoff are centralized, and a token failure fails closed.
Actual Behavior
- Search API quota is a separate, far tighter bucket (≈10/min unauthenticated, ≈30/min per token) than the 5,000/hr core budget that
checkRateBudget tracks. When exhausted, fetchMergedCount throws and the whole Promise.all rejects — the action surfaces github_api_error and the PR backfill is skipped. No retry/backoff, no Retry-After handling.
- Unauthenticated fallback is both leaky and broken: anonymous search requests share the server egress IP's 10/min quota — concurrent syncs can 403 each other. The GraphQL call can't work unauthenticated at all.
- Rate budget goes stale: raw fetches never parse rate-limit headers, so
gh:budget:install:{id} isn't updated by sync traffic, and issues-sweep/pr-backfill can over-consume believing the budget is fresh.
- Behavioral divergence:
fetchAndBackfillPRs swallows errors while the other two helpers throw; github-stats-sync.ts doesn't call fetchAndBackfillPRs at all, so background sync never backfills PRs.
Affected Files
src/lib/github/app.ts (lines 12, 71-98) — Enforced invariant + rate-budget tracking
src/app/actions/github-sync.ts (lines 49-74, 156-164) — Raw fetch, optional token, silent failure
src/app/actions/github-sync-helpers.ts (lines 50, 60, 90, 102) — Raw search + GraphQL fetches
src/inngest/functions/github-stats-sync.ts (lines 39-44) — Background job reusing raw helpers
Proposed Fix
Route all three helpers through getInstallOctokit(installId) using Octokit's search.issues, graphql, and paginated list methods; fail closed when the token can't be minted; add secondary rate-limit handling honoring Retry-After; centralize backfill into one implementation shared by the action and the Inngest job.
Description
src/lib/github/app.ts:12states the invariant: "Hard rule: no raw fetch to GitHub anywhere in the codebase." The Octokit install factory wraps every request in a hook that parsesx-ratelimit-remaining/x-ratelimit-resetand persists them viaupdateRateBudget(app.ts:71-98), which feedscheckRateBudgetused by all background jobs. The user-sync path never uses Octokit:src/app/actions/github-sync.ts:49-74builds headers manually, treats the install token as optional (catch →"proceed without auth — public PRs still visible", lines 54-61), then callsfetch(url, { headers })(line 67) against the search API (author:<handle>,per_page=100, line 64). If the request fails it silentlyreturn [](lines 72-74).src/app/actions/github-sync-helpers.ts:50(fetchMergedCount, search) and:90(fetchContributionCalendar, GraphQL) also use rawfetchandthrowon non-OK (lines 60, 102).src/inngest/functions/github-stats-sync.ts:39-44re-uses the same raw helpers in the background job.src/app/actions/github-sync.ts:156-164runs these three raw helpers in aPromise.all, so one search-API failure kills the entire sync.Expected Behavior
All GitHub traffic goes through
getInstallOctokit()so tokens, rate-budget, and secondary-rate-limit backoff are centralized, and a token failure fails closed.Actual Behavior
checkRateBudgettracks. When exhausted,fetchMergedCountthrows and the wholePromise.allrejects — the action surfacesgithub_api_errorand the PR backfill is skipped. No retry/backoff, noRetry-Afterhandling.gh:budget:install:{id}isn't updated by sync traffic, andissues-sweep/pr-backfillcan over-consume believing the budget is fresh.fetchAndBackfillPRsswallows errors while the other two helpers throw;github-stats-sync.tsdoesn't callfetchAndBackfillPRsat all, so background sync never backfills PRs.Affected Files
src/lib/github/app.ts(lines 12, 71-98) — Enforced invariant + rate-budget trackingsrc/app/actions/github-sync.ts(lines 49-74, 156-164) — Raw fetch, optional token, silent failuresrc/app/actions/github-sync-helpers.ts(lines 50, 60, 90, 102) — Raw search + GraphQL fetchessrc/inngest/functions/github-stats-sync.ts(lines 39-44) — Background job reusing raw helpersProposed Fix
Route all three helpers through
getInstallOctokit(installId)using Octokit'ssearch.issues,graphql, and paginated list methods; fail closed when the token can't be minted; add secondary rate-limit handling honoringRetry-After; centralize backfill into one implementation shared by the action and the Inngest job.