Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

コードレビューボットを作る

最終更新 Markdown で表示Agent セットアップ

pull request に応答する GitHub ボットを作ります。サンドボックスでリポジトリをクローンし、Claude で差分を分析して、レビューコメントを投稿します。

所要時間: 30 分

前提条件

  1. Cloudflare アカウント に登録します。
  2. Node.js をインストールします。

Node.js のバージョンマネージャー

権限の問題を避け、Node.js のバージョンを切り替えられるよう、Voltanvm などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。

次も必要です。

  • GitHub アカウント と、次の権限を持つ fine-grained personal access token:
    • Repository access: テスト対象の特定リポジトリを選びます
    • Permissions > Repository permissions:
      • Metadata: Read-only(必須)
      • Contents: Read-only(リポジトリのクローンに必須)
      • Pull requests: Read and write(レビューコメントの投稿に必須)
  • Claude 用の Anthropic API key
  • テスト用の GitHub リポジトリ

1. プロジェクトを作成する

npm create cloudflare@latest -- code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
cd code-review-bot

2. 依存関係をインストールする

npm i @anthropic-ai/sdk @octokit/rest

3. webhook ハンドラーを実装する

src/index.ts を置き換えます。

import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
import { Octokit } from "@octokit/rest";
import Anthropic from "@anthropic-ai/sdk";

export { Sandbox } from "@cloudflare/sandbox";

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	GITHUB_TOKEN: string;
	ANTHROPIC_API_KEY: string;
	WEBHOOK_SECRET: string;
}

export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		const url = new URL(request.url);

		if (url.pathname === "/webhook" && request.method === "POST") {
			const signature = request.headers.get("x-hub-signature-256");
			const contentType = request.headers.get("content-type") || "";
			const body = await request.text();

			// Verify webhook signature
			if (
				!signature ||
				!(await verifySignature(body, signature, env.WEBHOOK_SECRET))
			) {
				return Response.json({ error: "Invalid signature" }, { status: 401 });
			}

			const event = request.headers.get("x-github-event");

			// Parse payload (GitHub can send as JSON or form-encoded)
			let payload;
			if (contentType.includes("application/json")) {
				payload = JSON.parse(body);
			} else {
				// Handle form-encoded payload
				const params = new URLSearchParams(body);
				payload = JSON.parse(params.get("payload") || "{}");
			}

			// Handle opened and reopened PRs
			if (
				event === "pull_request" &&
				(payload.action === "opened" || payload.action === "reopened")
			) {
				console.log(`Starting review for PR #${payload.pull_request.number}`);
				// Use waitUntil to ensure the review completes even after response is sent
				ctx.waitUntil(
					reviewPullRequest(payload, env).catch(console.error),
				);
				return Response.json({ message: "Review started" });
			}

			return Response.json({ message: "Event ignored" });
		}

		return new Response(
			"Code Review Bot\n\nConfigure GitHub webhook to POST /webhook",
		);
	},
};

async function verifySignature(
	payload: string,
	signature: string,
	secret: string,
): Promise<boolean> {
	const encoder = new TextEncoder();
	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["sign"],
	);

	const signatureBytes = await crypto.subtle.sign(
		"HMAC",
		key,
		encoder.encode(payload),
	);
	const expected =
		"sha256=" +
		Array.from(new Uint8Array(signatureBytes))
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

	return signature === expected;
}

async function reviewPullRequest(payload: any, env: Env): Promise<void> {
	const pr = payload.pull_request;
	const repo = payload.repository;
	const octokit = new Octokit({ auth: env.GITHUB_TOKEN });
	const sandbox = getSandbox(env.Sandbox, `review-${pr.number}`);

	try {
		// Post initial comment
		console.log("Posting initial comment...");
		await octokit.issues.createComment({
			owner: repo.owner.login,
			repo: repo.name,
			issue_number: pr.number,
			body: "Code review in progress...",
		});
		// Clone repository
		console.log("Cloning repository...");
		const cloneUrl = `https://${env.GITHUB_TOKEN}@github.com/${repo.owner.login}/${repo.name}.git`;
		await sandbox.exec(
			`git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`,
		);

		// Get changed files
		console.log("Fetching changed files...");
		const comparison = await octokit.repos.compareCommits({
			owner: repo.owner.login,
			repo: repo.name,
			base: pr.base.sha,
			head: pr.head.sha,
		});

		const files = [];
		for (const file of (comparison.data.files || []).slice(0, 5)) {
			if (file.status !== "removed") {
				const content = await sandbox.readFile(
					`/workspace/repo/${file.filename}`,
				);
				files.push({
					path: file.filename,
					patch: file.patch || "",
					content: content.content,
				});
			}
		}

		// Generate review with Claude
		console.log(`Analyzing ${files.length} files with Claude...`);
		const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
		const response = await anthropic.messages.create({
			model: "claude-sonnet-4-5",
			max_tokens: 2048,
			messages: [
				{
					role: "user",
					content: `Review this PR:

Title: ${pr.title}

Changed files:
${files.map((f) => `File: ${f.path}\nDiff:\n${f.patch}\n\nContent:\n${f.content.substring(0, 1000)}`).join("\n\n")}

Provide a brief code review focusing on bugs, security, and best practices.`,
				},
			],
		});

		const review =
			response.content[0]?.type === "text"
				? response.content[0].text
				: "No review generated";

		// Post review comment
		console.log("Posting review...");
		await octokit.issues.createComment({
			owner: repo.owner.login,
			repo: repo.name,
			issue_number: pr.number,
			body: `## Code Review\n\n${review}\n\n---\n*Generated by Claude*`,
		});
		console.log("Review complete!");
	} catch (error: any) {
		console.error("Review failed:", error);
		await octokit.issues.createComment({
			owner: repo.owner.login,
			repo: repo.name,
			issue_number: pr.number,
			body: `Review failed: ${error.message}`,
		});
	} finally {
		await sandbox.destroy();
	}
}

4. ローカル用の環境変数を設定する

ローカル開発用に、プロジェクトルートへ .dev.vars ファイルを作成します。

cat > .dev.vars << EOF
GITHUB_TOKEN=your_github_token_here
ANTHROPIC_API_KEY=your_anthropic_key_here
WEBHOOK_SECRET=your_webhook_secret_here
EOF

プレースホルダーは次の値に置き換えます。

  • GITHUB_TOKEN: リポジトリ権限付きの GitHub personal access token
  • ANTHROPIC_API_KEY: Anthropic Console の API キー
  • WEBHOOK_SECRET: ランダムな文字列(例: openssl rand -hex 32

5. Cloudflare Tunnel でローカルサーバーを公開する

実際の GitHub webhook をローカルで試すには、Cloudflare Tunnel で開発サーバーを公開します。

開発サーバーを起動します。

npm run dev

別のターミナルで、ローカルサーバーへのトンネルを作成します。

cloudflared tunnel --url http://localhost:8787

公開 URL が出力されます(例: https://example.trycloudflare.com)。この URL を次の手順で使います。

6. ローカルテスト用に GitHub webhook を設定する

  1. GitHub でテスト用リポジトリを開きます
  2. Settings > Webhooks > Add webhook に進みます
  3. Payload URL に、手順 5 の Cloudflare Tunnel URL の末尾へ /webhook を付けた値を設定します(例: https://example.trycloudflare.com/webhook
  4. Content typeapplication/json にします
  5. Secret に、.dev.varsWEBHOOK_SECRET と同じ値を設定します
  6. Let me select individual events を選び、Pull requests にチェックを入れます
  7. Add webhook を選択します

7. pull request でローカル検証する

テスト用 PR を作成します。

git checkout -b test-review
echo "console.log('test');" > test.js
git add test.js
git commit -m "Add test file"
git push origin test-review

GitHub で PR を開きます。数秒以内に、ボットがレビューコメントを投稿するはずです。

8. 本番へデプロイする

Worker をデプロイします。

npx wrangler deploy

本番用シークレットを設定します。

# GitHub token (needs repo permissions)
npx wrangler secret put GITHUB_TOKEN

# Anthropic API key
npx wrangler secret put ANTHROPIC_API_KEY

# Webhook secret (use the same value from .dev.vars)
npx wrangler secret put WEBHOOK_SECRET

9. 本番用に webhook を更新する

  1. リポジトリの Settings > Webhooks を開きます
  2. 既存の webhook を選択します
  3. Payload URL を、デプロイした Worker の URL に更新します: https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook
  4. Update webhook を選択します

ボットは本番で動き、新しい pull request を自動でレビューします。

作成したもの

次の動作をする GitHub コードレビューボットです。

  • GitHub から webhook イベントを受け取る
  • 分離されたサンドボックスでリポジトリをクローンする
  • Claude でコード変更を分析する
  • レビューコメントを自動投稿する

次のステップ

役に立ちましたか?