1
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?

AIレビューコメントに自動対応させるCI設計 — GitHub Actionsで「無限ループしない・壊さない・高くつかない」を実装する

1
Posted at

はじめに:レビューは自動化できたのに、対応は手作業のままだった

当社(合同会社ジョインクラス)は、社内業務の約98%を launchd の定期ジョブと Claude Code の無人実行で回しています。PR にも AI レビューを入れていますが、しばらくは指摘への対応だけ人間がやるという状態でした。

「このnullチェック抜けてます」「この変数名わかりにくいです」程度の指摘に、毎回ブランチをチェックアウトして直して push する。これは自動化できるはずです。

ただし、何も考えずに「レビューコメントが来たら Claude に直させる」を作ると、次の3つの事故が起きます。

  1. 無限ループ:AIが修正 push → AIレビューが再実行 → また指摘 → また修正…
  2. 破壊:AIが勝手に git push --force したり、関係ないファイルを書き換える
  3. コスト爆発:1回の呼び出しが想定より高く、ループと掛け算になる

筆者は 2 を実際にやらかしています。ローカルの無人実行で AI が git push --force を実行し、ブランチが消えました。この記事は、その反省を踏まえて3つの事故をワークフローの構造で防ぐ設計を、そのまま使える YAML とスクリプトで紹介します。

全体設計

[AIレビュアー] --review comment--> [GitHub]
                                      │ pull_request_review / pull_request_review_comment
                                      ▼
                         [ai-review-fix.yml]
                          ├─ ガード1: 自分のbotの発言は無視(ループ防止)
                          ├─ ガード2: 対応回数ラベルで上限3回(ループ防止)
                          ├─ ガード3: fork PRは対象外(権限防止)
                          ├─ claude -p(allowedTools制限 + max-budget)
                          ├─ ガード4: 変更ファイルの許可リスト検査
                          └─ 通常pushのみ(force禁止)+ 結果をPRにコメント

ポイントは「AIにお願いして守らせる」のではなく、AIが何を出力しても事故にならない位置にガードを置くことです。プロンプトでの禁止は補助にすぎません。

手順1:ベースにした自社ラッパー

CI版を書く前に、当社で毎日動いている無人実行ラッパーを紹介します。CI版はこれをほぼそのまま移植しています。

.company/scripts/lib/claude-run.sh
#!/bin/bash
# claude を無人実行するときの共通ラッパー。実コストを記録する。
_CLAUDE_RUN_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

claude_run() {
  local label="$1"; shift
  claude --output-format json \
         --fallback-model claude-sonnet-5 \
         --max-budget-usd 3 \
         "$@" 2>/dev/null \
    | python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label"
}

呼び出し側はこうです(記事自動生成ジョブの実コード)。

claude_run replenish --effort low --allowedTools "Write,Read,Edit" -p "..."

ここから得た教訓が3つあります。

  • --max-budget-usd は必ず付ける。実測すると「2+2」を聞くだけでも1回 $0.27〜0.47 かかっていました。内訳の大半はキャッシュ作成の約41,000トークンで、自社の CLAUDE.md やスキルは5,800トークンだけ。残りは固定オーバーヘッドなので、削れるのは呼び出し回数だけです。CIではこれがループ回数の上限設計に直結します。
  • --output-format json でコストを取る。標準出力の total_cost_usd を記録しておくと、あとで「どのジョブが高いか」がわかります。
  • --bare は使わない。認証を読まず Not logged in で落ちます(筆者はこれで1晩分のジョブを全滅させました)。

手順2:ワークフロー本体

.github/workflows/ai-review-fix.yml を作ります。

.github/workflows/ai-review-fix.yml
name: ai-review-fix

on:
  pull_request_review:
    types: [submitted]

# 同じPRで並走させない(ループ・競合push防止)
concurrency:
  group: ai-review-fix-${{ github.event.pull_request.number }}
  cancel-in-progress: false

permissions:
  contents: write
  pull-requests: write

env:
  MAX_ROUNDS: 3
  FIXER_BOT: github-actions[bot]

jobs:
  fix:
    # ガード1: 指摘が付いたレビューのみ / 自分自身のレビューは無視
    # ガード3: fork PR は secrets を渡さないので対象外
    if: >
      github.event.review.state == 'changes_requested' &&
      github.event.review.user.login != 'github-actions[bot]' &&
      github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          fetch-depth: 0

      # ガード2: 対応回数をラベルで数える
      - name: Check round limit
        id: round
        env:
          GH_TOKEN: ${{ github.token }}
          PR: ${{ github.event.pull_request.number }}
        run: |
          n=$(gh pr view "$PR" --json labels \
               -q '[.labels[].name | select(startswith("ai-fix-round-"))] | length')
          echo "count=$n" >> "$GITHUB_OUTPUT"
          if [ "$n" -ge "$MAX_ROUNDS" ]; then
            gh pr comment "$PR" --body "AI自動対応は ${MAX_ROUNDS} 回で打ち切りました。人間の確認をお願いします。"
            exit 1
          fi

      - name: Collect review comments
        env:
          GH_TOKEN: ${{ github.token }}
          PR: ${{ github.event.pull_request.number }}
          REVIEW_ID: ${{ github.event.review.id }}
          # 本文は env 経由で渡す(run に直接埋め込むとスクリプトインジェクションになる)
          REVIEW_BODY: ${{ github.event.review.body }}
        run: |
          gh api "repos/${{ github.repository }}/pulls/$PR/reviews/$REVIEW_ID/comments" \
            --jq '.[] | "- \(.path):\(.line // .original_line) \(.body)"' > /tmp/comments.md
          printf '%s\n' "$REVIEW_BODY" >> /tmp/comments.md
          cat /tmp/comments.md

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Run fixer
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: bash .github/scripts/ai-fix.sh /tmp/comments.md

      # ガード4: 触ってよいファイル以外が変わっていたら中止
      - name: Verify changed files
        run: bash .github/scripts/verify-diff.sh

      - name: Test
        run: npm ci && npm test

      - name: Commit and push (no force)
        env:
          GH_TOKEN: ${{ github.token }}
          PR: ${{ github.event.pull_request.number }}
          ROUND: ${{ steps.round.outputs.count }}
        run: |
          git diff --quiet && { echo "no changes"; exit 0; }
          git config user.name  "$FIXER_BOT"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git commit -am "fix: AIレビュー指摘への自動対応 (round $((ROUND+1)))"
          git push origin HEAD
          gh pr edit "$PR" --add-label "ai-fix-round-$((ROUND+1))"
          gh pr comment "$PR" --body-file /tmp/fix-summary.md

なぜこれでループしないのか

ループ防止は3重にしています。

層 仕組み 止まるケース
トークン GITHUB_TOKEN での push は新しいワークフローを起動しない(GitHubの仕様) 修正pushでAIレビューが自動再実行されない
発言者 review.user.login != 'github-actions[bot]' 自分のコメントに反応しない
回数 ai-fix-round-N ラベルで上限3回 AIレビュアーが別トークンで毎回指摘しても3回で止まる

1層目だけで足りそうに見えますが、AIレビュアーを PAT や GitHub App で動かしていると修正 push を拾って再レビューします。回数上限はどの構成でも効く最後の砦なので必ず入れてください。上限に達したら失敗させて人間に通知する、が正解です。

手順3:修正スクリプト(claude -p 部分)

.github/scripts/ai-fix.sh
#!/bin/bash
set -euo pipefail
COMMENTS_FILE="$1"

RESULT=$(claude -p "$(cat <<EOF
あなたはPRのレビュー指摘に対応するエンジニアです。
以下の指摘のうち、妥当なものだけを最小差分で修正してください。

ルール:
- 指摘された箇所以外を変更しない
- 指摘が誤りだと判断したら修正せず、理由を書く
- git操作は一切しない(コミット・pushはCIが行う)

最後に「対応した指摘 / 対応しなかった指摘と理由」をMarkdownで出力すること。

## レビュー指摘
$(cat "$COMMENTS_FILE")
EOF
)" \
  --output-format json \
  --allowedTools "Read,Edit,Grep,Glob" \
  --max-budget-usd 1 \
  --max-turns 20)

echo "$RESULT" | jq -r '.result' > /tmp/fix-summary.md
echo "cost: $(echo "$RESULT" | jq -r '.total_cost_usd') USD"

設計上のポイント:

  • --allowedTools から Bash と Write を外す。Edit は既存ファイルの書き換えのみなので、新規ファイル生成も git 操作も物理的にできません。git push --force の事故は「禁止する」ではなく「Bash を渡さない」で防ぎます。
  • 「指摘が誤りなら直さない」を明示する。AIレビューは誤検知もします。従わせるだけだと、正しいコードを壊して指摘に合わせる挙動になります。対応しなかった理由がPRコメントに残るので、人間はそこだけ見ればよくなります。
  • --max-budget-usd 1 × 上限3回 = 1PRあたり最大 $3。上限が計算できることが大事です。

手順4:変更ファイルの検査

AIの出力を信用せず、結果の差分で判定します。

.github/scripts/verify-diff.sh
#!/bin/bash
set -euo pipefail

# 触ってはいけないパス
DENY='^(\.github/|package-lock\.json|.*\.env|migrations/)'
# 1回の自動対応で許す最大変更行数
MAX_LINES=200

changed=$(git diff --name-only)
if echo "$changed" | grep -Eq "$DENY"; then
  echo "::error::保護対象ファイルが変更されました"
  echo "$changed" | grep -E "$DENY"
  git checkout -- .
  exit 1
fi

lines=$(git diff --numstat | awk '{s+=$1+$2} END {print s+0}')
if [ "$lines" -gt "$MAX_LINES" ]; then
  echo "::error::変更が${lines}行あり上限${MAX_LINES}行を超えました"
  git checkout -- .
  exit 1
fi
echo "OK: ${lines} lines changed"

.github/ を保護しているのは、AIが自分のワークフローを書き換えてガードを外せないようにするためです。地味ですが一番大事な行です。

落とし穴

実装時にハマりやすい点をまとめます。

  • pull_request_review_comment で起動すると、コメント1件ごとにジョブが走る。レビューで5件指摘されると5並列で起動します。pull_request_review の submitted で受けて、そのレビューのコメントをまとめて取得するのが正解です。
  • concurrency の cancel-in-progress: true にしない。修正途中のジョブがキャンセルされ、中途半端な状態で次が走ります。
  • fork PR を対象にしない。pull_request_target に変えれば secrets は使えますが、第三者のコードを書き込み権限付きで実行することになります。社外コントリビュータのPRは人間が対応しましょう。
  • テストを AI 修正の後に必ず走らせる。テストが落ちたら push しないので、壊れたコードがPRに積まれません。

検証方法

導入したら、わざと指摘を出すPRで以下を確認してください。

# 1. テスト用ブランチで明らかな指摘が出るコードを入れる
git switch -c test/ai-fix
echo 'export const f = (x) => x.length' > src/tmp.ts
git add . && git commit -m "test: ai-fix" && git push -u origin HEAD
gh pr create --fill

# 2. 手動で changes_requested レビューを付ける(AIレビュアーの代わり)
gh pr review --request-changes -b "src/tmp.ts: x が undefined のときに落ちます"

# 3. ワークフローの実行とラベルを確認
gh run watch
gh pr view --json labels,comments -q '.labels[].name, .comments[-1].body'

確認するのは4点です。

  1. 修正コミットが1つ積まれ、ai-fix-round-1 ラベルが付く
  2. 修正 push の後にワークフローが再起動しない
  3. 同じレビューを3回付けると、4回目で「打ち切りました」コメントが出て止まる
  4. .github/ を変更させるような指摘を出すと、verify-diff で失敗する

特に 2 と 3 はループ防止の確認なので、本番導入前に必ず通してください。

まとめ:自動化は「安心して任せるための仕組み」

当社では以前、AIが書いた営業メールを確認せず自動送信し、取引先に敬語のおかしいメールが届いたことがあります。それ以来、対外アクションはすべて draft → 承認 → 実行 に通しています。

今回のCIも同じ考え方です。AIにレビュー対応を任せますが、マージするかどうかは人間が決める。AIが担当するのは「直す」と「直さなかった理由を書く」までです。

自動化は AI の能力を制限するためのものではなく、安心して任せるための仕組みです。ガードを構造で入れておけば、AIの出力が多少ぶれても事故にはなりません。

もっと深く知りたい方へ

この記事では「レビュー指摘への対応」だけを扱いましたが、要件定義 → 設計 → 実装 → レビュー → 修正の開発ライフサイクル全体を AI 前提で組み直す方法は、拙著『AI駆動開発ライフサイクル(AI-DLC)の教科書』で体系的にまとめています。CI へのレビュー組み込みや、人間が判断すべきポイントの切り分けも扱っています。

書籍一覧はこちら → https://zenn.dev/joinclass?tab=books

1
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
1
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?