Треды бота закрывают, а код не меняется
Бот пишет замечания, команда закрывает треды пачкой, а код под ними не меняется. И непонятно главное: замечания были невалидны или их закрыли, не читая.
Дело не в лени разработчика. Закрыть тред — самое простое действие в ответ на замечание бота: один клик — и нет проблемы, а если проект требует закрыть все треды до мержа, это ещё и открывает мерж. Некоторые разработчики возражают боту в треде, но бот ответит, только если включён режим ответов (reply.enabled), а по умолчанию он выключен. Реакции на комментарии бот не читает. Пока ответственный за качество кода (тимлид или CTO) не начнёт это контролировать, треды будут закрывать.
У этого симптома три разные причины, и лечится каждая по-своему. Поэтому не угадывайте: сначала измерьте, потом найдите причину и только потом меняйте настройки. Настройка, выбранная наугад, может сделать хуже.
Сначала измерьте через API GitLab или GitHub
Метрики ревью тут не помогут: они пишутся по прогонам — модели, токены, сколько замечаний судья оставил и сколько снял. Колонки «исправили или просто закрыли» в них нет. В сводке бот говорит о закрытых тредах одно: сколько повторённых замечаний он не поднял, потому что их треды закрыты. Какие треды закрыты, кем и менялся ли под ними код, хранит сам GitLab или GitHub, и спрашивать надо их API: в GitLab — треды MR (/merge_requests/:iid/discussions в API проекта), в GitHub — треды PR (поле reviewThreads в GraphQL API).
Спрашивать надо не «был ли коммит после закрытия», а «менялась ли строка под тредом с тех пор, как его открыли». Обычный порядок — исправить, запушить и только потом закрыть, так что коммитов после закрытия может не быть вовсе, даже если всё исправлено. GitLab отвечает на этот вопрос сам: когда строка под тредом меняется, он пишет в тред системную заметку «changed this line in version N of the diff». Скрипт ниже выводит автора MR и время мержа, а под ними — треды бота: кто и когда их закрыл и менялась ли под ними строка. API GitLab отдаёт треды только с токеном, даже у публичного проекта; хватит права read_api.
GITLAB=https://gitlab.com
PROJECT=reviewgate-group%2Finvoices-api # url-encoded path, or the numeric project id
MR=1
BOT=reviewgate-bot # the account the bot posts as
get() { curl -sf --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"$GITLAB/api/v4/projects/$PROJECT/$1"; }
get "merge_requests/$MR" |
jq -r '"MR author: \(.author.username) · merged: \((.merged_at // "no")[0:16])"'
get "merge_requests/$MR/discussions?per_page=100" | jq -r --arg bot "$BOT" '
def changed: any(.notes[]; .system and (.body | startswith("changed this")));
[ .[] | select(.notes[0].author.username == $bot and .notes[0].resolvable)
| .notes[0] + {changed: changed} ]
| "bot threads: \(length) · resolved: \(map(select(.resolved)) | length)",
( .[] | " \(.position.new_path):\(.position.new_line) "
+ (if .resolved
then "closed by \(.resolved_by.username) \(.resolved_at[0:16])"
else "open" end)
+ (if .changed then " · line changed" else "" end) ),
"resolved, line untouched: \(map(select(.resolved and (.changed | not))) | length)"'MR author: Novohudonosor · merged: no
bot threads: 6 · resolved: 4
src/pricing/pricing.service.ts:52 closed by Novohudonosor 2026-09-08T12:48
src/invoices/invoices.service.ts:72 closed by Novohudonosor 2026-09-08T12:48
src/invoices/invoices.service.ts:73 closed by Novohudonosor 2026-09-08T12:48
src/invoices/invoices.service.ts:69 closed by Novohudonosor 2026-09-08T12:48
src/invoices/invoices.service.ts:77 open
src/reports/reports.service.ts:23 open
resolved, line untouched: 4Главное — в последней строке: четыре треда закрыты, и ни под одним строка не менялась. Это сигнал, а не приговор: если замечание исправили в другом месте, замер этого не увидит, а правка той же строки — ещё не исправление. Спорные треды откройте и прочитайте.
На GitHub тот же вопрос задаётся одним GraphQL-запросом. Поле isOutdated GitHub ставит треду, чей код изменился после комментария, поэтому тред с isResolved: true и isOutdated: false — наш случай. Треды бота отличайте по автору первого комментария. Отправить запрос можно командой gh api graphql -f query='…'.
{
repository(owner: "ReviewGate", name: "invoices-api") {
pullRequest(number: 1) {
author { login }
mergedAt
reviewThreads(first: 100) {
nodes {
path line isResolved isOutdated resolvedBy { login }
comments(first: 1) { nodes { author { login } } }
}
}
}
}
}Теперь посмотрите, кто закрывал треды, — «closed by» в выводе скрипта. Если треды закрывает сам автор MR, это его мнение о замечаниях. Если чужие треды закрывает один и тот же человек, обычно тот, кто мержит, и все разом перед мержем, это процесс: треды закрывают, чтобы открылся мерж. По одному MR этого не понять — прогоните скрипт на пяти–десяти последних MR. И оговорка для GitLab: если в проекте включено автозакрытие устаревших тредов (Settings → Merge requests → Merge options), GitLab сам закрывает тред, под которым изменилась строка, и записывает закрывшим того, кто запушил. Скрипт покажет у такого треда «line changed», и к нашему случаю он не относится.
Один симптом — три причины
| Что видно в замере и в тредах | Диагноз | Что менять |
|---|---|---|
| Треды закрывает не автор MR, а один человек — все разом, перед мержем | Закрытие — налог: проект требует закрыть все треды до мержа, и их закрывают, чтобы открылся мерж | Убрать налог: min_severity, чтобы мелочь не становилась тредом |
| Автор закрывает свои; замечания верные, но про старый код, который он только задел или перенёс | «Верно, но чинить не будем» | Закрытие как сигнал · dont_flag · строка в описании MR |
| Автор закрывает свои; у замечаний пометка 📐 — они по вашим же правилам | Правило не то или слишком широкое | Переписать, сузить или понизить правило — либо спорить, не закрывая тред |
Налог. Если проект требует закрыть все треды до мержа (в GitLab — проверка «All threads must be resolved», в GitHub — «Require conversation resolution before merging»), закрытие ничего не говорит о замечании: тред закрывают, просто чтобы смержить. Не ищите в этом намерения — уберите налог. С min_severity: major бот не открывает треды на замечания ниже major: они приходят свёрнутым списком в сводке, и ничего не теряется. Учтите побочный эффект: приглушённые замечания не держат и гейт. Поэтому если поднять этот порог выше severity_gate, гейт начнёт срабатывать только с нового порога.
Верно, но чинить не будем. Частый и незаметный случай: замечание верное, но оно про старый код, который автор только задел или перенёс. Ходов три:
- Закрыть тред. До конца MR бот это замечание больше не поднимет — как он узнаёт повтор, рассказано в следующей главе. В новом MR будут новые треды.
dont_flagв конфиге — для того, о чём вы не хотите слышать вообще. Это текст в промпте, просьба к модели, а не фильтр, и действует он на весь репозиторий. Конфиг бот читает из ветки MR, поэтомуdont_flagсработает уже на пуше, который его добавил.- Строка в описании MR. Её на каждом прогоне читают и модель, и судья: «файлы под
legacy/перенесены как есть, не проверять». Она ничего не стоит и действует, пока жив MR. Правка описания прогона не запускает: строка сработает со следующего пуша.
Правило нельзя ограничить путями. У правила три поля — id, description и severity. Если дописать globs, бот на каждом прогоне предупредит в сводке, что ключ не распознан и не применён, а правило всё равно будет действовать на весь дифф. Убрать путь из ревью целиком можно через ignore, но тогда модель не увидит этих файлов вовсе.
Правило не то. Если закрывают замечания с пометкой 📐, то есть по вашим же правилам, проблема не в команде: правило шире, чем вы имели в виду, или требует того, во что вы больше не верите. Понизьте ему severity, сузьте формулировку или удалите его. Правило, которому никто не следует, — это оплаченные замечания, которые тут же закрывают, и команда привыкает, что закрыть тред — нормальный ответ. И одна особенность бота, которую стоит знать до любых споров: в закрытом треде бот молчит всегда, даже при включённом режиме ответов. Если ответить и сразу закрыть тред (в GitLab это галочка «Resolve thread» под ответом), бот не ответит.
Что бот делает с закрытым тредом
Закрыть тред — не значит удалить замечание: бот может найти его снова. Меняется то, как бот с ним поступает, и меняется не сразу: само закрытие прогона не запускает, всё, о чём ниже, случится на следующем прогоне — обычно это следующий пуш.
В этом MR замечание больше не поднимется. Найдя замечание снова, бот ищет его среди тредов MR по трём признакам, по очереди: тот же файл, имя замечания (оно стоит жирным в начале треда) и текст строки; тот же файл и текст строки; тот же файл и имя замечания. Нашёлся закрытый тред — замечание не публикуется. Съехавшая строка второй копии не даст: признаки смотрят на текст строки, а не на её номер. Если же на строке с тредом переименовали переменную, текст строки уже другой, и совпасть может только третий признак — когда модель назовёт замечание так же. Назовёт иначе — появится новый тред. У третьего признака есть и обратная сторона: один закрытый тред до конца MR глушит в своём файле все новые замечания с тем же именем, даже про другие строки.
Повторённое замечание бот не публикует, но считает. На учебном репозитории мы закрыли все четыре треда бота, не тронув ни строки, и запушили косметический коммит — переименование локальной переменной и докблок, — который ничего не чинит. Бот перепроверил оба изменённых файла, нашёл те же четыре проблемы и пятую сверх них, а комментарий оставил ровно один:
❌ Severity gate (`major`) НЕ пройден: блокирующих замечаний 4.
Замечаний: **5** (🔴 2 · 🟠 2 · 🔵 1); новых строчных: 1.
По правилам команды (📐): 3.
Из ранее найденного — закрыто вами (resolved), не поднимаю: 4.Четыре блокирующих замечания, а видно из них одно: остальные три повторились в тредах, которые вы закрыли, и нового треда не получили. Вердикт считается по тому, что прогон нашёл, а не по тому, что он опубликовал: в гейт попали все четыре. Где остальные, говорит только строка про закрытые треды — и то числом, без списка.
Закрытый блокер не держит гейт, если его файл больше не перепроверяют. После пуша бот по умолчанию проверяет только файлы, изменённые с прошлого прогона. Чтобы блокер не терялся на пушах в другие файлы, бот переносит в гейт блокирующие замечания прошлых прогонов — но только из открытых тредов. Закрытый тред не переносится. Значит, стоит закрыть блокирующий тред и запушить правку в другом файле — и гейт зелёный, хотя код под замечанием не менялся. Если же пуш затронет этот файл и модель повторит замечание, гейт снова красный, как в примере выше.
Позеленеть без правки кода гейт может и по-другому. Автор может поправить .reviewgate/config.yml прямо в своём MR — поднять severity_gate или отправить файл в ignore: конфиг бот читает из ветки MR. Такую правку бот отмечает в сводке заметкой «config.yml изменён в этом MR». А ещё модель при перепроверке может просто не повторить замечание.
Ограничить, кто закрывает треды бота, нельзя ни в GitLab, ни в GitHub: в GitLab тред закрывает автор MR и любой с ролью Developer и выше, в GitHub — автор PR и любой с правом записи в репозиторий. У бота такой настройки тоже нет. Если гейт для вас настоящий барьер, договоритесь в команде, кто закрывает блокирующие треды, и следите в сводке за заметкой об изменённом конфиге.
С какого репозитория начинать
Если вы только подключаете бота, порядок важнее настроек. Начинайте с репозитория, где ревью сегодня не делают, а не с того, где ревью делают хорошо. Там, где каждый MR и так внимательно читает человек, замечаниям бота приходится соперничать с замечаниями коллег. Там, где не читает никто, бот — единственный, кто читает код, и его треды — единственная обратная связь, которую получает автор.
Команда, которая не делает ревью, встретит молчанием и бота — никто не спорит, никто не отвечает, треды закрывают по дороге к мержу. Там гейт — единственное, что придаёт замечанию вес. Поставьте severity_gate на уровень, ради которого вы правда остановили бы мерж, а всё, что ниже, оставьте информацией. Проверьте, что без зелёного статуса мерж не пройдёт: в GitLab для этого нужна проверка «Pipelines must succeed», в GitHub — правило «Require status checks before merging» с проверкой бота в защите ветки. И помните, как гейт обходят без правки кода, — об этом глава «Что бот делает с закрытым тредом».
Дальше
- Правила, которые линтер проверить не может— если замечания по правилу всё время закрывают, правило пора переписать
- Спор с замечанием— что делать вместо закрытия треда
- Справочник конфига— min_severity, dont_flag, severity_gate и режим ответов целиком