Перейти к основному содержимому
Перевод от 2026-10-15; оригинал с тех пор изменился.

Rulesets репозитория

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

Политика, которую применяют эти правила, описана в Política de branches.

Текущее состояние: gh api repos/daneiel/brabo/rulesets возвращает 0. Ничего ещё не применено. До тех пор всё здесь — объявленное намерение, а не реальная защита.

Предусловие: все три ветки должны существовать​

У лестницы три ступени, и ruleset не нацеливается на то, чего нет.

git fetch origin
git push origin origin/dev:refs/heads/qa

dev и main уже существуют. qa рождается от dev — лестница начинается пустой и заполняется продвижениями.

Ветка rc была удалена из лестницы. Если она ещё существует в remote старого клона, удалите её: git push origin --delete rc. Постоянная ветка, которой нет в лестнице, — приглашение для PR нацелиться на ступень, которой больше нет.

Ruleset 1 — три постоянные ветки​

Имя: permanentes Enforcement: Active Цель: Branch → Include by pattern, три записи: dev, qa, main

Один ruleset на три, а не три ruleset: требование во всех идентично. Между ступенями различается кто одобряет, и это решает approval-ladder по назначению, а не защита.

Правила, которые нужно отметить​

правилозначениепочему
Restrict deletions✅случайно удалить main через интерфейс необратимо
Block force pushes✅force-push в постоянную ветку переписывает уже продвинутое; тег ступени в итоге указывал бы на несуществующий коммит
Require a pull request before merging✅это центральное правило: никакое изменение не входит без PR
↳ Required approvals0считает approval-ladder, который знает режим и назначение. Нативное число GitHub не отличает dev от main
↳ Dismiss stale approvals on push✅одобрение старого коммита не считается — одобренное не то, что будет смёржено
↳ Require review from Code Owners✅CODEOWNERS ставит владельца ревьюером всего
Require status checks to pass✅см. список ниже
↳ Require branches to be up to date❌заставило бы делать rebase при каждом merge в dev; CI уже запускается на merge-коммите
Block merge queue—не используется

Required approvals = 0 — это не ослабление. GitHub умеет только считать; он не знает, что main требует PO + управление, а dev — одного разработчика, и что в режиме solo PR самого владельца проходит без ревью. Поставить здесь 1 сломало бы режим solo: владелец не может одобрить собственный PR через интерфейс, и PR застрял бы навсегда. Настоящее требование живёт в approval-ladder, который является обязательной проверкой, — а обязательную проверку не обойти.

Две конфигурации рядом​

Ruleset один и тот же в обоих режимах. Меняется переменная — и поэтому миграция не проходит через Settings → Rules.

solo (сегодня)community (в будущем)
Required approvals в ruleset00
кто требуетпроверка Escada de aprovaçãoта же проверка
переменныеAPPROVAL_MODE=solo, OWNER_HANDLEAPPROVAL_MODE=community, APROVADORES_*
dev1 от владельца1 × devs
qa1 от владельца2 × devs
main1 от владельца1 × PO + 1 × управление
PR самого владельцапроходит без ревьюидёт по лестнице, как у любого
разные людиприостановленодействует в main

Required approvals остаётся 0 в обоих случаях, и это обдуманно. GitHub умеет только считать: не отличает dev от main, не знает ролей и не знает, что в режиме solo PR самого владельца проходит без ревью. Поставить там 1 сломало бы режим solo — владелец не может одобрить собственный PR через интерфейс, и каждый его PR застрял бы навсегда. Настоящее требование живёт в проверке, которая обязательна и не обходится.

Скопируйте и вставьте, чтобы включить каждый режим:

# solo — what's in effect today
gh variable set APPROVAL_MODE --body solo
gh variable set OWNER_HANDLE --body daneiel

# community — fill in the lists BEFORE flipping the switch
gh variable set APROVADORES_DEVS --body "ana,bruno,carla"
gh variable set APROVADORES_PO --body "paula"
gh variable set APROVADORES_GESTAO --body "gustavo"
gh variable set APPROVAL_MODE --body community

Обязательные проверки​

Имя точное, как его регистрирует GitHub (это name: job, а не workflow):

Колонка длительности измерена, а не оценена — три реальных запуска, холодный и тёплый кэш. Она существует, чтобы «оптимизировать CI» начиналось с числа, а не с догадки. * отмечает строку, измеренную по одному реальному запуску вместо трёх, — job новый (или только что переименован) и ещё не был перемерен холодный × тёплый; одно и то же число заполняет обе колонки, пока не будет.

проверкаworkflowхолодныйтёплый
Build, scan e smoke das imagens de produçãoci.yml295s109s
Testes TS (api)ci.yml177s*177s*
Testes TS (web)ci.yml169s*169s*
Testes TS (pacotes)ci.yml56s*56s*
Testes do engine (ExUnit)ci.yml124s39s
Auditoria de dependênciasci.yml99s85s
Lintci.yml66s69s
Drift, gerados e builddocs-check.yml53s51s
Manifests de Kubernetesci.yml14s13s
Gitleaks no repositórioci.yml5s7s
Política de branchespr-police.yml7s7s
Escada de aprovaçãoapproval-ladder.yml13s15s
Check de promoçãopromotion-check.yml9s9s
Backmerge gatebackmerge-gate.yml7s7s

После AT-303..306 (только бесплатные изменения, без платного runner). Имена в таблице выше не изменились — переименованная обязательная проверка блокирует каждый PR, — а числа выше — это ДО. Что изменилось под капотом:

  • Build, scan e smoke das imagens de produção пропускает тяжёлые шаги, когда diff не затрагивает образ (AT-303). Первый шаг классифицирует diff PR с помощью scripts/ci/diff-toca-imagem.ts (список разрешённого — того, что никогда не попадает в образ, в smoke или в E2E: docs/ кроме docs/gates.yml, website/, scripts/docs/, deploy/k8s/, .github/ кроме ci.yml, *.md в корне кроме THIRD_PARTY_NOTICES.md, design/*.md; spec доказывает список против настоящих Dockerfile, мутацией). Bake, Trivy, broker, smoke и E2E несут if: на шаге, и job всегда завершается — зелёным, говоря почему в сводке job. Никогда paths: в триггере и никогда if: на job: оба оставили бы обязательную проверку ожидающей или приклеенной к старому вердикту. В сомнении (пустой diff, HEAD, который не merge-коммит PR, падающий git diff) запускается всё.
  • Тот же job кэширует браузер Playwright, store pnpm из e2e/ и базу Trivy (AT-304) — см. что кэширует job образов.
  • Testes TS (api) и Testes TS (web) стали АГРЕГАТОРАМИ двух шардов каждый (AT-305). Jobs Testes TS (api) — shard N/2 и Testes TS (web) — shard N/2 запускают каждый половину файлов, с покрытием, но с обнулённой нижней границей, и пишут отчёт blob vitest; агрегатор — который сохраняет обязательное имя — проверяет, что оба шарда завершились success, что оба отчёта пришли, и запускает vitest --merge-reports --coverage, который применяет НЕИЗМЕНЁННЫЕ coverage.thresholds из vitest.config.ts/vite.config.ts к объединённому покрытию (измерено локально: объединённое покрытие идентично покрытию единого запуска). У агрегатора намеренно if: always(): без него красный шард оставил бы его skipped, а skipped считается зелёным для обязательной проверки. Имена шардов не являются обязательными проверками и не должны перечисляться в ruleset. Typecheck выполняется только в шарде 1.
  • Testes do engine (ExUnit) и шарды api ждут новый job, imagens / Ler as imagens do compose (ADR 0197): он читает ссылку на pgvector из docker/docker-compose.yml и передаёт её в их services:, так что ни один workflow не хранит литерал образа. Он не является обязательной проверкой и не должен перечисляться в ruleset — то, что он решит, проявляется в зависящих от него jobs. Эти jobs несут if: ${{ !cancelled() }} по той же причине, что и агрегаторы выше: если бы imagens упал, по умолчанию был бы ПРОПУЩЕН Testes do engine (ExUnit), а skipped считается зелёным для обязательной проверки; с ним job запускается, получает пустой образ и падает.
  • claude-code-review.yml получил concurrency (cancel-in-progress, на PR) и timeout-minutes: 30; docs-check.yml получил timeout-minutes в обоих jobs (AT-306).
проверкадо, холодныйдо, тёплыйпосле, холодныйпосле, тёплыйпосле, PR только docs
Build, scan e smoke das imagens de produção295s109sTODO(humano)TODO(humano)TODO(humano)
Testes TS (api) (шарды + агрегатор, время по часам)177s177sTODO(humano)TODO(humano)—
Testes TS (web) (шарды + агрегатор, время по часам)169s169sTODO(humano)TODO(humano)—

TODO(humano): заполнить колонки «после» запусками этого самого PR (холодный: первый push; тёплый: перезапуск того же коммита; только docs: PR, затрагивающий только docs/), а затем перенести числа в основную таблицу.

Следствие для критерия edited ниже: job образов теперь зависит и от БАЗЫ (diff берётся относительно неё), а ci.yml по-прежнему не перезапускается на edited. Единственное небезопасное направление — PR, пропустивший образы относительно одной базы и затем перенаправленный на базу, где его diff затронул бы образ, — редкость, поскольку продвижения запускаются от верхушек постоянных веток (чей diff всегда что-то затрагивает), а перенаправления обычно сужают diff. Следующий push переклассифицирует.

ci.yml параллелен, кроме двух агрегаторов — у test-api и test-web есть needs на свои шарды, больше ни у чего нет needs:. Полный вердикт PR стоит САМОЙ МЕДЛЕННОЙ цепочки, а не суммы (которая ~12 мин CPU). До AT-303..305 у того, кто хотел укоротить PR, было две цели, и только две:

  • холодный кэш: job образов, где 195s из 295s — это docker buildx bake, самый большой отдельный пункт во всей CI, в 3× больше второго. Bakefile уже собирает четыре образа параллельно с кэшем type=gha на образ, и комментарий в его начале измеряет, почему разбивать его на матрицу jobs было бы ХУЖЕ: 1,7 GB образов на артефакт стоят больше сборки, а smoke нужны все четыре в одном daemon;

  • старый job Testes TS (api + web), где 91s из 159s были pnpm --filter api test, сериализованным через fileParallelism: false в apps/api/vitest.config.ts, потому что ~80 spec, затрагивающих базу, делили ОДНУ brabo_test и выполняли TRUNCATE между тестами — параллельные заходы столкнулись бы. Это было исправлено (perf/banco-por-worker-nos-testes-da-api, CHANGELOG): каждый worker Vitest получает СОБСТВЕННУЮ базу — test/support/global-setup.ts один раз мигрирует TEMPLATE и клонирует по базе на worker через CREATE DATABASE ... TEMPLATE (дёшево, копия страниц, а не повторный прогон migrations), test/support/test-db-name.ts определяет, к какой из них подключается spec, по VITEST_POOL_ID, а vitest.config.ts включает fileParallelism: true с maxWorkers: 4 (фиксировано по 4 vCPU runner, не автоопределяется — число должно быть ОДИНАКОВЫМ на машине разработчика с большим числом ядер и на runner, иначе измеренное локально поведение — не то, что выполняется там). Измерено локально: длительность самого набора api упала с 792,58s до 471,23s на чистом запуске (~40%, три повтора без нестабильности). Перемерено на настоящем hosted runner GitHub (PR #491, ci.yml): сам шаг pnpm --filter api test упал до 96s — прямо на нижней границе 91s тёплого кэша, которую эта таблица называла теоретически лучшим случаем, а не случайный выброс.

    Это исправление изменило форму job, где оно выполнялось, и именно это сделало возможным ВТОРОЕ сокращение. Шаг тестов самого apps/web вырос до 125s в том же запуске, который измерил общие 343s job (PR #494, job 101317149006), — уже не тот маленький хвост, который описывал акцент ниже, а ровня api. perf/paraleliza-testes-ts (CHANGELOG) разделил job на три параллельных — test-api (сохраняет сервис postgres и окружение DATABASE_URL/TEST_DATABASE_URL, поскольку только он трогает базу), test-web и test-packages (скрипты CI, порт Docker, broker, runner — шесть компонентов, которым Postgres никогда не был нужен), — каждый со своими checkout + setup-node + pnpm install. Измерено на настоящем hosted runner GitHub (PR #495, ci.yml): 177s (test-api), 169s (test-web), 56s (test-packages) против 343s заменённого ими job — проверка, державшая PR, теперь САМАЯ МЕДЛЕННАЯ из трёх, а не их сумма, реальное сокращение на ~48%.

    test-web и test-packages были стабильны во всех запусках этого PR (164–169s и 55–56s). test-api — нет: второй push измерил его в 388s — хуже общих 343s старого job — при всех шагах по-прежнему зелёных, а затем простой перезапуск ТОГО ЖЕ коммита измерил 172s. Три реальных замера test-api в этом PR: 177s, 388s, 172s. Два из трёх ложатся туда же, куда test-web и test-packages (с запасом в пределах цели ~4 мин); запуск в 388s согласуется с конкуренцией за общий runner на стороне GitHub, а не с регрессией разделения — в job ничего не менялось между тем запуском и перезапуском, измерившим 172s. Записано как наблюдаемое, не сглажено: тот, кто будет разбирать следующий медленный запуск test-api, должен знать, что этот разброс уже существовал в день разделения job, и проверить конкуренцию, прежде чем предполагать новую регрессию.

Предупреждение о «двух издержках», жившее здесь, больше не действует — обе посылки изменились. Оно говорило, что разделение Testes TS (api + web) (1) удалит обязательную проверку при переименовании, навсегда блокируя каждый PR, если только job fan-in или изменение в Settings не сохранит имя, и (2) не окупится, потому что 91s api несли бы setup, на сколько бы jobs его ни делили, ради чистого выигрыша ~7s.

Посылка 2 перестала быть верной, когда набор web вырос до стоимости набора api — разделение трёх компонентов сравнимого размера, а не одного большого плюс маленьких хвостов, и превращает «≈150s против 159s» в «177s против 343s». Посылка 1 перестала действовать по ортогональной причине: к этому репозиторию ещё не применён ни один ruleset — gh api repos/daneiel/brabo/branches/dev/protection по-прежнему возвращает 404, как в заметке «Текущее состояние» вверху этой страницы, — так что удалять было нечего. Таблица обязательных проверок выше уже отражает новое имя. Если ruleset когда-нибудь будет применён, перечислите Testes TS (api), Testes TS (web) и Testes TS (pacotes) — никогда не выведенное из употребления имя Testes TS (api + web), которое молча ни с чем не совпало бы и блокировало бы каждый будущий PR, ровно как описывает общее предупреждение ниже.

Имя проверки меньше того, что она охраняет. Drift, gerados e build — три вещи в заголовке и пять gates в job: целостность .docmap.yml, актуальность сгенерированного, drift, сборка сайта и — с тех пор как справочник API был опубликован сломанным в двух релизах — справочник API рендерится (scripts/docs/api-render-check.mjs).

Последний существует, потому что зелёная сборка — не отрисовывающаяся страница: MDX компилируется, SSR пишет содержимое, docs:build проходит, а страница умирает при гидратации в браузере. Весь механизм описан в docs/explanation/documentation-workflow.md. При добавлении gate в этот job имя проверки не меняется — и поэтому его не нужно перерегистрировать в таблице выше, но это должно быть где-то записано. Записано здесь.

pull_request_target требует, чтобы workflow был в ветке ПО УМОЛЧАНИЮ. Быть в базовой ветке PR недостаточно. Это измерено, а не предположено: с pull_request_target у pr-police было ноль запусков, тогда как approval-ladder и docs-check — использующие pull_request — нормально запустились с того же коммита. Поскольку ветка по умолчанию — main, а она продвигается только по лестнице, которую охраняет сама проверка, триггер стал бы проблемой курицы и яйца. Оба используют pull_request.

Откуда GitHub читает каждый workflow — три семейства, три ответа. Это стоило трёх отдельных открытий за эту фазу и нигде не очевидно:

триггерчитает workflow изследствие здесь
pull_request, pushветки событияработает с первого PR
pull_request_targetветки по умолчаниюне запускался: main отстаёт
workflow_dispatchветки по умолчаниюдаже не появляется в списке workflows

Последние два создают проблему курицы и яйца, когда ветка по умолчанию устарела: workflow, который двигает конвейер, уже должен быть в main, чтобы его можно было запустить. Обходом было вручную запустить скрипт promote при первом продвижении — тот же скрипт, только ручной триггер. После того как main получает workflows, dispatch работает навсегда.

Обязательная проверка, которая никогда не запускается, блокирует PR навсегда. Поэтому триггер ci.yml покрывает все три постоянные ветки — до FASE 6 он срабатывал только на PR в dev, и требование этих проверок при продвижении dev→qa дало бы вечно ожидающий PR. Триггер push был удалён: при pull_request, покрывающем всё, он лишь дублировал запуски. Добавляя новый job в CI, либо внесите его в этот список, либо оставьте вне его намеренно и пусть кто-то напишет почему.

А обязательная проверка, которая не ПЕРЕЗАПУСКАЕТСЯ, приклеивается к старому вердикту. Это другая сторона урока выше, и она стоила одному PR ошибочного провала.

Типы pull_request по умолчанию — opened, reopened и synchronize, — ни один не покрывает смену базы. А смена базы — рутина: GitHub открывает PR против ветки по умолчанию, а автор затем исправляет её на dev. В PR #71 Drift, gerados e build запустился в первые полдюжины секунд, против origin/main...HEAD, и упал на семи файлах, которые уже были отревьюены и смёржены в #70. Перенаправление его не перезапустило; красное осталось.

Критерий того, кому нужен edited, — от чего зависит проверка:

проверка зависит от…нужен edited?кто
только от HEADнетci.yml — тестирует коммит, и база не меняет результат (кроме решения о пропуске в job образов с AT-303 — см. «Следствие для критерия edited» выше)
от БАЗЫ или от ТЕЛА PRдаpr-police, approval-ladder, promotion-check, backmerge-gate, docs-check

В docs-check — и то и другое: drift сравнивает интервал, начинающийся с базы, и читает тело в поисках строки docs-not-needed:. Без edited предохранительный клапан, документированный чуть ниже, был недостижим — запись обоснования в тело ничего не переоценивала, и только фиктивный коммит разблокировал бы PR.

claude-review намеренно не входит в этот список, и это и есть «пусть кто-то напишет почему»: ревью через LLM субъективно и стоит токенов, поэтому оно информирует PR, не имея права её блокировать. Поскольку проверка не обязательна, job можно пропустить, не оставив PR ожидающим, — и он пропускается в каждом PR, открытом ботом. Action отказывается работать с нечеловеческим актором ("Workflow initiated by non-human actor: <name> (type: Bot)"), так что без этого if проверка падает каждый раз. Это случилось дважды: в PR #64 и #65 цикла v0.3.1 (продвижение, открытые github-actions[bot]) и снова в PR #526 (Dependabot).

Второй раз стал следствием того, как было записано условие. Оно перечисляло логин — github-actions[bot], — что верно, пока множество не изменится, и молча неверно в день, когда появляется новый бот; dependabot.yml пришёл в PR #520, и следующий PR Dependabot воспроизвёл провал, урок которого уже был записан. Условие теперь проверяет type автора, повторяя ehAutorBot (scripts/ci/pr-police.ts), с суффиксом [bot] в логине как страховкой — GitHub не допускает [ в логине человека, так что его не подделать.

Для PR продвижения есть вторая, независимая причина: это был бы тот же diff, отревьюенный повторно, поскольку продвижение несёт только коммиты, уже отревьюенные в PR в dev. А для Dependabot есть третья, которая закрывает дверь сама по себе, — его PR запускаются с хранилищем секретов Dependabot, отдельным от хранилища Actions, так что CLAUDE_CODE_OAUTH_TOKEN приходит пустым. Даже если разрешить актора, у ревью не было бы учётных данных, а проверка, падающая из-за отсутствия секрета, хуже пропущенной: она читается как дефект кода.

Чего не может выполнить PR между постоянными ветками​

Drift, gerados e build является обязательной, поэтому запускается в каждом PR — но шаг drift объявляет себя неприменимым, когда head — постоянная ветка самого репозитория (продвижение dev→qa, qa→main; back-merge main→qa, qa→dev). Остальные шаги job — docmap, сгенерированное и сборка сайта — продолжают выполняться: они зависят только от HEAD и действуют на любой ступени.

Причина та же, что и у claude-review, с отягчающим обстоятельством. Избыточность, во-первых: PR между постоянными ветками не имеет авторства, он упаковывает коммиты, drift которых уже был взыскан в PR в dev, файл за файлом. Взыскивать снова — значит взыскивать тот же долг на каждой ступени. Но, в отличие от ревью через LLM, здесь требование было невыполнимым — и именно это провалило #72, продвижение цикла v1.0.1, из-за файлов docker/**, пришедших из #70:

кажущийся выходпочему его нет
обновить документацию в PR продвиженияpromotion-check требует чистого интервала — head должен быть верхушкой источника. Коммит там проваливает другую обязательную проверку
повторить docs-not-needed: из исходного PRтело PR продвижения генерируется promote; обоснование из #70 не пересекает ступень
ставить метку на каждое продвижениеэто учит пользоваться предохранительным клапаном рефлекторно, пока он ничего не будет значить, — противоположность тому, чего просит .docmap.yml

Фильтр живёт внутри шага, а не в if: на job, следуя тому же принципу, который записывает promotion-check: обязательная проверка, индексированная по sha, которая не запускается, оставляет PR ожидающим навсегда. Шаг запускается, решает, что не применим, и пишет это в сводку — вместо того чтобы исчезнуть.

Head с именем main из форка — не продвижение. Это рабочая ветка третьего лица, и она проходит drift, как любая другая. Поэтому условие сопоставляет имя и требует того же репозитория — та же оговорка, которую pr-police применяет при классификации семейства PR.

Bypass​

кторежимзачем
актор BRABO_BOT_TOKENAlwaysписать .release/gate.json в main

Ни у одного человека нет bypass — даже у владельца. Этот принадлежит боту и существует по причине без обхода: gate блокирует ветки, и PR для снятия блокировки был бы заблокирован самим gate. Коммит остаётся в git log, с датой и содержимым, и tag-release распознаёт его по тому, что он затрагивает (.release/ и ничего больше), а не по тому, кем он себя называет.

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

Секрет BRABO_BOT_TOKEN​

Классический PAT со scopes repo + workflow, в Settings → Secrets and variables → Actions:

gh secret set BRABO_BOT_TOKEN --body '<token>'

Это не удобство. От него зависят две вещи, и обе молча ломаются без него:

чтопочему
тег запускает Releaseтег, созданный с GITHUB_TOKEN, не запускает workflow
PR back-merge рождаются с проверкамиPR, открытый с GITHUB_TOKEN, не запускает workflow

Это правило GitHub против рекурсии, и оно уже взяло свою цену: Release v0.2.0 так и не была опубликована из-за него. Второй случай хуже — PR back-merge без проверки никогда не стал бы зелёным, и цепочка застряла бы навсегда. Поэтому job gate громко падает, когда секрета нет, а не идёт дальше, оставляя репозиторий в тупике.

Scope workflow — не излишество: PR back-merge несут ветку целиком, и она может содержать изменение в .github/workflows/**. Без этого scope push отклоняется ровно в том случае, который важнее всего, — при распространении исправления CI.

Текущее состояние: настроено и испытано. В v1.1.1 конвейер впервые замкнулся сам — tag-release пропустил шаг предупреждения, отправил тег с PAT, и это запустило release.yml, который опубликовал Release. Тег, отправленный с GITHUB_TOKEN, его бы не запустил; этот запуск — доказательство того, что токен действителен и имеет scope на push.

Шесть тегов остаются осиротевшими — v0.2.0, v0.3.0, v0.3.1, v1.0.0, v1.0.1 и v1.1.0 — из периода, когда секрета не было. PAT их не восстанавливает: он действует только для новых тегов. Для них процедура ниже.

Повторная публикация осиротевшего тега​

У release.yml есть workflow_dispatch с входом тега, и он существует из-за шести тегов выше. Один лишь триггер push: tags сделал бы провал необратимым: повторная публикация потребовала бы удалить и пересоздать тег, то есть переписать запись, чтобы исправить её следствие.

gh workflow run release.yml -f tag=v1.1.0

Три защиты, и каждая существует из-за конкретного режима отказа:

защитапротив чего
запустить его может только OWNER_HANDLE (режим solo)то же ограничение, что и у promote; триггер push здесь не проходит, потому что там авторизовал тег
вход должен совпадать с ^v[0-9]+\.[0-9]+\.[0-9]+$workflow_dispatch принимает свободный текст. Без этого -qa.1 опубликовал бы Release того, что никто не подтвердил как финальное
отказ, если Release уже существуетопубликованная заметка — это запись; молча перезаписать её значило бы стереть то, что кто-то уже прочитал или на что сослался

И checkout делается на теге, а не на ref события. При dispatch github.ref_name — ветка по умолчанию; без этой предосторожности повторная публикация породила бы Release с именем main, с changelog main, без какой-либо ошибки.

Dispatch работает только после того, как это дойдёт до main. Это проблема курицы и яйца из таблицы триггеров выше: workflow_dispatch читает workflow из ветки по умолчанию. Пока изменение только в dev, вход даже не появляется.

Ruleset 2 — теги версий​

Имя: tags-de-release Enforcement: Active Цель: Tag → Include by pattern → v*

правилозначение
Restrict creations✅
Restrict updates✅
Restrict deletions✅

Шаблон v* покрывает три формы, которые создаёт конвейер: -dev.N, -qa.N и финальную. Создавать их может только tag-release.

Что запускает финальный тег и почему ничто из этого не обязательная проверка. Отправка vX.Y.Z параллельно запускает release.yml, build-runner-binaries.yml и publish-runner.yml. Их jobs никогда не появляются в таблице обязательных проверок выше, и это структурно, а не упущение: обязательные проверки удерживают pull request, а когда тег существует, PR уже смёржен. Тег удерживает ruleset — создаёт его только tag-release, и только после конвейера, в конце которого он запускается.

Финальный тег также запускает install-e2e.yml, который испытывает опубликованный install.sh на чистой машине — а эфемерный runner Actions именно ею и является. С RN-549 он доходит до конца: пять звеньев, замыкающих установку, локальный агент поднят и ждёт, и первый проект runner подхвачен. Он не запускается в pull_request, и причина та же, что держит подпись вне PR: подписанный манифест существует только после финального тега, а запуск скрипта там дал бы ему дверь для пропуска проверки (RN-534). Следствие сказано, а не скрыто — PR, который меняет этот workflow, его не запускает, — а в каждом PR запускается scripts/dev/install-e2e.spec.ts, который проваливается, если триггер получит pull_request или если фраза, которую workflow ищет grep, перестанет существовать в install.sh.

С ADR 0149 у build-runner-binaries.yml два job вместо одного: матрица build (четыре цели, fail-fast: false) и job checksums. С RN-565 у второго нет needs: на матрицу — он ждёт, с потолком, assets Release, а не самый медленный job, — поэтому цель, упавшая при сборке или так и не получившая runner, не лишает остальные подписанного манифеста. Он называет в логе, какие цели манифест не покрывает, потому что checksums.txt, перечисляющий три и молчащий о четвёртой, хуже никакого: тот, кто проверяет три, заключает, что проверил релиз.

Цель darwin-x64 (Mac Intel) вышла из матрицы в ADR 0174, и причина измерена (AT-065, 2026-09-13): её метка macos-13 указывала на образ, который GitHub вывел из эксплуатации в декабре 2025 года, а замена на Intel, macos-15-intel, планируется за секунды и собирает, но проваливает --self-test-pty под Bun (onData из node-pty никогда не срабатывает — oven-sh/bun#25822, открыт). То же доказательство проходит под Node, поэтому Mac Intel устанавливает runner через npm install -g @brabo/runner. Запуск workflow с пустым входом tag — это репетиция: собирает и делает smoke, ничего не прикрепляя, и именно так это было измерено без создания тега.

Bypass​

кторежим
app/бот, запускающий workflow релизаAlways

Это исключение для push, предусмотренное политикой: версия рождается из workflow, никогда вручную. Тег, созданный вручную, не проходит проверку того, что финальный тег указывает на тот же коммит, что и последний -qa.N, а именно эта проверка не даёт опубликовать что-то отличное от проверенного.

Как применить​

Интерфейс: Settings → Rules → Rulesets → New ruleset.

Через API, если предпочитаете версионировать команду:

gh api -X POST repos/daneiel/brabo/rulesets --input ruleset-permanentes.json
gh api repos/daneiel/brabo/rulesets --jq '.[] | "\(.name): \(.enforcement)"'

TODO(humano): два файла .json с payload здесь не версионируются, потому что API rulesets требует repository_id и id приложений, различающиеся от установки к установке, — фиксированный файл скопировали бы неправильно. Если хотите их версионировать, сгенерируйте через gh api repos/daneiel/brabo/rulesets/<id> > docs/reference/... после применения через интерфейс, и этот документ начнёт указывать на них.

Settings → Pages: источник — ветка gh-pages​

Применение ручное, по той же причине, что и у rulesets: репозиторий версионирует источник, GitHub получает применение.

Settings → Pages → Build and deployment → Source: Deploy from a branch, с веткой gh-pages и папкой / (root).

Уже применено. gh api repos/daneiel/brabo/pages возвращает "build_type": "legacy" с source.branch: "gh-pages", и все три ступени отвечают в сети.

Промежуточный шаг стоит записи, потому что сбивает с толку: пока build_type был "workflow", docs-deploy.yml коммитил в gh-pages, и Pages ничего оттуда не отдавал — публикация оставалась в репозитории, не выходя в сеть, а старый сайт всё ещё отвечал из последнего артефакта actions/deploy-pages. Ничто в CI не краснеет в этом состоянии.

Почему переключение обязательно, а не предпочтение: actions/deploy-pages публикует один артефакт как весь сайт и не умеет обновлять часть дерева, что несовместимо с подкаталогом на ступень. Полный дизайн, отброшенные альтернативы и исключение для push, которое это открывает, описаны в ADR 0034.

веткаURLиндексируется
— (индекс трёх)https://daneiel.github.io/brabo/❌ noindex, follow
mainhttps://daneiel.github.io/brabo/prd/✅
qahttps://daneiel.github.io/brabo/qa/❌ noIndex
devhttps://daneiel.github.io/brabo/dev/❌ noIndex

Все три стали симметричными в ADR 0071, который также объясняет сгенерированный корень и 404.html, перенаправляющий старые ссылки на стабильную ступень. ADR 0073 отделил ПУТЬ от ВЕТКИ: main публикуется в /prd/, потому что адрес называет среду для читающего. 404.html стал переписывать /brabo/main/<algo> в /brabo/prd/<algo> — старый каталог уходит из дерева, и keep_files: false его удаляет.

Конфигурация Pages не меняется ни с одним из двух — источником остаётся ветка gh-pages в папке / (root); изменилось содержимое, которое workflow собирает перед push. И prd не ветка: её нет ни в одном ruleset, её нет в git, это лишь имя каталога в gh-pages.

gh-pages НЕ входит в ruleset постоянных веток. Она не постоянная, и боту нужно пушить в неё — её включение заблокировало бы саму публикацию. Поэтому там же хватает GITHUB_TOKEN, без bypass и без BRABO_BOT_TOKEN.

И она вне охвата Gitleaks, по причине, которая проявилась лишь позже. gitleaks detect сканирует каждый достижимый коммит, а не только историю HEAD, — а fetch-depth: 0 приносит все refs. Как только появилась gh-pages, собранный сайт попал в сканирование: 112 находок generic-api-key в одном коммите, все в dev/assets/js/*.js, — это имена файлов аватаров внутри минифицированного bundle со слишком высокой для правила энтропией. Это провалило PR продвижения #78, первый просканированный после её появления.

ci.yml теперь удаляет refs/remotes/origin/gh-pages перед сканированием. Не через allowlist путей в .gitleaks.toml: он действовал бы для каждого коммита, и dev/, появившийся однажды в исходном коде, молча перестал бы сканироваться. Исключение ref исключает ровно те коммиты, которые не являются исходным кодом.

Измерено gitleaks 8.30.1, версией CI: 133 коммита и 112 находок до, 132 и ни одной после — а PAT GitHub с реальной энтропией, подложенный в apps/api/src/, по-прежнему обнаруживается, что доказывает, что дыра не открылась.

Метки семейств должны существовать​

pr-police применяет одну из четырёх меток, и gh pr edit --add-label падает, если метки нет, — что оставило бы PR без классификации, молча.

gh label create trabalho --color 0E8A16 --description "Work PR: taxonomy prefix for dev"
gh label create promocao --color 1D76DB --description "Promotion between adjacent rungs, going up"
gh label create retropropagacao --color 5319E7 --description "Back-propagation between adjacent rungs, going down"
gh label create correcao-alta --color D93F0B --description "hotfix: a fix that's born high on the ladder"

Без || true намеренно: если команда упадёт, вам нужно это увидеть.

Проверить, что всё на месте​

# do the rulesets exist and are they active?
gh api repos/daneiel/brabo/rulesets --jq '.[] | "\(.name): \(.enforcement)"'

# does the protection respond? (should be REJECTED)
git push origin HEAD:dev

Вторая команда — тот тест, который важен. Ruleset, настроенный, но не проверенный, неотличим от отсутствующего — и разница проявляется только в тот день, когда кто-то пушит прямо в main.