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

ADR 0030 — Механизированная политика веток

Контекст​

Политика веток Brabo существовала как презентация и как соглашение в CLAUDE.md: конвейер dev → qa → main, таксономия веток, продвижение только между соседними ступенями, вычисляемая версия, hotfix, возвращающийся обратным распространением.

Записанное соглашение — не механизм. Пока правило живёт только в документе, оно зависит от того, чтобы все о нём помнили в неподходящий час, — а неподходящий час это всегда час инцидента, в 3 часа ночи, когда помнить — ровно то, что не происходит. Хуже: правило, которое никто не проверяет, не может быть нарушено заметно. Оно нарушается молча, а симптом появляется месяцы спустя, далеко от причины.

ФАЗА 6 механизировала всю политику в самом репозитории Brabo. Этот ADR закрывает фазу: отображает правило → механизм, фиксирует, что было отрезано, с ценой возвращения каждого среза, и отделяет то, что из этого становится шаблоном для bootstrap Gitflow продукта, — это будущая фаза, а не код сейчас.

Две ограничения сформировали всё. Первое: это репозиторий пользователя, а не организации. Команд GitHub здесь не существует, а GITHUB_TOKEN не читает членство в команде даже в организации. Второе: продукт нельзя трогать в этой фазе. ФАЗА 6 — это CI/CD репозитория; то, чему она учит продукт, становится этим ADR, а не кодом.

Решение​

Правило → механизм​

правило политикимеханизмгде живёт логика
имя funcao/descritivo, regex ^.{0,15}/\S{0,32}$check pr-policescripts/ci/pr-police.ts
префикс в закрытом списке из 9 функцийpr-policeто же
работа рождается из dev, hotfix — из mainpr-police, через загрязнението же
назначение согласовано с функциейpr-policeто же
продвижение только между соседними ступенямиpr-police + promotepr-police.ts, version.ts
метка семейства на каждом PRpr-policeто же
одобрение, требуемое по назначениюcheck approval-ladderscripts/ci/approval-ladder.ts
два режима одобрения по переменнойapproval-ladderто же
разные люди в mainсопоставление перебором с возвратомто же
версия по наибольшему воздействию циклаpromote + tag-releasescripts/ci/version.ts
N увеличивается при отклоненииподсчёт существующих теговто же
финальная привязана к последней -qa.Ntag-release, через дерево + родителято же
hotfix порождает PATCH без привязкиtag-release, через второго родителято же
прямой push заблокированrulesetsdocs/reference/rulesets.md
обратное распространение обязательно и упорядоченоcheck backmerge-gatescripts/ci/gate.ts
merge в постоянную ветку всегда ручнойотсутствие механизма, по решению—

Вся логика — тестируемый скрипт; workflow — тонкая оболочка, читающая окружение и вызывающая скрипт. 149 тестов покрывают счастливые пути и случаи отказа.

Семь уроков, которые стоили больше кода​

Они здесь, потому что каждый стоил эмпирического открытия, и ни один не очевиден в сторонней документации.

1. Каждое семейство триггеров читает workflow из разного места. pull_request и push читают из ветки события; pull_request_target и workflow_dispatch — из ветки по умолчанию. С pull_request_target у pr-police было ноль запусков — main отставала на 65 коммитов. Это курица и яйцо: check, который двигает конвейер, должен уже быть в main, чтобы иметь возможность запуститься.

2. Тег и PR, созданные через GITHUB_TOKEN, не запускают workflow. Правило против рекурсии. Release v0.2.0 никогда не опубликовалась из-за этого, а PR обратного распространения без check никогда не стал бы зелёным — цепочка застряла бы навсегда. Отсюда BRABO_BOT_TOKEN.

3. «Не смог проверить» никогда не может стать «всё в порядке». promotion-check читал настройку merge через gh api --jq и сравнивал с 'true'. Когда у токена нет разрешения, команда завершается успешно, возвращая пустоту — а пустота не 'true', так что проверка начинала проваливать по неправильной причине или, хуже, одобрять. Сегодня есть три состояния, и невозможность становится предупреждением, тогда как настоящая гарантия смотрит на свершившийся факт: у коммита два родителя.

4. Равенство дерева сильнее равенства коммита. Привязка сравнивала sha — невыполнимо, потому что --no-ff создаёт новый коммит. Сравнение дерева и требование, чтобы -qa.N была родителем, сильнее: если бы другая сторона merge принесла файл, дерево изменилось бы.

5. Загрязнение, а не происхождение. Выяснить, откуда родилась ветка, неразрешимо задним числом: при P ⊆ Q ⊆ head argmin расстояния выбирает самую продвинутую содержащуюся постоянную ветку, а не исходную. Правильный вопрос другой — «несёт ли эта ветка вершину постоянной ветки, более продвинутой, чем та, которую она объявляет?», — и на него есть ответ.

6. Освобождение по автору, никогда по префиксу. Освобождать ветки, начинающиеся с dependabot/, было бы открытой брешью: любой может так назвать ветку.

7. Объявленное состояние должно быть проверяемым. .release/gate.json едет в PR обратного распространения в qa и dev; продвижение может поднять эту старую копию обратно и воскресить замок без стоящего за ним hotfix — заперев репозиторий навсегда, потому что нет ожидающего обратного распространения, которое бы его разрешило. Check спрашивает git, спустился ли hotfix, и сбрасывает выполненные замки. locked — запись намерения; содержимость — истина.

Что было отрезано и цена возвращения​

отрезанопочемучто стоит вернуть
ступень rc / UATбез окружения и без людей для её исполнения это была бы церемониальная ступеньсоздать ветку, строку в ESCADA, строку в ESTAGIO_POR_BRANCH, строку в лестнице одобрения, и порядок gate получает ступень — цепочка hotfix становится четырьмя PR
таксономия с rcfixумерла вместе со ступенью rcзапись в FUNCOES_DE_CORRECAO_ALTA плюс тесты происхождения и назначения
команды GitHub для ролейу репозитория пользователя нет команд, а GITHUB_TOKEN не читает членство даже в orgроли — списки handles в переменных; переход на команды потребовал бы org и токен с read:org
deployшаг, который никогда не запускается, гниёт: его никто не тестирует, и в день включения он будет неправильнымсобственный workflow, запускаемый тегом, плюс Environments — здесь нечего переподключать
GitHub Environmentsто жесоздать, когда появится окружение
GOVERNANCE.mdотрезан в ФАЗЕ DOC; критерий живёт в branching-policy.mdнаписать файл и перенести туда раздел о ролях

Лестница одобрения в режиме community не отрезана: она реализована, протестирована и выключена конфигурацией. Разница намеренная — код, выключенный переменной, с тестом обеих сторон, демонстрируем сегодня; шаг конвейера, выключенный переменной, — нет.

Что становится шаблоном bootstrap продукта​

Продукт подготавливает репозитории с Gitflow (ФАЗА 2). Что ФАЗА 6 выучила и что должно войти в этот шаблон, в будущей фазе:

входит в шаблонне входит
конвейер и таксономия как данные, а не код — у продукта есть проекты с разными лестницамиworkflows Brabo, скопированные буквально
gate обратного распространения: это правило, которое дороже всего стоит, когда его нетBRABO_BOT_TOKEN — каждому репозиторию нужны свои учётные данные
версия, вычисляемая из тега, без PR bumpсписки одобряющих этого репозитория
«не смог проверить» как явное третье состояние в gates—
освобождение бота по автору—

GitProvider уже выставляет capabilities, а ADR 0028 фиксирует, что защита ветки расходится между providers. Шаблону придётся деградировать: где нет ruleset, правило становится check, а сообщение говорит, что не удалось гарантировать, — вместо того чтобы изображать гарантию.

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

Политика перестала зависеть от памяти. Неправильное имя ветки, продвижение через ступень, hotfix без обратного распространения и непривязанный финальный тег теперь — провалы check с обучающим сообщением, а не поздние открытия.

Конвейер был пройден от начала до конца, а не только протестирован модульно. v0.1.0 → v0.2.0-dev.1..4 → -qa.1..3 → v0.2.0, с разыгранным отклонением между штампами qa, чтобы доказать, что N считает сам. Финальная привязалась к -qa.3 с идентичным деревом.

Обязательный check, который никогда не запускается, запирает PR навсегда. Это самое неудобное последствие: список обязательных checks теперь — связь между ruleset (применяемым вручную) и workflows (версионируемыми). Новый job в CI либо входит в список rulesets.md, либо остаётся вне намеренно, и кто-то пишет почему. Именно поэтому у job backmerge-gate нет if:: старый вердикт, приклеенный к sha, который не запускался, освободил бы merge во время замка.

Обход gate принадлежит актору, а не пути. Rulesets GitHub не ограничивают обход по пути. Кто может писать .release/gate.json, технически может писать что угодно в main. Реально ограничивают workflow и история. Зафиксировано как ограничение инструмента.

Применение ruleset остаётся ручным. Репозиторий версионирует источник (docs/reference/rulesets.md); применение — действие owner. Автоматизация потребовала бы токена, способного менять собственные защиты, — что свело бы на нет их смысл.

Сосредоточение ролей у owner записано, а не подразумевается. Ответственный за релиз и дежурный по hotfix — owner, пока APPROVAL_MODE=solo. Обе обязанности вновь открываются при переходе на community, где запасной вариант дежурного должен будет быть задокументированным исключением, никогда обходом.

Ничего в продукте не изменилось. Ни одна строка apps/ не тронута. То, чему фаза научила продукт, — в таблице шаблона выше и стало планом, а не кодом.

Ссылки: ADR 0028 (расхождение защиты между providers), 0029 (механизм документации, используемый этой фазой) и полная политика в branching-policy.