Azure OpenAIとGitHub Actionsを組み合わせてプルリクエストのAIレビューを実施する手順

# 1. Azure OpenAIを使用したプルリクエストの自動レビューの導入

今やChatGPTなどの生成AIを使用して開発をすることが当たり前になり、多くのエンジニアが生成AIを活用して効率的に業務を進めています。

一方で、セキュリティの観点から、業務に生成AIを使用することは制約を設けている企業も少なくありません。

生成AIを使用して業務の効率化を図りたいが、使用してはならないルールがある。そういった場合、[Azure OpenAI](https://azure.microsoft.com/ja-jp/products/ai-services/openai-service)を活用することで解決できる可能性があります。

**Azure OpenAI**とは、Microsoft社が提供する生成AIモデルを使用してカスタムコパイロットを構築するサービスのことであり、このサービスを使用して高水準のセキュリティでセキュアな環境を構築することができます。

当記事では、その**Azure OpenAI**と**GitHub Actions**を組み合わせてプルリクエストが作成(更新)された時に自動でレビューする仕組みを構築するための手順を紹介します。

なお、こちらの記事では**Azure OpenAI**上でのサービス構築については取り扱いません。

# 2. 自動レビューの概要

ここでのプルリクエストの自動レビューの手順については以下のようなイメージとなります。

![自動レビューイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/d7052391-08e1-264b-5ac1-458292211641.png)

このフローでは、以下の手順で自動レビューが行われます。

1. **プルリクエストの作成・更新**: 開発者がコードの変更をプッシュし、プルリクエストを作成または更新します。

2. **GitHub Actionsのトリガー**: プルリクエストのイベントを検知して、GitHub Actionsが自動的に起動し、変更されたファイルの差分（diff）を取得します。

3. **Azure OpenAIへのリクエスト**: 差分情報をAzure OpenAIのAPIに送信し、コードレビューを依頼します。

4. **レビュー結果の取得**: Azure OpenAIからのレビュー結果を受け取ります。

5. **プルリクエストへのコメント投稿**: 取得したレビュー結果をプルリクエストのコメントとして投稿し、開発者に対して自動レビューの結果を通知します。

これにより、プルリクエストが作成・更新されるたびに、自動でプルリクエスト内に存在する差分に対するコードレビューが行われます。

# 3. GitHub Actionsの設定

1. リポジトリのページで、「**Actions**」タブをクリックします。

   ![Actionsタブを開く](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/a370a983-be1d-ba7c-a1ab-d025034a3dfe.png)

2. 左側の「**New workflow**」ボタンをクリックします。

   ![New workflowをクリック](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/f2290d60-1b46-b729-d472-442b3b2863be.png)

3. 「**set up a workflow yourself**」をクリックします。

   ![set up a workflow yourselfをクリック](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/c0240f64-9a65-03cd-c806-a470f1679115.png)

4. エディタに以下のスクリプトを入力します。
ここではプロジェクト内のSwiftファイルをレビューする仕様とします。

   ![スクリプトを入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/556c840d-1c3f-f3a0-9bfd-35e24b7740ce.png)
   
   ```yaml
   name: ChatGPT Review and Comment

   on:
     pull_request:
       types: [opened, synchronize]

   jobs:
     review_and_comment:
       runs-on: ubuntu-latest
       timeout-minutes: 20

       steps:
       - name: Checkout Repository with Full History
         uses: actions/checkout@v2
         with:
           fetch-depth: 0  # フル履歴を取得

       - name: Find Branch Point Commit
         id: find-base
         run: |
           # 現在のブランチ（HEAD）とベースブランチの分岐点を特定
           BASE_COMMIT=$(git merge-base HEAD origin/${{ github.base_ref }})
           echo "BASE_COMMIT=$BASE_COMMIT" >> $GITHUB_ENV

       - name: Checkout Base Branch at Branch Point
         run: |
           mkdir base
           git --work-tree=base checkout $BASE_COMMIT -- .

       - name: Checkout Head Branch (After Changes)
         run: |
           mkdir head
           git --work-tree=head checkout HEAD -- .

       - name: Install dependencies
         run: |
           sudo apt-get update
           sudo apt-get install -y curl jq diffutils parallel

       - name: Generate Diffs for Each Swift File
         run: |
           mkdir -p diffs
           find head/ -type f -name "*.swift" | while read file; do
             base_file="base/${file#head/}"
             head_file="$file"
             if [ -f "$base_file" ]; then
               diff -u "$base_file" "$head_file" > "diffs/$(basename "$file").diff" || true
             else
               diff -u /dev/null "$head_file" > "diffs/$(basename "$file").diff" || true
             fi

             # 空のdiffファイルを削除
             if [ ! -s "diffs/$(basename "$file").diff" ]; then
               rm "diffs/$(basename "$file").diff"
             fi
           done

       - name: Review Each Diff in Parallel
         env:
           GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESSTOKEN }}
           OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
           OPENAI_ENDPOINT_URL: ${{ secrets.OPENAI_ENDPOINT_URL }}
         run: |
           PR_NUMBER=$(jq -r .pull_request.number $GITHUB_EVENT_PATH)
           PR_TITLE=$(jq -r .pull_request.title $GITHUB_EVENT_PATH)

           process_diff() {
             local PART=$1
             local FILE_NAME=$(basename "$PART" .diff)

             PR_BODY=$(cat "$PART" | sed 's/"/\\"/g' | jq -Rs .)

             JSON_STRING=$(jq -n \
               --arg title "$PR_TITLE" \
               --arg body "$PR_BODY" \
               '{messages: [
                   {role: "system", content: "あなたは優秀なテックリードです。最新のSwift,SwiftUIの基本的なコードの書き方は熟知しており、それらのコンポーネントやiOSアプリのアンチパターンなどに関しても世界でも有数の知見を持っています。
                   以下のルールに従ってコードレビューを行ってください。
                   レビューはプルリクエストが反映されたコードに対して行います。プルリクエストが反映される前のコードを見ることは一切禁じます。
                   以下で記載する修正前はプルリクエストが反映されたコードを指し、修正後はあなたがレビューを行い修正を反映したものです。
                   レビューの結果問題がなければそのファイルについては一切の記載を禁じます。
                   レビューは問題点に対してのみ行い、問題点のないファイルについては記載しないでください。
                   問題点がある場合には修正前と修正後のコードを提示し、どういった観点から修正すべきかを指摘しているかも含めて提示してください。
                   問題文の指摘がある場合、Markdown形式で折り畳みが行えるように指摘全体を<details></details>タグで囲み、<summary></summary>タグで指摘を行うファイル名を囲むようにしてください。
                   "},
                   {role: "user", content: ($title + "\n\n" + $body)}
                 ], max_tokens: 1500}')

             # JSONデータをファイルに保存
             echo "$JSON_STRING" > request.json

             RESPONSE=$(curl -X POST "$OPENAI_ENDPOINT_URL" \
             -H "Content-Type: application/json" \
             -H "api-key: $OPENAI_API_KEY" \
             --data @request.json)

             REVIEW=$(echo "$RESPONSE" | jq -r .choices[0].message.content | sed 's/\n/<br>/g')

             if [ "$REVIEW" != "null" ] && [ -n "$REVIEW" ]; then
               echo "Review for $FILE_NAME" > "reviews/review_$(basename "$PART")"
               echo "$REVIEW" >> "reviews/review_$(basename "$PART")"
             fi
           }

           export -f process_diff
           mkdir -p reviews
           parallel process_diff ::: diffs/*.diff

       - name: Combine Reviews
         run: |
           echo "### Combined Review Comments<br>" > combined_reviews.md
           if [ -z "$(ls -A reviews)" ]; then
             echo "No reviews to combine<br>" >> combined_reviews.md
           else
             for review in reviews/*; do
               cat "$review" >> combined_reviews.md
               echo -e "<br>---<br>" >> combined_reviews.md
             done
           fi

       - name: Post Combined Review Comment
         env:
           GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESSTOKEN }}
         run: |
           PR_NUMBER=$(jq -r .pull_request.number $GITHUB_EVENT_PATH)
           COMBINED_REVIEW=$(cat combined_reviews.md)

           POST_DATA=$(jq -n --arg body "$COMBINED_REVIEW" '{"body": $body}')

           curl -X POST "https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
             -H "Authorization: Bearer $GITHUB_TOKEN" \
             -H "Content-Type: application/json" \
             --data "$POST_DATA"
   ```


以下では、YAMLファイルで行われている主な処理を順番にまとめています。
(※ 実際のGitHub Actionsのステップ数とは必ずしも一致しませんが、全体の流れを4つに大きく分けて解説しています)

### 1. リポジトリのクローンとベースコミットの特定

**フル履歴を取得（checkout）**
```yaml
- name: Checkout Repository with Full History
  uses: actions/checkout@v2
  with:
    fetch-depth: 0
```

リポジトリの全履歴を取得して、後の差分比較が行えるようにします。

**ブランチの分岐点（BASE_COMMIT）を特定**
```yaml
- name: Find Branch Point Commit
  id: find-base
  run: |
    BASE_COMMIT=$(git merge-base HEAD origin/${{ github.base_ref }})
    echo "BASE_COMMIT=$BASE_COMMIT" >> $GITHUB_ENV
```

`git merge-base` で、Pull Requestの`headブランチ`と`baseブランチ`が分岐した共通のコミットを探し、そのコミットハッシュを環境変数として保存します。

### 2. ベースブランチとHEADブランチの状態をワークツリーに展開

**ベースブランチの状態をフォルダに展開**
```yaml
- name: Checkout Base Branch at Branch Point
  run: |
    mkdir base
    git --work-tree=base checkout $BASE_COMMIT -- .
```

分岐点（baseコミット）時点のファイルを`base`フォルダに展開して、差分取得時の「比較元」として利用します。

**HEADブランチ（変更後）の状態をフォルダに展開**
```yaml
- name: Checkout Head Branch (After Changes)
  run: |
    mkdir head
    git --work-tree=head checkout HEAD -- .
```
最新（HEAD）のファイルを`head`フォルダに展開して、差分取得時の「比較先」として利用します。

### 3. Swiftファイルの差分作成とAzure OpenAIへの送信

**差分の生成**
```yaml
- name: Generate Diffs for Each Swift File
  run: |
    mkdir -p diffs
    find head/ -type f -name "*.swift" | while read file; do
      base_file="base/${file#head/}"
      head_file="$file"
      if [ -f "$base_file" ]; then
        diff -u "$base_file" "$head_file" > "diffs/$(basename "$file").diff" || true
      else
        diff -u /dev/null "$head_file" > "diffs/$(basename "$file").diff" || true
      fi

      # 空のdiffファイルを削除
      if [ ! -s "diffs/$(basename "$file").diff" ]; then
        rm "diffs/$(basename "$file").diff"
      fi
    done
```
`head`ディレクトリ内のすべての `.swift` ファイルを対象に、`diff`コマンドでベースとの差分を生成し、`diffs`フォルダに保存します。ファイルが新規追加された場合は `/dev/null` との差分を取得します。
仮に`Swiftファイル`以外をレビューしたい場合にはそのファイルの拡張子を指定することで対象に含めることが可能です。

**OpenAI APIを呼び出してレビューを並列実行**
```yaml
- name: Review Each Diff in Parallel
    env:
        GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESSTOKEN }}
        OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        OPENAI_ENDPOINT_URL: ${{ secrets.OPENAI_ENDPOINT_URL }}
    run: |
        PR_NUMBER=$(jq -r .pull_request.number $GITHUB_EVENT_PATH)
        PR_TITLE=$(jq -r .pull_request.title $GITHUB_EVENT_PATH)
        
        process_diff() {
            local PART=$1
            local FILE_NAME=$(basename "$PART" .diff)
            
            PR_BODY=$(cat "$PART" | sed 's/"/\\"/g' | jq -Rs .)

            JSON_STRING=$(jq -n \
                --arg title "$PR_TITLE" \
                --arg body "$PR_BODY" \
                '{messages: [
                    {role: "system", content: "あなたは優秀なテックリードです。最新のSwift,SwiftUIの基本的なコードの書き方は熟知しており、それらのコンポーネントやiOSアプリのアンチパターンなどに関しても世界でも有数の知見を持っています。
                   以下のルールに従ってコードレビューを行ってください。
                   レビューはプルリクエストが反映されたコードに対して行います。プルリクエストが反映される前のコードを見ることは一切禁じます。
                   以下で記載する修正前はプルリクエストが反映されたコードを指し、修正後はあなたがレビューを行い修正を反映したものです。
                   レビューの結果問題がなければそのファイルについては一切の記載を禁じます。
                   レビューは問題点に対してのみ行い、問題点のないファイルについては記載しないでください。
                   問題点がある場合には修正前と修正後のコードを提示し、どういった観点から修正すべきかを指摘しているかも含めて提示してください。
                   問題文の指摘がある場合、Markdown形式で折り畳みが行えるように指摘全体を<details></details>タグで囲み、<summary></summary>タグで指摘を行うファイル名を囲むようにしてください。
                   "},
                   {role: "user", content: ($title + "\n\n" + $body)}
                 ], max_tokens: 1500}')

             # JSONデータをファイルに保存
             echo "$JSON_STRING" > request.json

             RESPONSE=$(curl -X POST "$OPENAI_ENDPOINT_URL" \
             -H "Content-Type: application/json" \
             -H "api-key: $OPENAI_API_KEY" \
             --data @request.json)

             REVIEW=$(echo "$RESPONSE" | jq -r .choices[0].message.content | sed 's/\n/<br>/g')

             if [ "$REVIEW" != "null" ] && [ -n "$REVIEW" ]; then
               echo "Review for $FILE_NAME" > "reviews/review_$(basename "$PART")"
               echo "$REVIEW" >> "reviews/review_$(basename "$PART")"
             fi
           }

           export -f process_diff
           mkdir -p reviews
           parallel process_diff ::: diffs/*.diff
```

ここでは、`parallel`コマンドを使って `.diff`ファイルを並列処理し、`Azure OpenAI`に差分内容を送信してレビューを行っています。
同時に、レビュー観点を`content`以降に記述することで独自のレビュー観点からの指摘が可能です。

そして、各差分ファイルごとに`JSON`を作成した後、`curl`でAPIを呼び出し、返ってきたレビュー結果をファイルに保存しています。

### 4. レビューコメントをまとめてPull Requestに投稿

**レビュー結果を結合**
```yaml
- name: Combine Reviews
  run: |
    echo "### Combined Review Comments<br>" > combined_reviews.md
    if [ -z "$(ls -A reviews)" ]; then
      echo "No reviews to combine<br>" >> combined_reviews.md
    else
      for review in reviews/*; do
        cat "$review" >> combined_reviews.md
        echo -e "<br>---<br>" >> combined_reviews.md
      done
    fi
```

`reviews` ディレクトリに保存された各ファイルごとのレビュー結果を一つにまとめて、`combined_reviews.md`を作成します。

**Pull Requestのコメントとして投稿**
```yaml
- name: Post Combined Review Comment
  run: |
    PR_NUMBER=$(jq -r .pull_request.number $GITHUB_EVENT_PATH)
    COMBINED_REVIEW=$(cat combined_reviews.md)
    POST_DATA=$(jq -n --arg body "$COMBINED_REVIEW" '{"body": $body}')
    curl -X POST "https://api.github.com/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
      -H "Authorization: Bearer $GITHUB_TOKEN" \
      -H "Content-Type: application/json" \
      --data "$POST_DATA"
```
最後に、生成したレビューコメントを`JSON`として`GitHub API`に送り、`Pull Request`のコメント欄に投稿する、という流れになっています。



# 4. GitHub ActionsのSecret設定

`yaml`ファイル内に記述されている
 - `OPENAI_ENDPOINT_URL`
 - `OPENAI_API_KEY`
 - `PERSONAL_ACCESSTOKEN`

に関しては、ファイル内にハードコードするのはセキュリティ的な観点からよろしくないため、上記の三点をGitHub Actions Secretとして登録します。

Secretの設定手順は以下の通りです。

1.リポジトリのトップページで、「**Settings**」タブをクリックします。

![Group 10694.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/72b6e9b9-b561-9932-1db0-2e819fef05da.png)

2.左側のメニューから「**Secrets and variables**」 > 「**Actions**」を選択します。

![Group 10693.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/24ddd79c-cd6e-278a-54ba-7b3b843dbed7.png)


3.「**New repository secret**」ボタンをクリックし、以下のキーと値をそれぞれ追加します。

![Group 10695.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/6011b580-6cd7-dc22-54ed-f6bc14bef00a.png)


   - `OPENAI_ENDPOINT_URL`: Azure OpenAIのAPIエンドポイントURL
   - `OPENAI_API_KEY`: Azure OpenAIのAPIキー
   - `PERSONAL_ACCESSTOKEN`: GitHubの個人用アクセス トークン

※GitHubの個人用アクセス トークンは、プルリクエストにコメントを投稿するために必要です。トークン作成時に、`repo` 権限を付与してください。
作成方法が分からない場合、[こちら](https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)を参照してください。


# 5. 動作テスト

自動レビューの設定が完了したら、実際にプルリクエストを作成して動作を確認します。

1. **新しいブランチを作成**: リポジトリで新しいブランチを作成し、Swiftファイルを追加または変更します。

2. **プルリクエストを作成**: 変更をプッシュし、ベースブランチに対してプルリクエストを作成します。

3. **ワークフローの実行確認**: プルリクエストを作成すると、GitHub Actionsが自動的にトリガーされます。リポジトリの「Actions」タブからワークフローの実行状況を確認できます。

4. **レビュー結果の確認**: ワークフローが正常に完了すると、プルリクエストに自動レビューのコメントが投稿されます。
成功した場合、以下のようなコメントが自動的にプルリクエストのコメント欄に作成されます。

![Group 10696.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/f76e2091-4113-e0cb-e757-90f3b51a8d5a.png)



# 6. おわりに

以上で、`Azure OpenAI`と`GitHub Actions`を使用したプルリクエストの自動レビューの設定手順について解説しました。

この仕組みを導入することで、コードレビューの効率化や品質向上が期待できます。また、`Azure OpenAI`を使用することで、企業内のセキュリティポリシーに準拠した形で生成AIを活用できます。

ぜひ、自社の開発フローに取り入れてみてください！