Building a Leaner PR Pipeline with AI Code Review: A Step-by-Step Developer Tutorial

1 comment
(Developer Tutorials) - Stop wasting days on PR back-and-forth. This hands-on tutorial shows you how to build an automated AI code review pipeline using GitHub Actions and the Claude API. No fluff. Just working code.

Building a Leaner PR Pipeline with AI Code Review: A Step-by-Step Developer Tutorial

I’ve been maintaining open-source projects for over a decade. And honestly? Code reviews are the worst bottleneck in every team I’ve worked with—from five-person startups to fifty-engineer product teams.

The average PR sits for 2.7 days before a human even looks at it. That’s not an opinion. That’s a real metric I pulled from our internal team’s GitHub insights data last quarter.

AI Coding Tools: How We Built a Validation Pipeline That Catches 94% of AI-Generated Code Bugs Before Review

AI Coding Tools: How We Built a Validation Pipeline That Catches 94% of AI-Generated Code Bugs Before Review

AI Coding Tools: How We Built a Validation Pipeline That Catches 94% of AI-Generated Code Bugs Before Review… ...

But here’s the thing: AI code review isn’t a replacement for senior engineers. It’s a force multiplier. A well-tuned AI review catches the boring stuff—the lint violations, the missing error handling, the security anti-patterns that slip past even experienced devs after eight hours of staring at a screen.

In this tutorial, I’ll walk you through building an AI code review pipeline that you can drop into any GitHub repo in under an hour. We’ll use the Claude API for review logic and GitHub Actions to automate the whole thing.

Outsourcing Software in 2025: Why Vietnam Is the Smartest Bet for Your Engineering Team

Outsourcing Software in 2025: Why Vietnam Is the Smartest Bet for Your Engineering Team

TL;DR: Vietnam is outpacing India and the Philippines in outsourcing software due to higher developer retention, stronger English… ...

You’ll end up with a pipeline that:

  • Triggers automatically on every pull request
  • Reviews diffs and provides actionable feedback
  • Links directly to lines of code so your team knows exactly where to look
  • Blocks merging if critical issues are found

Let’s get into it.

Why Most AI Code Review Tools Fail

I tested five different AI code review tools before building my own. They all shared the same three problems:

  1. They’re too verbose. They write a novel for every PR. Real developers don’t read essays. They want concise, line-specific feedback.
  2. They hallucinate false positives. Nothing kills developer trust faster than an AI telling you a perfectly good lambda is a “potential memory leak.”
  3. They ignore context. A review that doesn’t understand your project’s coding conventions, dependency constraints, or architecture is worse than useless. It’s noise.

The pipeline we’re building avoids all three traps. Here’s how.

The Architecture: What We’re Building

Our AI PR reviewer has four components:

  • A GitHub Actions workflow that triggers when a PR is opened or updated
  • A Python review script that fetches the diff, chunks it, and sends it to the API
  • A Claude API integration with a custom system prompt that enforces concise, line-specific feedback
  • A PR comment action that posts the results directly on the diff

No external services. No monthly subscriptions for a SaaS tool that disappears when you need it. Just your GitHub repo and an API key.

**Pro tip:** This is the exact architecture that our engineering teams in Ho Chi Minh City and Can Tho use internally. It’s battle-tested across 50+ client projects.

Step 1: Set Up Your GitHub Action Workflow

Create a new file at `.github/workflows/ai-code-review.yml`:

yaml
name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths-ignore:
      - '**.md'
      - '**.txt'
      - 'LICENSE'

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      checks: write

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install requests anthropic PyGithub

      - name: Run AI review
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: python scripts/ai_review.py

Notice the `paths-ignore` block. We skip markdown files and license changes. No reason to waste tokens on a README update.

Also notice: we use `fetch-depth: 0` to get the full git history. This matters because we need to diff against the base branch, and GitHub’s shallow clone doesn’t always play nice with that.

Step 2: Write the AI Review Script

Here’s the core logic. Create `scripts/ai_review.py`:

python
import os
import requests
from anthropic import Anthropic
from github import Github

def get_pr_diff():
    """Fetch the full diff for the pull request."""
    g = Github(os.environ['GITHUB_TOKEN'])
    repo = g.get_repo(os.environ['GITHUB_REPOSITORY'])
    pr = repo.get_pull(int(os.environ['PR_NUMBER']))
    
    # Get the diff as a string
    diff_url = pr.diff_url
    headers = {'Authorization': f'token {os.environ["GITHUB_TOKEN"]}'}
    response = requests.get(diff_url, headers=headers)
    response.raise_for_status()
    
    return response.text, pr

def chunk_diff(diff_text, max_chars=60000):
    """Split large diffs into chunks to avoid token limits."""
    if len(diff_text) <= max_chars:
        return [diff_text]
    
    chunks = []
    current_chunk = []
    current_size = 0
    
    for line in diff_text.split('\n'):
        current_chunk.append(line)
        current_size += len(line) + 1  # +1 for newline
        
        if current_size >= max_chars:
            chunks.append('\n'.join(current_chunk))
            current_chunk = []
            current_size = 0
    
    if current_chunk:
        chunks.append('\n'.join(current_chunk))
    
    return chunks

def review_chunk(chunk, client):
    """Send a chunk of diff to Claude for review."""
    system_prompt = """You are a senior code reviewer. Review the following git diff.
    
CRITICAL RULES:
- Only flag REAL problems. No false positives.
- Be concise. Max 3 bullet points per file.
- Reference exact line numbers from the diff.
- Use severity labels: CRITICAL, WARNING, SUGGESTION
- CRITICAL: Security vulnerabilities, data loss, race conditions
- WARNING: Bug-prone patterns, performance issues
- SUGGESTION: Style, readability, refactoring ideas
- If the code looks good, say nothing.

Return your review as JSON:
{
  "reviews": [
    {
      "severity": "WARNING",
      "file": "src/app.ts",
      "line": 42,
      "message": "This async function lacks error handling. Wrap in try-catch."
    }
  ]
}"""

    message = client.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=2000,
        temperature=0.1,
        system=system_prompt,
        messages=[
            {"role": "user", "content": f"Review this diff:\n\n{chunk}"}
        ]
    )
    
    return message.content[0].text

def post_review(reviews, pr):
    """Post review comments on the PR."""
    if not reviews:
        return
    
    # Group by severity for the summary
    critical_count = sum(1 for r in reviews if r['severity'] == 'CRITICAL')
    warning_count = sum(1 for r in reviews if r['severity'] == 'WARNING')
    suggestion_count = sum(1 for r in reviews if r['severity'] == 'SUGGESTION')
    
    summary = f"## AI Code Review Results\n\n"
    summary += f"| Severity | Count |\n|----------|-------|\n"
    summary += f"| 🔴 Critical | {critical_count} |\n"
    summary += f"| 🟡 Warning | {warning_count} |\n"
    summary += f"| 🔵 Suggestion | {suggestion_count} |\n\n"
    
    for review in reviews:
        emoji = '🔴' if review['severity'] == 'CRITICAL' else '🟡' if review['severity'] == 'WARNING' else '🔵'
        summary += f"{emoji} **{review['severity']}**: `{review['file']}` line {review['line']}\n"
        summary += f"> {review['message']}\n\n"
    
    pr.create_issue_comment(summary)

def main():
    # Initialize clients
    client = Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
    
    # Get the diff
    diff_text, pr = get_pr_diff()
    
    if not diff_text.strip():
        print("No diff to review. Skipping.")
        return
    
    # Chunk and review
    chunks = chunk_diff(diff_text)
    all_reviews = []
    
    for chunk in chunks:
        try:
            result = review_chunk(chunk, client)
            import json
            # Parse the JSON response
            parsed = json.loads(result)
            if 'reviews' in parsed and parsed['reviews']:
                all_reviews.extend(parsed['reviews'])
        except Exception as e:
            print(f"Error reviewing chunk: {e}")
            continue
    
    # Post the review
    post_review(all_reviews, pr)
    print(f"Posted review with {len(all_reviews)} comments.")

if __name__ == '__main__':
    main()

That’s a lot of code. Let me explain the critical parts.

The system prompt is the secret sauce. Notice the temperature is set to `0.1`—we don’t want creativity, we want consistency. The JSON output format makes parsing predictable and reliable.

Chunking matters more than you think. A large PR can easily exceed Claude’s context window. We split at 60,000 characters per chunk, which gives the model room to reason without hitting limits.

The severity system is non-negotiable. I’ve found that teams ignore AI reviews that don’t prioritize. When everything is “critical,” nothing is. This three-tier system keeps engineers actually reading the feedback.

Step 3: Add the Required Secrets

In your GitHub repo, go to Settings > Secrets and variables > Actions and add:

  • `ANTHROPIC_API_KEY`: Your Claude API key

That’s it. The `GITHUB_TOKEN` is automatically provided by GitHub Actions.

Step 4: Test It

Open a pull request in your repo. Any PR. The action should trigger within seconds.

Here’s what a real review comment looks like on one of our internal repos:

🟡 **WARNING**: `services/payment_handler.py` line 142

> Stripe API exception caught with a bare `except`. This swallows rate-limit errors (`stripe.error.RateLimitError`) that should trigger a retry. Use `except stripe.error.StripeError` and handle `RateLimitError` explicitly.

That’s the kind of feedback that saves hours of debugging. It’s specific. It tells you exactly what line to look at. And it suggests a concrete fix.

Real Numbers: What Happened When We Deployed This

We rolled this pipeline out across three client projects last quarter. The results were immediate:

Metric Before AI Review After AI Review Improvement
PR cycle time (days) 2.7 1.1 59% faster
Code quality score (SonarQube) 74/100 89/100 +15 points
Production hotfixes/month 4.2 1.8 57% fewer
Senior dev time on reviews (hrs/week) 8.5 3.1 63% less

The team doesn’t ignore the AI. They use it as a first-pass filter. By the time a human reviews the code, the obvious issues are already resolved. That’s where the real time savings come from.

Common Pitfalls (And How to Avoid Them)

Problem 1: The AI flags too many false positives

Fix: Lower the temperature to `0.0` or `0.1`. Also, adjust your system prompt to explicitly tell the model to err on the side of silence. I add: “If you’re not 80% sure it’s a real issue, don’t flag it.”

Problem 2: Review comments are too generic

Fix: Make sure you’re sending the full diff context, not just the changed lines. Our `get_pr_diff()` function does this correctly by fetching the full `.diff` URL.

Problem 3: The Action times out on large PRs

Fix: Increase the chunk size or add a maximum file count filter. I’ve found that skipping files under 10 lines of change (single-line fixes) saves a lot of tokens with no quality loss.

python
def filter_meaningful_changes(diff_text, min_lines=10):
    """Skip trivial changes that don't need AI review."""
    lines = diff_text.split('\n')
    meaningful = []
    current_file_lines = 0
    
    for line in lines:
        if line.startswith('+++ b/'):
            if current_file_lines < min_lines:
                meaningful = [l for l in meaningful if not l.startswith('diff --git')]
            current_file_lines = 0
        elif line.startswith('+') and not line.startswith('+++'):
            current_file_lines += 1
        meaningful.append(line)
    
    return '\n'.join(meaningful)

Why This Approach Works Better Than SaaS Tools

I've been in the software engineering space long enough to see a hundred "AI code review" products come and go. They all look good in demos. They all fall apart in production.

Why? Because they try to be everything to everyone. A generic AI model that's trained on millions of repos can't understand your specific codebase conventions. It doesn't know that in your project, you always use `snake_case` for functions or that you avoid `lodash` in favor of native array methods.

With this pipeline, you own the prompt. Want it to enforce your team's React patterns? Add them to the system prompt. Want it to flag `console.log` statements left in production code? Two lines in the prompt. The customization is infinite.

This is exactly why we use this approach at ECO

Related reading: Why You Should Hire Vietnamese Developers for Your Next Tech Project

Leave a Comment

Your email address will not be published. Required fields are marked *

Ready to Build with AI-Powered Developers?

Hire Vietnamese engineers augmented by ECOA AI Platform + Claude Code. 5x faster, 40% cheaper.