Skip to content

feat(github): poll PRs via one GraphQL call per repo - #42

Merged
borght-dev merged 12 commits into
masterfrom
github-graphql-polling
Jun 4, 2026
Merged

borght-dev merged 12 commits into
masterfrom
github-graphql-polling

Conversation

@borght-dev

Copy link
Copy Markdown
Owner

Summary

Collapses the GitHub polling fan-out (~22 REST calls per repo per cycle: open-PR list + 2N detail/review hydration + N check-runs) into a single PollOpenPrs GraphQL query per repo, while preserving every UI-visible field and feature. Implements docs/superpowers/specs/2026-05-07-github-graphql-polling-design.md.

Key changes

  • New services/github/polling.ts — owns the query and the response adapter. Rollup contexts are mapped to CheckRun-shaped objects and fed through the existing aggregatePrWithChecks, so status semantics (including cancelled-as-non-blocking from 28f0294) match the REST path by construction.
  • PullRequestWithChecks slims down — drops the raw checks: CheckRun[] array; gains totalCheckCount and failedCheckSuiteIds (what the rerun actions actually need). Raw runs survive only as the PR-detail pop-out's local state (REST cold path), threaded to ChecksTab via a new optional PrDetailPanel prop; getPRWithChecks returns { pr, checks } and PR_REFRESHED_EVENT forwards the runs so the Checks tab stays fresh after mutations.
  • Repo-level failure fallback — a failed poll keeps that repo's last-known PRs instead of dropping them for a cycle (previously caused spurious new-PR notifications on recovery). Observed working live against a misconfigured repo.
  • Rate limits split — REST and GraphQL are separate 5000/h pools; the client tracks both, isRateLimitLow trips on either (adaptive polling), graphql() gains start/ok/failed log lines with query name + point cost and proper GitHubAuthError/GitHubRateLimitError handling. Settings shows both pools; the sidebar surfaces the tighter one.
  • Cache migration — pre-migration SQLite-cached PRs lack the new fields and crashed PrCardContainer at boot (failedCheckSuiteIds[0]); loadCachedPRs now re-derives the summary from the legacy checks array.
  • Storybook: polling mock + alias following the per-submodule pattern; also fixed the web-tree-sitter wasm copy path that broke build-storybook under hoisted installs.

Deliberate behavior changes

  • commentCount on polled PRs is now real (comments + reviewThreads totals) — the REST list endpoint had no comment fields, so it was always 0 until a single-PR refresh.
  • mergeable: UNKNOWN maps to undefined (no mergeable_state fallback in GraphQL); resolves on a later poll.
  • Connection caps: labels(20), reviewRequests(20), latestReviews(50), contexts(100) per PR, 100 PRs per repo — truncation past these logs a warning.

Verification

  • 2763 unit tests across 252 files (new: adapter, rate-limit pools, hook rewrite, cache migration)
  • tsc, biome, and build-storybook green; typecheck gated every commit via lefthook
  • Live smoke on real repos: one graphql start/ok pair per repo per cycle (cost=5, GraphQL pool decrementing), zero REST pulls?state=open/check-runs calls in steady state, repo-failure fallback exercised
  • Playwright e2e not run locally (port-1420 collision with a running dev server) — CI gates it

🤖 Generated with Claude Code

borght-dev and others added 12 commits June 4, 2026 13:30
…estWithChecks

Additive step toward dropping the raw checks array from the polled PR shape: aggregatePrWithChecks derives the two new fields, and every fixture/test literal gains them (values coherent with each literal's existing counts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One PollOpenPrs query returns open PRs + check rollups + latest reviews + counts per repo. The adapter maps rollup contexts to CheckRun-shaped lites and reuses aggregatePrWithChecks, so status semantics (including cancelled-as-non-blocking) match the REST path exactly. rateLimit sits at the query root for the client to read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All consumers move off the raw checks array: lengths become totalCheckCount, rerun lookups read failedCheckSuiteIds[0]. The PR-detail pop-out keeps raw CheckRun[] as window-local state threaded to ChecksTab via a new optional PrDetailPanel prop; getPRWithChecks now returns { pr, checks } and PR_REFRESHED_EVENT forwards the runs so the Checks tab stays fresh after mutations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the REST fan-out (open-PRs list + 2N hydration + N check-runs ≈ 22 calls/repo/cycle) with a single PollOpenPrs query. The prior-state snapshot now backs a repo-level fallback: a failed poll keeps that repo's last-known PRs instead of dropping them for a cycle, which previously caused spurious new-PR notifications on recovery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphql calls

GitHub's REST and GraphQL quotas are separate 5000/h pools; the client now tracks them independently (getGraphqlRateLimit) and isRateLimitLow trips on either, so adaptive polling slows when the GraphQL pool runs low. graphql() gains GET-style start/ok/failed log lines (with query name, duration, point cost), reads the inline root rateLimit field, and now raises GitHubAuthError / GitHubRateLimitError like the REST verbs so token-cache invalidation keeps working on a GraphQL-only poll path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e ids replace it

The raw CheckRun array no longer rides along on every polled PR; consumers read totalCheckCount / failedCheckSuiteIds instead. Raw runs survive only as the PR-detail window's local state (REST cold path) and component props. Fixtures export openPrChecks / approvedPrChecks for stories that mock the check fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fetchGitHubRateLimit returns both pools; the settings panel shows two labeled bars, and the polling hook surfaces whichever pool is tighter to the sidebar's rate-limit display (the REST pool no longer updates during polling, so it alone would go stale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the existing per-submodule mock pattern so the barrel re-export of pollOpenPrsAggregate never pulls the real module into story bundles. Also ports vite.config.ts's package-manager-agnostic web-tree-sitter wasm resolution to the storybook config — the literal node_modules path only worked when the package happened to be nested under the app dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stall, e2e port reuse

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cached PullRequestWithChecks entries written before the polling migration carry a raw checks array and lack totalCheckCount / failedCheckSuiteIds; the init sequence seeds the store from this cache, and failedCheckSuiteIds[0] crashed PrCardContainer at boot (ErrorBoundary unmounted the tree before the first poll could overwrite the stale entries). Normalize at the single load boundary by re-deriving the summary through aggregatePrWithChecks from the legacy checks array.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in e2e

The e2e harness answers all api.github.com traffic with a bare [] body, so client.graphql resolved with undefined data and the adapter TypeError'd into a console.error every poll cycle, tripping the render-smoke boot contract on CI. The adapter now degrades to an empty result (warn) when repository.pullRequests is absent, and the e2e route answers /graphql with a well-formed empty repository so cycles no-op like the REST path used to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@borght-dev
borght-dev merged commit 246f945 into master Jun 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant