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

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 по-прежнему не существует, и gate platform остаётся 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.