Code review нейросетью в GitHub Actions: workflow с curl и gh CLI

Один YAML-файл — и каждый пул-реквест получает комментарий с замечаниями от модели. Разбираем workflow по шагам, права токена, ограничения для форков и стоимость одного ревью.

5 мин · · автоматизация · разработка

Ревьюер-человек устаёт, пропускает очевидное в длинных диффах и не всегда доступен в пятницу вечером. Code review нейросетью в GitHub Actions не заменяет его, но снимает первый слой: забытые проверки на None, утечки секретов в логи, опечатки в условиях. Ниже — готовый workflow на curl и gh, который оставляет комментарий к каждому пул-реквесту, и расчёт, сколько стоит одно ревью.

Как устроен workflow

Три шага на раннере GitHub:

  1. Получить diff пул-реквеста командой gh pr diff.
  2. Отправить diff модели через OpenAI-совместимый эндпоинт https://api.zukko.pro/v1/chat/completions с ключом из секретов репозитория.
  3. Опубликовать ответ комментарием через gh pr comment.

GitHub CLI предустановлен на всех раннерах, которые предоставляет GitHub; по документации GitHub, для каждого шага с gh нужно задать переменную окружения GH_TOKEN. Утилиты curl и jq на ubuntu-latest тоже есть, так что ставить ничего не нужно.

Шаг 0. Ключ и секрет

  1. Создайте отдельный ключ в кабинете на странице API-ключи. Задайте ему дневной лимит в долларах и разрешите только модель, которую будет звать workflow, — тогда ошибка в YAML не обернётся неожиданным счётом. Подробнее — в разделе о лимитах ключа.
  2. В репозитории откройте Settings → Secrets and variables → Actions и добавьте секрет ZUKKO_API_KEY со значением fetch_....

Code review нейросетью в GitHub Actions: готовый workflow

Сохраните файл как .github/workflows/ai-review.yml:

bash
name: AI code review

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    # секреты не передаются в запуски из форков — такие PR пропускаем
    if: github.event.pull_request.head.repo.full_name == github.repository

Что здесь важно

  • Триггер. По документации GitHub, событие pull_request по умолчанию срабатывает на opened, synchronize и reopened; здесь они перечислены явно, чтобы было видно, что ревью запускается и на каждый новый пуш в ветку.
  • Права. pull-requests: write нужен, чтобы оставить комментарий; contents: read — минимальный доступ к коду. Остальные права токена при явном блоке permissions отключены.
  • Без checkout. gh pr diff берёт diff через API GitHub, поэтому клонировать репозиторий не нужно; флаг --repo указывает, в каком репозитории искать PR.
  • Обрезка диффа. head -c 150000 ограничивает размер запроса и стоимость. Огромные PR модель всё равно читает хуже — лучше дробить их. В свежих версиях gh у gh pr diff есть флаг --exclude для исключения файлов по шаблону, например --exclude '*.lock'.
  • JSON через jq. --rawfile читает diff как строку и экранирует кавычки и переносы — собирать JSON склейкой строк в bash ненадёжно.
  • Ошибки. curl --fail-with-body завершает шаг с ошибкой при HTTP-статусе 400 и выше, и в логе видно тело ответа: 401 — неверный ключ, 402 — не хватает баланса, 403 model_not_allowed_for_key — модель не разрешена ключу.

Пулл-реквесты из форков

По документации GitHub, в запуски из форков не передаются секреты (кроме GITHUB_TOKEN), а сам GITHUB_TOKEN получает только права на чтение. Поэтому условие if: в примере пропускает такие PR. Событие pull_request_target даёт доступ к секретам, но запускать с ним код или данные из чужого форка рискованно — для публичных репозиториев безопаснее запускать ревью вручную после просмотра PR.

Вариант на Python

Если промпт сложный или нужно разбить большой diff на части, шаг «Ask the model» удобнее написать на Python:

bash
import os
from pathlib import Path

from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key=os.environ["ZUKKO_API_KEY"],
)

diff = Path("pr_trimmed.diff").read_text(encoding="utf-8", errors="replace")
response = client.chat.completions.create(
    model=os.environ.get("MODEL", "claude-sonnet-5"),
    max_tokens=2000,
    messages=[
        {"role": "system", "content": "Ты опытный ревьюер кода. Ищи ошибки и проблемы "
                                      "безопасности, не комментируй стиль."},
        {"role": "user", "content": f"Diff пул-реквеста:\n\n{diff}"},
    ],
)
Path("review.md").write_text(response.choices[0].message.content, encoding="utf-8")
print("usage:", response.usage)

В workflow замените шаг на pip install openai и python review.py. Подробнее о параметрах запроса — в справочнике chat/completions.

Сколько стоит одно ревью

Формула:

стоимость = входные_токены × цена_входа / 1 000 000 + выходные_токены × цена_выхода / 1 000 000

Входные токены — это diff плюс системный промпт; точное число workflow печатает в логе шагом jq '.usage'. Посмотрите его на нескольких реальных PR. Для примера возьмём 20 000 входных и 1 500 выходных токенов (цены на 6 октября 2026, $ за 1M токенов):

МодельВходВыходИтого за ревью
deepseek-v4.1-flash20 000 × 0.013 / 1M = $0.000261 500 × 0.038 / 1M = $0.000057$0.000317
claude-sonnet-520 000 × 0.2 / 1M = $0.0041 500 × 1.013 / 1M = $0.0015195$0.0055195
gpt-6.1-sol20 000 × 0.27 / 1M = $0.00541 500 × 1.32 / 1M = $0.00198$0.00738
claude-opus-5-520 000 × 0.4 / 1M = $0.0081 500 × 2 / 1M = $0.003$0.011

Учтите, что synchronize запускает ревью на каждый пуш. Если в месяц набирается 300 запусков на claude-sonnet-5, это 300 × $0.0055195 ≈ $1.66. Если модель рассуждает, у вендоров токены рассуждений обычно учитываются как выходные и увеличивают вторую часть суммы — смотрите фактические completion_tokens в логе. Подробнее — в статье о стоимости рассуждений.

Какая модель находит больше реальных проблем в вашем коде, проверьте сами: прогоните 10–15 закрытых PR с известными багами через 2–3 модели и сравните. Актуальный список и цены — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете. Похожий сценарий на Anthropic-формате разобран в статье об автоматическом код-ревью через API.

Частые вопросы

Почему workflow не запускается для PR из форка?

Так задумано: в запуски из форков GitHub не передаёт секреты, и ключа ZUKKO_API_KEY там не будет. Условие if: пропускает такие PR, чтобы шаг не падал с ошибкой 401.

Как не плодить новый комментарий на каждый пуш?

У gh pr comment есть флаги --edit-last и --create-if-none: первый редактирует последний комментарий текущего пользователя, второй создаёт комментарий, если редактировать нечего. Проверьте, что версия gh на раннере их поддерживает.

Можно ли оставлять комментарии к конкретным строкам?

Да, через REST API ревью пул-реквестов (например, командой gh api), но тогда нужно сопоставлять строки диффа с позициями в файле. Начните с общего комментария — его проще поддерживать.

Уходит ли код за пределы GitHub?

Да: diff отправляется модели через шлюз. Если политика компании это ограничивает, согласуйте использование заранее или исключайте чувствительные файлы из диффа.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.