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とBedrockでPRレビューBotを作ってみた

Posted at

背景・目的

プルリクエスト(PR)レビューでは、下記のような課題があります。

  • レビューに時間がかかり、開発のボトルネックになる
  • レビュー内容が人によってバラつく
  • 小さな修正でも人手が必要になる

以前は、下記の通りClaudeで試してみましたが、今回はGitHub ActionsとBedrockと組み合わせて
自動でPRをレビューする仕組みを試してみます。

実践

今回、下記のような構成で試しました。

github-action-bedrock-example % tree -a
├── .github
│   ├── scripts
│   │   └── bedrock_review.py
│   └── workflows
│       ├── bedrock-pr-review.yml
│       ├── bedrock-smoke.yml
│       └── test.yml
├── .gitignore
├── demo
│   └── bad_example.py
├── README.md
└── tests
    ├── __init__.py
    └── test_bedrock_review.py
% 

前提

下記の環境で試します

  • Mac
  • Cursor

準備

環境の作成

  1. プロジェクトを作成します

    % gh repo create github-action-bedrock-example --private --clone
    ✓ Created repository XXXX/github-action-bedrock-example on github.com
      https://github.com/XXXX/github-action-bedrock-example
    % 
    
  2. Cursorのワークスペースに、作成したプロジェクトを追加します

AWSの設定

OIDCの設定

  1. AWSにサインインします

  2. IAMに移動します

  3. ナビゲーションペインのIDプロバイダーをクリックします

  4. 「プロバイダを追加」をクリックします

  5. 下記を入力し、「プロバイダを追加」をクリックします

  6. できました
    image.png

ロールと信頼ポリシーの構成

  1. IAMのナビゲーションペインで、「ロール」をクリックします

  2. 「ロールを作成」をクリックします

  3. 下記を指定し、「次へ」をクリックします

    • ①信頼されたエンティティタイプ:ウェブアイデンティティ
    • ②アイデンティティプロバイダー:上記で追加した、IDプロバイダ
    • ③Audience:sts.amazonaws.com(IDプロバイダを選択するとデフォルトで設定)
    • ④GitHub 組織:Githubアカウント
    • ⑤GitHubリポジトリ:リポジトリを指定
    • ⑥GitHub branch:指定なし

    image.png

  4. 許可ポリシーを選択せずに「次へ」をクリックします

  5. ロール名を指定してクリックします

  6. 作成したロールに対して、インラインポリシーを作ります

  7. 下記のアクションを追加し、次へをクリックします

    • bedrock:InvokeModel
    • bedrock:InvokeModelWithResponseStream
  8. 「ポリシーの作成」をクリックします

GitHubの設定

Secretsの設定

  1. GitHubにサインインし、対象リポジトリを選択します

  2. ①「Settings」>②「Secrets and variables > Actions」>③「Secrets」>④「New repository secrets」をクリックします
    image.png

  3. 下記の2つのシークレットを追加します

    • AWS_ID:AWSアカウントID
    • ROLE_NAME:作成したロール名
      image.png

Variableの設定

  1. 事前に今回利用するモデルのIDを調べます

    hirotoshi@MacBook-Pro workflows % aws bedrock list-foundation-models --region us-west-2 \
      --query "modelSummaries[?modelName=='Claude 3.7 Sonnet'].[modelId,modelName]" \
      --output table
    
    --------------------------------------------------------------------
    |                       ListFoundationModels                       |
    +--------------------------------------------+---------------------+
    |  anthropic.claude-3-7-sonnet-20250219-v1:0 |  Claude 3.7 Sonnet  |
    +--------------------------------------------+---------------------+
    % 
    
  2. ①「Settings」>②「Secrets and variables > Actions」>③「Variables」>④「New Repository Variable」をクリックします

  3. 「New envrironment」をクリックします
    image.png

  4. 下記を設定します

    • AWS_REGION
    • MDOEL_ID

MODEL_IDには、下記のように指定します。

To call the US Anthropic Claude 3.7 Sonnet inference profile, specify the following inference profile ID in one of the source Regions:

us.anthropic.claude-3-7-sonnet-20250219-v1:0

https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html

モデルの有効化

  1. 事前にBedrockのモデルを有効化しておきます

  2. 今回は、Claude 3.7 Sonnetをリクエストします
    image.png

  3. 有効化されました
    image.png

疎通

疎通テスト用のコード(bedrock-smoke.yml)

GitHub Actions → AWS OIDC ロール Assume が正しくできるか確認します。

  1. 下記のコードを作成します

    
    name: bedrock-smoke
    on:
      workflow_dispatch:
    
    permissions:
      id-token: write
      contents: read
    
    jobs:
      smoke:
        runs-on: ubuntu-latest
        env:
          AWS_REGION: ${{ vars.AWS_REGION }}   
          MODEL_ID:  ${{ vars.MODEL_ID }}      # 例: anthropic.claude-3-7-sonnet-20250219-v1:0
        steps:
          - uses: actions/checkout@v4
    
          - name: Configure AWS (OIDC)
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::${{ secrets.AWS_ID }}:role/${{ secrets.ROLE_NAME }}
              aws-region: ${{ env.AWS_REGION }}
    
          - name: Show caller identity
            run: aws sts get-caller-identity
    
          - name: Quick Bedrock call (hello)
            run: |
              python - <<'PY'
              import os, json, boto3
              client = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
              body = {
                "anthropic_version":"bedrock-2023-05-31",
                "max_tokens":128,
                "temperature":0,
                "messages":[{"role":"user","content":[{"type":"text","text":"丁寧な日本語で短く挨拶してください。"}]}]
              }
              resp = client.invoke_model(modelId=os.environ["MODEL_ID"], body=json.dumps(body))
              out = json.loads(resp["body"].read())
              print("\n=== Model Response ===\n" + out["content"][0]["text"])
              PY
    
    
    
  2. pushします

  3. 「Actions」> Action名をクリックします
    image.png

  4. 「Run workflow」> 「Run workflow」をクリックします

  5. 成功して、下記のように表示されました

image.png

PRレビューBot

PRが作成/更新されたときに、差分コードを自動でチェックし、AIがレビューコメントを自動投稿する仕組みを作ります。
下記を目的としています。

  • 自動化: 人手を介さずにAIがコードレビュー
  • 品質向上: 潜在的な問題やベストプラクティスを指摘
  • 効率化: レビュー時間の短縮

GitHub Actionsの作成

bedrock-pr-review.yml

  1. 下記のコードを作成します

    # .github/workflows/bedrock-pr-review.yml
    name: bedrock-pr-review
    
    on:
      pull_request:
        types: [opened, synchronize, reopened]
    
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    
    jobs:
      review:
        runs-on: ubuntu-latest
        env:
          AWS_REGION: ${{ vars.AWS_REGION }}   # 例: us-west-2
          MODEL_ID:  ${{ vars.MODEL_ID }}      # 例: us.anthropic.claude-3-7-sonnet-20250219-v1:0(もしくは inference profile ARN)
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0   # すべての履歴を取得
        
          - name: Configure AWS (OIDC)
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::${{ secrets.AWS_ID }}:role/${{ secrets.ROLE_NAME }}
              aws-region: ${{ env.AWS_REGION }}
    
          - name: Set up Python
            uses: actions/setup-python@v5
            with:
              python-version: "3.11"
    
          - name: Install deps
            run: pip install boto3
    
          - name: Collect diff
            id: diff
            shell: bash
            run: |
              set -euo pipefail
              BASE_SHA='${{ github.event.pull_request.base.sha }}'
              HEAD_SHA='${{ github.event.pull_request.head.sha }}'
              
              # base/head のコミットだけ浅く取得
              git fetch --no-tags --prune --depth=2 origin "$BASE_SHA" "$HEAD_SHA"
              
              TMP="$(mktemp)"
              trap 'rm -f "$TMP"' EXIT
              
              # パイプのSIGPIPEで失敗しないよう一時的にpipefailを無効化
              set +o pipefail
              if git merge-base --is-ancestor "$BASE_SHA" "$HEAD_SHA"; then
                git diff --unified=0 "$BASE_SHA...$HEAD_SHA" -- \
                  . ':(exclude)*.lock' ':(exclude)*.min.*' \
                  ':(exclude)package-lock.json' ':(exclude)pnpm-lock.yaml' \
                | head -c 180000 > "$TMP"
              else
                git diff --unified=0 "$BASE_SHA" "$HEAD_SHA" -- \
                  . ':(exclude)*.lock' ':(exclude)*.min.*' \
                  ':(exclude)package-lock.json' ':(exclude)pnpm-lock.yaml' \
                | head -c 180000 > "$TMP"
              fi
              set -o pipefail
              
              # Base64エンコードで安全に出力(特殊文字対策)
              DIFF_B64=$(cat "$TMP" | base64 -w 0)
              echo "DIFF_B64=$DIFF_B64" >> "$GITHUB_OUTPUT"
              
              # 念のため通常の形式でも出力(デバッグ用)
              {
                echo "DIFF<<EOF"
                cat "$TMP"
                echo ""  # 確実に改行を追加
                echo "EOF"
              } >> "$GITHUB_OUTPUT"
    
          - name: Run Bedrock review
            id: bedrock
            env:
              DIFF: ${{ steps.diff.outputs.DIFF }}
              DIFF_B64: ${{ steps.diff.outputs.DIFF_B64 }}
            run: |
              # Bedrockレビューを実行して結果を保存
              REVIEW_OUTPUT=$(python .github/scripts/bedrock_review.py)
              
              # GitHub Actionsの出力として設定
              {
                echo "REVIEW<<EOF"
                echo "$REVIEW_OUTPUT"
                echo "EOF"
              } >> "$GITHUB_OUTPUT"
    
          - name: Post review comment
            uses: actions/github-script@v7
            env:
              REVIEW_OUTPUT: ${{ steps.bedrock.outputs.REVIEW }}
            with:
              script: |
                const reviewOutput = process.env.REVIEW_OUTPUT || '';
                const body = `## 🤖 Bedrock Review\n\n${reviewOutput}`;
                
                console.log('Review output length:', reviewOutput.length);
                console.log('Review preview:', reviewOutput.substring(0, 200));
                
                github.rest.issues.createComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: context.issue.number,
                  body
                })
    
    

    下記の処理を行っています。

    • PRの変更内容を取得
    • レビュー(スクリプトを呼び出す)
    • 結果をポスト
  2. 上記のコードをGitHubにPushします

レビュースクリプト(Python)の作成

bedrock_review.py

  1. 下記のコードを作成します

    #!/usr/bin/env python3
    import os, sys, json, boto3
    
    def main():
        # Base64エンコードされた差分があるかチェック
        if "DIFF_B64" in os.environ:
            import base64
            diff = base64.b64decode(os.environ["DIFF_B64"]).decode("utf-8")
        else:
            diff = sys.stdin.read().strip()
        
        if not diff:
            print("差分がありません(スキップ)。")
            return
    
        MAX_BYTES = 180_000
        b = diff.encode("utf-8")
        if len(b) > MAX_BYTES:
            diff = b[:MAX_BYTES].decode("utf-8", errors="ignore") + \
                    "\n\n[...truncated for cost guardrail...]"
    
        prompt = f"""You are a senior software reviewer.
    Review the following unified diff and list:
    1) Critical issues (security, correctness)
    2) Suggested improvements (readability, performance)
    3) Tests to add
    Output in GitHub Markdown bullets, concise Japanese.
    Diff:
    {diff}
    """
    
        client = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
        body = {
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 1200,
            "temperature": 0.2,
            "messages": [
                {"role": "user", "content": [{"type": "text", "text": prompt}]}
            ],
        }
        resp = client.invoke_model(modelId=os.environ["MODEL_ID"], body=json.dumps(body))
        out = json.loads(resp["body"].read())
        text = out["content"][0]["text"]
        print(text)
    
    if __name__ == "__main__":
        main()
    
    

    下記の処理を行っています。

    • 入力を受け取り、DIFFを取ります
    • コスト抑制のためDIFFを指定したサイズで切り落とします
    • シニアソフトウェアエンジニア(レビューアー)として、下記をレビューします(してもらいます)
      • セキュリティ、正確性
      • 可読性、性能
      • テストの提案
    • アウトプットは、Markdownの箇条書き(日本語で)
  2. 上記のコードをGitHubにPushします

実行と確認

  1. ブランチを作成します
    % git checkout -b test origin/main
    branch 'test' set up to track 'origin/main'.
    Switched to a new branch 'test'
    % 
    

テストコードの作成(bad_example.py)

意図的に指摘されるコードを作ります。

  1. 問題のあるコードを作成します

    import os, requests
    
    def divide(a, b):
        # TODO: zero division not handled
        return a / b
    
    def run_user_code(src):
        # DANGEROUS: arbitrary code execution
        return eval(src)
    
    def fetch(url):
        # BAD: TLS verification disabled
        r = requests.get(url, timeout=5, verify=False)
        return r.text[:100]
    
    # Hardcoded secret (for bot to flag)
    API_TOKEN = "sk_test_1234567890"
    
    if __name__ == "__main__":
        print(divide(10, 0))
    
  2. GitHubにPushします

  3. PRを作成します

  4. しばらくすると、GitHub Actionsが実行されます
    image.png

  5. 終了しました
    image.png

  6. でました!指摘されました!
    image.png

  • bad_example.pyだけではなく、GitHub Actionsのコードも指摘されています

レビュー指摘対応

下記のほか、テストコードも追加しています。

bedrock-pr-review.yml
  1. 上記で指摘された内容を取り込むためコードを修正します

    # .github/workflows/bedrock-pr-review.yml
    name: bedrock-pr-review
    
    on:
      pull_request:
        types: [opened, synchronize, reopened]
    
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    
    jobs:
      review:
        runs-on: ubuntu-latest
        env:
          AWS_REGION: ${{ vars.AWS_REGION }}   # 例: us-west-2
          MODEL_ID:  ${{ vars.MODEL_ID }}      # 例: us.anthropic.claude-3-7-sonnet-20250219-v1:0(もしくは inference profile ARN)
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0   # すべての履歴を取得
        
          - name: Configure AWS (OIDC)
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::${{ secrets.AWS_ID }}:role/${{ secrets.ROLE_NAME }}
              aws-region: ${{ env.AWS_REGION }}
    
          - name: Set up Python
            uses: actions/setup-python@v5
            with:
              python-version: "3.11"
    
          - name: Install deps
            run: pip install boto3
    
          - name: Collect diff
            id: diff
            shell: bash
            run: |
              set -euo pipefail
              
              # PRイベントからベース/ヘッドのコミットSHAを取得
              # フォールバック: PRイベントがない場合はエラーで終了
              BASE_SHA='${{ github.event.pull_request.base.sha }}'
              HEAD_SHA='${{ github.event.pull_request.head.sha }}'
              
              if [[ -z "$BASE_SHA" || -z "$HEAD_SHA" ]]; then
                echo "⚠️ PR情報が取得できません。"
                echo "このワークフローはPRでのみ動作します。"
                exit 1
              fi
              
              echo "=== Git差分収集開始 ==="
              echo "Base SHA: $BASE_SHA"
              echo "Head SHA: $HEAD_SHA"
              
              # 必要なコミットを取得
              # マージベース判定に十分な深さで取得(浅いクローンの問題を軽減)
              echo "必要なコミットを取得中..."
              if ! git fetch --no-tags --prune --depth=10 origin "$BASE_SHA" "$HEAD_SHA"; then
                echo "⚠️ 浅いフェッチに失敗しました。"
                echo "完全フェッチを試行します..."
                git fetch --unshallow origin "$BASE_SHA" "$HEAD_SHA" || {
                  echo "❌ Git フェッチに失敗しました"
                  exit 1
                }
              fi
              
              TMP="$(mktemp)"
              trap 'rm -f "$TMP"' EXIT
              
              # 差分生成: マージベースがあるかチェックして適切な方法を選択
              echo "差分を生成中..."
              
              # 一時的にpipefailを無効化(headコマンドでのSIGPIPE対策)
              set +o pipefail
              
              # マージベースの存在確認と差分生成
              if git merge-base --is-ancestor "$BASE_SHA" "$HEAD_SHA" 2>/dev/null; then
                echo "マージベースが見つかりました。三点差分を使用します。"
                DIFF_RANGE="$BASE_SHA...$HEAD_SHA"
              else
                echo "⚠️ マージベースが見つかりません。二点差分を使用します。"
                DIFF_RANGE="$BASE_SHA $HEAD_SHA"
              fi
              
              # 差分生成(不要なファイルを除外)
              # 除外パターン: ロックファイル、最小化ファイル、マップファイル
              git diff --unified=3 $DIFF_RANGE -- \
                . ':(exclude)*{.lock,.min.*,.map}' \
                ':(exclude){package-lock.json,pnpm-lock.yaml,yarn.lock}' \
              | head -c 180000 > "$TMP"
              
              # pipefailを再有効化
              set -o pipefail
              
              # 差分サイズを確認
              DIFF_SIZE=$(wc -c < "$TMP")
              echo "生成された差分サイズ: ${DIFF_SIZE}バイト"
              
              if [[ $DIFF_SIZE -eq 0 ]]; then
                echo "⚠️ 差分が空です。除外ファイルのみの変更かもしれません。"
              fi
              
              # Base64エンコードで安全に出力(特殊文字対策)
              echo "Base64エンコード中..."
              if command -v base64 >/dev/null 2>&1; then
                # Linux/macOS互換性のため -w オプションをチェック
                # -w 0: 改行なしのBase64出力
                if base64 --help 2>&1 | grep -q "\-w"; then
                  DIFF_B64=$(cat "$TMP" | base64 -w 0)
                else
                  DIFF_B64=$(cat "$TMP" | base64)
                fi
                echo "DIFF_B64=$DIFF_B64" >> "$GITHUB_OUTPUT"
                echo "Base64エンコード完了(${#DIFF_B64}文字)"
              else
                echo "⚠️ base64コマンドが見つかりません。"
                echo "通常の形式で出力します。"
              fi
              
              # 念のため通常の形式でも出力(デバッグ用)
              {
                echo "DIFF<<EOF"
                cat "$TMP"
                echo ""  # 確実に改行を追加
                echo "EOF"
              } >> "$GITHUB_OUTPUT"
    
          - name: Run Bedrock review
            id: bedrock
            env:
              DIFF: ${{ steps.diff.outputs.DIFF }}
              DIFF_B64: ${{ steps.diff.outputs.DIFF_B64 }}
            run: |
              # Bedrockレビューを実行して結果を保存
              REVIEW_OUTPUT=$(python .github/scripts/bedrock_review.py)
              
              # GitHub Actionsの出力として設定
              {
                echo "REVIEW<<EOF"
                echo "$REVIEW_OUTPUT"
                echo "EOF"
              } >> "$GITHUB_OUTPUT"
    
          - name: Post review comment
            uses: actions/github-script@v7
            env:
              REVIEW_OUTPUT: ${{ steps.bedrock.outputs.REVIEW }}
            with:
              script: |
                const reviewOutput = process.env.REVIEW_OUTPUT || '';
                const body = `## 🤖 Bedrock Review\n\n${reviewOutput}`;
                
                console.log('Review output length:', reviewOutput.length);
                console.log('Review preview:', reviewOutput.substring(0, 200));
                
                github.rest.issues.createComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: context.issue.number,
                  body
                })
    
    
  2. Pushし、テストを実行します

  3. 下記のような結果になりました。今回はここまでとします。
    image.png

考察

今回の取り組みを通して、以下の点を確認できました。

  • AIによるレビューは即効性が高い
  • セキュリティ上の懸念や、パフォーマンス・可読性の指摘が自動で検出できた

今後も、継続して使っていき改善していきたいと思います。

参考

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?