取り組みの背景 — なぜ AI コード品質監査パイプラインが必要なのか
ソフトウェア開発チームが直面する共通の課題として、コードレビューの属人化とレビュー待ち時間の長期化があります。特にチーム規模が拡大するほど、レビューの品質にばらつきが生じ、セキュリティ上の問題が見落とされるリスクが高まります。
- AI コードレビュー — 変更差分を分析し、バグの可能性・設計上の問題・改善提案をコメントとして投稿
- セキュリティ脆弱性スキャン — ハードコードされたシークレット・SQL インジェクション・XSS などの一般的な脆弱性パターンを検出
- ドキュメント自動生成 — 変更された関数やクラスの JSDoc / docstring を自動生成し、提案コメントとして投稿
この記事で紹介するアーキテクチャは、実際に本番環境で運用されているパターンをベースにしており、小規模チームから大規模チームまでスケーラブルに適用できます。
対象読者は、GitHub Actions の基本的な仕組みを理解しており、Gemini API を活用した開発自動化に関心のあるエンジニアです。基本的な PR レビューボットの構築方法についてはGemini API × GitHub で AI コードレビューBotを構築する完全ガイドをご参照ください。
前提知識と環境準備
必要な環境
- GitHub リポジトリ(Actions が有効化されていること)
- Google AI Studio の API キー(ai.google.dev から取得)
- Node.js 20 以上(GitHub Actions ランナーにプリインストール済み)
GitHub Secrets の設定
リポジトリの Settings → Secrets and variables → Actions に以下のシークレットを登録します。
# GitHub リポジトリの Secrets に登録する値
GEMINI_API_KEY=your-gemini-api-key-hereAPI キーの取得方法やセキュリティのベストプラクティスについては、Gemini API エラーハンドリング&リトライ完全ガイドも参考になります。
プロジェクト構造
.github/
workflows/
ai-code-review.yml # メインワークフロー定義
scripts/
ai-reviewer.mjs # Gemini API コードレビュー
security-scanner.mjs # セキュリティスキャン
doc-generator.mjs # ドキュメント生成
lib/
gemini-client.mjs # Gemini API クライアント(共通)
github-api.mjs # GitHub API ヘルパー
アーキテクチャ設計 — 3 レイヤーの AI 監査パイプライン
パイプラインは3つの独立したレイヤーで構成され、それぞれが並列に実行されます。各レイヤーは独自の Gemini API プロンプトとスコアリングロジックを持ち、結果を GitHub PR コメントとして統合的にフィードバックします。
レイヤー構成図
PR Created / Updated
│
▼
┌─────────────────────────────────────┐
│ GitHub Actions Workflow │
│ │
│ ┌──────┐ ┌──────┐ ┌──────────┐ │
│ │Review│ │Scan │ │Doc Gen │ │
│ │Layer │ │Layer │ │Layer │ │
│ └──┬───┘ └──┬───┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Result Aggregator │ │
│ │ (Summary Comment) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
│
▼
PR Comment Posted
Gemini API クライアントの共通実装
まず、3つのレイヤーが共通で使用する Gemini API クライアントを実装します。レート制限対策として指数バックオフリトライを組み込みます。
// .github/scripts/lib/gemini-client.mjs
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
// Gemini 2.5 Flash を使用(コスト効率と速度のバランス)
const model = genAI.getGenerativeModel({
model: "gemini-2.5-flash",
generationConfig: {
temperature: 0.2, // 精度重視で低めに設定
maxOutputTokens: 8192,
},
});
/**
* 指数バックオフ付き Gemini API 呼び出し
* @param {string} prompt - 送信するプロンプト
* @param {number} maxRetries - 最大リトライ回数(デフォルト: 3)
* @returns {Promise<string>} - Gemini の応答テキスト
*/
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;
// 429 (Rate Limit) または 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; // その他のエラーは即座にスロー
}
}
}
}期待する動作: callGemini("Hello") を実行すると Gemini の応答テキストが返り、429 エラー時は自動的にリトライが行われます。
レイヤー1: AI コードレビュー
コードレビューレイヤーは PR の差分(diff)を取得し、Gemini API に送信して品質分析を行います。
レビュープロンプトの設計
効果的なレビューを行うためのプロンプト設計が重要です。以下のプロンプトは、具体的なレビュー観点を明示することで、Gemini に構造化されたフィードバックを生成させます。
// .github/scripts/ai-reviewer.mjs
import { callGemini } from "./lib/gemini-client.mjs";
const REVIEW_PROMPT = `
あなたは経験豊富なシニアソフトウェアエンジニアです。
以下のコード差分をレビューし、JSON形式で結果を返してください。
## レビュー観点
1. **バグリスク**: null参照、境界値、非同期エラーハンドリング
2. **設計品質**: SOLID原則、DRY、関心の分離
3. **パフォーマンス**: N+1クエリ、不要な再レンダリング、メモリリーク
4. **保守性**: 命名規則、マジックナンバー、コメントの適切性
## 出力形式(厳守)
{
"score": 0-100,
"summary": "全体的な評価(2-3文)",
"issues": [
{
"severity": "critical|warning|info",
"file": "ファイルパス",
"line": 行番号,
"message": "問題の説明",
"suggestion": "改善案のコード(あれば)"
}
]
}
## コード差分
`;
/**
* PR の差分を分析し、レビュー結果を返す
* @param {string} diff - git diff の出力
* @returns {Promise<object>} - レビュー結果オブジェクト
*/
export async function reviewCode(diff) {
// 差分が大きすぎる場合はファイル単位で分割
const MAX_DIFF_LENGTH = 30000; // トークン制限を考慮
if (diff.length > MAX_DIFF_LENGTH) {
return await reviewInChunks(diff);
}
const response = await callGemini(REVIEW_PROMPT + diff);
// JSON 部分を抽出してパース
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error("Gemini response is not valid JSON");
}
return JSON.parse(jsonMatch[0]);
}
/**
* 大きな差分をファイル単位で分割してレビュー
*/
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: `${fileDiffs.length}ファイルを分割レビューしました。`,
issues: allIssues,
};
}スコアリングロジック
レビュースコアは100点満点で算出され、以下の基準でPRの品質を判定します。
| スコア | 判定 | アクション |
|---|---|---|
| 90〜100 | Excellent | 自動承認候補 |
| 70〜89 | Good | 軽微な修正提案 |
| 50〜69 | Needs Work | 要修正(マージブロック推奨) |
| 0〜49 | Critical | 要修正(マージブロック必須) |
レイヤー2: セキュリティ脆弱性スキャン
セキュリティスキャンレイヤーは、Gemini API のコード理解能力を活用して、静的解析ツールでは検出が困難なコンテキスト依存の脆弱性を検出します。
// .github/scripts/security-scanner.mjs
import { callGemini } from "./lib/gemini-client.mjs";
const SECURITY_PROMPT = `
あなたはセキュリティ専門家です。以下のコードをセキュリティ観点で分析してください。
## 検出対象
1. **シークレット漏洩**: APIキー、パスワード、トークンのハードコード
2. **インジェクション**: SQL、NoSQL、OS コマンド、XSS
3. **認証・認可**: 認証バイパス、権限チェック漏れ
4. **暗号化**: 弱いアルゴリズム、安全でないランダム生成
5. **依存関係**: 既知の脆弱性を持つパターン
## 出力形式(JSON厳守)
{
"vulnerabilities": [
{
"severity": "critical|high|medium|low",
"type": "脆弱性の種類(例: SQL Injection)",
"file": "ファイルパス",
"line": 行番号,
"description": "脆弱性の説明",
"remediation": "修正方法",
"cwe": "CWE番号(該当する場合)"
}
],
"risk_score": 0-100
}
## コード
`;
/**
* コード差分のセキュリティスキャンを実行
* @param {string} diff - git diff の出力
* @returns {Promise<object>} - 脆弱性レポート
*/
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]);
// critical または high の脆弱性がある場合はログ出力
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;
}セキュリティレポートの自動生成
スキャン結果は構造化されたMarkdownレポートとしてPRコメントに投稿されます。critical / high レベルの脆弱性が検出された場合は、PRにラベルを自動付与してマージをブロックする仕組みも組み込めます。
/**
* セキュリティスキャン結果を Markdown レポートに変換
*/
export function formatSecurityReport(result) {
if (result.vulnerabilities.length === 0) {
return "### 🔒 セキュリティスキャン\n\n✅ 脆弱性は検出されませんでした。";
}
let report = `### 🔒 セキュリティスキャン\n\n`;
report += `**リスクスコア**: ${result.risk_score}/100\n\n`;
report += `| 深刻度 | 種類 | ファイル | 説明 |\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;
}レイヤー3: ドキュメント自動生成
変更されたコード内の関数やクラスに対して、JSDoc / docstring を自動生成し、GitHub の Suggestion 形式で提案コメントを投稿します。
// .github/scripts/doc-generator.mjs
import { callGemini } from "./lib/gemini-client.mjs";
const DOC_PROMPT = `
あなたはテクニカルライターです。以下のコードに対して適切なドキュメントコメントを生成してください。
## ルール
1. JSDoc(JavaScript/TypeScript)または docstring(Python)形式で記述
2. パラメータの型と説明を必ず含める
3. 戻り値の型と説明を必ず含める
4. 関数の目的を1文で要約する
5. 例外がスローされる場合は @throws を含める
## 出力形式(JSON厳守)
{
"suggestions": [
{
"file": "ファイルパス",
"line": 挿入する行番号,
"doc_comment": "生成されたドキュメントコメント"
}
]
}
## コード差分
`;
/**
* 変更されたコードのドキュメントを自動生成
* @param {string} diff - git diff の出力
* @returns {Promise<object>} - ドキュメント提案
*/
export async function generateDocs(diff) {
// 新規追加または変更された関数・クラスのみを対象にする
const addedLines = diff
.split("\n")
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
.join("\n");
// 関数定義が含まれない差分はスキップ
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]);
}GitHub Actions ワークフロー — 統合パイプライン定義
3つのレイヤーを統合するメインのワークフロー定義です。各レイヤーは並列実行され、最後に結果を統合してPRコメントとして投稿します。
# .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 # 差分取得のため全履歴が必要
- 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.txtパイプライン実行スクリプト
// .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");
// 3つのレイヤーを並列実行
const [review, security, docs] = await Promise.allSettled([
reviewCode(diff),
scanSecurity(diff),
generateDocs(diff),
]);
// 結果を統合コメントとして構築
let comment = "## 🤖 AI Code Quality Report\n\n";
// レビュー結果
if (review.status === "fulfilled") {
const r = review.value;
const emoji = r.score >= 90 ? "🟢" : r.score >= 70 ? "🟡" : "🔴";
comment += `### 📝 コードレビュー ${emoji} ${r.score}/100\n\n`;
comment += `${r.summary}\n\n`;
if (r.issues?.length > 0) {
comment += "| 深刻度 | ファイル | 指摘内容 |\n|--------|---------|----------|\n";
for (const issue of r.issues) {
comment += `| ${issue.severity} | \`${issue.file}:${issue.line}\` | ${issue.message} |\n`;
}
comment += "\n";
}
} else {
comment += "### 📝 コードレビュー\n\n⚠️ レビュー実行中にエラーが発生しました。\n\n";
}
// セキュリティ結果
if (security.status === "fulfilled") {
comment += formatSecurityReport(security.value) + "\n\n";
}
// ドキュメント提案
if (docs.status === "fulfilled" && docs.value.suggestions?.length > 0) {
comment += `### 📄 ドキュメント提案\n\n`;
comment += `${docs.value.suggestions.length}件のドキュメント追加を提案します。\n\n`;
}
comment += "\n---\n*Powered by Gemini API + GitHub Actions*";
// PR にコメントを投稿
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: comment,
});
console.log("✅ AI Code Quality Report posted to PR");
// critical な脆弱性がある場合は非ゼロ終了(CI ブロック)
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);
});期待する出力: PRが作成されるとワークフローが自動実行され、レビュー結果・セキュリティスキャン・ドキュメント提案がPRコメントとして投稿されます。
本番運用のためのコスト最適化
Gemini API の利用コストを抑えながら品質を維持するための設計パターンを紹介します。
モデル選択の戦略
// コスト効率に応じてモデルを使い分ける
function selectModel(diffSize, scanType) {
// セキュリティスキャンは精度重視で Pro を使用
if (scanType === "security") {
return "gemini-2.5-pro";
}
// 小さな差分は Flash で十分
if (diffSize < 5000) {
return "gemini-2.5-flash";
}
// 大きな差分も Flash(コスト効率優先)
return "gemini-2.5-flash";
}キャッシュ戦略
同じファイルに対する重複スキャンを防ぐため、ファイルのハッシュ値をキーにしたキャッシュを導入します。
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 コスト試算
| 項目 | Flash(1PR あたり) | Pro(1PR あたり) |
|---|---|---|
| 入力トークン | 約 10,000 | 約 10,000 |
| 出力トークン | 約 2,000 | 約 2,000 |
| 1PR あたりコスト | 約 $0.002 | 約 $0.02 |
| 月100PR の場合 | 約 $0.20 | 約 $2.00 |
Flash を基本とし、セキュリティスキャンのみ Pro を使用するハイブリッド戦略で、月100PRでも $1未満 に抑えられます。
応用: ブランチ保護ルールとの統合
AI レビュースコアを GitHub のブランチ保護ルールと連携させることで、品質基準を満たさない PR のマージを自動的にブロックできます。
# ブランチ保護ルールの設定例
# Settings → Branches → Branch protection rules
#
# Required status checks:
# - "AI Code Quality Review" ← このワークフローを必須にする
#
# ai-code-review.yml に以下を追加:
- name: Quality Gate
if: steps.review.outputs.score < 50
run: |
echo "::error::AI review score is below threshold (50)"
exit 1ストリーミングを活用したリアルタイム処理パターンについては、Gemini API ストリーミング&Function Calling 実践ガイドも参考にしてください。
まとめ
重要なのは、AI レビューは人間のレビューを「置き換える」のではなく「補完する」ものだということです。ルーティンワークを AI に委ねることで、人間のレビュアーはアーキテクチャの妥当性やビジネスロジックの正確性といった、より高次の判断に集中できるようになります。