ADR 0119 — Образы, публикуемые в GHCR с каждым финальным тегом, и production overlay, закреплённый по digest
- Статус: принят
- Дата: 2026-08-29
- Ссылки (без редактирования): ADR 0025 и
ADR 0027 (оба зафиксировали
«публикацию образа в registry» как вне охвата — этот ADR то, что
закрывает это), ADR 0030 (исключения
push, которые это решение намеренно не расширяет),
ADR 0036 (версия, записанная в
артефакт, — причина, по которой
VERSIONотделена отTAG).
Контекст
release.yml собирал четыре production-образа с каждым финальным тегом с
load: true и push: false и говорил об этом в комментарии: сборка существовала,
чтобы доказать, что тег собираем, а публикация «войдёт, когда будет решён
registry». Тем временем production overlay указывал на
ghcr.io/OWNER/brabo-* с newTag: REPLACE_WITH_DIGEST — маркер владельца
и маркер, который ни один шаг никогда не заменял.
Последствие не было косметическим. Пока это действовало, «production-деплой, исполнимый от начала до конца» был ложью, и всё, что стояло за ним, оставалось позади: настоящий rollout, отрепетированное восстановление против production-образов и история подписи кода бинарника runner (которой нужен опубликованный артефакт для подписи). Внешний обзор репозитория классифицировал это как самый дорогой объявленный долг, и этот ADR — этот пункт.
По пути обнаружился второй дефект: блок images: overlay перечислял
три образа, а не четыре. brabo-backup никогда не переотображался, поэтому
CronJob backup наследовал brabo-backup:prod из базы — имя, которое не
разрешается ни в одном registry. Окружение, которому больше всего нужен его backup,
оказалось бы тем, чей pod backup никогда не поднимался. CI этого не поймал, потому что
проверка overlay проверяет, что overlay собирается, а образ с локальным
именем собирается без проблем.
Решение
1. release.yml публикует четыре образа в GHCR с каждым финальным тегом.
Вход использует собственный GITHUB_TOKEN job с packages: write — никакого
нового секрета для ротации, и область умирает вместе с job. Пакеты
публичны: код уже публичен в том же репозитории, секреты никогда не
записываются в образ (четыре родственных ключа отказываются подниматься с примерным значением,
RN-114), а публичные пакеты сохраняют production overlay в режиме
«скачай и запусти», без imagePullSecret, который нужно создавать, указывать и ротировать в
каждом namespace.
2. Префикс registry — переменная bake, а не второй bakefile.
REGISTRY по умолчанию "", поэтому ci.yml продолжает собирать короткие
локальные имена (brabo-api:prod), которые он сканирует и тестирует в smoke;
release.yml задаёт ghcr.io/<owner>/. Завершающий слэш живёт в значении —
пустой префикс должен давать ровно старое имя, и любой фиксированный
разделитель в интерполяции сломал бы ci.yml. OUTPUT следует той же
форме: type=docker (загружает локально) по умолчанию, type=registry (push) для
релиза. type=registry — не оптимизация: это единственный режим, в котором
bake возвращает containerimage.digest, потому что digest рождается из push.
3. То, что опубликовал каждый тег, записывается по DIGEST в
.release/images.json. Теги (3.2.0, sha commit) продолжают
публиковаться: они служат для человеческого общения и ручного docker pull. Они никогда
не являются тем, на что ссылается манифест, — изменяемый тег, указывающий на разное
содержимое во времени, уничтожает детерминированный rollback и позволяет двум
pod одного ReplicaSet запускать разные бинарники, что сам
комментарий overlay говорил с Фазы 5.
4. Файл — это asset GitHub Release, и он попадает в репозиторий через PR
CHANGELOG. Он не получает третьего исключения push. У политики
веток ровно два (теги ботом релиза, .release/gate.json
ботом gate), а механизм, которому нужно новое исключение каждый раз, когда он
растёт, — механизм, перестающий быть политикой. Asset релиза — авторитетная
копия, доступная в момент появления тега; версионируемая копия
приходит через PR, который release.yml уже открывает против dev.
5. Overlay сохраняет маркер; make imagens-do-release применяет
digest. Репозиторий не объявляет, какой релиз в production, — это решает
тот, кто делает деплой, когда делает деплой. Репозиторий хранит
запись того, что опубликовал каждый тег, и инструмент, который её применяет. Инструмент —
kustomize edit set image, а не sed: kustomize знает, что
name=repo@sha256:… означает newName + digest и что newTag должен
уйти, тогда как sed угадал бы текст и ошибся бы в схеме.
6. brabo-backup входит в блок images: overlays, в prod и в
staging.
Последствия
- Production overlay называет настоящего владельца (
ghcr.io/daneiel/*) вместо маркера, и его четыре образа разрешаются во что-то существующее. - Применение digests переписывает файл overlay целиком.
kustomize editделает круговой проход по YAML: переупорядочивает ключи и теряет комментарии. Это приемлемо для операции во время деплоя, и именно поэтому результат явно не предназначен для commit — это зафиксировано в самом комментарии файла и в runbook, чтобы следующий человек не обнаружил это в diff. - Релиз теперь проваливается, если любая из четырёх целей вернётся без digest.
Это намеренно: три новых образа и один старый в production хуже, чем никакого
релиза, а ничто в частичном файле не сказало бы, что одного образа не хватило. Та же
дисциплина, что у пакетного
embed(ADR 0075), — частичный ответ необнаружим потом. - Ещё не сделано и не заявлено: ничто не делает деплой само. Нет
окружения, куда деплоить,
DEPLOY_ENABLEDпо-прежнему не существует, и gateplatformостаётсяplanned(ADR 0091/0092). Этот ADR делает production overlay исполнимым; он его не исполняет. - Также не сделано: подпись или аттестация образов. Никакой подписи cosign, никакой проверки происхождения SLSA. Это относится к пункту подписи кода бинарников runner (ADR 0112), заблокированному тем же действием оператора — получением идентичности подписи.
- Первым настоящим запуском этого пути будет следующий финальный тег. Как и
build-runner-binaries.yml(ADR 0112), workflow запускается по тегу, поэтому его нельзя доказать через PR. Чистая логика (images-manifest.ts,aplicar-imagens.ts) покрыта тестами; отрисовка bake была проверена черезdocker buildx bake --printв обоих режимах; путьkustomize editбыл по-настоящему выполнен против prod overlay с синтетическим манифестом. Чего не даст ни один тест — это GHCR, принимающего push, и это ждёт тега.
Рассмотренные альтернативы
Бот релиза переписывает kustomization.yaml и делает push.
Отвергнуто по двум причинам: это открыло бы третье исключение push в политике,
у которой их два, и позволило бы тегу самому решать, что в production.
Приватные пакеты с imagePullSecret. Отвергнуто: добавляет
секрет, который нужно создавать, указывать в каждом namespace и ротировать, чтобы
скрыть образ, собранный из публичного кода, не несущего секретов. Цена
реальна и постоянна; выгоды здесь нет.
Закреплять по тегу версии вместо digest. Отвергнуто по причине, которую
overlay уже записал до появления этого ADR и которую теперь требует тест:
ни один аргумент, испускаемый argumentosDeSetImage, не может нести
:tag.