How I Built My GitHub Code Review Assistant
By Kisho Jeyapragash
I built my AI-Powered GitHub Code Review Assistant to make pull request information and review findings easier to inspect in one place. The name describes the broader project; the review workflow I discuss here is the implemented static-analysis pipeline. It connects repository and pull request data with persisted findings and a dashboard, rather than treating a review as a disposable command-line result.
A review needs a specific version of the code
A pull request can change while a review is running. I tied review work to the pull request's head commit SHA so that a finding belongs to a particular version of the code. The backend fetches changed-file metadata and file contents at that SHA, then normalizes analyzer output into review findings. This gives the dashboard something more useful than an unstructured tool transcript.
Keeping the interface and review work separate
I used Next.js for the interface, FastAPI for backend services and PostgreSQL for repository, pull request, review and finding records. Repository synchronization supplies the stored context; the review pipeline consumes that context. A dashboard read should not need to run an analyzer just to show an earlier result.
| Layer | Responsibility |
|---|---|
| Next.js | Repository views, review controls and findings presentation |
| FastAPI | Authentication, scoped APIs, synchronization and review orchestration |
| PostgreSQL | Repository context, reviews, findings, activity and job state |
| Worker | Claim queued work, run bounded analysis and persist results |
GitHub identity and repository access are different concerns
The authentication implementation uses GitHub's authorization-code flow with PKCE. GitHub OAuth identifies the person signing in; GitHub App installations provide a separate model for repository access. I kept those responsibilities distinct instead of assuming that a successful login grants access to every repository. The dashboard and review APIs use account-scoped access checks.
Making review requests safe to repeat
Review requests are associated with the pull request and head SHA. A completed review for that SHA can be reused, and duplicate requests should not create duplicate active analysis. The durable worker model includes bounded attempts, leases and stale-job recovery. These are practical concerns when a request can arrive from a user action as well as an event-processing path.
- Keep the commit SHA with each review so results remain interpretable.
- Persist job state separately from dashboard reads.
- Recheck repository access and pull request state before processing queued work.
- Normalize findings into a shared shape before the frontend renders them.
Static findings belong in a readable dashboard
Synchronize context before analyzing code
I separated synchronization from review execution because they answer different questions. Synchronization records what GitHub currently says about repositories and pull requests. Analysis asks what can be found in the code at one particular commit. Repository access comes through the GitHub App installation, while the stored pull request supplies the head SHA used by the review runner. The dashboard reads the resulting records instead of making every page load fetch and analyze source again.
The distinction also matters for GitHub data types. Issue synchronization excludes records that GitHub marks as pull requests, rather than counting the same work twice. Commit synchronization records commit metadata; it does not turn every stored commit into a claimed push event. I store activity separately so that the interface can distinguish repository state from the events that produced it. That makes filters and dashboard totals easier to interpret.
Receive an event without doing the whole review in the request
A GitHub webhook starts with a signed request, not a trusted instruction. The handler verifies the signature against the raw request body and uses the delivery identifier to avoid processing the same delivery twice. Accepted work is persisted for a worker. Keeping the event receipt and its queued work together avoids relying on a browser request staying open while synchronization or analysis runs.
The review queue has a different identity from the webhook queue. Its meaningful unit is a pull request at a head SHA. Before processing, the worker rechecks repository access, pull request state and the commit being reviewed. A job requested earlier may no longer describe the current pull request. Leases, bounded attempts and stale-job recovery make that state explicit instead of leaving a request looking permanently active after an interrupted process.
await session.execute(insert(ReviewJob).values(
pull_request_id=pull_request.id, head_sha=pull_request.head_sha,
trigger_type=trigger_type,
status="completed" if completed else "queued",
review_id=completed.id if completed else None,
completed_at=datetime.now(UTC) if completed else None,
).on_conflict_do_nothing(index_elements=[ReviewJob.pull_request_id, ReviewJob.head_sha]))Three analyzers, one finding format
The Python analysis pipeline combines Bandit, Ruff and a conservative AST-based validation analyzer. Bandit produces security findings. Ruff runs with repository configuration disabled and selects BLE001 for broad exception handling. The local AST checks examine straightforward parameter flows into SQL-sensitive operations and recognize simple validation evidence. Those checks have a narrower purpose than proving an entire program safe.
I normalize their outputs before persistence. A shared finding has a source, rule, file location, category, severity and explanation. A deterministic fingerprint built from stable fields helps deduplicate findings. Temporary paths, timestamps and generated record identifiers do not belong in that identity: they would make the same finding look different on another run. The frontend can then present one coherent review instead of three unrelated tool formats.
Analyze source without running the submitted application
The runner downloads file contents at the recorded SHA into an application-created temporary directory. It checks paths, file types, sizes and encoding before analysis, and skips unsupported material with reason codes. Analyzer processes use argument arrays, controlled working directories and bounded timeouts. This is static inspection of supported Python source; executing a repository installation script is not part of the review flow.
This boundary shaped the dashboard too. A skipped file, a completed review with no findings and a failed analyzer are different outcomes. Persisted review state lets the interface explain which outcome occurred. Repository, pull request, review and finding lists use server-side pagination and filters, while account-scoped reads keep dashboard summaries aligned with the repositories a person can access.
What I verify at the boundaries
The repository contains backend tests using mocked GitHub transport and session doubles, plus frontend tests for the application views. The useful cases include duplicate work, a changed head SHA, denied access, malformed analyzer output and a worker that loses its lease. Those are different from a live GitHub installation test. For another developer building this kind of system, I would keep both types of verification visible: deterministic tests establish application behavior, while a configured integration check establishes that the external service is connected.
The main lesson is that a review result is more than a message. It has an owner, a repository, a code version and a processing history. Once those are explicit, a dashboard can make the result understandable. That foundation also keeps a possible model-generated review stage separate from the static analysis implemented here.