Бот промолчал: как найти место обрыва
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=false | MR — черновик | переведите в Ready или поставьте review_drafts: true в .reviewgate/config.yml ветки этого MR |
| ⏭ Skipped MR !N: state closed | MR закрыли или смержили раньше, чем до него дошла очередь | ничего |
| ⏭ 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 не пришло ничего
Если на репозитории ревью не приходило ни разу, проверьте шесть вещей именно в таком порядке — первые три видны в журнале доставок.
- Адрес. GitLab шлёт на
/api/webhooks/gitlab, GitHub — на/api/webhooks/github: всё, что отдаёт бот, живёт под/api. - События.
- для ревью: 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 продолжит слать старый набор событий.
- для ревью: Merge request events в GitLab или Pull request в GitHub App. Push events не подойдут — на них бот отвечает
- Секрет. Secret token в GitLab совпадает с
GITLAB_WEBHOOK_SECRETу бота, секрет вебхука App — сGITHUB_WEBHOOK_SECRET. Расхождение — это тот самый401из таблицы выше; после правки.envперезапустите контейнер бота. - GitLab или GitHub дотягивается до бота. Self-hosted GitLab запрещает запросы на локальные адреса, пока их не разрешит администратор: Admin → Settings → Network → Outbound requests. Отказ обычно виден уже при сохранении хука. А github.com доставляет события только на публичный адрес: бот за корпоративным фаерволом их не получит.
- MR не черновик. Черновики по умолчанию пропускаются: переведите MR в Ready или поставьте
review_drafts: trueв.reviewgate/config.yml— бот читает его из ветки самого MR. - Учётка бота видит проект и может его комментировать. Без права чтения прогон ломается в самом начале, и после пяти попыток в логе появится
The review did not start (change set read failed…). Без права комментировать хуже: ревью проходит и оплачивается, а ломается уже публикация —The summary was not published.
Дальше
- Подключение к GitLab— события, секрет, права, которые нужны токену
- Подключение к GitHub— права App, события, принятие новых прав
- Решение проблем— распознанные сбои и их лечение
- «Замечаний нет» — а бот вообще смотрел?— если сводка есть, но пустая
- Треды бота закрывают, а код не меняется— когда бот говорит, а команда закрывает