mergegate/.github/workflows/ai-review.yml
林 駿甫 (Shunsuke Hayashi) 2d500c3654 feat(ci): add Copilot full automation pipeline
- copilot-assign.yml: GraphQL agentAssignment
- ai-review.yml: Claude Opus auto-review
- auto-merge.yml: CI + APPROVE squash merge
- decompose.yml: Issue decomposition + sub-issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:48:22 +09:00

248 lines
9.5 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

name: AI Code Review
# Copilot が作成した PR を Claude Opus で自動レビュー。
# APPROVE されたものだけ auto-merge.yml がマージする。
# Claude Code CLIclaude --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.',
});