name: AI Code Review # Copilot が作成した PR を Claude Opus で自動レビュー。 # APPROVE されたものだけ auto-merge.yml がマージする。 # Claude Code CLI(claude --print)+ OAuth トークンローテーションで動作。 on: pull_request: types: [opened, synchronize, ready_for_review] branches: [master, main] jobs: ai-review: name: Claude Opus Review runs-on: ubuntu-latest if: | github.event.pull_request.user.login == 'Copilot' || contains(github.event.pull_request.labels.*.name, 'ai-review') permissions: pull-requests: write contents: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Install claude CLI run: npm install -g @anthropic-ai/claude-code@latest - name: Get PR diff id: diff run: | git fetch origin master 2>/dev/null || git fetch origin main 2>/dev/null || true BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo "master") git diff origin/${BASE}...HEAD > /tmp/pr.diff BYTES=$(wc -c < /tmp/pr.diff) echo "Diff size: ${BYTES} bytes" # ドキュメント専用 PR 判定(全ファイルが .md なら documentation-only) CHANGED=$(git diff --name-only origin/${BASE}...HEAD) echo "Changed files:" echo "$CHANGED" if [ -z "$CHANGED" ]; then echo "doc_only=false" >> $GITHUB_OUTPUT else NON_MD=$(echo "$CHANGED" | grep -v '\.md$' | head -1) if [ -z "$NON_MD" ]; then echo "doc_only=true" >> $GITHUB_OUTPUT echo "WARNING: PR contains only .md files — will auto-reject." else echo "doc_only=false" >> $GITHUB_OUTPUT fi fi # 18000文字に制限(Opus のコンテキスト節約) python3 -c " with open('/tmp/pr.diff') as f: diff = f.read() MAX = 18000 truncated = len(diff) > MAX if truncated: diff = diff[:MAX] + '\n...[diff truncated due to length]' with open('/tmp/pr.diff.limited', 'w') as f: f.write(diff) print('truncated=true' if truncated else 'truncated=false') " >> $GITHUB_ENV - name: Reject documentation-only PR if: steps.diff.outputs.doc_only == 'true' uses: actions/github-script@v7 with: script: | const pr = context.payload.pull_request; await github.rest.pulls.createReview({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, event: 'REQUEST_CHANGES', body: [ '## AI Code Review — Documentation-Only PR Rejected', '', '**[REQUEST_CHANGES]**', '', 'This PR contains **only `.md` documentation files** and no executable code.', '', 'Copilot Coding Agent must produce runnable code files (`.js`, `.ts`, `.py`, `.sh`, etc.).', 'Please check the Issue requirements and ensure the implementation includes actual code.', '', '### Required Changes', '1. Add at least one executable code file that fulfills the Issue acceptance criteria.', '2. Documentation files (`.md`) are optional — code is required.', ].join('\n'), }); core.setFailed('Documentation-only PR auto-rejected.'); - name: Run AI Review id: review if: steps.diff.outputs.doc_only != 'true' env: CLAUDE_CODE_OAUTH_TOKEN_1: ${{ secrets.CLAUDE_CODE_TOKEN }} CLAUDE_CODE_OAUTH_TOKEN_2: ${{ secrets.CLAUDE_CODE_TOKEN_2 }} CLAUDE_CODE_OAUTH_TOKEN_3: ${{ secrets.CLAUDE_CODE_TOKEN_3 }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} PR_BRANCH: ${{ github.event.pull_request.head.ref }} run: | set -euo pipefail SYSTEM_PROMPT="You are a principal engineer performing an automated code review of a GitHub Pull Request created by Copilot Coding Agent. Evaluate the PR for: (1) correctness — does it fulfill the linked Issue requirements? (2) code quality — no dead code, no placeholder implementations; (3) security — no hardcoded secrets, proper input validation; (4) tests — new logic should have tests. Be strict. Only APPROVE if the PR is production-ready and contains real executable code. If the PR contains only .md documentation files with no code, immediately REQUEST_CHANGES." USER_PROMPT="PR #${PR_NUMBER}: ${PR_TITLE} Branch: ${PR_BRANCH} Review the diff below. Output MUST follow this exact format: ## Verdict **[APPROVE]** <- write exactly this if production-ready OR **[REQUEST_CHANGES]** <- write exactly this if issues found ## Summary (2-3 sentences, plain language) ## Findings (use: OK good WARNING warning CRITICAL critical) ## Required Changes (numbered list if REQUEST_CHANGES, else: None) Be strict. Only APPROVE if safe and correct." # .claude/ ディレクトリの影響を避けるため /tmp から実行 cd /tmp # 3トークンをローテーションして試みる USED_KEY=0 for i in 1 2 3; do case $i in 1) TOKEN="$CLAUDE_CODE_OAUTH_TOKEN_1" ;; 2) TOKEN="$CLAUDE_CODE_OAUTH_TOKEN_2" ;; 3) TOKEN="$CLAUDE_CODE_OAUTH_TOKEN_3" ;; esac if [ -z "${TOKEN:-}" ]; then echo "Token $i is empty, skipping." continue fi echo "Trying token $i/3..." set +e REVIEW=$(cat /tmp/pr.diff.limited | \ ANTHROPIC_API_KEY="" \ CLAUDE_CODE_OAUTH_TOKEN="$TOKEN" \ CLAUDE_NO_ANALYTICS=1 \ NO_UPDATE_NOTIFIER=1 \ claude \ --model claude-opus-4-6 \ --print "$USER_PROMPT" \ --system-prompt "$SYSTEM_PROMPT" \ --no-session-persistence \ 2>/tmp/claude_stderr.txt) EXIT_CODE=$? set -e if [ $EXIT_CODE -eq 0 ] && echo "$REVIEW" | grep -qE '\*\*\[(APPROVE|REQUEST_CHANGES)\]\*\*'; then echo "Success with token $i" USED_KEY=$i break else echo "Token $i failed (exit: $EXIT_CODE):" head -5 /tmp/claude_stderr.txt 2>/dev/null || true if grep -q "429\|rate.limit\|overloaded" /tmp/claude_stderr.txt 2>/dev/null; then echo "Rate limited, waiting 10s before next token..." sleep 10 fi fi done if [ "$USED_KEY" -eq 0 ]; then echo "All tokens failed. Cannot post AI review." exit 1 fi echo "$REVIEW" > /tmp/review_output.txt echo "used_key=$USED_KEY" >> $GITHUB_OUTPUT if echo "$REVIEW" | grep -q '\*\*\[APPROVE\]\*\*'; then echo "verdict=APPROVE" >> $GITHUB_OUTPUT else echo "verdict=REQUEST_CHANGES" >> $GITHUB_OUTPUT fi echo "Verdict: $(grep -o '\*\*\[.*\]\*\*' /tmp/review_output.txt | head -1)" - name: Post review to PR if: steps.review.outcome == 'success' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const review = fs.readFileSync('/tmp/review_output.txt', 'utf8'); const verdict = '${{ steps.review.outputs.verdict }}'; const usedKey = '${{ steps.review.outputs.used_key }}'; const truncated = process.env.truncated === 'true'; const pr = context.payload.pull_request; const event = verdict === 'APPROVE' ? 'APPROVE' : 'REQUEST_CHANGES'; const body = [ '## AI Code Review -- Claude Opus 4.6 (via Claude Code CLI)', '', review, '', '---', `_Model: claude-opus-4-6 | Diff: ${truncated ? 'truncated' : 'full'} | PR: #${pr.number} | Token: #${usedKey}_`, ].join('\n'); await github.rest.pulls.createReview({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, event, body, }); console.log(`Posted ${event} review for PR #${pr.number} (token #${usedKey})`); if (event === 'REQUEST_CHANGES') { core.setFailed('AI review requested changes. Fix the issues before merging.'); } - name: Notify review failure if: steps.review.outcome == 'failure' uses: actions/github-script@v7 with: script: | const pr = context.payload.pull_request; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, body: '⚠️ AI Code Review failed (all 3 OAuth tokens exhausted or rate-limited). Manual review required.', });