0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub Actions × Gemini Flash-Liteで動くPR自動レビューbotを構築する

0
Posted at

【完全無料】GitHub Actions × Gemini APIでPR自動コードレビューを自作する【個人開発・SaaS代節約】

はじめに

個人開発や少人数チームの開発において、「コードレビューをしてくれる相手がいない」 というのは共通の悩みです。
初歩的な構文ミスやロジックバグはもちろん、APIキーの誤コミットやSQLインジェクション脆弱性 などのセキュリティ事故は、マージ前に防ぎたいところです。

CodeRabbitやGitHub Copilotなどの商用レビューツールは優秀ですが、月額15〜40ドル/人ほどのコストがかかります。「個人開発でそこまでの固定費は払えない…」という方に向けて、GitHub ActionsとGoogle Gemini API(無料枠)を組み合わせ、完全0円で自律稼働するPR自動コードレビュー環境 を構築しました。

実際に導入・運用してみて遭遇した 「初心者が引っかかりやすいエラーと対処法」 もまとめていますので、ぜひ参考にしてください。

サンプルリポジトリ

実際に動作確認を行ったコード一式および、AIがレビューコメントを投稿したPRの実行例は以下で公開しています。


全体アーキテクチャ

仕組みは非常にシンプルで、外部サーバーを立てる必要はありません(完全サーバーレス)。

[開発者] --- PR作成/更新 ---> [GitHub リポジトリ]
                                     │ (ワークフロー自動起動)
                                     ▼
                            [GitHub Actions]
                                     │
                                     ├─ 1. git diff で変更差分を取得
                                     ├─ 2. 不要ファイル(lock, 画像等)を除外
                                     ├─ 3. 差分をGemini APIへ送信
                                     │
                                     ▼
                          [Google Gemini API] (flash-lite / flash)
                                     │ (セキュリティ・バグ・設計の解析)
                                     ▼
                           [GitHub REST API]
                                     │
                                     ▼
                             PRへレビューコメント自動投稿!


前提条件・準備

  1. Googleアカウント(Gemini APIキー取得用)
  2. GitHubリポジトリ(動作確認用に新規のテストリポジトリを作成するのがおすすめ)

実装ステップ

1. Gemini APIキーの取得

  1. Google AI Studio にアクセスし、Googleアカウントでログインします。
  2. 左メニューの 「Get API key」「Create API key」 をクリックします。
  3. プロジェクトを選択(または新規作成)し、発行されたAPIキー(AIzaSy...)を控えておきます。

2. GitHubリポジトリ側の設定

リポジトリの Settings から2箇所の設定を行います。

① APIキーの登録(Secrets)

  1. 対象リポジトリの SettingsSecrets and variablesActions を開きます。
  2. New repository secret をクリックし、以下を登録します。
  • Name: GEMINI_API_KEY(完全一致・大文字)
  • Secret: 取得したAPIキー文字列

② PRへの書き込み権限を許可(★重要)

  1. 同じく Settings > 左メニュー ActionsGeneral を開きます。
  2. ページ下部の Workflow permissions「Read and write permissions」 にチェックを入れて Save します。
  • ※デフォルトの読み取り専用のままだと、PRコメント投稿時に 403 Forbidden エラーになります。

3. ファイルの配置

リポジトリ直下に以下の2つのファイルを作成します。

.
└── .github/
    ├── workflows/
    │   └── ai-code-review.yml
    └── scripts/
        └── review_code.py

.github/workflows/ai-code-review.yml

PRの作成・更新を検知してPythonスクリプトを実行する定義です。

name: "AI Automated Code Review"

on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths-ignore:
      - '**/*.md'
      - '**/*.txt'
      - '**/*.png'
      - '**/*.jpg'
      - '**/*.svg'
      - '**/package-lock.json'
      - '**/yarn.lock'
      - '**/pnpm-lock.yaml'
      - '**/poetry.lock'

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout target repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Python Environment
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          # 注意: requirements.txt がない状態で cache: 'pip' を指定するとエラーになるため外しています

      - name: Install Dependencies
        run: |
          python -m pip install --upgrade pip
          pip install google-genai requests

      - name: Run AI Code Review Script
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPOSITORY: ${{ github.repository }}
          BASE_REF: ${{ github.event.pull_request.base.sha }}
          HEAD_REF: ${{ github.event.pull_request.head.sha }}
        run: |
          python .github/scripts/review_code.py

.github/scripts/review_code.py

Git差分を抽出し、フィルタリングした上でGeminiに構造化レビューを生成させ、GitHub PRにコメントするスクリプトです。

import os
import subprocess
import sys
from typing import List
import requests
from google import genai
from google.genai import types

# ------------------------------------------------------------------------------
# 環境変数の検証
# ------------------------------------------------------------------------------
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
PR_NUMBER = os.getenv("PR_NUMBER")
REPOSITORY = os.getenv("REPOSITORY")
BASE_REF = os.getenv("BASE_REF")
HEAD_REF = os.getenv("HEAD_REF")

if not all([GEMINI_API_KEY, GITHUB_TOKEN, PR_NUMBER, REPOSITORY, BASE_REF, HEAD_REF]):
    print("[Error] 必須の環境変数が設定されていません。GitHub Secretsを確認してください。", file=sys.stderr)
    sys.exit(1)

IGNORED_PATTERNS = [
    ".lock", "-lock.json", ".min.js", ".min.css", ".map",
    ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico",
    ".pdf", ".csv", ".tsv", ".json", ".xml"
]
MAX_DIFF_CHAR_LIMIT = 25000  # トークンあふれ・費用防止のための文字数制限

def get_pr_diff() -> str:
    """BaseブランチとHeadブランチ間の差分(unified diff)を抽出"""
    cmd = ["git", "diff", f"{BASE_REF}...{HEAD_REF}"]
    try:
        result = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=True,
            encoding="utf-8",
            errors="replace"
        )
        return result.stdout
    except subprocess.CalledProcessError as e:
        print(f"[Error] git diff の実行に失敗しました: {e.stderr}", file=sys.stderr)
        sys.exit(1)

def filter_diff_content(raw_diff: str) -> str:
    """不要なバイナリや自動生成ファイルの差分を除外・トリミング"""
    filtered_chunks: List[str] = []
    current_chunk: List[str] = []
    skip_current_file = False

    for line in raw_diff.splitlines(keepends=True):
        if line.startswith("diff --git"):
            if current_chunk and not skip_current_file:
                filtered_chunks.append("".join(current_chunk))
            current_chunk = [line]
            skip_current_file = any(pattern in line.lower() for pattern in IGNORED_PATTERNS)
        else:
            if not skip_current_file:
                current_chunk.append(line)

    if current_chunk and not skip_current_file:
        filtered_chunks.append("".join(current_chunk))

    combined_diff = "".join(filtered_chunks)
    if len(combined_diff) > MAX_DIFF_CHAR_LIMIT:
        print(f"[Warning] 差分サイズが上限を超えたため切り詰めます。")
        combined_diff = combined_diff[:MAX_DIFF_CHAR_LIMIT] + "\n\n...[一部の差分はサイズ上限のため省略されました]..."
    return combined_diff

def generate_review_content(diff_text: str) -> str:
    """Gemini APIを呼び出してコードレビューを生成"""
    client = genai.Client(api_key=GEMINI_API_KEY)
    
    system_instruction = """
あなたは最高水準のセキュリティ感覚と可読性基準を持つ、シニアソフトウェアエンジニア兼テックリードです。
提示されたGit差分(Pull Requestのdiff)を検証し、以下の【厳格なレビュー基準】に従って日本語で構造化レビューを作成してください。

【レビュー基準】
1. セキュリティリスクの最優先チェック(APIキーやPWのハードコード、SQLインジェクション、XSS等)
2. 堅牢性とバグの検出(境界値チェック、null参照、リソース解放漏れ等)
3. コード設計と可読性(命名の曖昧さ、不要な複雑性等)

【出力フォーマット】
挨拶などの前置きや結びは一切不要です。以下のMarkdown構造のみを出力してください。

## 🔍 AIコードレビュー サマリー
- **判定**: [✅ LGTM (マージ推奨) / ⚠️ 要改善 (修正推奨) / 🚨 警告 (マージ阻止)]
- **変更の概要**: (2〜3行で簡潔に要約)

## 🛠️ 詳細フィードバック
指摘事項ごとに記述(問題がない場合は「重大な問題は検出されませんでした」と記載):
- **対象ファイル / 該当行**: `ファイルパス` (行番号または関数名)
- **重大度**: [🚨 Critical / ⚠️ Warning / 💡 Suggestion]
- **問題の指摘**: 具体的なリスクや欠陥
- **改善案・修正コード**: 修正コード例(コードブロックで明示)

## 🧪 推奨テストケース
- この変更に対して追加検証すべきテスト観点(1〜3点)
"""
    user_prompt = f"以下のPull Requestの変更差分をレビューしてください。\n```diff\n{diff_text}\n```"

    try:
        # 無料枠の利用制限(RPD)を考慮し、軽量・高速な flash-lite 系を推奨
        response = client.models.generate_content(
            model="gemini-3.5-flash-lite",  # 必要に応じて gemini-3.5-flash 等に変更
            contents=user_prompt,
            config=types.GenerateContentConfig(
                system_instruction=system_instruction,
                temperature=0.2,
            ),
        )
        return response.text.strip()
    except Exception as e:
        print(f"[Error] Gemini API呼び出しに失敗しました: {e}", file=sys.stderr)
        return f"🚨 **AIコードレビュー実行エラー**: Gemini APIの推論中にエラーが発生しました。\n```\n{str(e)}\n```"

def post_github_comment(review_markdown: str):
    """GitHub REST APIでPRにコメント投稿"""
    url = f"https://api.github.com/repos/{REPOSITORY}/issues/{PR_NUMBER}/comments"
    headers = {
        "Authorization": f"Bearer {GITHUB_TOKEN}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }
    payload = {"body": review_markdown}
    try:
        response = requests.post(url, headers=headers, json=payload, timeout=30)
        response.raise_for_status()
        print(f"[Success] PR #{PR_NUMBER} へのレビューコメント投稿が完了しました。")
    except requests.exceptions.RequestException as e:
        print(f"[Error] GitHubコメント投稿に失敗しました: {e}", file=sys.stderr)
        sys.exit(1)

def main():
    print(f"=== PR #{PR_NUMBER} AI自動コードレビュー処理開始 ===")
    raw_diff = get_pr_diff()
    if not raw_diff.strip():
        print("[Info] 差分が存在しません。")
        return
    filtered_diff = filter_diff_content(raw_diff)
    if not filtered_diff.strip():
        print("[Info] 対象となるソースコードの変更がありませんでした。")
        return

    review_result = generate_review_content(filtered_diff)
    post_github_comment(review_result)
    print("=== 全処理が正常に完了しました ===")

if __name__ == "__main__":
    main()


動作確認(テストPRの作成)

実際に脆弱性のあるコードを含んだブランチを作成し、PRを出してみます。

# 1. テストブランチ作成
git checkout -b feature/test-ai-review

# 2. わざと脆弱性のあるコードを作成 (test_sample.py)
cat << 'EOF' > test_sample.py
def fetch_user_data(user_id):
    # ダミーAPIキーのハードコード
    api_key = "AIzaSyFakeKeyTest123456789"
    # SQLインジェクション脆弱性
    query = f"SELECT * FROM users WHERE id = {user_id}"
    return query
EOF

# 3. コミット&プッシュ
git add test_sample.py
git commit -m "feat: add test code"
git push -u origin feature/test-ai-review

GitHub上でPRを作成すると、約20〜30秒後にGitHub Actionsが自動実行され、PRコメントに以下のようなレビューが届きます!

🔍 AIコードレビュー サマリー

  • 判定: 🚨 警告 (マージ阻止)
  • 変更の概要: 新規追加されたテストファイルにハードコードされたAPIキーとSQLインジェクションの脆弱性が含まれています。

🛠️ 詳細フィードバック

  • 対象ファイル / 該当行: test_sample.py (3-8行目)
  • 重大度: 🚨 Critical
  • 問題の指摘: テスト用コードであっても、ハードコードされたAPIキーの混入は流出リスクがあります。また、文字列フォーマットによるSQL構築はSQLインジェクションを引き起こします。
  • 改善案・修正コード: (パラメータ化クエリや環境変数取得のコード例が提示される)

実際に遭遇した「ハマりどころ」と解決策

実装時に引っかかりやすいポイントをまとめました。

1. PATでプッシュ時に workflow scope エラーが出る

.github/workflows/ 配下のファイルをローカルからプッシュしようとすると、以下のエラーで弾かれることがあります。

! [remote rejected] main -> main (refusing to allow a Personal Access Token to create or update workflow ... without `workflow` scope)

  • 原因: GitHubのPersonal Access Token(PAT)に workflow 権限が付与されていないためです。
  • 解決策: GitHubの Settings > Developer settings > Personal access tokens を開き、トークン編集画面で workflow にチェックを入れて保存してください。

2. Actionsが「No file matched to requirements」で即座に失敗する

ワークフロー実行時、Pythonセットアップのステップで落ちるケースです。

No file in ... matched to [**/requirements*]
Process completed with exit code 1.

  • 原因: actions/setup-pythoncache: 'pip' を指定しているのに、リポジトリに requirements.txt が存在しないためです。
  • 解決策: 今回のように直接 pip install でライブラリを入れる場合は、YAMLファイルから cache: 'pip' の行を削除します。

3. スクリプトが「必須の環境変数が設定されていません」で落ちる

  • 原因: リポジトリのSecretsに登録したキー名と、スクリプト側で読み込んでいる名前が不一致(スペルミスや登録漏れ)になっています。
  • 解決策: リポジトリの Settings > Secrets and variables > Actions で、シークレット名が厳密に GEMINI_API_KEY となっているか確認してください。

4. コメント投稿時に 403 Forbidden エラーになる

  • 原因: Actionsのデフォルトトークン(GITHUB_TOKEN)にPRへの書き込み権限がありません。
  • 解決策: リポジトリの Settings > Actions > General > Workflow permissions「Read and write permissions」 に変更してください。

5. Gemini APIのモデル選択と無料枠制限(20回/日の壁)

Geminiの通常Flashモデル(gemini-3.5-flash など)を無料枠で利用する場合、1日のリクエスト上限(RPD)が少なめに設定されている ことがあります。

  • 対策: 個人開発のテストや小規模運用であれば、gemini-3.5-flash-lite を指定するのがおすすめです。軽量モデルでありながら差分レビューの精度は十分で、より緩い制限枠で安定して運用できます。

補足:レビュー投稿時にメールが届くのはなぜ?

PRにAIレビューが投稿された際、GitHubから通知メールが届きます。
これはPythonスクリプトでメール送信しているわけではなく、GitHubの標準通知機能 によるものです。「自分のPRに新しいコメント(Botからのコメント)がついた」ため、GitHubが自動配信しています。通知が不要な場合は、PR画面右側の Notifications > Unsubscribe で個別解除できます。


まとめ

  • 外部サーバー不要・ホスティング代0円
  • GitHub Actionsの無料枠(月2,000分)とGemini APIの無料枠で完全無料運用が可能
  • PRを出すだけで、APIキー露出や初歩的バグを即座にブロック

わずか20分程度の設定で、個人開発環境に「24時間稼働してくれる専属テックリード」を導入できます。ぜひ試してみてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?