AINode.jsAutomation
AI Code Reviewer for Pull Requests
TT
TopicTrick Team
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 dotenvbash
# .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/repo2GitHub 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
- Go to your repository → Settings → Webhooks → Add webhook
- Payload URL:
https://your-server.com/webhook - Content type:
application/json - Secret: your
GITHUB_WEBHOOK_SECRET - Events: Select Pull requests
- Click Add webhook
Testing Locally with ngrok
bash
ngrok http 3000
# Use the ngrok URL as your webhook payload URLBuild 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
criticalbefore merging,suggestionis 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 →
