AINode.jsAutomation

AI Code Reviewer for Pull Requests

TT
TopicTrick Team
AI Code Reviewer for Pull Requests

AI Code Reviewer for Pull Requests

Code review is a bottleneck in most engineering teams. This bot integrates with GitHub webhooks to automatically review every pull request with GPT-4o — posting inline comments on specific lines and a summary review — catching bugs, security issues, and style problems before a human reviewer even opens the PR.

This is Tool 22 of the Build 50 AI Automation Tools course.


What You'll Build

  • GitHub webhook that triggers on PR open/update events
  • Fetch PR diff via GitHub API
  • GPT-4o review with inline comments posted to GitHub
  • Summary review with overall assessment

Setup

bash
mkdir ai-code-reviewer && cd ai-code-reviewer
npm init -y
npm install express @octokit/rest openai dotenv
bash
# .env
GITHUB_TOKEN=ghp_your-personal-access-token
GITHUB_WEBHOOK_SECRET=your-webhook-secret
OPENAI_API_KEY=sk-your-key-here
PORT=3000

# Optional: restrict to specific repos
ALLOWED_REPOS=yourorg/repo1,yourorg/repo2

GitHub Webhook Validation

js
// src/middleware/validateWebhook.js
import crypto from 'crypto';

export function validateGitHubWebhook(req, res, next) {
  const signature = req.headers['x-hub-signature-256'];
  const payload   = JSON.stringify(req.body);
  const expected  = `sha256=${crypto
    .createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
    .update(payload)
    .digest('hex')}`;

  if (!crypto.timingSafeEqual(Buffer.from(signature || ''), Buffer.from(expected))) {
    return res.status(401).json({ error: 'Invalid webhook signature' });
  }
  next();
}

Code Review Service

js
// src/services/reviewService.js
import { Octokit } from '@octokit/rest';
import OpenAI from 'openai';

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const openai  = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function getPRDiff(owner, repo, pullNumber) {
  const { data } = await octokit.pulls.get({
    owner, repo, pull_number: pullNumber,
    mediaType: { format: 'diff' },
  });
  return data;
}

async function reviewCode(diff, prTitle, prDescription) {
  // Truncate very large diffs
  const truncatedDiff = diff.length > 80_000 ? diff.slice(0, 80_000) + '\n[Diff truncated]' : diff;

  const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: `You are a senior software engineer doing a thorough code review.
Analyze the git diff and identify issues. Focus on:
1. Bugs and logic errors
2. Security vulnerabilities (SQL injection, XSS, hardcoded secrets, missing auth)
3. Performance issues (N+1 queries, missing indexes, inefficient loops)
4. Missing error handling
5. Code clarity and maintainability

Return ONLY a JSON object:
{
  "overallAssessment": "approve | request_changes | comment",
  "summary": "2-3 paragraph overall assessment",
  "score": 1-10,
  "comments": [
    {
      "path": "file path relative to repo root",
      "line": number (line number in the diff where the issue is),
      "severity": "critical | major | minor | suggestion",
      "title": "short issue title",
      "body": "detailed explanation with suggestion for improvement",
      "suggestedFix": "code snippet showing the fix, or null"
    }
  ],
  "positives": ["things done well in this PR"],
  "blockers": ["critical issues that must be fixed before merging"]
}`,
      },
      {
        role: 'user',
        content: `PR Title: ${prTitle}\nDescription: ${prDescription || 'None'}\n\nDiff:\n${truncatedDiff}`,
      },
    ],
    temperature: 0.3,
    response_format: { type: 'json_object' },
  });

  return JSON.parse(response.choices[0].message.content);
}

async function postReview(owner, repo, pullNumber, review, commitSha) {
  const comments = review.comments.map(c => ({
    path: c.path,
    line: c.line,
    body: `**[${c.severity.toUpperCase()}] ${c.title}**\n\n${c.body}${c.suggestedFix ? `\n\n**Suggested fix:**\n\`\`\`\n${c.suggestedFix}\n\`\`\`` : ''}`,
  })).filter(c => c.path && c.line);

  const reviewBody = `## AI Code Review Summary\n\n${review.summary}\n\n` +
    (review.positives.length > 0 ? `### ✅ Positives\n${review.positives.map(p => `- ${p}`).join('\n')}\n\n` : '') +
    (review.blockers.length > 0 ? `### 🚨 Blockers\n${review.blockers.map(b => `- ${b}`).join('\n')}` : '');

  await octokit.pulls.createReview({
    owner,
    repo,
    pull_number: pullNumber,
    commit_id: commitSha,
    body: reviewBody,
    event: review.overallAssessment === 'approve' ? 'APPROVE'
      : review.overallAssessment === 'request_changes' ? 'REQUEST_CHANGES'
      : 'COMMENT',
    comments: comments.length > 0 ? comments : undefined,
  });
}

export async function reviewPullRequest(owner, repo, pullNumber, commitSha, prTitle, prDescription) {
  const diff = await getPRDiff(owner, repo, pullNumber);
  const review = await reviewCode(diff, prTitle, prDescription);
  await postReview(owner, repo, pullNumber, review, commitSha);
  return review;
}

Webhook Server

js
// src/server.js
import 'dotenv/config';
import express from 'express';
import { validateGitHubWebhook } from './middleware/validateWebhook.js';
import { reviewPullRequest } from './services/reviewService.js';

const app = express();
app.use(express.json());

const ALLOWED_REPOS = process.env.ALLOWED_REPOS?.split(',') || [];

app.post('/webhook', validateGitHubWebhook, async (req, res) => {
  const event = req.headers['x-github-event'];

  // Only process pull request events
  if (event !== 'pull_request') return res.json({ ignored: true, event });

  const { action, pull_request: pr, repository } = req.body;
  const repoFullName = repository.full_name;

  // Only process opened/synchronize events
  if (!['opened', 'synchronize'].includes(action)) return res.json({ ignored: true, action });

  // Check allowlist
  if (ALLOWED_REPOS.length > 0 && !ALLOWED_REPOS.includes(repoFullName)) {
    return res.json({ ignored: true, reason: 'Repository not in allowlist' });
  }

  const [owner, repo] = repoFullName.split('/');

  // Respond immediately — GitHub expects a fast response
  res.json({ success: true, message: 'Review started' });

  // Run review asynchronously
  reviewPullRequest(
    owner,
    repo,
    pr.number,
    pr.head.sha,
    pr.title,
    pr.body,
  ).then(review => {
    console.log(`Reviewed PR #${pr.number}: ${review.overallAssessment} (score: ${review.score}/10)`);
  }).catch(err => {
    console.error(`Review failed for PR #${pr.number}:`, err.message);
  });
});

app.get('/health', (_req, res) => res.json({ status: 'ok' }));
app.listen(process.env.PORT ?? 3000, () => console.log('AI Code Reviewer running'));

GitHub Webhook Setup

  1. Go to your repository → SettingsWebhooksAdd webhook
  2. Payload URL: https://your-server.com/webhook
  3. Content type: application/json
  4. Secret: your GITHUB_WEBHOOK_SECRET
  5. Events: Select Pull requests
  6. Click Add webhook

Testing Locally with ngrok

bash
ngrok http 3000
# Use the ngrok URL as your webhook payload URL

Build 50 AI Automation Tools — Tool 22 of 50

GitHub PR reviewer is live. Continue to Tool 23 to build a bug report to GitHub issue converter.


    Summary

    • Webhook signature validation prevents malicious POST requests from triggering reviews
    • Async processing responds to GitHub immediately, then runs the review — avoids webhook timeout
    • Inline comments post to the exact line where the issue was found — not just a general review
    • Severity levels help developers prioritise: fix critical before merging, suggestion is optional
    • Add a cost guardrail — skip review for PRs with diffs over 1000 lines or only documentation changes

    Continue to Tool 23: Bug Report to GitHub Issue Converter →