Azure DevOps で Claude Code にコードレビューさせよう

Tsubasa Yoshino Tsubasa Yoshino
Azure DevOps で Claude Code にコードレビューさせよう

はじめに

ここ数年の生成 AI の進化は凄まじく、多くの場面で通常の人間の能力を超えつつあります。

ソフトウェア開発の現場でも、多くの人が AI にコーディングを任せているのが現状だと思います。

それはコードレビューも然りで、GitHub では Copilot Review が GA しており、AI が書いたコードを AI がレビューする時代が既に到来しています。

Azure DevOps (以降 AZD) にも Copilot Review が Public Preview として導入されていますが、今回はそれとは別に Claude Code を使って AZD 上でコードレビューする方法について解説します。

この記事で解説すること

  • Claude に AZD の Pull Request をレビューさせる手法について

この記事で触れないこと

  • AZD における Copilot Review について
  • Claude やその他 AI エージェントの性能比較

記事を読む上での前提条件

  • Claude Code がインストールされている

Claude を選択する理由

AZD には、既に Copilot Review がビルトインで提供されていますが、完全従量課金の為、コストが読めず、全社的に利用した場合の月々のコストが気になります。

Claude の場合、サブスクリプションの範囲内で使用量のキャップが設けられている為、使い倒した場合に青天井で請求が発生する事態を防ぐことが可能です。

構築する仕組み

Claude によるコードレビューを実現する為に、次のような仕組みを構築します。

  • Azure Pipelines に Claude Code を実際に走らせる為のパイプラインを作成する
  • ブランチポリシーで作成したパイプラインをブランチと紐づける
  • パイプラインをトリガーする

Claude の認証トークンを取得する

次のコマンドを実行します。

claude setup-token

このコマンドにより、1 年間有効な OAuth トークンが生成されます。

生成したトークンには、user:inference のスコープが割り当てられます。 Claude の公式ドキュメントにこのスコープの詳細は明記されていませんが、setup-token で取得したトークンは、モデルへの推論リクエストのみ可能という記載があります。

その為、コードを読む、生成する、設定された MCP サーバーのツールを呼び出すといったことが可能です。

一方で、Remote Control セッションの確立はできない為、このトークンで実行中のセッションの途中経過を、PC やスマートフォンなどの外部から覗くといったことはできません。

生成した認証トークンに対して、claude.ai から行える操作は Revoke のみです。

claude.ai では、トークンを発行したアプリケーション、スコープ、発行日時のみが判別できる為、いつどのトークンを発行したかは、自分自身で管理する必要がある点に注意してください。

トークンを Revoke するには、claude.ai の左下のアカウントメニューから Settings を開きます。

claude.ai のアカウントメニュー

Settings の Claude Code を開くと、Authorization tokens に発行済みのトークンが一覧表示されるので、ここから対象のトークンを Revoke します。

Authorization tokens の一覧

パイプラインを構築する

今回使用するパイプラインの全体は次の通りです。

parameters:
  - name: variableGroups
    displayName: リンクする変数グループ(Claude Code のトークンを含むもの)
    type: object
    default: []
  - name: tokenVariable
    displayName: Claude Code のトークンを保持する secret 変数名
    type: string
    default: CLAUDE_CODE_OAUTH_TOKEN
  - name: model
    type: string
    default: claude-opus-5
  - name: pullRequestId
    displayName: レビュー対象の PR 番号(空なら自動判定)
    type: string
    default: ''
  - name: reviewInstructions
    displayName: リポジトリ固有の追加レビュー指示
    type: string
    default: ''
  - name: guidelinesFile
    displayName: リポジトリ内のレビュー指針ファイル(あれば自動で読み込む)
    type: string
    default: .claude/review-guidelines.md
  - name: excludePaths
    displayName: 差分から除外する追加グロブ(カンマ区切り)
    type: string
    default: ''
  - name: maxDiffBytes
    type: number
    default: 400000
  - name: claudeArgs
    displayName: claude への追加引数
    type: string
    default: ''
  - name: timeoutSeconds
    type: number
    default: 1800
  - name: pool
    type: object
    default:
      vmImage: ubuntu-latest
  - name: jobTimeoutMinutes
    type: number
    default: 60
  - name: nodeVersion
    type: string
    default: 22.x
  - name: claudeVersion
    displayName: Claude Code CLI のバージョン(サプライチェーン対策で固定を推奨)
    type: string
    default: 2.1.223
  - name: additionalEnv
    displayName: Claude 実行時の追加環境変数
    type: object
    default: {}

stages:
  - stage: ClaudeReview
    displayName: Claude Code Review
    jobs:
      - job: review
        displayName: Claude Code Review
        timeoutInMinutes: ${{ parameters.jobTimeoutMinutes }}
        pool: ${{ parameters.pool }}
        variables:
          - ${{ each group in parameters.variableGroups }}:
              - group: ${{ group }}
        steps:
          - checkout: self
            path: repo
            fetchDepth: 0
            persistCredentials: true
          - checkout: templates
            path: tmpl

          - task: NodeTool@0
            displayName: Use Node.js ${{ parameters.nodeVersion }}
            inputs:
              versionSpec: ${{ parameters.nodeVersion }}

          # バージョンは固定する。latest のままだとパッケージが侵害された際に
          # 検知なく CI に取り込まれ、ここで使うトークンが危険にさらされる。
          - bash: npm install -g "@anthropic-ai/claude-code@${{ parameters.claudeVersion }}"
            displayName: Install Claude Code CLI

          - bash: bash "$(Agent.BuildDirectory)/tmpl/scripts/review.sh"
            displayName: Review the pull request
            env:
              # SYSTEM_ACCESSTOKEN は review.sh が PR コメント投稿に使う。
              # claude の子プロセスには渡さない(review.sh 側で env -u している)。
              SYSTEM_ACCESSTOKEN: $(System.AccessToken)
              REVIEW_REPO: $(Agent.BuildDirectory)/repo
              CLAUDE_CODE_OAUTH_TOKEN: $(${{ parameters.tokenVariable }})
              CLAUDE_CONFIG_DIR: $(Agent.TempDirectory)/claude-config
              DISABLE_AUTOUPDATER: '1'
              DISABLE_TELEMETRY: '1'
              REVIEW_PR_ID: ${{ parameters.pullRequestId }}
              REVIEW_MODEL: ${{ parameters.model }}
              REVIEW_INSTRUCTIONS: ${{ parameters.reviewInstructions }}
              REVIEW_GUIDELINES_FILE: ${{ parameters.guidelinesFile }}
              REVIEW_EXCLUDE_PATHS: ${{ parameters.excludePaths }}
              REVIEW_MAX_DIFF_BYTES: ${{ parameters.maxDiffBytes }}
              REVIEW_CLAUDE_ARGS: ${{ parameters.claudeArgs }}
              REVIEW_TIMEOUT_SECONDS: ${{ parameters.timeoutSeconds }}
              ${{ each pair in parameters.additionalEnv }}:
                ${{ pair.key }}: ${{ pair.value }}

          - task: PublishPipelineArtifact@1
            displayName: Publish diagnostics
            condition: always()
            continueOnError: true
            inputs:
              targetPath: $(Agent.TempDirectory)/claude-review
              artifact: claude-review-$(System.JobAttempt)

このパイプラインを中央のリポジトリに配置しておき、実際に利用する際は、各リポジトリに次のような薄いパイプラインを作って参照するのが良いと思います。

trigger: none
pr: none

resources:
  repositories:
    - repository: templates          # ← エイリアスは templates 固定
      type: git
      name: {プロジェクト名}/{リポジトリ名}   # <TeamProject>/<Repository>
      ref: refs/heads/main

extends:
  template: {テンプレート名}@templates
  parameters:
    variableGroups:
      - claude-code                  # CLAUDE_CODE_OAUTH_TOKEN を持つ変数グループ

    # 必要に応じて(すべて省略可)
    # reviewInstructions: |
    #   - 本リポジトリは .NET 10 / ASP.NET Core の API です。
    #   - Controller から DbContext を直接触っていたら優先度「高」で指摘してください。
    # excludePaths: '**/*.Designer.cs,**/Migrations/**'
    # model: claude-opus-5

テンプレートの内容について解説します。

パラメーター

テンプレートは、利用するリポジトリ毎に挙動を変えられるよう、パラメーターで設定を受け取るようにしています。

パラメーター 既定値 説明
variableGroups なし Claude のトークンを保持する変数グループ
tokenVariable CLAUDE_CODE_OAUTH_TOKEN トークンを保持するシークレット変数名
model claude-opus-5 レビューに使用するモデル
pullRequestId 空 レビュー対象の PR 番号。空の場合は自動判定する
reviewInstructions 空 リポジトリ固有の追加レビュー指示
guidelinesFile .claude/review-guidelines.md リポジトリ内のレビュー指針ファイル
excludePaths 空 差分から除外するファイルのグロブ (カンマ区切り)
maxDiffBytes 400000 Claude に渡す差分の最大サイズ
timeoutSeconds 1800 Claude の実行のタイムアウト (秒)
claudeVersion 2.1.223 インストールする Claude Code のバージョン

checkout

レビュー対象のリポジトリ (self) と、テンプレートを配置したリポジトリ (templates) の 2 つを checkout しています。

レビュー対象のリポジトリは、ターゲットブランチとの差分を取得する為、fetchDepth: 0 で全履歴を取得します。 また、後続の処理でターゲットブランチを fetch する為、persistCredentials: true で認証情報を保持させています。

テンプレート側のリポジトリは、レビュー処理を記述したスクリプトを実行する為に checkout しています。 YAML 内でエイリアス名 templates を直接参照している為、利用側のパイプラインでもエイリアスは templates にする必要があります。

Install Claude Code CLI

npm から Claude Code をインストールします。

ここでは、バージョンを固定してインストールしている点がポイントです。 latest を指定すると、万が一パッケージが侵害された場合に、侵害されたバージョンが気付かないうちに CI へ取り込まれ、パイプラインで扱うトークンが漏洩する危険があります。

バージョンを上げる際は、リリースノートを確認した上で claudeVersion を更新する運用をおすすめします。

Review the pull request

実際のレビュー処理は、テンプレート側のリポジトリに配置した review.sh で行っています。

review.sh がこちらです。

#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# Claude Code review for an Azure DevOps pull request.
#
#   PR の差分を集める -> Claude にレビューさせる -> コメントを 1 件投稿する
#
# Inputs (environment)
#   SYSTEM_ACCESSTOKEN            Azure DevOps 用トークン(PR へのコメント投稿に使用)
#   CLAUDE_CODE_OAUTH_TOKEN       Claude Code の認証トークン(claude setup-token で取得)
#   REVIEW_REPO                   レビュー対象リポジトリのパス(既定 Build.SourcesDirectory)
#   REVIEW_PR_ID                  レビュー対象 PR(空なら自動判定)
#   REVIEW_MODEL                  既定 claude-opus-5
#   REVIEW_INSTRUCTIONS           リポジトリ固有の追加指示(任意)
#   REVIEW_GUIDELINES_FILE        リポジトリ内の指針ファイル(既定 .claude/review-guidelines.md)
#   REVIEW_EXCLUDE_PATHS          差分から除外する追加グロブ(カンマ区切り)
#   REVIEW_MAX_DIFF_BYTES         既定 400000
#   REVIEW_CLAUDE_ARGS            claude への追加引数(任意)
#   REVIEW_TIMEOUT_SECONDS        既定 1800
# ---------------------------------------------------------------------------
set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO="${REVIEW_REPO:-${BUILD_SOURCESDIRECTORY:-$PWD}}"
OUT="${AGENT_TEMPDIRECTORY:-/tmp}/claude-review"
MARKER='<!-- claude-code-review -->'
MODEL="${REVIEW_MODEL:-claude-opus-5}"

log()  { printf '%s\n' "$*"; }
warn() { printf '##vso[task.logissue type=warning]%s\n' "$*"; }
fail() { printf '##vso[task.logissue type=error]%s\n' "$*"; exit 1; }

for cmd in git curl jq claude; do
  command -v "$cmd" >/dev/null || fail "'$cmd' が PATH にありません。"
done
[[ -n "${SYSTEM_ACCESSTOKEN:-}" ]] || fail "SYSTEM_ACCESSTOKEN が未設定です。ステップの env に \
SYSTEM_ACCESSTOKEN: \$(System.AccessToken) を追加してください。"
[[ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]] || fail "CLAUDE_CODE_OAUTH_TOKEN が未設定です。\
ローカルで 'claude setup-token' を実行して取得したトークンを、secret 変数として設定してください。"
for v in SYSTEM_COLLECTIONURI SYSTEM_TEAMPROJECTID BUILD_REPOSITORY_ID; do
  [[ -n "${!v:-}" ]] || fail "${v} が未設定です。Azure Pipelines のジョブ内で実行してください。"
done

# shellcheck disable=SC2153  # Azure Pipelines が設定する変数(TEAMPROJECT とは別物)
API="${SYSTEM_COLLECTIONURI%/}/${SYSTEM_TEAMPROJECTID}/_apis/git/repositories/${BUILD_REPOSITORY_ID}"
mkdir -p "$OUT"

# api METHOD PATH [BODY_FILE] [QUERY] -> RESP / API_ERR
api() {
  local method="$1" path="$2" body="${3:-}" query="${4:-}" raw code
  local url="${API}/${path}?api-version=7.1"
  [[ -n "$query" ]] && url="${url}&${query}"
  local -a opts=(
    -sS --max-time 120 -X "$method"
    -H "Authorization: Bearer ${SYSTEM_ACCESSTOKEN}"
    -H 'Accept: application/json'
    -w $'\n%{http_code}'
  )
  [[ -n "$body" ]] && opts+=(-H 'Content-Type: application/json; charset=utf-8' --data-binary "@${body}")

  raw="$(curl "${opts[@]}" "$url" 2>&1)" ||
    { API_ERR="curl の実行に失敗: ${raw}"; return 1; }
  raw="${raw//$'\r'/}"          # Git Bash 対策: curl -w は CRLF を出す
  code="${raw##*$'\n'}"
  RESP="${raw%$'\n'*}"
  [[ "$code" =~ ^2 ]] || { API_ERR="${method} ${path} -> HTTP ${code}: $(head -c 400 <<<"$RESP")"; return 1; }
}

# --------------------------------------------------------------- 1. 対象 PR
PR_ID="$(tr -d '[:space:]' <<<"${REVIEW_PR_ID:-${SYSTEM_PULLREQUEST_PULLREQUESTID:-}}")"
if [[ -z "$PR_ID" ]]; then
  # パイプラインを手動実行した場合: ソースブランチからアクティブな PR を探す
  q="searchCriteria.sourceRefName=$(jq -rn --arg s "${BUILD_SOURCEBRANCH:-}" '$s|@uri')&searchCriteria.status=active"
  api GET "pullRequests" "" "$q" || fail "PR の検索に失敗しました: ${API_ERR}"
  PR_ID="$(jq -r '.value[0].pullRequestId // empty' <<<"$RESP" | tr -d '\r')"
  [[ -n "$PR_ID" ]] || fail "ソースブランチ ${BUILD_SOURCEBRANCH:-} に対応するアクティブな PR が \
見つかりません。PR を作成するか REVIEW_PR_ID を指定してください。"
fi
api GET "pullRequests/${PR_ID}" || fail "PR ${PR_ID} を取得できません: ${API_ERR}"
jq '.' <<<"$RESP" >"${OUT}/pr.json"

TARGET_REF="$(jq -r '.targetRefName // ""' "${OUT}/pr.json" | tr -d '\r')"
BRANCH="${TARGET_REF#refs/heads/}"
log "PR !${PR_ID}: $(jq -r '.title // ""' "${OUT}/pr.json") (-> ${BRANCH})"

# ----------------------------------------------------------------- 2. 差分
git -C "$REPO" fetch --no-tags --prune origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" \
  >/dev/null 2>&1 || warn "origin/${BRANCH} の fetch に失敗しました。ローカルの ref で続行します。"

BASE=''
for ref in "origin/${BRANCH}" "$BRANCH"; do
  BASE="$(git -C "$REPO" merge-base "$ref" HEAD 2>/dev/null)" && [[ -n "$BASE" ]] && break
  BASE=''
done
[[ -n "$BASE" ]] || fail "${BRANCH} との共通の親コミットが見つかりません。\
checkout の fetchDepth: 0 を確認してください。"

EXCLUDES='**/package-lock.json,**/yarn.lock,**/pnpm-lock.yaml,**/*.min.js,**/*.min.css
**/*.map,**/*.snap,**/*.svg,**/*.png,**/*.jpg,**/*.ico,**/*.pdf
**/bin/**,**/obj/**,**/dist/**,**/node_modules/**'
spec=(-- .)
while IFS= read -r p; do [[ -n "$p" ]] && spec+=(":(exclude,glob)${p}"); done < <(
  printf '%s\n%s\n' "$EXCLUDES" "${REVIEW_EXCLUDE_PATHS:-}" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | awk 'NF'
)

git -C "$REPO" diff --name-status "$BASE" HEAD "${spec[@]}" >"${OUT}/files.txt"
git -C "$REPO" diff --unified=5 --no-color "$BASE" HEAD "${spec[@]}" >"${OUT}/diff.patch"

if [[ ! -s "${OUT}/diff.patch" ]]; then
  log "レビュー対象の変更がありません。"
  printf '%s\n## 🤖 Claude Code レビュー結果\n\n✅ **指摘事項なし**\n\nレビュー対象となるコードの変更が検出されませんでした。\n' \
    "$MARKER" >"${OUT}/comment.md"
else
  MAX="${REVIEW_MAX_DIFF_BYTES:-400000}"
  NOTE=''
  if [[ "$(wc -c <"${OUT}/diff.patch")" -gt "$MAX" ]]; then
    head -c "$MAX" "${OUT}/diff.patch" >"${OUT}/diff.trunc" && mv "${OUT}/diff.trunc" "${OUT}/diff.patch"
    NOTE="> 注意: 差分が大きいため先頭 ${MAX} バイトのみ掲載しています。残りは変更ファイル一覧を見て、
> 必要なファイルを Read ツールで直接読んでレビューしてください。"
    warn "差分を ${MAX} バイトに切り詰めました。"
  fi

  # ------------------------------------------------------------ 3. プロンプト
  # 動的な部分を先に出し、静的な指示(review-prompt.md)を後ろに連結する。
  {
    printf '# Pull Request 情報\n\n'
    jq -r '"- タイトル: \(.title // "(なし)")",
           "- 作成者: \(.createdBy.displayName // "(不明)")",
           "- ソースブランチ: \(.sourceRefName // "")",
           "- ターゲットブランチ: \(.targetRefName // "")"' "${OUT}/pr.json"
    printf '\n## 説明(description)\n\n%s\n\n' \
      "$(jq -r 'if (.description // "") == "" then "(説明なし)" else .description end' "${OUT}/pr.json" | tr -d '\r')"
    printf '# 変更ファイル一覧\n\n```\n%s\n```\n\n' "$(cat "${OUT}/files.txt")"
    printf '# 差分\n\n%s\n\n```diff\n' "$NOTE"
    cat "${OUT}/diff.patch"
    printf '```\n'
    cat "${SCRIPT_DIR}/review-prompt.md"

    GUIDE="${REVIEW_GUIDELINES_FILE:-.claude/review-guidelines.md}"
    if [[ -n "$GUIDE" && -f "${REPO}/${GUIDE}" ]]; then
      printf '\n# このリポジトリ固有のレビュー指針\n\n上記の観点に加えて必ず考慮してください。\n\n'
      cat "${REPO}/${GUIDE}"
      log "レビュー指針を読み込みました: ${GUIDE}"
    fi
    if [[ -n "${REVIEW_INSTRUCTIONS:-}" ]]; then
      printf '\n# 追加のレビュー指示\n\n%s\n' "${REVIEW_INSTRUCTIONS}"
    fi
  } >"${OUT}/prompt.md"

  # -------------------------------------------------------------- 4. Claude
  # 読み取り専用のツールのみ許可する。編集系を許可していないので
  # --permission-mode は指定しない(acceptEdits は書き込みの自動承認モードで矛盾する)。
  log "Claude を実行します (model=${MODEL}, prompt=$(wc -c <"${OUT}/prompt.md") bytes)"
  args=(-p --model "$MODEL" --allowed-tools 'Read,Grep,Glob')
  [[ -n "${REVIEW_CLAUDE_ARGS:-}" ]] && { read -r -a extra <<<"${REVIEW_CLAUDE_ARGS}"; args+=("${extra[@]}"); }

  runner=(); command -v timeout >/dev/null && runner=(timeout "${REVIEW_TIMEOUT_SECONDS:-1800}s")
  set +e
  # SYSTEM_ACCESSTOKEN は Claude の子プロセスに渡さない。
  # レビュー対象の PR 本文や差分にプロンプトインジェクションが仕込まれていても、
  # 環境変数にリポジトリ書き込み権限のトークンが無ければ読み出して漏洩させられない。
  # 作業ディレクトリもレビュー対象リポジトリに固定する。
  (
    cd "$REPO" || exit 127
    exec env -u SYSTEM_ACCESSTOKEN "${runner[@]}" claude "${args[@]}"
  ) <"${OUT}/prompt.md" >"${OUT}/body.md" 2>"${OUT}/claude-stderr.log"
  status=$?
  set -e
  [[ -s "${OUT}/claude-stderr.log" ]] && { log '--- claude stderr ---'; tail -n 40 "${OUT}/claude-stderr.log"; }
  [[ "$status" -eq 124 ]] && fail "Claude がタイムアウトしました。REVIEW_TIMEOUT_SECONDS を延ばすか差分を減らしてください。"
  [[ "$status" -eq 0 ]] || fail "Claude が異常終了しました (exit ${status})。claude-review アーティファクトを確認してください。"
  [[ -s "${OUT}/body.md" ]] || fail "Claude の出力が空でした。claude-review アーティファクトを確認してください。"

  { printf '%s\n' "$MARKER"; cat "${OUT}/body.md"; } >"${OUT}/comment.md"
fi

# 実行情報のフッター
BUILD_LINK="${BUILD_BUILDNUMBER:-${BUILD_BUILDID:--}}"
[[ -n "${BUILD_BUILDID:-}" && -n "${SYSTEM_TEAMPROJECT:-}" ]] &&
  BUILD_LINK="[${BUILD_BUILDNUMBER:-$BUILD_BUILDID}](${SYSTEM_COLLECTIONURI%/}/$(jq -rn --arg s "$SYSTEM_TEAMPROJECT" '$s|@uri')/_build/results?buildId=${BUILD_BUILDID})"
printf '\n<sub>🤖 Claude Code / model: `%s` / build: %s</sub>\n' "$MODEL" "$BUILD_LINK" >>"${OUT}/comment.md"

# ------------------------------------------------- 5. 前回のコメントを閉じる
if api GET "pullRequests/${PR_ID}/threads"; then
  while IFS= read -r tid; do
    [[ -n "$tid" ]] || continue
    jq -n '{status:"closed"}' >"${OUT}/close.json"
    api PATCH "pullRequests/${PR_ID}/threads/${tid}" "${OUT}/close.json" ||
      warn "前回のコメント ${tid} を閉じられませんでした: ${API_ERR}"
  done < <(jq -r --arg m "$MARKER" '
      (.value // [])[] | select(.isDeleted != true)
      | select(((.status // "active") | tostring) == "active")
      | select(((.comments // [])[0].content // "") | contains($m)) | .id' <<<"$RESP" | tr -d '\r')
else
  warn "既存コメントの一覧取得に失敗しました: ${API_ERR}"
fi

# ------------------------------------------------------------- 6. 投稿する
# 指摘も説明への要望も無ければ解決済みで投稿し、未解決コメントとして残さない。
# 見出し文言は review-prompt.md 側で固定している。
STATUS=active
if grep -q '指摘事項なし' "${OUT}/comment.md" &&
   ! grep -q 'PR の説明について' "${OUT}/comment.md"; then
  STATUS=closed
fi
jq -n --rawfile body "${OUT}/comment.md" --arg status "$STATUS" \
  '{comments:[{parentCommentId:0, commentType:"text", content:$body}], status:$status}' \
  >"${OUT}/thread.json"
api POST "pullRequests/${PR_ID}/threads" "${OUT}/thread.json" ||
  fail "コメントの投稿に失敗しました: ${API_ERR}"

log "PR !${PR_ID} にレビューコメントを投稿しました (status=${STATUS})。"

review-prompt.md がこちらです。


# 依頼

あなたは経験豊富なシニアソフトウェアエンジニアです。
上の Pull Request をレビューし、**PR に投稿する Markdown コメントの本文**を出力してください。

# レビューの進め方

**必ずこの順序で進めてください。**

1. **最初に「説明(description)」を読み、この PR が「何を」「なぜ」変更しようと
   しているのかを把握してください。** 差分を読み始めるのはその後です。
   説明が無い・薄い場合も、まず変更ファイル一覧から全体像を推測し、
   「作者は何をしようとしているのか」の仮説を立ててから差分に進んでください。
2. 次に差分を読み、1 で把握した意図が正しく実現できているかを確認してください。
   - 説明に書かれた目的が、差分によって実際に達成されているか
   - 説明に無い変更が混ざっていないか(無関係な変更の混入、スコープの逸脱)
   - 目的の達成に必要なのに欠けている変更が無いか(設定・マイグレーション・テストなど)
3. 差分だけでは判断できない箇所は `Read` / `Grep` / `Glob` で関連ファイル
   (呼び出し元、型定義、既存のテスト、設定ファイルなど)を必ず確認してください。
   差分の見た目だけで推測して指摘しないでください。

# レビュー観点

- 不具合・ロジック誤り(境界値、null / 未定義、型の不一致、条件分岐の抜け)
- 例外処理とエラーハンドリング(握り潰し、リソースリーク、リトライの妥当性)
- セキュリティ(インジェクション、認証・認可の抜け、機密情報の混入、入力検証の不足)
- 性能(N+1、不要なループやアロケーション、同期的な I/O、計算量)
- 並行処理・スレッド安全性(競合状態、デッドロック、共有状態の変更)
- 破壊的変更・後方互換性(公開 API、DB スキーマ、既定値の変更、マイグレーション漏れ)
- テスト(変更に対するテストの不足、意味のないアサーション)
- 可読性・保守性(重複、責務の混在、不適切な命名、デッドコード)
- 既存の実装パターン・規約との整合性
- 説明に書かれた意図と実装の乖離

# 指摘のルール

- **優先度を必ず 高 / 中 / 低 のいずれかに分類してください。**
  - **高**: そのままマージすると本番障害・データ不整合・セキュリティ事故・重大な仕様違反に
    つながるもの。マージ前に修正が必要。
  - **中**: 不具合になりうる、保守性を著しく損なう、必要なテストが無いなど。修正を推奨。
  - **低**: 軽微な改善提案・可読性の向上など。対応は任意。
- **この PR で変更された箇所**を対象にしてください。既存の無関係な問題は、変更によって
  顕在化する場合のみ理由を添えて指摘してください。
- 自動フォーマッタやリンタで解決する些末な指摘(インデント、import 順など)や、
  好みの問題は指摘しないでください。
- 推測で断定しないでください。確証が無い場合は指摘しないか、「低」として
  「〜になっていないか確認してください」という形で書いてください。
- 同じ原因の指摘は 1 件にまとめてください。件数を稼ぐ必要はありません。
- **問題が無ければ無理に指摘を作らないでください。**

# PR の説明(description)の評価

説明が、**レビュアーがこの PR を理解し安全にマージ判断をするのに十分か**を判定して
ください。判定は説明の**長さ**ではなく、変更の規模・影響範囲に対して必要な情報が
書かれているかで行ってください。

次のいずれかに当てはまる場合のみ、記載を促すセクションを出力してください。

- 説明が空、または「(説明なし)」となっている
- PR テンプレートの雛形だけが残っており、中身が書かれていない
- 変更の規模・影響範囲に対して説明が明らかに不足している
- 「何を変えたか」しか書かれておらず、**なぜ**必要なのかが分からない
- レビュー・リリースに必要な情報が欠けている(破壊的変更の有無、マイグレーション手順、
  動作確認の方法など)
- 説明の内容と実際の差分が食い違っている

十分な場合はこのセクションを**丸ごと省略**してください。typo 修正や依存更新のような
軽微な変更で説明が短いことは問題ありません。書く場合は、この PR に即した具体的な項目を
3〜5 個の箇条書きで挙げてください(「詳細を記載してください」のような汎用文は避ける)。
作者を責める書き方はしないでください。

# 出力形式

**Markdown コメントの本文だけ**を出力してください。前置き(「レビューしました」など)、
後書き、全体を囲むコードフェンスは不要です。

レビュアーが**ざっと眺めて要点をつかめる**ことを最優先にしてください。
だらだらとした説明文を書かず、以下のテンプレートに厳密に従ってください。

## 指摘がある場合

````
## 🤖 Claude Code レビュー結果

| 🔴 高 | 🟠 中 | 🟡 低 |
|:---:|:---:|:---:|
| 2 | 1 | 2 |

認証方式を Cookie から Bearer トークンへ変更する PR です。トークン検証に漏れがあります。

---

### 🔴 高(マージ前に要修正)

**`src/Api/AuthService.cs:42`** — トークン期限を検証していない
> `ValidateToken` が `exp` クレームを見ておらず、期限切れトークンで認証を通過できます。
> **修正案:** `TokenValidationParameters.ValidateLifetime = true` を設定してください。

**`src/Api/AuthService.cs:88`** — 例外を握り潰している
> `catch (Exception) { }` で握り潰しており、認証失敗が検知できません。
> **修正案:**
> ```csharp
> catch (SecurityTokenException ex) { _logger.LogWarning(ex, "..."); throw; }
> ```

### 🟠 中(修正推奨)

**`src/Api/OrderService.cs`** — 変更に対するテストがありません
> 認証フローの分岐が増えていますが、対応するテストが追加されていません。
> **修正案:** 期限切れ・改ざんの 2 ケースを `AuthServiceTests` に追加してください。

### 🟡 低(任意対応)

| 箇所 | 指摘 |
|:---|:---|
| `src/Api/AuthService.cs:120` | 変数名 `x` が実態と合っていない |
| `src/Api/Program.cs:15` | 未使用の `using` が残っている |
````

**厳守事項:**

- **冒頭の件数表は必ず 3 列すべて出力**してください(0 件でも `0` と書く)。
- 件数表の直後に、**変更内容とレビュー結果の要約を 1〜3 文**で書いてください。長くしない。
- 続けて `---` で区切ってから、優先度セクションを **高 → 中 → 低 の順**に並べます。
  **該当が 0 件の優先度は、見出しごと省略**してください。
- **高・中** の指摘は 1 件ずつ次の形にします。
  - 1 行目: `**\`ファイルパス:行番号\`** — 指摘の要約(30 字程度)`
  - 2 行目以降: `>` の引用ブロックで「なぜ問題か」を **1〜2 文**、続けて `**修正案:**`
  - 指摘と指摘の間には空行を 1 行入れてください。
- **低** の指摘は上の例のように**表 1 つにまとめ**、1 件 1 行で簡潔に書いてください
  (引用ブロックや修正案は不要)。
- 行番号が特定できない場合はファイルパスのみ、PR 全体の話なら箇所を省略してください。
- コードを示すときは引用ブロックの中でコードフェンスを使ってください。

## 指摘が無い場合

件数表は出さず、次の形にしてください。

```
## 🤖 Claude Code レビュー結果

✅ **指摘事項なし**

追加された `div` は既存の実装パターンに沿っており、呼び出し元とテストも確認しました。
```

「指摘事項なし」という文字列は、**指摘が 1 件も無いときだけ**使ってください。

## 説明への要望を書く場合

上記のいずれの場合でも、説明が不十分と判定したときは**末尾**に次を追加してください。
見出しは**この文言のまま**にしてください(パイプラインが検知に使用します)。

```
---

### 📝 PR の説明について

以下の追記をお願いします。

- タイムアウト値を 30s から 120s に変更した理由
- 変更による既存バッチへの影響範囲
```

Claude Code の実行に関わる環境変数として、次のものを設定しています。

  • CLAUDE_CODE_OAUTH_TOKEN: claude setup-token で取得したトークン
  • CLAUDE_CONFIG_DIR: 設定ファイルの保存先。ジョブ毎の一時ディレクトリにすることで、他のジョブと設定が混ざらないようにしています
  • DISABLE_AUTOUPDATER: CI 上での自動アップデートを無効化し、固定したバージョンを使い続けるようにしています
  • DISABLE_TELEMETRY: テレメトリの送信を無効化しています

review.sh は、次の流れで処理を行います。

  1. レビュー対象の PR を特定する
  2. ターゲットブランチとの差分を作成する
  3. プロンプトを組み立てる
  4. Claude Code でレビューする
  5. 前回のレビューコメントを閉じる
  6. レビュー結果を PR に投稿する

1. レビュー対象の PR を特定する

ビルド検証から実行された場合は、System.PullRequest.PullRequestId から PR の Id を取得します。

パイプラインを手動で実行した場合はこの値が設定されない為、実行したブランチをソースブランチとするアクティブな PR を、REST API で検索しています。 パラメーター pullRequestId を指定した場合は、その PR を優先してレビューします。

2. ターゲットブランチとの差分を作成する

PR のターゲットブランチを fetch し、共通の親コミット (merge-base) からの差分を作成します。

ロックファイルやビルド成果物、画像ファイルなど、レビューする意味の薄いファイルは、既定で差分から除外しています。 リポジトリ固有で除外したいファイルがある場合は、excludePaths で追加できます。

また、差分が大きすぎる場合は maxDiffBytes で指定したサイズで切り詰め、残りは Claude に変更ファイル一覧から必要なファイルを直接読むよう指示しています。

なお、persistCredentials: true により、.git/config には System.AccessToken が保存された状態になります。 このトークンはジョブの終了と共に失効する為、本記事では削除処理を省略していますが、より厳密に対策する場合は、fetch 後に認証情報を削除してから Claude を実行してください。

3. プロンプトを組み立てる

プロンプトは、次の順番で連結しています。

  • PR のタイトル、作成者、ブランチ、説明
  • 変更ファイルの一覧
  • 差分
  • レビューの観点や出力形式を定めた指示 (review-prompt.md)
  • リポジトリ固有のレビュー指針 (.claude/review-guidelines.md)
  • パラメーターで指定した追加指示 (reviewInstructions)

PR の説明や差分といった、PR 作成者が自由に書ける内容を先に置き、静的な指示を後ろに置くことで、差分などに紛れ込んだ指示よりも、本来のレビュー指示が優先されやすいようにしています。

4. Claude Code でレビューする

組み立てたプロンプトを標準入力から渡し、claude -p のヘッドレスモードで実行します。

--allowed-tools で使用できるツールを Read、Grep、Glob の読み取り系のみに制限している為、Claude がファイルを書き換えたり、任意のコマンドを実行したりすることはありません。

また、PR へのコメント投稿に使う SYSTEM_ACCESSTOKEN は、env -u で Claude の実行時の環境変数から除外しています。 このトークンはリポジトリへの書き込み権限を持つ為、万が一プロンプトインジェクションが成功した場合でも、Claude から参照できないようにしておくのが安全です。

実行が timeoutSeconds を超えた場合は、タイムアウトとしてパイプラインを失敗させます。

5. 前回のレビューコメントを閉じる

PR が更新される度にレビューが実行されると、古いレビューコメントが溜まっていきます。

その為、投稿するコメントには <!-- claude-code-review --> という HTML コメントを目印として埋め込んでおき、次回の実行時に、この目印を含むアクティブなスレッドを Closed にしています。

6. レビュー結果を PR に投稿する

Claude の出力に、モデル名とビルドへのリンクをフッターとして付け、PR にスレッドとして投稿します。

指摘事項が無く、PR の説明への要望も無い場合は、スレッドを Closed の状態で投稿します。 未解決のコメントとして残らない為、ブランチポリシーで「コメントの解決」を必須にしている場合でも、マージの妨げになりません。

Publish diagnostics

Claude に渡したプロンプトや実行ログなどを、Pipeline Artifact として保存しています。

レビュー結果が期待通りでなかった場合に、Claude がどのような判断をしたのかを後から確認する為のものです。 condition: always() を指定している為、レビュー処理が失敗した場合でも保存されます。

利用側のパイプライン

利用側のパイプラインでは、resources.repositories でテンプレートを配置したリポジトリを参照し、extends でテンプレートを呼び出しています。

PR でのパイプライン実行はブランチポリシーから行う為、trigger と pr はどちらも none にしています。

リポジトリ固有のルールがある場合は、reviewInstructions に追記することで、レビュー観点を追加できます。 より詳細な指針がある場合は、リポジトリ内の .claude/review-guidelines.md に記述しておくと、自動で読み込まれます。

トークンを変数グループに登録する

取得したトークンは、YAML に直接書かず、変数グループにシークレットとして登録します。

  1. Pipelines > Library を開き、+ Variable group から claude-code という名前の変数グループを作成します
  2. CLAUDE_CODE_OAUTH_TOKEN という名前で変数を追加し、値に取得したトークンを貼り付けます
  3. 鍵アイコンをクリックし、シークレット変数にします
  4. Pipeline permissions から、利用側のパイプラインに変数グループの利用を許可します

パイプラインに権限を割り当てる

このパイプラインは、PR へのコメント投稿と、テンプレートリポジトリの checkout を行う為、それぞれ権限を付与する必要があります。

PR へのコメント投稿

  1. Project Settings > Repositories を開き、レビュー対象のリポジトリを選択します
  2. Security タブを開き、<プロジェクト名> Build Service (<組織名>) を選択します
  3. Contribute to pull requests を Allow に変更します

なお、Project Settings > Pipelines > Settings の Limit job authorization scope to current project for non-release pipelines が無効になっている場合、パイプラインは Project Collection Build Service (<組織名>) として実行される為、こちらに権限を付与してください。

テンプレートリポジトリの参照

Project Settings > Pipelines > Settings の Protect access to repositories in YAML pipelines が有効な場合、パイプラインの初回実行時に、テンプレートリポジトリへのアクセス許可を求められます。

実行画面に表示される Permit をクリックして許可してください。

ブランチポリシーを設定する

作成したパイプラインが PR 作成時に実行されるよう、ブランチポリシーのビルド検証 (Build validation) に登録します。

  1. Project Settings > Repositories > 対象のリポジトリ > Policies を開きます
  2. Branch Policies から、レビュー対象とするブランチ (例: main) を選択します
  3. Build Validation の + をクリックし、利用側のパイプラインを選択します
  4. 次の項目を設定し、保存します
項目 設定値 説明
Trigger Automatic / Manual PR の作成・更新時に自動で実行するか、手動で実行するか
Policy requirement Optional Required にすると、パイプラインが失敗した場合に PR をマージできなくなる
Build expiration Immediately / Never など ターゲットブランチが更新された際に、再実行を必要とするか
Display name Claude Review PR 画面に表示される名前

AI のレビューはあくまで参考意見として扱う為、Policy requirement は Optional にしておくことをおすすめします。

パイプラインをトリガーする

Trigger を Automatic にした場合、PR の作成時と、ソースブランチへの push 時に自動でパイプラインが実行されます。

Manual にした場合は、PR 画面の Checks から Queue をクリックすることで、任意のタイミングで実行できます。

push の度にレビューが走るとサブスクリプションの使用量を消費しやすくなる為、レビューを依頼したいタイミングで手動実行する運用も検討してください。

レビュー結果を確認する

パイプラインの実行が完了すると、PR にレビューコメントが投稿されます。

パイプラインの制約

このパイプラインは、ブランチポリシーのビルド検証から実行された際に設定される、System.PullRequest.PullRequestId を使って PR の Id を取得しています。

この値は、PR が作成されたリポジトリと、パイプラインの YAML が配置されたリポジトリが同一でないと取得できません。

その為、テンプレート自体は中央のリポジトリに集約しつつ、レビューを導入したいリポジトリには、それぞれ利用側のパイプラインを配置する必要があります。

まとめ

AI を利用したコードレビューは、人間よりも高速にリポジトリを横断的にチェックすることができます。

その為、実装箇所毎の方針のずれなど、人間が見逃しがちな部分の指摘漏れを減らすことが期待できます。

一方で、重箱の隅を突くような指摘や、実装経緯を知らないが故の頓珍漢な指摘なども出てくることが多々あり、まだ人間のハンドリングも必要な段階であると筆者は考えています。

ですが、それは人間のレビューにも言えることなので、AI のレビューも人間のレビューも等しく参考にし、レビューをする人手が増えたといった受け取り方で良いのかなと思います。

お問い合わせはこちらから

問い合わせる
TOP