PRのレビューをClaudeで超楽にする
はじめに
コードレビューは開発プロセスにおいて重要な工程ですが、時間がかかる作業でもあります。特に大規模なPRや複雑な変更に対しては、レビューの準備だけでも相当な時間を要します。
最近は、GitHub上でPR作成時にDevinやClaudeでレビューさせる方法もあり、これもかなり有用です。
一方で、レビューをそこで完結させようとすると、prompt tuningや詳細なコンテキストの必要性があります。
そこで、Claude APIを活用してレビューを完結させるのではなく、補助してもらいレビュー効率を劇的に向上させるシステムを構築しました。
システムの概要
このシステムは以下の3つのコンポーネントで構成されています:
- Claude Codeのカスタムコマンド
- レビュー結果の保存・管理システム
- レビュー結果の表示システム
1. Claude Codeのカスタムコマンド
Claude Codeにはカスタムコマンドを定義できる機能があり、.claude/commands/
ディレクトリなどにMarkdownファイルを配置することで、独自のコマンドを作成できます。
今回作成したpr-review.md
は、GitHubのPRを分析し、コード品質やパフォーマンス観点から構造化されたレビューコメントを生成します。
また、このコマンドでAIに全てをレビューさせるのではなく、人がレビューする時に手助けとなるコメントを生成するようにしました。
今回はこんな感じのMarkdownを用意しました。この指示自体も50%くらいはClaudeに書いてもらいました。
Review the PR: $ARGUMENTS
あなたはプルリクエストのレビューを支援するAIです。
このタスクは非常に高レベルなスキルが要求されるので、 ultrathink するようにしてください。
プルリクエストの内容を分析し、以下の観点からコメントを作成してください:
1. 品質
2. セキュリティ
3. パフォーマンス
4. テスト
5. ドキュメント
分析結果を以下のフォーマットでMarkdownコードブロック内に出力してください。
```markdown
## コードレビュー: [PR タイトル]
### 📋 PRの概要
[このPRの目的と実装内容を2-3文で簡潔に説明]
### このPRの背景知識として抑えておくべきコードの知識
[このPRを理解するために必要なコードの知識や背景情報]
### 人間が重点的にチェックする場所
- [人間がチェックすべきポイント1(ファイルのパスも含めて)]
- [人間がチェックすべきポイント2(ファイルのパスも含めて)]
### 分かりづらい箇所の解説
- [分かりづらい箇所1の説明(ファイルのパスも含めて)]
- [分かりづらい箇所2の説明(ファイルのパスも含めて)]
### その他のコメント
[全体的なフィードバック]
フィードバックのタイプ:
- POSITIVE: 良い点や成功している部分を強調する場合
- IMPROVEMENT: 具体的な改善が必要な点がある場合
- SUGGESTION: 新しいアイデアや提案を提供する場合
### PRのリンク
[PRのリンク](PRのURL)
実はここに書いてある内容は、PRのdescriptionとして書くべき内容にも近しいです。
ただ、実装者以外の目線で出力されることが多く、やってみると意外と有用でした。
2. レビュー結果の保存・管理システム
上記の変更によって、claude "/pr-review <PRのURL>"
を実行すると、標準出力にレビューがMarkdownとして表示されるようになりました。
しかし、このコマンドをレビュー依頼が来るたびに自分で打つのは面倒です。そこで、以下の機能を持つshell scriptを作りました
- 以下の条件に合致するPRのレビュー一覧を取得
- 自分がレビュワーにアサインされている
- まだ自分がレビューを行っていない
- openされている(mergeやcloseされていない)
- それらを順次
claude "/pr-review <PRのURL>
を通してレビューする - 標準出力の内容から、Markdown部分だけを切り出して、特定のディレクトリに保存する
- 一度レビューしたPRはログに記述して再度実行してもレビューしないようにする
これを cron 経由で定期実行することで、いつの間にかClaude Codeによってレビューが完了しています
# 平日の8:00-21:55まで10分ごとに実行
*/10 8-21 * * 1-5 cd <このシェルスクリプトの場所> && ./check-pr-reviews.sh >> $HOME/.pr-review-check.log 2>&1
このスクリプトは、中で claude
コマンドを呼び出しています。claude /pr-review
では gh
の実行が必要になるので、permissionsを適切に設定したディレクトリで実行する必要があります
シェルスクリプト(check-pr-reviews.sh)
#!/bin/bash
# PR Review 自動チェックスクリプト(汎用版)
# 使用方法:
# ./pr-review-check.sh
set -euo pipefail
# 設定
REPO="owner/repository" # GitHubリポジトリを指定
REPO_URL="https://github.com/${REPO}"
HISTORY_FILE="$HOME/.pr-review-history.log"
LOCK_FILE="/tmp/pr-review-check.lock"
LOG_FILE="$HOME/.pr-review-check.log"
# ログ関数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 重複実行防止
acquire_lock() {
if [[ -f "$LOCK_FILE" ]]; then
local lock_pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
if [[ -n "$lock_pid" ]] && kill -0 "$lock_pid" 2>/dev/null; then
log "別のプロセスが実行中です (PID: $lock_pid)"
exit 0
else
log "古いロックファイルを削除します"
rm -f "$LOCK_FILE"
fi
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
}
# GitHub CLIの認証チェック
check_gh_auth() {
if ! gh auth status &>/dev/null; then
log "ERROR: GitHub CLIの認証が必要です: gh auth login"
exit 1
fi
}
# 履歴ファイルの初期化
init_history_file() {
if [[ ! -f "$HISTORY_FILE" ]]; then
touch "$HISTORY_FILE"
log "履歴ファイルを作成しました: $HISTORY_FILE"
fi
}
# Claude コマンドの存在チェック
check_claude_command() {
if ! command -v claude &>/dev/null; then
log "ERROR: claude コマンドが見つかりません"
exit 1
fi
}
# PRレビュー依頼をチェック
check_review_requests() {
log "レビュー依頼をチェック中..."
# 自分に対するレビュー依頼を取得
local review_requests
review_requests=$(gh pr list \
--repo "$REPO" \
--search "review-requested:@me" \
--json number,url,title,author \
--jq '.[] | "\(.number)|\(.url)|\(.title)|\(.author.login)"' 2>/dev/null) || {
log "ERROR: PRリストの取得に失敗しました"
return 1
}
if [[ -z "$review_requests" ]]; then
log "新しいレビュー依頼はありません"
return 0
fi
# 未処理のPRをフィルタリング
local unprocessed_prs=()
while IFS='|' read -r pr_number pr_url pr_title author; do
[[ -z "$pr_number" ]] && continue
# 既にチェック済みかどうかを確認
if grep -q "${REPO_URL}/pull/${pr_number}" "$HISTORY_FILE" 2>/dev/null; then
log "PR#$pr_number は処理済みのためスキップします"
continue
fi
unprocessed_prs+=("$pr_number|$pr_url|$pr_title|$author")
done <<< "$review_requests"
if [[ ${#unprocessed_prs[@]} -eq 0 ]]; then
log "新しいレビュー依頼はありません(全て処理済み)"
return 0
fi
log "未処理のレビュー依頼: ${#unprocessed_prs[@]} 件"
# 一度に処理するPR数を制限(1回に最大10件)
local max_per_run=10
local processed_count=0
# 各PRを順次処理
for pr_data in "${unprocessed_prs[@]}"; do
if [[ $processed_count -ge $max_per_run ]]; then
log "1回の実行で処理するPR数の上限($max_per_run件)に達しました"
log "残りのPRは次回の実行で処理されます"
break
fi
IFS='|' read -r pr_number pr_url pr_title author <<< "$pr_data"
log "=== PR#$pr_number の処理を開始 ==="
log "作成者: $author"
log "タイトル: $pr_title"
log "URL: $pr_url"
# Claudeにレビューを依頼
if execute_claude_review "$pr_url" "$pr_number"; then
# 成功した場合のみ履歴に追加
echo "$pr_url" >> "$HISTORY_FILE"
((processed_count++))
log "✅ PR#$pr_number のレビューが完了しました"
# 次のPRとの間隔を空ける(API負荷軽減)
if [[ $processed_count -lt $max_per_run ]] && [[ $processed_count -lt ${#unprocessed_prs[@]} ]]; then
log "次のPR処理まで10秒待機します..."
sleep 10
fi
else
log "❌ PR#$pr_number のレビューに失敗しました(次回再試行されます)"
fi
log "=== PR#$pr_number の処理終了 ==="
echo
done
if [[ $processed_count -gt 0 ]]; then
log "処理完了: $processed_count 件のレビューを実行しました"
fi
local remaining=$((${#unprocessed_prs[@]} - processed_count))
if [[ $remaining -gt 0 ]]; then
log "残り $remaining 件のPRは次回の実行で処理されます"
fi
}
# GitHub上でレビュー済みまたはPRが完了しているかチェック
check_if_reviewed_on_github() {
local pr_number="$1"
# PRの状態を取得
local pr_state
pr_state=$(gh pr view "$pr_number" \
--repo "$REPO" \
--json state \
--jq '.state' \
2>/dev/null) || return 1
# PRがマージまたはクローズされている場合
if [[ "$pr_state" == "MERGED" ]] || [[ "$pr_state" == "CLOSED" ]]; then
log "PR#${pr_number} は既に${pr_state}されています"
return 0
fi
# 自分がレビューを送信したかチェック
local review_state
review_state=$(gh pr view "$pr_number" \
--repo "$REPO" \
--json reviews \
--jq '.reviews[] | select(.author.login == "'$(gh api user --jq .login)'") | .state' \
2>/dev/null | tail -1) || return 1
# レビュー状態が存在する場合はレビュー済み
if [[ -n "$review_state" ]]; then
log "PR#${pr_number} はGitHub上でレビュー済みです (状態: $review_state)"
return 0
else
return 1
fi
}
# GitHub上でレビュー済みまたは完了したPRファイルを移動
move_reviewed_prs() {
local review_dir="$HOME/.pr-reviews"
local reviewed_dir="$review_dir/reviewed"
log "GitHub上でレビュー済みまたは完了したPRファイルをチェック中..."
# レビューディレクトリ内の全ファイルをチェック
for file in "$review_dir"/*-pr-*-*.md; do
[[ -f "$file" ]] || continue
# ファイル名からPR番号を抽出
if [[ $(basename "$file") =~ -pr-([0-9]+)- ]]; then
local pr_number="${BASH_REMATCH[1]}"
# GitHub上でレビュー済みかチェック
if check_if_reviewed_on_github "$pr_number"; then
local moved_name=$(basename "$file")
mv "$file" "$reviewed_dir/$moved_name"
log "レビュー済みファイルを移動: $file -> $reviewed_dir/$moved_name"
fi
fi
done
}
# Claude レビューコマンドを実行
execute_claude_review() {
local pr_url="$1"
local pr_number="$2"
# レビュー保存ディレクトリを作成
local review_dir="$HOME/.pr-reviews"
local reviewed_dir="$review_dir/reviewed"
mkdir -p "$review_dir" "$reviewed_dir"
# ファイル名を生成(リポジトリ名とPR番号から)
local repo_name=$(echo "$REPO" | cut -d'/' -f2)
local date=$(date +%Y-%m-%d)
local review_file="$review_dir/${repo_name}-pr-${pr_number}-${date}.md"
log "Claude レビューを開始します: $pr_url"
# 一時ファイルにClaude出力を保存
local temp_file=$(mktemp)
local timeout_success=false
local timeout_exit_code=0
# macOS対応のタイムアウト実装
if command -v timeout &>/dev/null; then
# GNU timeout が利用可能な場合
if timeout 300 claude "/pr-review $pr_url" > "$temp_file" 2>&1; then
timeout_success=true
else
timeout_success=false
timeout_exit_code=$?
fi
else
# macOS の場合、バックグラウンドでタイムアウトを実装
log "GNU timeout が見つかりません。macOS版のタイムアウトを使用します"
# バックグラウンドでClaude実行(出力を一時ファイルへ)
claude "/pr-review $pr_url" > "$temp_file" 2>&1 &
local claude_pid=$!
# 5分(300秒)待機
local timeout_duration=300
local elapsed=0
local sleep_interval=5
while [[ $elapsed -lt $timeout_duration ]]; do
if ! kill -0 $claude_pid 2>/dev/null; then
# プロセスが終了している
wait $claude_pid
timeout_exit_code=$?
timeout_success=true
break
fi
sleep $sleep_interval
elapsed=$((elapsed + sleep_interval))
# 進捗ログ(1分ごと)
if [[ $((elapsed % 60)) -eq 0 ]]; then
log "Claude レビュー実行中... ($((elapsed / 60))分経過)"
fi
done
# タイムアウトした場合
if [[ $elapsed -ge $timeout_duration ]]; then
log "Claude レビューがタイムアウトしました(5分)"
kill $claude_pid 2>/dev/null || true
wait $claude_pid 2>/dev/null || true
timeout_success=false
timeout_exit_code=124
fi
fi
# 結果の処理
if [[ "$timeout_success" == "true" ]] && [[ $timeout_exit_code -eq 0 ]]; then
# 一時ファイルからMarkdownブロックを抽出
if grep -q '```markdown' "$temp_file"; then
# sedでmarkdownブロックの内容を抽出
sed -n '/```markdown/,/```/{//!p;}' "$temp_file" > "$review_file"
if [[ -s "$review_file" ]]; then
log "✅ レビューが成功しました: $pr_url"
log "📝 レビューを保存しました: $review_file"
rm -f "$temp_file"
return 0
else
log "ERROR: Markdownコンテンツの抽出に失敗しました"
rm -f "$temp_file" "$review_file"
return 1
fi
else
log "ERROR: Claude出力にMarkdownブロックが見つかりません"
cat "$temp_file" >> "$LOG_FILE" # デバッグ用に出力をログに保存
rm -f "$temp_file"
return 1
fi
else
if [[ $timeout_exit_code -eq 124 ]]; then
log "ERROR: Claude レビューコマンドがタイムアウトしました(5分)"
else
log "ERROR: Claude レビューコマンドが失敗しました (exit code: $timeout_exit_code)"
fi
rm -f "$temp_file"
return 1
fi
}
# 履歴ファイルのクリーンアップ(古いエントリを削除)
cleanup_history() {
if [[ ! -f "$HISTORY_FILE" ]]; then
return 0
fi
# 履歴ファイルが1000行を超えた場合、古いエントリを削除
local line_count=$(wc -l < "$HISTORY_FILE")
if [[ $line_count -gt 1000 ]]; then
log "履歴ファイルをクリーンアップ中..."
tail -500 "$HISTORY_FILE" > "${HISTORY_FILE}.tmp"
mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
log "履歴ファイルをクリーンアップしました ($line_count -> 500行)"
fi
}
# メイン処理
main() {
log "PR Review チェックを開始します"
acquire_lock
check_gh_auth
check_claude_command
init_history_file
# レビュー依頼をチェック
check_review_requests
move_reviewed_prs
cleanup_history
log "PR Review チェックが完了しました"
}
# エラーハンドリング
handle_error() {
local line_number=$1
log "ERROR: スクリプトでエラーが発生しました (行: $line_number)"
exit 1
}
trap 'handle_error $LINENO' ERR
# スクリプト実行
main "$@"
3. レビュー結果の表示システム
これは正直なんでも良かったのですが、
- CLIから表示できる
- 複数あるPRのレビューMarkdownからfuzzy searchできる
- プレビュー表示できる
あたりをやりたかったので、現在使っていたfish shellのカスタム関数を使って、fzf
と bat
を組み合わせて簡単な function をシェルで定義しました。
function pr-review --description "Fuzzy search and open PR review markdown files"
set review_dir "$HOME/.pr-reviews"
# ディレクトリが存在しない場合は警告
if not test -d $review_dir
echo "Review directory not found: $review_dir"
return 1
end
# fzfが利用可能かチェック
if not command -v fzf >/dev/null 2>&1
echo "Error: fzf is required"
echo "Install with: brew install fzf"
return 1
end
# fzfでファイル選択
set selected (
find $review_dir -maxdepth 1 -name "*.md" -type f 2>/dev/null | \
sed "s|$review_dir/||" | \
fzf --preview="bat --style=numbers --color=always $review_dir/{}" \
--preview-window=right:80% \
--header="Select PR review to open" \
--prompt="PR Review > "
)
if test -n "$selected"
code "$review_dir/$selected"
end
end
苦労したところ
これは僕が完全に悪いのですが、claudeのpermissionsの仕様をあまり理解しておらず、最初は適当なディレクトリでスクリプトを実行していて、「この操作を行うにはpermissionが必要です」と言われることが多かったです。
claude
コマンドが実行される際に、どのディレクトリにいるか(permissionを設定しているィレクトリ)に注意を払うようにしたら解消されました。
スクリプト系は全てClaudeに書いてもらったのでその点はゼロコストでした。いい時代です。
導入効果
このシステムを導入してから、以下のような効果が得られました
- MTG中など作業の手を止めているタイミングでレビューが送られてきても、裏側で勝手にレビューが進んでいる
- レビューするか〜と思ったタイミングで、既にどこを重点的にレビューするかが分かっている
- 自分が普段レビューする時に気をつけている部分をコマンドに入れることで、レビューしやすくなる
- これはむしろチームで積極的に知見を共有してもいいかも
- 「あれここなんだろう」と思った時に、レビューしたmarkdownをclaudeにもう一度読んでもらって、レポジトリのコードと合わせて質問することができる
- GitHubを見に行かなくてもreviewできる
まとめ
Claude APIとシェルスクリプトを組み合わせることで、PRレビューのワークフローを個人的には効率化できました。
AIの発展で、実装速度が向上したり、並列に様々なものを開発できるようになったのは良いことですが、レビューはどうしてもまだボトルネックに感じる部分があるので、今後も積極的に楽にしていきたいです。
Discussion