actions/ai-inferenceを使ったGitHub ActionsによるAIコードレビュー自動化

Cover Image for actions/ai-inferenceを使ったGitHub ActionsによるAIコードレビュー自動化
monotalk
monotalk

actions/ai-inference: An action for calling AI models with GitHub Models でGitHub Actionsから簡単にAIモデルを呼び出せるようになったので、何か利用できないかと考え、Pull Requestのレビューを実施するGitHub Actionsを作成しました。

Copilot コード レビュー は使えるのになぜ、GitHub Actions から利用するのか?

過渡期だと思いますが現状(2025年6月)だと以下のような制約があるためです。

  • pull request上で、Copilotへの指示はできるが少し限定的 GitHub Copilot コードレビュー機能でプルリクエストを日本語でレビューしてもらいたいのようにpull requestのテンプレートにコメントで指示はできるが、コーディング規約を読み込んで指示するという利用はできない。

  • .github/copilot-instructions.md を読み込んでくれる機能がまだ、Public Preview 最近、.github/copilot-instructions.md を読み込んでくれるようになりましたが、まだPublic Previewの段階です。
    これは近いうちにGAされるのでそのタイミングでこのGitHub Actionsは不要になるかなとも考えています。

  • 中央集権的にGitHub Copilotを利用したい 開発チームのメンバー全員がGitHub Copilotを利用できないという状況で、コードレビューのみAIにサポートをしてほしいという状況で利用できるかなと考えています。

作成したGitHub Actions

以下作成したGitHub Actionになります。

manual-code-review-ai.yml

name: 'Manual AI Code Review'

on:
  workflow_dispatch:
    inputs:
      pr_id:
        description: 'Pull Request ID (URL number part)'
        required: true
        type: string
      coding_standard_file:
        description: 'Coding Standard Markdown File (relative path from repository root)'
        required: false
        type: string
        default: '.github/prompts/coding-standards-all.prompt.md'
      ai_model:
        description: 'AI Model to use for code review'
        required: false
        type: string
        default: 'openai/gpt-4o'

# レビュー対象ファイルパターンの定義
env:
  TARGET_FILE_PATTERNS: "\\.(ts|tsx|js|jsx|css|sh)$"
  TARGET_FILE_DESCRIPTION: 'TypeScript, JavaScript, CSS, Shell files'

jobs:
  # PR情報取得とブランチ情報の特定
  fetch-pr-info:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
    outputs:
      source_branch: ${{ steps.pr-info.outputs.source_branch }}
      target_branch: ${{ steps.pr-info.outputs.target_branch }}
      pr_exists: ${{ steps.pr-info.outputs.pr_exists }}
      pr_title: ${{ steps.pr-info.outputs.pr_title }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get PR Information
        id: pr-info
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_ID: ${{ inputs.pr_id }}
        run: |
          # PR情報を取得
          echo "Fetching PR information for PR #$PR_ID"

          # PRが存在するかチェック
          if ! PR_DATA=$(gh pr view $PR_ID --json headRefName,baseRefName,title,state 2>/dev/null); then
            echo "pr_exists=false" >> $GITHUB_OUTPUT
            echo "Error: PR #$PR_ID not found"
            exit 1
          fi

          echo "pr_exists=true" >> $GITHUB_OUTPUT

          # ブランチ情報を抽出
          SOURCE_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName')
          TARGET_BRANCH=$(echo "$PR_DATA" | jq -r '.baseRefName')
          PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
          PR_STATE=$(echo "$PR_DATA" | jq -r '.state')

          echo "source_branch=$SOURCE_BRANCH" >> $GITHUB_OUTPUT
          echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT
          echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT

          echo "PR Information:"
          echo "  Title: $PR_TITLE"
          echo "  State: $PR_STATE"
          echo "  Source Branch: $SOURCE_BRANCH"
          echo "  Target Branch: $TARGET_BRANCH"

  # ブランチチェックアウトとdiff生成(簡素化)
  generate-diff:
    needs: fetch-pr-info
    if: needs.fetch-pr-info.outputs.pr_exists == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      has-changes: ${{ steps.diff.outputs.has-changes }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set UTF-8 encoding
        run: |
          git config --local core.quotepath false
          git config --global core.quotepath false
          export LC_ALL=C.UTF-8
          export LANG=C.UTF-8

      - name: Generate unified diff
        id: diff
        env:
          SOURCE_BRANCH: ${{ needs.fetch-pr-info.outputs.source_branch }}
          TARGET_BRANCH: ${{ needs.fetch-pr-info.outputs.target_branch }}
          TARGET_PATTERNS: ${{ env.TARGET_FILE_PATTERNS }}
          TARGET_DESCRIPTION: ${{ env.TARGET_FILE_DESCRIPTION }}
        run: |
          echo "Generating unified diff between $TARGET_BRANCH and $SOURCE_BRANCH"
          echo "Target file patterns: $TARGET_PATTERNS"
          echo "Target file description: $TARGET_DESCRIPTION"

          # ブランチの最新情報を取得
          git fetch origin $TARGET_BRANCH
          git fetch origin $SOURCE_BRANCH

          # 変更されたファイルを取得(対象ファイルのみ)
          CHANGED_FILES=$(git diff --name-only --diff-filter=AMR origin/$TARGET_BRANCH..origin/$SOURCE_BRANCH)
          echo "Changed files: $CHANGED_FILES"

          # ファイルパターンでフィルタリング(環境変数から取得)
          RELEVANT_FILES=""
          for file in $CHANGED_FILES; do
            if [[ "$file" =~ $TARGET_PATTERNS ]]; then
              if [ -n "$RELEVANT_FILES" ]; then
                RELEVANT_FILES="$RELEVANT_FILES $file"
              else
                RELEVANT_FILES="$file"
              fi
            fi
          done

          if [ -z "$RELEVANT_FILES" ]; then
            echo "has-changes=false" >> $GITHUB_OUTPUT
            echo "No relevant files changed (pattern: $TARGET_PATTERNS)"
            echo "# No relevant changes detected for $TARGET_DESCRIPTION" > unified-diff.txt
            exit 0
          fi

          echo "has-changes=true" >> $GITHUB_OUTPUT
          echo "Relevant files: $RELEVANT_FILES"

          # 統一されたdiffファイルを生成
          echo "Generating unified diff for: $RELEVANT_FILES"
          # --unified=3 文脈も多少含める
          git diff --unified=3 --minimal origin/$TARGET_BRANCH..origin/$SOURCE_BRANCH -- $RELEVANT_FILES > unified-diff.txt

          echo "Unified diff generated (first 50 lines):"
          head -50 unified-diff.txt

      - name: Upload diff file
        if: steps.diff.outputs.has-changes == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: diff-file-${{ github.run_id }}
          path: unified-diff.txt
          retention-days: 1

  # AI レビュー実行(簡素化)
  ai-review:
    needs: [fetch-pr-info, generate-diff]
    if: needs.generate-diff.outputs.has-changes == 'true'
    runs-on: ubuntu-latest
    permissions:
      models: read
      contents: read
      pull-requests: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Download diff file
        uses: actions/download-artifact@v4
        with:
          name: diff-file-${{ github.run_id }}

      - name: Validate coding standard file
        id: validate-file
        env:
          CODING_STANDARD_FILE: ${{ inputs.coding_standard_file }}
        run: |
          if [ -f "$CODING_STANDARD_FILE" ]; then
            echo "file_exists=true" >> $GITHUB_OUTPUT
            echo "file_path=$CODING_STANDARD_FILE" >> $GITHUB_OUTPUT
            echo "Using coding standard file: $CODING_STANDARD_FILE"
          else
            echo "file_exists=false" >> $GITHUB_OUTPUT
            echo "file_path=.github/prompts/coding-standards-all.prompt.md" >> $GITHUB_OUTPUT
            echo "Warning: Specified file '$CODING_STANDARD_FILE' not found, using default"
          fi

      - name: AI Code Review
        id: review
        uses: actions/ai-inference@v1.1.0
        continue-on-error: true
        env:
          AI_MODEL: ${{ inputs.ai_model }}
        with:
          model: ${{ env.AI_MODEL }}
          system-prompt-file: ${{ steps.validate-file.outputs.file_path }}
          prompt-file: unified-diff.txt
          max-tokens: 4000

      - name: Save review to file
        if: steps.review.outcome == 'success' && steps.review.outputs.response != ''
        env:
          RESPONSE: ${{ steps.review.outputs.response }}
          PR_ID: ${{ inputs.pr_id }}
          CODING_STANDARD_FILE: ${{ inputs.coding_standard_file }}
          AI_MODEL: ${{ inputs.ai_model }}
        run: |
          cat > review-result.txt << 'EOF'
          ## 🤖 AI Code Review

          **PR:** #${{ env.PR_ID }}
          **AI Model:** ${{ env.AI_MODEL }}
          **Coding Standard:** ${{ env.CODING_STANDARD_FILE }}

          ${{ env.RESPONSE }}

          ---
          *このレビューは手動実行されたAIによって生成されました*
          EOF

      - name: Upload review result
        if: steps.review.outcome == 'success' && steps.review.outputs.response != ''
        uses: actions/upload-artifact@v4
        with:
          name: review-result-${{ github.run_id }}
          path: review-result.txt
          retention-days: 7

      - name: Handle Review Error
        if: steps.review.outcome == 'failure'
        env:
          PR_ID: ${{ inputs.pr_id }}
          CODING_STANDARD_FILE: ${{ inputs.coding_standard_file }}
          AI_MODEL: ${{ inputs.ai_model }}
        run: |
          echo "⚠️ AIレビューでエラーが発生しました"
          cat > review-error.txt << 'EOF'
          ## ⚠️ AI Code Review - エラー

          **PR:** #${{ env.PR_ID }}
          **AI Model:** ${{ env.AI_MODEL }}
          **Coding Standard:** ${{ env.CODING_STANDARD_FILE }}

          AIレビューの実行中にエラーが発生しました。手動レビューをお願いします。

          ---
          *このメッセージは手動実行されたAIレビュー中のエラーです*
          EOF

      - name: Cleanup
        if: always()
        run: |
          rm -f unified-diff.txt review-*.txt

  # レビュー結果をPRにコメント(簡素化)
  post-review:
    needs: [fetch-pr-info, generate-diff, ai-review]
    if: always() && needs.generate-diff.outputs.has-changes == 'true'
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      actions: read
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Download review result
        uses: actions/download-artifact@v4
        with:
          name: review-result-${{ github.run_id }}
        continue-on-error: true

      - name: Post review result to PR
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_ID: ${{ inputs.pr_id }}
          CODING_STANDARD_FILE: ${{ inputs.coding_standard_file }}
          AI_MODEL: ${{ inputs.ai_model }}
        run: |
          echo "Posting review result to PR #$PR_ID"

          if [ -f "review-result.txt" ]; then
            # レビュー結果ファイルが存在する場合
            cat > final-review.txt << 'EOF'
          # 🤖 Manual AI Code Review Results

          **Requested by:** @${{ github.actor }}
          **AI Model:** ${{ env.AI_MODEL }}
          **Coding Standard File:** ${{ env.CODING_STANDARD_FILE }}
          **Execution ID:** ${{ github.run_id }}

          EOF
            cat review-result.txt >> final-review.txt
            gh pr comment $PR_ID --body-file final-review.txt
            echo "Review posted successfully to PR #$PR_ID"
          else
            # レビューファイルが見つからない場合(エラーまたはレビュー失敗)
            gh pr comment $PR_ID \
              --body "## 🤖 Manual AI Code Review

              **Requested by:** @${{ github.actor }}
              **AI Model:** ${{ env.AI_MODEL }}
              **Coding Standard File:** ${{ env.CODING_STANDARD_FILE }}

              ⚠️ レビュー結果の生成中にエラーが発生しました。ワークフロー実行ログを確認してください。

              **Execution ID:** ${{ github.run_id }}"
            echo "Error message posted to PR #$PR_ID"
          fi

  # アーティファクトのクリーンアップ(簡素化)
  cleanup:
    needs: [fetch-pr-info, generate-diff, ai-review, post-review]
    if: always() && needs.fetch-pr-info.outputs.pr_exists == 'true'
    runs-on: ubuntu-latest
    permissions:
      actions: write
    steps:
      - name: Delete artifacts
        uses: actions/github-script@v7
        with:
          script: |
            const runId = context.runId;
            const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
              owner: context.repo.owner,
              repo: context.repo.repo,
              run_id: runId,
            });

            for (const artifact of artifacts.data.artifacts) {
              if (artifact.name.includes(`-${runId}`)) {
                await github.rest.actions.deleteArtifact({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  artifact_id: artifact.id,
                });
                console.log(`Deleted artifact: ${artifact.name}`);
              }
            }

  # 変更がない場合の通知
  no-changes-notification:
    needs: [fetch-pr-info, generate-diff]
    if: needs.fetch-pr-info.outputs.pr_exists == 'true' && needs.generate-diff.outputs.has-changes == 'false'
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Post no changes notification
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_ID: ${{ inputs.pr_id }}
          CODING_STANDARD_FILE: ${{ inputs.coding_standard_file }}
          AI_MODEL: ${{ inputs.ai_model }}
          TARGET_DESCRIPTION: ${{ env.TARGET_FILE_DESCRIPTION }}
        run: |
          gh pr comment $PR_ID \
            --body "## 🤖 Manual AI Code Review

            **Requested by:** @${{ github.actor }}
            **AI Model:** ${{ env.AI_MODEL }}
            **Coding Standard File:** ${{ env.CODING_STANDARD_FILE }}

            ℹ️ 対象ファイル(${{ env.TARGET_DESCRIPTION }})に変更が検出されませんでした。

            **Execution ID:** ${{ github.run_id }}"

内容の説明

以下、GitHub Copilotに作成してもらった処理概要になります。
pull requestのID、コーディング規約(プロンプト)、モデル名を引数としてworkflowを手動実行すると、レビュー結果がpull requestのコメントに自動で追記されます。

## 📋 **ワークフロー概要**

**目的**: 指定PRに対するAI手動コードレビューの実行  
**実行**: `workflow_dispatch`による手動トリガー  
**対象**: TypeScript、JavaScript、CSS、Shellファイル

## 🔄 **主要処理フロー**

### 1. **PR情報取得** (`fetch-pr-info`)
- 指定PR IDの存在確認・基本情報取得
- ソース/ターゲットブランチの特定

### 2. **差分生成** (`generate-diff`)
- 対象ファイルパターンでフィルタリング
- 統一diffファイル生成・アーティファクト保存

### 3. **AIレビュー実行** (`ai-review`)
- `actions/ai-inference@v1.1.0`でレビュー実行
- 指定コーディング規約ファイルをシステムプロンプトとして使用
- 最大4000トークンでレビュー生成

### 4. **結果投稿** (`post-review`)
- AIレビュー結果をPRにコメント投稿
- エラー時は適切なフィードバック提供

### 5. **後処理**
- **クリーンアップ**: アーティファクト自動削除
- **変更なし通知**: 対象ファイル変更なしの場合

## ⚙️ **パラメータ**

| パラメータ | 必須 | デフォルト値 | 説明 |
|------------|------|--------------|------|
| `pr_id` | ✅ | - | レビュー対象PR番号 |
| `coding_standard_file` | - | coding-standards-all.prompt.md | コーディング規約ファイル |
| `ai_model` | - | `openai/gpt-4o` | 使用AIモデル |

コーディング規約 Markdownファイル

以下のようなファイルを作成して、コードレビューをお願いしています。
これも、Copilotに自動生成してもらったものになります。

アウトプット

GitHub Actionsの実行後は以下のようなアウトプットが得られました。 Pull Requestのコメントとして記載されます。

検討事項

以下の点は検討事項であり、実際に利用しながら調整していく必要があると考えています。

  • ソース差分の検知をどのように行うか?
    プレミアムリクエストを消費してしまうため、actions/ai-inferenceの実行回数はなるべく少なくしたいと考え、gitのdiff全体をInputとして利用しています。ただし、ソースの変更量が多い場合はInputの上限に抵触したり、レビューの精度が落ちる可能性があります。pull requestの状況によっては、拡張子やプロダクションコード・テストコードなどのカテゴリごとにdiffを分割する必要があるかもしれません。その場合はtj-actions/changed-filesのようなGitHub Actionsを使って、ファイルの差分や更新状態ごとにレビューを行う方法も有効だと考えています。

  • pull request先の変更も検知する
    pull request先の変更も検知してレビュー対象にする挙動になっているので、git diffの3点リーダー(...)構文を使う形にするのが良いのかもしれません。
    git diff origin/$TARGET_BRANCH...origin/$SOURCE_BRANCH -- $RELEVANT_FILES

  • コーディングルールのサイズ 現在(2025年6月)1つのファイルに全部入りのコーディングルールをプロンプトに設定しています。各観点ごとに分けた、短めのルールを複数ファイル、一括実行したときの挙動も確認してみたいです。

  • Pull Requestのコンテキストを含める

    • Pull Requestの本文を含めた方が、レビュー精度が上がりそうな気もしています。
  • 実行に失敗することがある
    実際に10ファイル程度の比較的大きなレビューを依頼した際に、変更行数の影響なのかエラーが発生することがあり、エラーになる場合とならない場合があるため、pull requestトリガーでの自動実行よりも手動実行の方が安定するのではないかと感じました。 Image from Gyazo

今後、実施してみたいこと

ai-inferenceはよい感じに、AIモデルの実行手続きがまとめられていて、curlでAPIを直叩きするより大分簡易に実行できると感じました。
現時点でのアイデアとしては、pull requestの概要や、リリースノート作成の自動化あたりを試してみようと考えています。

関連記事