команда · внедрение

Треды бота закрывают, а код не меняется

Бот пишет замечания, команда закрывает треды пачкой, а код под ними не меняется. И непонятно главное: замечания были невалидны или их закрыли, не читая.

Дело не в лени разработчика. Закрыть тред — самое простое действие в ответ на замечание бота: один клик — и нет проблемы, а если проект требует закрыть все треды до мержа, это ещё и открывает мерж. Некоторые разработчики возражают боту в треде, но бот ответит, только если включён режим ответов (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='…'.

GraphQL
{
  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» с проверкой бота в защите ветки. И помните, как гейт обходят без правки кода, — об этом глава «Что бот делает с закрытым тредом».

Дальше