Перейти к основному содержимому

ADR 0139 — Сигнал тревоги о merge конвейера получает адресата и правило для dev

Контекст​

Политика с самого начала говорит, что продвижение — это --no-ff (branching-policy.md): "a squash would flatten the lower step's commits, and the stage's tag would end up pointing to a commit that no longer exists". Правило было нарушено три раза — #367 (2026-08-24), #394 (2026-08-25) и #464 (2026-09-04), — и третий раз обошёлся дороже всех, потому что это был как раз PR, исправлявший первые два.

То, что обнаружилось при расследовании, было не дырой в обнаружении. Было обратное:

Проверка существовала и сработала. tag-release отклоняет merge в qa без второго родителя с 2026-07-27, и журнал прогона от 2026-08-25 говорит прямым текстом:

[tag-release] o merge em `qa` não é merge commit (1 pai)
##[error]o merge em `qa` não é merge commit (1 pai)

Она прозвенела на обоих squash-продвижениях. А история осталась сломанной точно так же, на 293 коммита, пока следующее продвижение не открылось с 11 файлами в "конфликте", который не был конфликтом, — лишь потерянное происхождение.

Не хватило того, КОМУ звенеть. Workflow на push, падающий на постоянной ветке, не имеет PR, где стать красным: merge уже произошёл. Остаётся прогон на вкладке Actions, которую никто не открывает, потому что ничто не сообщает о его существовании. В репозитории было ноль issues за всю историю — канала не существовало. Обнаружение без адреса — это тревога, звенящая в пустой комнате.

Почему просто не отключить squash​

Это был первый рассмотренный выход, и он единственный с абсолютной гарантией: allow_squash_merge: false убирает кнопку. Он был измерен и отвергнут, с числом на руках: шесть последних merge в dev — это squash с одним родителем — squash и есть соглашение для рабочих PR здесь, — а scripts/changelog.mjs генерирует CHANGELOG через git log --no-merges, рассчитывая на это: сегодня squash — одна строка; с merge commit каждый промежуточный WIP-коммит стал бы записью. Отключение squash чинит продвижение, облагая налогом каждый рабочий PR и вмешиваясь в генерацию changelog. Проблема — в конвейере; счёт оплачивала бы работа.

Зафиксировано и то, что тоже не годится, чтобы никто не искал снова: merge queue решил бы это фиксированным методом на очередь, а CLAUDE.md её явно запрещает; а нативный контроль GitHub по ветке в этом пространстве — required_linear_history, который делает обратное — запрещает merge commit. Не существует "требовать merge commit" для ветки.

Решение​

1. Тревога получает job с адресатом. tag-release.yml получает avisar, if: failure(), который открывает issue, называющую ветку, коммит, прогон и исправление, — и комментирует уже открытую issue вместо открытия второй для той же ветки, потому что поток одинаковых issues — быстрейший способ научить кого-то их игнорировать. Заголовок — ключ дедупликации, зафиксированный тестом (tituloDoAlarme).

Он аутентифицируется через github.token, никогда через PAT: невалидный BRABO_BOT_TOKEN — один из сбоев, о которых он должен сообщать (checkout умирает с could not read Username), а тревога, зависящая от того, что может быть сломано, — это тревога, которая молчит ровно тогда, когда должна звенеть. Тело идёт через --body-file, никогда не интерполируется в run: — в тексте есть обратные кавычки и $, а интерполяция содержимого внутри bash -e — это инъекция.

2. Правило начинает покрывать dev, и покрытие УЗКОЕ. dev получает работу постоянно и намеренно через squash, поэтому "один родитель — дефект" там не действует — это отклоняло бы каждый PR. Дефектом в dev является PR, который приносил новое ребро: его head был merge, чей второй родитель ещё не был в базе. Это был #464, вся поставка которого и была этим ребром.

Различие важно, потому что существует почти идентичный и безобидный сосед: рабочий PR, втянувший dev в себя перед merge, тоже имеет merge в head — но второй родитель уже в базе, и squash ничего не теряет.

3. Запрос, идентифицирующий PR, — по merge_commit_sha, и это не стиль. gh api repos/{repo}/commits/{sha}/pulls возвращает каждый PR, связанный с коммитом, — а поскольку dev является head PR продвижения, остающегося открытым весь цикл, каждый коммит dev связан с ним. Чтение .[0].head.ref вернуло бы dev (постоянную ветку) для любого squash рабочего PR, и правило отклонило бы весь репозиторий. Ошибка была совершена и поймана проверкой на реальных данных до отправки; записано, чтобы не вернулась.

4. null — это ответ, и в dev он не становится дефектом. Невозможность узнать, было ли ребро (API недоступен, PR удалён, объект отсутствует в клоне), отличается от знания, что его не было. В dev это проходит, потому что базовая частота — легитимная работа. В qa/main ничего не меняется: туда входит только конвейер, и один родитель — дефект с известным источником или без него. Это намеренно обратное доктрине promotion-check ("verificação impossível conta como reprovada") — там вселенная — только продвижение, здесь — нет.

Последствия​

Это не предотвращает, и в этом различии суть. Метод выбирается в клике; настоящее предотвращение потребовало бы одного из двух отвергнутых выше выходов или изъятия merge из рук человека — что упирается в правило CLAUDE.md, что merge в защищённую ветку всегда ручной. Меняется время обнаружения: с "в следующем цикле продвижения" на "минутой позже, в issue с владельцем", когда откат ещё тривиален.

Предотвращение остаётся открытым, и это продуктовое решение. Выход, который решает без налога на работу, — promote.yml, который уже существует, уже является workflow_dispatch и уже имеет пару конвейера как input, — начинает сам выполнять git merge --no-ff. Это сохраняет полномочия (ничто не мержится без запуска человеком) и механизирует лишь метод, но требует решить, считается ли человеческий dispatch "ручным merge" в смысле CLAUDE.md, и открыло бы третье исключение из "никогда прямой push в постоянную ветку". Здесь не принято.

Проверено на реальных данных, а не рассуждением. Правило было запущено против трёх коммитов, которые его мотивировали: отклоняет bf6db00c9 (squash #464) и одобряет 1eb71c322 и 108d81cc7 (обычные рабочие PR) — без ложных срабатываний.

Находка по пути, вне досягаемости этого ADR: BRABO_BOT_TOKEN невалиден или просрочен с вечера 2026-09-03, и поэтому ни один тег не ставится ни на одной ветке — checkout tag-release падает до любой логики. Решает только ротация секрета, и это TODO(humano). Зафиксировано в CLAUDE.md.