Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions .github/workflows/claude-pr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Claude によるPRレビュー(Anthropic API キーを使わない構成)
#
# 認証は **Claude サブスクリプションの長期トークン**。従量課金の API キーは使わない。
# ローカルで: claude setup-token # 1年有効・scope=user:inference
# 登録: gh secret set CLAUDE_CODE_AUTH_TOKEN --repo RCOSDP/weko
#
# 通信はすべてアウトバウンド(ランナー → Anthropic / GitHub)。
# 公開エンドポイント・固定IP・ポート開放・常駐プロセスは不要。
#
# 【このリポジトリは public】
# Secret 名は CLAUDE_CODE_AUTH_TOKEN、CLI が読む環境変数は CLAUDE_CODE_OAUTH_TOKEN。
# - Secret は fork からの PR には渡らない。下の if で同一リポジトリに限定する。
# - レビュー結果を PR に投稿すると誰でも読める。既定では投稿せず artifact 止まり。
# POST_TO_PR を true にすると投稿する。
#
# 注: cloud-hosted の `claude ultrareview` は 2026-08 時点でこのアカウントでは
# 利用できなかった("Ultrareview is currently unavailable")。ここでは
# ヘッドレス実行(`claude -p`)を使う。動作は確認済み。

name: Claude PR Review

on:
workflow_dispatch:
inputs:
pr_number:
description: 'レビュー対象の PR 番号'
required: true
# TODO(試行中): 動作確認のため有効化。確認後は workflow_dispatch のみに戻す。
pull_request:
branches: ['**']

env:
POST_TO_PR: 'false'
MODEL: 'sonnet'
MAX_DIFF_BYTES: '200000' # これを超える差分はレビューしない(分割が必要)

jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check token
id: cfg
env:
TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }}
run: |
if [ -n "$TOKEN" ]; then echo "enabled=true" >> "$GITHUB_OUTPUT"
else echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::CLAUDE_CODE_AUTH_TOKEN が未設定のためスキップします"; fi

- name: Install Claude Code
if: steps.cfg.outputs.enabled == 'true'
run: |
curl -fsSL https://claude.ai/install.sh | bash
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Collect diff
if: steps.cfg.outputs.enabled == 'true'
id: diff
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.inputs.pr_number || github.event.pull_request.number }}
run: |
gh pr diff "$PR" > diff.patch
size=$(stat -c%s diff.patch)
echo "差分: ${size} bytes"
if [ "$size" -gt "${MAX_DIFF_BYTES}" ]; then
echo "::warning::差分が大きすぎます(${size} > ${MAX_DIFF_BYTES})。スキップします"
echo "skip=true" >> "$GITHUB_OUTPUT"
fi

- name: Review
if: steps.cfg.outputs.enabled == 'true' && steps.diff.outputs.skip != 'true'
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }}
run: |
# Read/Grep/Glob だけを許可してリポジトリを読ませる。差分だけを見せると
# 文脈不足で誤検知が出る(初回試行で「%% は SyntaxError」という誤指摘が出た。
# 実際はその文字列が後で % 展開される前提だった)。
# 変更系のツールは許可せず、--permission-mode plan も併用する。
set +e
claude -p "$(cat <<'PROMPT'
このリポジトリの Pull Request をレビューしてください。
差分は標準入力から渡されます。

## 最重要の規則: 指摘する前に必ず裏を取る

差分は前後の文脈が欠けています。差分の見た目だけで判断すると誤検知になります。
指摘を書く前に、必ず Read/Grep/Glob で該当ファイルの実物を読み、
その指摘が本当に成立するかを確認してください。

確認せずに指摘してはいけない例:
- 「この変数は未定義に見える」→ ファイル全体を読めば定義されている
- 「この書式は誤り」→ その文字列が後で加工される前提かもしれない
- 「呼び出し側の追随が無い」→ 差分外のファイルを grep すれば分かる

裏が取れなかったものは **書かない**。件数を稼ぐ必要はありません。
指摘ゼロは正当な結論です。

## 観点(この順で重視)

1. 認可の欠落・後退
デコレータの削除、permission factory の無効化(None 代入等)、
所有者チェックの欠落、ロール判定の緩和
2. 破壊的操作の追加・条件緩和
削除/上書き処理の新設、既定値が安全側から危険側に変わる変更
3. 入力検証の不足
外部入力をそのまま使う、パス連結、スキーマ検証なし
4. 既存挙動を変える変更で、呼び出し側への影響が未考慮のもの
関数シグネチャ、戻り値の形、列名・キー名の変更など。
**grep で実際に呼び出し箇所を確認してから指摘すること**

## 出力

最後に次のJSONだけを出力してください。前後に文章を付けないこと。

{"findings":[{"file":"","line":0,"severity":"high|medium|low",
"title":"","detail":"","evidence":"","verified":""}]}

detail : 何が問題で何が起きるかを1〜2文で
evidence : 該当行の抜粋
verified : **どのファイルを読んで裏を取ったか**(例 "utils.py:120-140 を確認")
ここが埋まらない指摘は出力しないこと

指摘が無ければ {"findings":[]} を返してください。
PROMPT
)" --output-format json --model "$MODEL" --permission-mode plan \
--allowed-tools "Read,Grep,Glob" \
< diff.patch > raw.json 2> claude.err
rc=$?
set -e
echo "claude exit=$rc"
echo "----- stderr -----"; head -c 3000 claude.err || true
echo "----- stdout(先頭) -----"; head -c 1500 raw.json || true
if [ $rc -ne 0 ]; then
echo "::warning::claude の実行に失敗しました(exit=$rc)。診断のためジョブは継続します"
exit 0
fi
python3 - <<'PY' > review.md
import json, re
raw = json.load(open('raw.json'))
text = raw.get('result') or raw.get('text') or ''
m = re.search(r'\{.*\}', text, re.S)
data = json.loads(m.group(0)) if m else {'findings': []}
f = data.get('findings', [])
json.dump(data, open('findings.json', 'w'), ensure_ascii=False, indent=1)
order = {'high': 0, 'medium': 1, 'low': 2}
f.sort(key=lambda x: order.get(x.get('severity'), 9))
print(f"## Claude によるレビュー\n\n指摘 {len(f)} 件"
f"(コスト ${raw.get('total_cost_usd', 0):.4f})\n")
for x in f:
loc = f"`{x.get('file','')}:{x.get('line','')}`"
print(f"- **[{x.get('severity','?')}] {x.get('title','')}** {loc}")
if x.get('detail'):
print(f" - {x['detail']}")
if x.get('verified'):
print(f" - 確認: {x['verified']}")
PY
cat review.md

- name: Upload result
if: always() && steps.cfg.outputs.enabled == 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review
path: |
review.md
findings.json
raw.json
claude.err

- name: Comment on PR
if: steps.cfg.outputs.enabled == 'true' && env.POST_TO_PR == 'true' &&
github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let body = '(レビュー結果を生成できませんでした)';
try { body = fs.readFileSync('review.md', 'utf8'); } catch (e) {}
await github.rest.issues.createComment({
issue_number: context.issue.number, owner: context.repo.owner,
repo: context.repo.repo, body: body.slice(0, 60000),
});
193 changes: 193 additions & 0 deletions tools/api-inventory/ci/claude-pr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Claude によるPRレビュー(Anthropic API キーを使わない構成)
#
# 認証は **Claude サブスクリプションの長期トークン**。従量課金の API キーは使わない。
# ローカルで: claude setup-token # 1年有効・scope=user:inference
# 登録: gh secret set CLAUDE_CODE_AUTH_TOKEN --repo RCOSDP/weko
#
# 通信はすべてアウトバウンド(ランナー → Anthropic / GitHub)。
# 公開エンドポイント・固定IP・ポート開放・常駐プロセスは不要。
#
# 【このリポジトリは public】
# Secret 名は CLAUDE_CODE_AUTH_TOKEN、CLI が読む環境変数は CLAUDE_CODE_OAUTH_TOKEN。
# - Secret は fork からの PR には渡らない。下の if で同一リポジトリに限定する。
# - レビュー結果を PR に投稿すると誰でも読める。既定では投稿せず artifact 止まり。
# POST_TO_PR を true にすると投稿する。
#
# 注: cloud-hosted の `claude ultrareview` は 2026-08 時点でこのアカウントでは
# 利用できなかった("Ultrareview is currently unavailable")。ここでは
# ヘッドレス実行(`claude -p`)を使う。動作は確認済み。

name: Claude PR Review

on:
workflow_dispatch:
inputs:
pr_number:
description: 'レビュー対象の PR 番号'
required: true
# TODO(試行中): 動作確認のため有効化。確認後は workflow_dispatch のみに戻す。
pull_request:
branches: ['**']

env:
POST_TO_PR: 'false'
MODEL: 'sonnet'
MAX_DIFF_BYTES: '200000' # これを超える差分はレビューしない(分割が必要)

jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check token
id: cfg
env:
TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }}
run: |
if [ -n "$TOKEN" ]; then echo "enabled=true" >> "$GITHUB_OUTPUT"
else echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::CLAUDE_CODE_AUTH_TOKEN が未設定のためスキップします"; fi

- name: Install Claude Code
if: steps.cfg.outputs.enabled == 'true'
run: |
curl -fsSL https://claude.ai/install.sh | bash
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Collect diff
if: steps.cfg.outputs.enabled == 'true'
id: diff
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.inputs.pr_number || github.event.pull_request.number }}
run: |
gh pr diff "$PR" > diff.patch
size=$(stat -c%s diff.patch)
echo "差分: ${size} bytes"
if [ "$size" -gt "${MAX_DIFF_BYTES}" ]; then
echo "::warning::差分が大きすぎます(${size} > ${MAX_DIFF_BYTES})。スキップします"
echo "skip=true" >> "$GITHUB_OUTPUT"
fi

- name: Review
if: steps.cfg.outputs.enabled == 'true' && steps.diff.outputs.skip != 'true'
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }}
run: |
# Read/Grep/Glob だけを許可してリポジトリを読ませる。差分だけを見せると
# 文脈不足で誤検知が出る(初回試行で「%% は SyntaxError」という誤指摘が出た。
# 実際はその文字列が後で % 展開される前提だった)。
# 変更系のツールは許可せず、--permission-mode plan も併用する。
set +e
claude -p "$(cat <<'PROMPT'
このリポジトリの Pull Request をレビューしてください。
差分は標準入力から渡されます。

## 最重要の規則: 指摘する前に必ず裏を取る

差分は前後の文脈が欠けています。差分の見た目だけで判断すると誤検知になります。
指摘を書く前に、必ず Read/Grep/Glob で該当ファイルの実物を読み、
その指摘が本当に成立するかを確認してください。

確認せずに指摘してはいけない例:
- 「この変数は未定義に見える」→ ファイル全体を読めば定義されている
- 「この書式は誤り」→ その文字列が後で加工される前提かもしれない
- 「呼び出し側の追随が無い」→ 差分外のファイルを grep すれば分かる

裏が取れなかったものは **書かない**。件数を稼ぐ必要はありません。
指摘ゼロは正当な結論です。

## 観点(この順で重視)

1. 認可の欠落・後退
デコレータの削除、permission factory の無効化(None 代入等)、
所有者チェックの欠落、ロール判定の緩和
2. 破壊的操作の追加・条件緩和
削除/上書き処理の新設、既定値が安全側から危険側に変わる変更
3. 入力検証の不足
外部入力をそのまま使う、パス連結、スキーマ検証なし
4. 既存挙動を変える変更で、呼び出し側への影響が未考慮のもの
関数シグネチャ、戻り値の形、列名・キー名の変更など。
**grep で実際に呼び出し箇所を確認してから指摘すること**

## 出力

最後に次のJSONだけを出力してください。前後に文章を付けないこと。

{"findings":[{"file":"","line":0,"severity":"high|medium|low",
"title":"","detail":"","evidence":"","verified":""}]}

detail : 何が問題で何が起きるかを1〜2文で
evidence : 該当行の抜粋
verified : **どのファイルを読んで裏を取ったか**(例 "utils.py:120-140 を確認")
ここが埋まらない指摘は出力しないこと

指摘が無ければ {"findings":[]} を返してください。
PROMPT
)" --output-format json --model "$MODEL" --permission-mode plan \
--allowed-tools "Read,Grep,Glob" \
< diff.patch > raw.json 2> claude.err
rc=$?
set -e
echo "claude exit=$rc"
echo "----- stderr -----"; head -c 3000 claude.err || true
echo "----- stdout(先頭) -----"; head -c 1500 raw.json || true
if [ $rc -ne 0 ]; then
echo "::warning::claude の実行に失敗しました(exit=$rc)。診断のためジョブは継続します"
exit 0
fi
python3 - <<'PY' > review.md
import json, re
raw = json.load(open('raw.json'))
text = raw.get('result') or raw.get('text') or ''
m = re.search(r'\{.*\}', text, re.S)
data = json.loads(m.group(0)) if m else {'findings': []}
f = data.get('findings', [])
json.dump(data, open('findings.json', 'w'), ensure_ascii=False, indent=1)
order = {'high': 0, 'medium': 1, 'low': 2}
f.sort(key=lambda x: order.get(x.get('severity'), 9))
print(f"## Claude によるレビュー\n\n指摘 {len(f)} 件"
f"(コスト ${raw.get('total_cost_usd', 0):.4f})\n")
for x in f:
loc = f"`{x.get('file','')}:{x.get('line','')}`"
print(f"- **[{x.get('severity','?')}] {x.get('title','')}** {loc}")
if x.get('detail'):
print(f" - {x['detail']}")
if x.get('verified'):
print(f" - 確認: {x['verified']}")
PY
cat review.md

- name: Upload result
if: always() && steps.cfg.outputs.enabled == 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review
path: |
review.md
findings.json
raw.json
claude.err

- name: Comment on PR
if: steps.cfg.outputs.enabled == 'true' && env.POST_TO_PR == 'true' &&
github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let body = '(レビュー結果を生成できませんでした)';
try { body = fs.readFileSync('review.md', 'utf8'); } catch (e) {}
await github.rest.issues.createComment({
issue_number: context.issue.number, owner: context.repo.owner,
repo: context.repo.repo, body: body.slice(0, 60000),
});
Loading
Loading