← Sandbox SDK / sandbox / tutorials
Создайте бота для ревью кода
Создайте GitHub-бота, который реагирует на pull request, клонирует репозиторий в песочнице, анализирует изменения кода с помощью Claude и оставляет комментарии к ревью.
Время выполнения: 30 минут
Предварительные требования
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
Node.js↗.
менеджер версий Node.js
Используйте менеджер версий Node, например Volta ↗ или nvm ↗ чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.
Также потребуется:
- A Учётная запись GitHub ↗ и детализированный персональный токен доступа ↗ со следующими разрешениями:
- Доступ к репозиторию: Выберите конкретный репозиторий, с которым будете тестировать
- Разрешения > Права доступа к репозиторию:
- Метаданные: Только чтение (обязательно)
- Содержание: Только чтение (требуется для клонирования репозитория)
- Pull requests: Чтение и запись (требуется для публикации комментариев к ревью)
- Одна Ключ API Anthropic ↗ для Claude
- Репозиторий GitHub для тестирования
1. Создайте проект
npm create cloudflare@latest -- code-review-bot --template=cloudflare/sandbox-sdk/examples/minimalcd code-review-bot2. Установите зависимости
npm i @anthropic-ai/sdk @octokit/rest3. Постройте обработчик вебхуков
Замените 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 с правами repoANTHROPIC_API_KEY: Ваш API-ключ из Anthropic Console ↗WEBHOOK_SECRET: Случайная строка (например:openssl rand -hex 32)
5. Открытие локального сервера через Cloudflare Tunnel
Чтобы протестировать работу с реальными вебхуками GitHub локально, используйте Cloudflare Tunnel чтобы открыть доступ к локальному серверу разработки.
Запустите сервер разработки:
npm run devВ отдельном терминале создайте туннель к локальному серверу:
cloudflared tunnel --url http://localhost:8787В результате будет выведен публичный URL (например, https://example.trycloudflare.com). Скопируйте этот URL для следующего шага.
6. Настройка вебхука GitHub для локального тестирования
- Перейдите в свой тестовый репозиторий на GitHub
- Перейдите в Настройки > Webhooks > Добавить webhook
- Задайте URL полезной нагрузки: URL-адрес Cloudflare Tunnel из шага 5 с
/webhookдобавляется (например,https://example.trycloudflare.com/webhook) - Задайте Тип контента:
application/json - Задайте Секрет: То же значение, что вы использовали для
WEBHOOK_SECRETв вашем.dev.varsфайл - Выберите Выбрать события по отдельности → Проверьте Pull requests
- Нажмите Добавить 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Откройте PR в GitHub. Бот должен оставить комментарий проверки в течение нескольких секунд.
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_SECRET9. Обновление вебхука для продакшена
- Перейдите в свой репозиторий Настройки > Webhooks
- Нажмите на существующий вебхук
- Обновите URL полезной нагрузки к URL-адресу вашего развёрнутого Worker:
https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook - Нажмите Обновление webhook
Теперь ваш бот работает в продакшене и будет автоматически проверять все новые pull request'ы.
Что вы создали
Бот для ревью кода в GitHub, который:
- Получает вебхук-события от GitHub
- Клонирует репозитории в изолированных песочницах
- Использует Claude для анализа изменений в коде
- Автоматически публикует комментарии к проверке
Следующие шаги
- Операции Git - Расширенная работа с репозиторием
- Sessions API - Управляйте длительными операциями в песочнице
- GitHub Apps ↗ - Создайте полноценное GitHub App