ADR 0028 — Защита веток: расхождение между providers и матрица одобрения
Контекст
ADR 0001 нормализовал девять операций
git в единый контракт. Восемь из них расходятся между GitHub и GitLab лишь
ФОРМОЙ API — имя поля, формат id, код ошибки, — и нормализация
это решает. protectBranch — исключение: там оба расходятся в модели, и
перевод невозможен без потери смысла.
Расхождение было зафиксировано в комментарии в обоих providers с Фазы 2.
Этот ADR повышает его до явного решения, потому что начиная с Фазы 4 у домена
появилась собственная матрица одобрения — QA → SecOps → пользователь, с
пределом замка merge в decide.ts, — и теперь существуют два источника
полномочий над одним и тем же merge. Этого наложения не было, когда
писался комментарий.
Что каждый provider делает сегодня
ProtectBranchInput (packages/shared) несёт лишь externalId,
branchName и accessToken — никакой конфигурации политики. Каждая
реализация выбирает "самое строгое разумное", и это оказывается в разных
местах:
| provider | модель платформы | что мы применяем |
|---|---|---|
| GitHub | независимые правила в богатом payload | enforce_admins: true, required_approving_review_count: 1, без status checks, без ограничения push |
| GitLab | два уровня доступа | pushAccessLevel: MAINTAINER, mergeAccessLevel: MAINTAINER |
| local | платформы не существует | capabilities.protectBranch: false; вызов отклоняется с GitNotSupportedError |
Шаг protect_branches в bootstrap проверяет capability перед действием, а
контрактная suite утверждает по capability, а не по provider, — поэтому
расхождение никогда не ломало тест: оба пути одинаково валидны для
контракта.
Асимметрия, которую выявляет матрица одобрения
Две защиты — не "одно и то же, записанное по-разному". Они взаимодействуют противоположным образом с матрицей домена:
- В GitHub мы создаём второго носителя полномочий.
required_approving_review_count: 1требует одобрения ПЛАТФОРМЫ, которого домен не знает и не заполняет, — заключения QA и SecOps — это наши события, а не reviews GitHub. В сочетании сenforce_admins: true, который убирает обход администратором, ручной merge пользователя — который CLAUDE.md делает обязательным — может оказаться заблокированным платформой, когда нет второго человека для одобрения PR. - В GitLab мы не создаём никаких полномочий. Нет подсчёта одобрений: тот, у кого роль Maintainer, делает push и merge напрямую. Матрица домена — ЕДИНСТВЕННЫЕ ворота, и токен Maintainer обходит их целиком.
- В local существуют только ворота домена, по построению.
То есть: одна и та же система строже задуманного у одного provider и мягче
у другого, одним и тем же protectBranch() без аргументов.
Решение
1. Матрица одобрения домена — источник истины. QA → SecOps →
пользователь, с пределом decide.ts (merge с назначением в защищённую ветку никогда
не автоодобряется). Защита платформы — эшелонированная защита от
доступа в обход Brabo — это не ворота, и никакая логика домена не должна
от неё зависеть.
2. Расхождение остаётся. Перевод двух моделей к общему знаменателю
означал бы опустить GitHub до уровня GitLab (потеряв enforce_admins) или
выдумать в GitLab понятие одобрения, которого платформа не имеет на
бесплатном тарифе. Каждая сторона применяет самое строгое, что способна выразить, а контракт
обещает лишь наблюдаемое: listBranches возвращает protected: true.
3. ProtectBranchInput сейчас НЕ получает конфигурацию. Добавление
requiredApprovals, enforceAdmins и подобного создало бы словарь, который лишь
один из providers умеет соблюдать, а другому пришлось бы молча игнорировать, —
что хуже нынешнего расхождения, потому что начало бы лгать. Когда появится
реальная необходимость, путь — нормализованный ProtectionPolicy, где provider объявляет
через capabilities, что умеет применять, а bootstrap сообщает, что было
проигнорировано.
Последствия
Принятые:
- Разная жёсткость по provider, задокументированная здесь и в комментариях обоих файлов, которые начинают указывать на этот ADR.
- В GitHub у репозитория с единственным владельцем ручной merge может быть заблокирован
той самой защитой, которую мы применяем. Обход — за оператором (снизить
required_approving_review_countдо 0 в репозитории), а не за кодом, — изменение по умолчанию ослабило бы защиту во всех репозиториях ради решения частного случая.
Не проверено:
- Ни один из этих двух путей не был задействован против реального API в этом
репозитории. Smokes (
github-provider.smoke.spec.ts,gitlab-provider.smoke.spec.ts) ручные и пропускаются безGITHUB_TEST_TOKEN/GITLAB_TEST_TOKEN; CI используетLocalGitProvider, чья capability —false. Описанная выше блокировка merge — вывод из задокументированного поведения двух платформ, а не наблюдение. При подключении первого реального репозитория проверка этого — первый тест, который нужно сделать.
Вне области:
- Отражать заключения QA/SecOps как status checks GitHub, что устранило бы наложение, превратив матрицу домена в условие merge платформы. Это правильное решение, и оно зависит от того, будет ли у Brabo публичный endpoint, который мог бы вызывать GitHub, — см. ожидающий пункт registry/публикации в ADR 0027.