команда · эксплуатация

Бот промолчал: как найти место обрыва

MR висит без сводки, без комментариев, без статуса гейта — и ничто не объясняет почему. Гадать не придётся: почти каждый исход оставляет след. Следы лежат в трёх местах — в журнале доставок GitLab или GitHub, в логе бота и в самом MR, — а если бот пишет метрики в Postgres, то ещё и в таблице метрик.

Один вопрос делит поиск пополам: приходило ли на этот репозиторий ревью хоть раз? Если ревью не приходило ни разу — обрыв на пути от GitLab или GitHub к боту: адрес вебхука, его события, секрет, сеть. Если раньше работало — это событие пропущено намеренно, прогон упал или ещё повторяется, и все эти случаи видны строкой в логе или заметкой в самом MR.

Дальше — где лежат следы и как их читать, что значит каждый след, когда ждать и когда искать и что проверить на репозитории, где ревью не было ни разу.

Где лежат следы

Журнал доставок GitLab или GitHub — доступ к серверу не нужен. Каждая доставка лежит там вместе с ответом бота, дословно. В GitLab это Settings → Webhooks → Edit → Recent events, за последние двое суток. Через API видно неделю, но нужна роль Maintainer — токен самого бота обычно не подойдёт:

терминал
GITLAB=https://gitlab.com
PROJECT=reviewgate-group%2Finvoices-api   # url-encoded path, or the numeric project id
HOOK=88628352                             # GET /projects/$PROJECT/hooks lists them

# the webhook API needs the Maintainer role; the events of the last 7 days
curl -s --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "$GITLAB/api/v4/projects/$PROJECT/hooks/$HOOK/events?per_page=6" \
| jq -r '.[] | "\(.created_at[0:19])  \(.response_status)  \(.response_body)"'
терминал
2026-09-08T13:06:08  202  {"queued":false,"reason":"ignored note action=update"}
2026-09-08T13:06:06  202  {"queued":true}
2026-09-08T13:02:37  202  {"queued":true}
2026-09-08T12:48:48  202  {"queued":false,"reason":"ignored action=update"}
2026-09-08T08:56:21  202  {"queued":true}
2026-09-08T08:55:59  202  {"queued":true}

Читается в два шага. Есть ли доставка вообще? Если вашего события нет, обрыв до бота: хук не настроен, тип события не отмечен или GitLab отключил хук после череды сбоев. Дальше читайте тело ответа, а не код. 202 бот отвечает и на осознанный пропуск, а различает их поле queued. "queued":true значит, что джоба создана; при "queued":false рядом стоит reason — бот посмотрел на событие и решил, что ревьюить нечего. Обе строки с reason выше — правильное поведение: одна про обновление MR без новых коммитов, другая про правку уже существующего комментария.

На GitHub тот же список — в Developer settings → GitHub Apps → Edit → Advanced → Recent deliveries (или в Settings → Webhooks у репозиторного хука), за последние трое суток. Тело ответа такое же, отличаются только причины — например, ignored event=push или ignored action=labeled.

Лог бота — строка на каждый исход. Любое завершение пишет строку, и лог английский, какой бы язык ни стоял у вас в конфиге. Хватает двух команд: первая показывает, что бот сделал с доставками, вторая — всё про один MR: каждая строка его джоб помечена [group/project!42 @sha], на GitHub — [org/repo#42 @sha]:

терминал
# what the bot did with the deliveries: queued, skipped, rejected
docker compose logs --since 2h app | grep -E "Webhook|webhook|Queued a review"

# everything about one merge request (on GitHub: org/repo#42)
docker compose logs --since 2h app | grep -F 'group/project!42'

При LOG_FORMAT=json тега в тексте нет — фильтруйте по полям project_path и mr_iid.

Сам MR. Сорвавшийся прогон обычно оставляет в нём заметку: сразу — если повтор не поможет, как при неверном ключе или не поместившемся контексте, и после последней попытки — во всех остальных случаях. Оставляет её и прогон, которому нечего было смотреть. Но отсутствие заметки не значит, что обрыв случился раньше: черновик, закрытый MR, политика веток, повтор той же вершины, пуш из одних lock-файлов — всё это пропускается молча, и знает об этом только лог бота.

Таблица метрик, если у бота есть Postgres: каждый завершённый прогон и каждый сбой получает строку со своим status — reviewed, repeat_head, publish_failed, key_error и другими. Пропуски вроде черновика или закрытого MR строки не оставляют.

Что значит каждый след

В журнале доставокЧто произошлоЧто делать
доставки нет вовсехук не настроен, тип события не отмечен или GitLab отключил хук после череды сбоевSettings → Webhooks: адрес, заканчивающийся на /api/webhooks/gitlab, отмеченные Merge request events (и Comments, если нужны ответы в тредах). Отключённый хук включает обратно кнопка Test
401секрет не совпал с GITLAB_WEBHOOK_SECRET или вендор на этой инсталляции не настроенлог бота скажет, что именно: Webhook rejected: invalid X-Gitlab-Token или GitLab webhook rejected: the vendor is not configured…. На GitHub это GitHub webhook rejected: invalid X-Hub-Signature-256, а секрет — GITHUB_WEBHOOK_SECRET
5xx или таймаутбот не ответил или упал на самом событии: контейнер лежит, порт закрыт, мешает проксиcurl -s https://<бот>/api/health — живой бот отвечает "status":"ok" и своей версией. Если бот жив, ищите в его логе 500 on POST
202 {"queued":true}джоба создана; обрыв дальшечитайте лог бота — вторая таблица
202 …"reason":"ignored object_kind=push"отмечен не тот тип события; на GitHub то же выглядит как ignored event=pushотметьте Merge request events (и Comments для ответов)
202 …"reason":"ignored action=update"обновление без новых коммитов: описание, метки, закрытие всех тредов. Аппрув приходит своими действиями, approval и approvedничего — так задумано
Строка в логеЧто произошлоЧто делать
⏭ Skipped: draft, review_drafts=falseMR — черновикпереведите в Ready или поставьте review_drafts: true в .reviewgate/config.yml ветки этого MR
⏭ Skipped MR !N: state closedMR закрыли или смержили раньше, чем до него дошла очередьничего
⏭ Skipped: head … is already reviewedэту вершину уже отревьюили: дубль зависшей джобы или повторная доставканичего; модель не вызывалась, денег это не стоило
⏭ Skipped: dev → release — not a feature-into-integration reviewполитика integration_branches: ревьюится только MR из рабочей ветки в интеграционную, а здесь источник сам стоит в списке или цели в нём нетпоправьте список integration_branches
Incremental: no changed files to review — skipping
Skipped: the scope contains only lock files (…)
нового для ревью нет: файлы не менялись с прошлого прогона или изменились только lock-файлыничего; в MR остаётся сводка прошлого прогона
No files left to review (hidden by ignore: N, excluded by marker: N, empty diff: N)после отбора не осталось ни одного файла; счётчики в скобках говорят, куда они делисьзаметка уже в MR; точные счётчики причин — в этой строке лога
✖ Review failed: …попытка сорвалась, впереди повторыподождите — сколько, сказано в следующей главе
LLM provider unavailable (key/balance), …сбой, который бот распознал: ключ, баланс, адрес модели, контекст, отказ модели, перегрузка провайдера, сетьзаметка в MR называет причину и лечение
The review did not start (… failed, attempts exhausted)не удалось прочитать MR или его файлы: права токена, сеть до GitLab или GitHubпричина — в этой же строке. Заметку бот пытается оставить, но токен без прав её тоже не опубликует
The review did not finish (unexpected error, attempts exhausted)прогон сорвался, и класс ошибки не распознанв MR будет общая заметка без причины; сырое сообщение — в этой строке лога
The summary was not published (…)прогон прошёл и оплачен, но сводка не легла: токен не может комментировать, оборвалась сеть, MR удалиливерните токену права; следующий пуш запустит ревью заново

Бот распознаёт восемь видов сбоев — неверный ключ или кончившийся баланс, неверный адрес модели, не поместившийся контекст, обрезанный ответ, LLM_MAX_TOKENS выше потолка модели, отказ модели, перегрузку провайдера и сеть, которая не поднялась, — и о каждом приходит заметка с причиной и лечением. Всё остальное получает общую заметку: прогон сорвался, смотрите лог. Если строка лога называет то, что мы должны были распознать, — об этом стоит нам сказать.

Где бот молчит по-настоящему

Сначала дайте время. Сбой, который может пройти, повторяется: пять попыток с паузами 30 с, 1 мин, 2 мин и 4 мин. Это семь с половиной минут пауз плюс сами попытки, а зависший вызов модели живёт до LLM_TIMEOUT_MS — по умолчанию десять минут. Повторяются сеть, перегрузка провайдера, нераспознанная ошибка и любой сбой чтения MR; неверный ключ, не поместившийся контекст или отказ модели — нет. Пока идут повторы, в логе стоит ✖ Review failed с try N/5 в теге.

Джоба создана, и её никто не взял. В доставке "queued":true, в логе Queued a review of MR !N — и дальше тишина. Либо воркер не разбирает очередь: не запущен, не достаёт до Redis или все слоты заняты. Либо джоба ждёт намеренно: тот же MR сейчас ревьюит другой прогон (в логе deferring for 30 s, и так может длиться около десяти минут), или её заменил новый пуш. GET /api/health показывает пределы слотов и то, что выполняется в этом процессе, но не видит ни очереди, ни Redis: жив ли Redis, скажет docker compose ps.

Прогон закончился, а сводка не легла. Ревью прошло и оплачено, но публикация сорвалась: у токена пропало право комментировать, оборвалась сеть до GitLab или GitHub, MR удалили. Повторно прогон не запускается. В логе — The summary was not published, в таблице метрик — publish_failed, а в самом MR могут оказаться строчные треды и статус гейта без сводки: они публикуются раньше неё.

И ловушка: после сбоя Resend не делает ничего. Джоба опознаётся по проекту, MR и головному коммиту. Завершённая джоба — в том числе пропуск — хранится час, упавшая — не меньше суток, и повторная доставка того же события за это время новой джобы не создаст: журнал доставок покажет "queued":false с причиной, что такая джоба уже есть, и то же запишет лог бота. Работает только новая вершина или новое состояние: пуш, перевод Draft → Ready, смена целевой ветки. Закрыть и переоткрыть MR не поможет.

На первый MR не пришло ничего

Если на репозитории ревью не приходило ни разу, проверьте шесть вещей именно в таком порядке — первые три видны в журнале доставок.

  1. Адрес. GitLab шлёт на /api/webhooks/gitlab, GitHub — на /api/webhooks/github: всё, что отдаёт бот, живёт под /api.
  2. События.
    • для ревью: Merge request events в GitLab или Pull request в GitHub App. Push events не подойдут — на них бот отвечает ignored object_kind=push;
    • для ответов бота в тредах, если они нужны: ещё Comments в GitLab или Issue comment, Pull request review comment и Pull request review в GitHub — и ответы, включённые у самого бота: REPLY_ENABLED=true или reply.enabled: true, по умолчанию они выключены;
    • включение ответов у бота хук не меняет: в уже созданном хуке GitLab галочку Comments ставят руками, а у GitHub App новые права должна принять каждая установка — иначе GitHub продолжит слать старый набор событий.
  3. Секрет. Secret token в GitLab совпадает с GITLAB_WEBHOOK_SECRET у бота, секрет вебхука App — с GITHUB_WEBHOOK_SECRET. Расхождение — это тот самый 401 из таблицы выше; после правки .env перезапустите контейнер бота.
  4. GitLab или GitHub дотягивается до бота. Self-hosted GitLab запрещает запросы на локальные адреса, пока их не разрешит администратор: Admin → Settings → Network → Outbound requests. Отказ обычно виден уже при сохранении хука. А github.com доставляет события только на публичный адрес: бот за корпоративным фаерволом их не получит.
  5. MR не черновик. Черновики по умолчанию пропускаются: переведите MR в Ready или поставьте review_drafts: true в .reviewgate/config.yml — бот читает его из ветки самого MR.
  6. Учётка бота видит проект и может его комментировать. Без права чтения прогон ломается в самом начале, и после пяти попыток в логе появится The review did not start (change set read failed…). Без права комментировать хуже: ревью проходит и оплачивается, а ломается уже публикация — The summary was not published.

Дальше