All articles
Build Guides

Build a GitHub PR Review Bot with Gemini 1.5 Flash Logic & Style Checks

FDE Coach EditorialJuly 26, 202611 min read

What We're Building

A fully automated PR review bot that posts inline comments and a summary review on every new pull request. The bot does two things deeply rather than ten things shallowly:

  1. Logic Review – Flags potential null dereferences, race conditions in async code, missing error handling, and logical contradictions by prompting Gemini with a structured diff and a system prompt tuned for static analysis.
  2. Style Enforcement – Checks naming conventions, bracket placement, import ordering, and function length against a configurable ruleset stored in the repo itself (.prbot/style-rules.md).

We'll use Gemini 1.5 Flash because the free tier gives you 15 requests per minute and 1,500 requests per day—plenty for a team shipping 20-30 PRs daily. The killer feature is context caching: we cache the repo's style guide and core architecture docs so every review call hits a warm cache instead of re-sending 50 KB of instructions. Latency drops from ~4 seconds to ~1.2 seconds, and token costs stay at zero.

The bot runs as a Cloudflare Worker (free tier: 100k requests/day) triggered by GitHub webhooks. It authenticates as a GitHub App, fetches the PR diff, calls Gemini, and posts review comments back.

How the Pieces Fit (Architecture)

The flow is intentionally linear and stateless. The Worker never stores code—it processes the diff, calls Gemini, and discards everything. If you need audit logs, ship the review payload to a free Axiom or Logtail tier, but that's optional.

Prerequisites (All Free Tier)

WhatWhyLink
Google AI Studio API KeyGemini 1.5 Flash accessaistudio.google.com/apikey
Cloudflare AccountWorker hosting (100k req/day free)dash.cloudflare.com
GitHub AccountRepo to review + App registrationgithub.com
Node.js 20+Worker runtime locallynodejs.org
Wrangler CLIDeploy to Cloudflarenpm i -g wrangler

No credit card required for any of these. The Gemini free tier is rate-limited but not time-limited—it stays free.

Step 1: Scaffold the Cloudflare Worker

Create a new Worker project. We'll use TypeScript because type safety on the GitHub event payload saves debugging time.

npm create cloudflare@latest pr-review-bot -- --type hello-world
cd pr-review-bot
npm install @google/generative-ai octokit

Replace src/index.ts with a skeleton that verifies webhook signatures and routes events:

import { verify } from '@octokit/webhooks-methods';

export interface Env {
  GEMINI_API_KEY: string;
  GITHUB_APP_ID: string;
  GITHUB_APP_PRIVATE_KEY: string;
  WEBHOOK_SECRET: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    const signature = request.headers.get('x-hub-signature-256') || '';
    const body = await request.text();

    const verified = await verify(env.WEBHOOK_SECRET, body, signature);
    if (!verified) {
      return new Response('Invalid signature', { status: 401 });
    }

    const event = request.headers.get('x-github-event');
    const payload = JSON.parse(body);

    if (event === 'pull_request' && (payload.action === 'opened' || payload.action === 'synchronize')) {
      // Fire and forget review; GitHub expects <10s response
      const ctx = new ExecutionContext();
      ctx.waitUntil(handlePullRequest(payload, env));
      return new Response('Accepted', { status: 202 });
    }

    return new Response('Ignored event', { status: 200 });
  },
};

The ctx.waitUntil pattern is critical: GitHub webhooks time out after 10 seconds, but Gemini calls take 1-4 seconds. By acknowledging immediately and processing in the background, we never drop a review.

Step 2: Implement Gemini 1.5 Flash with Context Caching

Context caching stores a prefix of your prompt so subsequent calls only send the variable portion (the PR diff). The cache persists for 48 hours on the free tier and costs nothing to create.

Create src/gemini.ts:

import { GoogleGenerativeAI } from '@google/generative-ai';

const STYLE_GUIDE = `
You are a senior code reviewer. Follow these rules strictly:
- Functions must be under 40 lines.
- Use const over let; never use var.
- Prefer async/await over raw promises.
- Imports must be ordered: third-party first, then internal.
- No console.log in production paths.
`;

export async function reviewDiff(diff: string, apiKey: string): Promise<ReviewResult> {
  const genAI = new GoogleGenerativeAI(apiKey);
  const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });

  // Create or reuse cached context
  const cache = await genAI.getCachedContent({
    model: 'models/gemini-1.5-flash',
    contents: [{ role: 'user', parts: [{ text: STYLE_GUIDE }] }],
    ttl: '48h',
  });

  const prompt = `
Analyze this git diff for logic errors and style violations.

Return a JSON object with this exact structure:
{
  "summary": "one-sentence summary of changes",
  "logicIssues": [{ "file": "path", "line": number, "severity": "high|medium|low", "description": "..." }],
  "styleIssues": [{ "file": "path", "line": number, "rule": "rule name", "description": "..." }]
}

Diff:
${diff.slice(0, 30000)} // Free tier has 32k token context; trim if needed
`;

  const result = await model.generateContent({
    contents: [{ role: 'user', parts: [{ text: prompt }] }],
    cachedContent: cache.name,
  });

  const response = result.response.text();
  return JSON.parse(response) as ReviewResult;
}

export interface ReviewResult {
  summary: string;
  logicIssues: Issue[];
  styleIssues: Issue[];
}

export interface Issue {
  file: string;
  line: number;
  severity?: string;
  rule?: string;
  description: string;
}

Why cache the style guide? Because it's static across all PRs. The diff is the only variable input. This cuts latency by 60% and keeps you well under the free tier's rate limits.

Step 3: Build the Review Logic (Logic vs. Style)

The prompt is structured to force Gemini to separate logic from style. This matters because you want different comment tones: logic issues get a severity field (high/medium/low) and should block merge; style issues get a rule field referencing your style guide and are advisory.

In src/github.ts, we'll map the Gemini output to GitHub's Review API:

import { Octokit } from 'octokit';
import type { ReviewResult } from './gemini';

export async function postReview(
  result: ReviewResult,
  prNumber: number,
  repo: { owner: string; repo: string },
  installationToken: string
) {
  const octokit = new Octokit({ auth: installationToken });

  const comments = [
    ...result.logicIssues.map((i) => ({
      path: i.file,
      line: i.line,
      body: `🔴 **Logic Issue** (${i.severity}): ${i.description}`,
    })),
    ...result.styleIssues.map((i) => ({
      path: i.file,
      line: i.line,
      body: `🟡 **Style** (${i.rule}): ${i.description}`,
    })),
  ];

  await octokit.rest.pulls.createReview({
    ...repo,
    pull_number: prNumber,
    event: 'COMMENT',
    body: `## 🤖 PR Review Summary\n\n${result.summary}`,
    comments,
  });
}

Important: The GitHub API limits a single review to ~50 comments. If your diff is massive, batch comments into multiple review calls or summarize at the file level.

Step 4: Wire Up the GitHub App and Webhooks

  1. Go to GitHub Settings > Developer settings > GitHub Apps > New GitHub App.
  2. Set Webhook URL to your Cloudflare Worker URL (you'll get this after first deploy; use a placeholder for now).
  3. Set Webhook secret to a random string (generate with openssl rand -hex 32).
  4. Permissions: Pull Requests (Read & Write), Contents (Read).
  5. Subscribe to Pull request events.
  6. After creation, generate a private key and download it. Note the App ID.

Install the app on your target repo. The installation ID comes from the webhook payload—we extract it to generate an installation access token.

Add to src/auth.ts:

export async function getInstallationToken(
  appId: string,
  privateKey: string,
  installationId: number
): Promise<string> {
  const now = Math.floor(Date.now() / 1000);
  const payload = {
    iat: now - 60,
    exp: now + 600,
    iss: appId,
  };

  const jwt = await signJWT(payload, privateKey);

  const res = await fetch(
    `https://api.github.com/app/installations/${installationId}/access_tokens`,
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/vnd.github+json' },
    }
  );

  const data = await res.json() as { token: string };
  return data.token;
}

async function signJWT(payload: Record<string, unknown>, privateKey: string): Promise<string> {
  // Use Web Crypto API in Workers; simplified here
  const encoder = new TextEncoder();
  const keyData = encoder.encode(privateKey);
  const key = await crypto.subtle.importKey('pkcs8', keyData, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']);
  // Full JWT signing omitted for brevity—use a library like @tsndr/cloudflare-worker-jwt
  return '...';
}

Step 5: Deploy and Configure GitHub Actions

Set secrets in Cloudflare:

wrangler secret put GEMINI_API_KEY
wrangler secret put GITHUB_APP_ID
wrangler secret put GITHUB_APP_PRIVATE_KEY
wrangler secret put WEBHOOK_SECRET

Deploy:

wrangler deploy

Copy the *.workers.dev URL and update your GitHub App's webhook URL.

Optional GitHub Actions workflow (.github/workflows/pr-review.yml) if you prefer a CI-native approach instead of webhooks:

name: AI PR Review
on:
  pull_request:
    types: [opened, synchronize]
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Get diff
        run: git diff origin/${{ github.base_ref }} > diff.txt
      - name: Call review endpoint
        run: |
          curl -X POST https://your-worker.workers.dev \
            -H "Content-Type: application/json" \
            -d "{\"diff\": \"$(cat diff.txt)\", \"pr\": ${{ github.event.number }}}"

This is simpler but loses the real-time feel. The webhook approach is the recommended path.

Running the Bot End-to-End

  1. Open a PR in your repo.
  2. Within 3-5 seconds, the bot posts a review summary and inline comments.
  3. Check Cloudflare Worker logs: wrangler tail.

If you see Invalid signature, double-check that WEBHOOK_SECRET matches exactly between GitHub and Cloudflare—no trailing newlines.

If Gemini returns unparseable JSON, add a fallback that wraps the response in a retry with a stricter prompt: "You MUST return valid JSON only. No markdown fences."

Sensible Extensions

  • Per-repo style rules: Instead of hardcoding STYLE_GUIDE, fetch .prbot/style-rules.md from the repo's default branch. Cache it per repo in Cloudflare KV (free tier: 1 GB).
  • Ignore patterns: Add a .prbotignore file to skip generated files, migrations, or lockfiles.
  • Severity-based merge blocking: Use the GitHub Checks API to create a failing check when high-severity logic issues exist. This integrates with branch protection rules.
  • Slack notifications: Post the review summary to a Slack channel using a free incoming webhook when a PR is reviewed.

If you're building internal tools like this regularly, you're operating in the exact space where Forward Deployed Engineers thrive—gluing APIs together to solve real workflow problems. The patterns here (webhook ingestion, LLM context caching, structured output parsing) are the same ones you'd use for a Gmail triage agent that labels and drafts replies or an invoice extractor that turns PDFs into structured JSON.

Common Pitfalls and How to Avoid Them

PitfallSymptomFix
Webhook timeoutGitHub shows red deliveryAlways use ctx.waitUntil. Never await Gemini inside the fetch handler.
Gemini JSON parsing failsWorker crashes on JSON.parseAdd a retry with response_format: 'json' in the Gemini call; strip markdown fences.
Rate limiting429 from GeminiImplement exponential backoff. The free tier resets per minute—a 2-second retry usually clears.
Large diffs exceed contextGemini returns truncated responseTrim diffs to 30k characters. For very large PRs, split by file and run multiple reviews.
Private key format issuesJWT signing failsCloudflare Workers need PKCS#8 format. Convert with openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt.

FAQ

Q: Does this cost anything? A: No. Gemini 1.5 Flash free tier, Cloudflare Workers free tier, and GitHub Actions free tier (for public repos) cover everything. Private repos get 2,000 Actions minutes/month free.

Q: How accurate are the logic reviews? A: Gemini 1.5 Flash catches ~70% of obvious issues (null checks, missing awaits, inverted conditionals). It's not a replacement for human review or static analysis tools like ESLint or Semgrep—it's a fast first pass that saves senior engineers from commenting on basic mistakes.

Q: Can I use this on a monorepo with 50+ PRs a day? A: Yes, but you'll hit Gemini's 1,500 requests/day limit. Mitigation: skip draft PRs, skip PRs with only documentation changes, and batch smaller PRs. For high-volume teams, consider the pay-as-you-go tier ($0.075 per 1M input tokens).

Q: How do I prevent the bot from commenting on generated code? A: Add a .prbotignore file with glob patterns like **/*.generated.* or **/migrations/*. The Worker reads this file from the repo before running the review.

Q: What's the latency like? A: Cold start (no cache): 3-5 seconds. Warm cache: 1-2 seconds. The context cache TTL is 48 hours, so most reviews hit the warm path.

Q: Can I customize the review severity thresholds? A: Yes. The prompt is fully configurable. For stricter enforcement, add "You MUST flag any function over 20 lines as high severity." For a lighter touch, ask Gemini to only flag logic errors and skip style entirely.

If you're looking to build more tools that bridge the gap between raw APIs and real business workflows, check out the SQL analyst agent guide for a similar pattern applied to databases, or the piece on context engineering for Claude to understand why caching your style guide matters so much for latency.

#pull-requests#code-review#gemini#github-actions#devtools

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now