- 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>
248 lines
9.5 KiB
YAML
248 lines
9.5 KiB
YAML
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.',
|
||
});
|