Setup and context — Why You Need an AI Code Quality Pipeline
Every development team faces the same challenges: code reviews become bottlenecked by a handful of senior engineers, review quality varies depending on who's available, and security issues slip through the cracks when reviewers are pressed for time.
This article walks you through building a comprehensive AI-powered pipeline using Gemini API and GitHub Actions that automatically performs three types of analysis every time a Pull Request is created or updated:
- AI Code Review — Analyzes the diff for bugs, design issues, and improvement opportunities, then posts findings as PR comments
- Security Vulnerability Scanning — Detects hardcoded secrets, injection vulnerabilities, XSS patterns, and other common security issues
- Automated Documentation Generation — Generates JSDoc/docstring suggestions for new or modified functions and classes
The architecture presented here is based on patterns proven in production environments and scales from small teams to large organizations.
This article assumes familiarity with GitHub Actions basics and an interest in leveraging Gemini API for development automation. For a foundational guide on building a basic PR review bot, see Building an AI Code Review Bot with Gemini API and GitHub.
Prerequisites and Environment Setup
Required Tools
- A GitHub repository with Actions enabled
- A Google AI Studio API key (obtain from ai.google.dev)
- Node.js 20+ (pre-installed on GitHub Actions runners)
Configuring GitHub Secrets
Navigate to your repository's Settings → Secrets and variables → Actions and add the following secret:
# Add to your repository's Actions secrets
GEMINI_API_KEY=your-gemini-api-key-hereFor best practices around API key management and error handling, Gemini API Error Handling and Retry Patterns is a helpful reference.
Project Structure
.github/
workflows/
ai-code-review.yml # Main workflow definition
scripts/
ai-reviewer.mjs # Gemini API code review
security-scanner.mjs # Security scanning
doc-generator.mjs # Documentation generation
lib/
gemini-client.mjs # Shared Gemini API client
github-api.mjs # GitHub API helpers
Architecture — A Three-Layer AI Audit Pipeline
The pipeline consists of three independent layers that execute in parallel. Each layer has its own Gemini API prompt and scoring logic, and results are aggregated into a unified PR comment.
Architecture Diagram
PR Created / Updated
│
▼
┌─────────────────────────────────────┐
│ GitHub Actions Workflow │
│ │
│ ┌──────┐ ┌──────┐ ┌──────────┐ │
│ │Review│ │Scan │ │Doc Gen │ │
│ │Layer │ │Layer │ │Layer │ │
│ └──┬───┘ └──┬───┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────┐│
│ │ Result Aggregator ││
│ │ (Summary Comment) ││
│ └─────────────────────────────────┘│
└─────────────────────────────────────┘
│
▼
PR Comment Posted
Shared Gemini API Client
First, let's implement the shared Gemini API client used by all three layers. It includes exponential backoff retry logic for rate limit handling.
// .github/scripts/lib/gemini-client.mjs
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
// Using Gemini 2.5 Flash for cost-efficiency and speed
const model = genAI.getGenerativeModel({
model: "gemini-2.5-flash",
generationConfig: {
temperature: 0.2, // Lower temperature for precision
maxOutputTokens: 8192,
},
});
/**
* Call Gemini API with exponential backoff retry
* @param {string} prompt - The prompt to send
* @param {number} maxRetries - Maximum retry attempts (default: 3)
* @returns {Promise<string>} - Gemini response text
*/
export async function callGemini(prompt, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await model.generateContent(prompt);
return result.response.text();
} catch (error) {
if (attempt === maxRetries) throw error;
// Retry on 429 (Rate Limit) or 503 (Overloaded)
if (error.status === 429 || error.status === 503) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms...`);
await new Promise((r) => setTimeout(r, delay));
} else {
throw error; // Throw immediately for other errors
}
}
}
}Expected behavior: calling callGemini("Hello") returns Gemini's response text, with automatic retries on 429 errors.
Layer 1: AI Code Review
The code review layer fetches the PR diff and sends it to Gemini API for quality analysis.
Designing the Review Prompt
The key to effective AI reviews lies in prompt design. The following prompt provides explicit review criteria, which helps Gemini produce structured, actionable feedback.
// .github/scripts/ai-reviewer.mjs
import { callGemini } from "./lib/gemini-client.mjs";
const REVIEW_PROMPT = `
You are an experienced senior software engineer.
Review the following code diff and return results in JSON format.
## Review Criteria
1. **Bug Risk**: Null references, boundary conditions, async error handling
2. **Design Quality**: SOLID principles, DRY, separation of concerns
3. **Performance**: N+1 queries, unnecessary re-renders, memory leaks
4. **Maintainability**: Naming conventions, magic numbers, comment quality
## Output Format (strict JSON)
{
"score": 0-100,
"summary": "Overall assessment (2-3 sentences)",
"issues": [
{
"severity": "critical|warning|info",
"file": "file path",
"line": line_number,
"message": "Description of the issue",
"suggestion": "Suggested fix (if applicable)"
}
]
}
## Code Diff
`;
/**
* Analyze PR diff and return review results
* @param {string} diff - git diff output
* @returns {Promise<object>} - Review result object
*/
export async function reviewCode(diff) {
// Split into chunks if diff is too large
const MAX_DIFF_LENGTH = 30000; // Account for token limits
if (diff.length > MAX_DIFF_LENGTH) {
return await reviewInChunks(diff);
}
const response = await callGemini(REVIEW_PROMPT + diff);
// Extract and parse JSON from response
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error("Gemini response is not valid JSON");
}
return JSON.parse(jsonMatch[0]);
}
/**
* Review large diffs by splitting into per-file chunks
*/
async function reviewInChunks(diff) {
const fileDiffs = diff.split(/^diff --git/m).filter(Boolean);
const allIssues = [];
let totalScore = 0;
for (const fileDiff of fileDiffs) {
try {
const result = await reviewCode(fileDiff);
allIssues.push(...(result.issues || []));
totalScore += result.score || 0;
} catch (e) {
console.warn(`Skipping chunk: ${e.message}`);
}
}
return {
score: Math.round(totalScore / fileDiffs.length),
summary: `Reviewed ${fileDiffs.length} files in chunked mode.`,
issues: allIssues,
};
}Scoring Criteria
Review scores are calculated on a 100-point scale with the following thresholds:
| Score | Rating | Action |
|---|---|---|
| 90–100 | Excellent | Auto-approve candidate |
| 70–89 | Good | Minor suggestions |
| 50–69 | Needs Work | Fixes required (merge block recommended) |
| 0–49 | Critical | Fixes required (merge block mandatory) |
Layer 2: Security Vulnerability Scanning
The security layer leverages Gemini API's code comprehension capabilities to detect context-dependent vulnerabilities that static analysis tools often miss.
// .github/scripts/security-scanner.mjs
import { callGemini } from "./lib/gemini-client.mjs";
const SECURITY_PROMPT = `
You are a security expert. Analyze the following code for security vulnerabilities.
## Detection Targets
1. **Secret Leakage**: Hardcoded API keys, passwords, tokens
2. **Injection**: SQL, NoSQL, OS command, XSS
3. **Auth Issues**: Authentication bypass, missing authorization checks
4. **Cryptography**: Weak algorithms, insecure random generation
5. **Dependencies**: Known vulnerability patterns
## Output Format (strict JSON)
{
"vulnerabilities": [
{
"severity": "critical|high|medium|low",
"type": "Vulnerability type (e.g., SQL Injection)",
"file": "file path",
"line": line_number,
"description": "Description of the vulnerability",
"remediation": "How to fix it",
"cwe": "CWE number (if applicable)"
}
],
"risk_score": 0-100
}
## Code
`;
/**
* Run security scan on code diff
* @param {string} diff - git diff output
* @returns {Promise<object>} - Vulnerability report
*/
export async function scanSecurity(diff) {
const response = await callGemini(SECURITY_PROMPT + diff);
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
return { vulnerabilities: [], risk_score: 0 };
}
const result = JSON.parse(jsonMatch[0]);
// Log warning for critical/high vulnerabilities
const criticalCount = result.vulnerabilities.filter(
(v) => v.severity === "critical" || v.severity === "high"
).length;
if (criticalCount > 0) {
console.log(`⚠️ ${criticalCount} critical/high vulnerabilities found`);
}
return result;
}Automated Security Report Generation
Scan results are posted as structured Markdown reports in PR comments. When critical or high-severity vulnerabilities are detected, the pipeline can automatically label the PR and block merging.
/**
* Convert security scan results to Markdown report
*/
export function formatSecurityReport(result) {
if (result.vulnerabilities.length === 0) {
return "### 🔒 Security Scan\n\n✅ No vulnerabilities detected.";
}
let report = `### 🔒 Security Scan\n\n`;
report += `**Risk Score**: ${result.risk_score}/100\n\n`;
report += `| Severity | Type | File | Description |\n|----------|------|------|-------------|\n`;
for (const vuln of result.vulnerabilities) {
const icon =
vuln.severity === "critical" ? "🔴" :
vuln.severity === "high" ? "🟠" :
vuln.severity === "medium" ? "🟡" : "🟢";
report += `| ${icon} ${vuln.severity} | ${vuln.type} | \`${vuln.file}:${vuln.line}\` | ${vuln.description} |\n`;
}
return report;
}Layer 3: Automated Documentation Generation
This layer generates JSDoc/docstring suggestions for new or modified functions and classes, posting them as GitHub suggestion comments.
// .github/scripts/doc-generator.mjs
import { callGemini } from "./lib/gemini-client.mjs";
const DOC_PROMPT = `
You are a technical writer. Generate appropriate documentation comments for the following code.
## Rules
1. Use JSDoc (JavaScript/TypeScript) or docstring (Python) format
2. Always include parameter types and descriptions
3. Always include return type and description
4. Summarize the function's purpose in one sentence
5. Include @throws if exceptions are thrown
## Output Format (strict JSON)
{
"suggestions": [
{
"file": "file path",
"line": insertion_line_number,
"doc_comment": "generated documentation comment"
}
]
}
## Code Diff
`;
/**
* Generate documentation for changed code
* @param {string} diff - git diff output
* @returns {Promise<object>} - Documentation suggestions
*/
export async function generateDocs(diff) {
// Only target newly added or modified functions/classes
const addedLines = diff
.split("\n")
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
.join("\n");
// Skip diffs without function definitions
const hasFunctions =
/function\s+\w+|const\s+\w+\s*=\s*(?:async\s*)?\(|class\s+\w+|def\s+\w+/.test(
addedLines
);
if (!hasFunctions) {
return { suggestions: [] };
}
const response = await callGemini(DOC_PROMPT + diff);
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
return { suggestions: [] };
}
return JSON.parse(jsonMatch[0]);
}The GitHub Actions Workflow — Integrated Pipeline Definition
Here's the main workflow definition that orchestrates all three layers. Each layer runs in parallel, and results are aggregated into a single PR comment.
# .github/workflows/ai-code-review.yml
name: AI Code Quality Review
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
ai-review:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for diff
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm install @google/generative-ai @octokit/rest
- name: Get PR diff
id: diff
run: |
git diff origin/${{ github.base_ref }}...HEAD > /tmp/pr-diff.txt
echo "diff_size=$(wc -c < /tmp/pr-diff.txt)" >> $GITHUB_OUTPUT
- name: Run AI Code Review Pipeline
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: node .github/scripts/run-pipeline.mjs /tmp/pr-diff.txtPipeline Execution Script
// .github/scripts/run-pipeline.mjs
import { readFileSync } from "fs";
import { Octokit } from "@octokit/rest";
import { reviewCode } from "./ai-reviewer.mjs";
import { scanSecurity, formatSecurityReport } from "./security-scanner.mjs";
import { generateDocs } from "./doc-generator.mjs";
const diff = readFileSync(process.argv[2], "utf-8");
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const [owner, repo] = process.env.REPO.split("/");
const prNumber = parseInt(process.env.PR_NUMBER);
async function main() {
console.log("🚀 AI Code Quality Pipeline started");
// Run all three layers in parallel
const [review, security, docs] = await Promise.allSettled([
reviewCode(diff),
scanSecurity(diff),
generateDocs(diff),
]);
// Build unified comment
let comment = "## 🤖 AI Code Quality Report\n\n";
// Review results
if (review.status === "fulfilled") {
const r = review.value;
const emoji = r.score >= 90 ? "🟢" : r.score >= 70 ? "🟡" : "🔴";
comment += `### 📝 Code Review ${emoji} ${r.score}/100\n\n`;
comment += `${r.summary}\n\n`;
if (r.issues?.length > 0) {
comment += "| Severity | File | Issue |\n|----------|------|-------|\n";
for (const issue of r.issues) {
comment += `| ${issue.severity} | \`${issue.file}:${issue.line}\` | ${issue.message} |\n`;
}
comment += "\n";
}
} else {
comment += "### 📝 Code Review\n\n⚠️ An error occurred during review.\n\n";
}
// Security results
if (security.status === "fulfilled") {
comment += formatSecurityReport(security.value) + "\n\n";
}
// Documentation suggestions
if (docs.status === "fulfilled" && docs.value.suggestions?.length > 0) {
comment += `### 📄 Documentation Suggestions\n\n`;
comment += `${docs.value.suggestions.length} documentation additions suggested.\n\n`;
}
comment += "\n---\n*Powered by Gemini API + GitHub Actions*";
// Post comment to PR
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: comment,
});
console.log("✅ AI Code Quality Report posted to PR");
// Exit non-zero if critical vulnerabilities found (block merge)
if (
security.status === "fulfilled" &&
security.value.vulnerabilities?.some((v) => v.severity === "critical")
) {
console.error("❌ Critical vulnerabilities detected — blocking merge");
process.exit(1);
}
}
main().catch((error) => {
console.error("Pipeline failed:", error);
process.exit(1);
});Expected output: when a PR is created, the workflow runs automatically and posts a unified report with review results, security findings, and documentation suggestions as a PR comment.
Cost Optimization for Production
Here are design patterns to keep Gemini API costs low while maintaining quality.
Model Selection Strategy
// Choose models based on task requirements and cost
function selectModel(diffSize, scanType) {
// Use Pro for security scanning (accuracy matters most)
if (scanType === "security") {
return "gemini-2.5-pro";
}
// Flash is sufficient for small diffs
if (diffSize < 5000) {
return "gemini-2.5-flash";
}
// Flash for large diffs too (cost-efficiency priority)
return "gemini-2.5-flash";
}Caching Strategy
Prevent redundant scans of the same files by implementing hash-based caching.
import { createHash } from "crypto";
const reviewCache = new Map();
async function cachedReview(fileContent, filePath) {
const hash = createHash("sha256").update(fileContent).digest("hex");
const cacheKey = `${filePath}:${hash}`;
if (reviewCache.has(cacheKey)) {
console.log(`Cache hit: ${filePath}`);
return reviewCache.get(cacheKey);
}
const result = await reviewCode(fileContent);
reviewCache.set(cacheKey, result);
return result;
}API Cost Estimates
| Item | Flash (per PR) | Pro (per PR) |
|---|---|---|
| Input tokens | ~10,000 | ~10,000 |
| Output tokens | ~2,000 | ~2,000 |
| Cost per PR | ~$0.002 | ~$0.02 |
| 100 PRs/month | ~$0.20 | ~$2.00 |
Using Flash as the default with Pro only for security scanning, you can keep costs under $1/month even with 100 PRs.
Advanced: Integrating with Branch Protection Rules
Connect AI review scores with GitHub's branch protection rules to automatically block PRs that don't meet quality standards.
# Branch protection rule configuration
# Settings → Branches → Branch protection rules
#
# Required status checks:
# - "AI Code Quality Review" ← Make this workflow required
#
# Add to ai-code-review.yml:
- name: Quality Gate
if: steps.review.outputs.score < 50
run: |
echo "::error::AI review score is below threshold (50)"
exit 1For real-time processing patterns using streaming, see Gemini API Streaming and Function Calling Guide for additional techniques.
Summary
In this article, we've designed and implemented a three-layer AI code quality pipeline combining Gemini API and GitHub Actions. By integrating automated code review, security vulnerability detection, and documentation generation, development teams can simultaneously improve both productivity and code quality.
The key insight is that AI review doesn't "replace" human review — it complements it. By delegating routine checks to AI, human reviewers can focus on higher-order judgments like architectural fitness and business logic correctness.