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

ADR 0178 — Тег стороннего образа живёт ВНУТРИ ссылки, перед digest

Статус​

Accepted. 2026-09-29 (AT-139, история HS-024, эпик EP-016). Поверх ADR 0159, который продолжает действовать во всём, что не является ФОРМОЙ ссылки: digest индекса, три дерева, то, что check не покрывает и почему. Решение сопровождающего от 2026-09-27, записанное в заметке AT-139, которая называла его «ADR 0171» — номер, оставшийся зарезервированным и так и не написанный; этот ADR и есть тот, а 0171 остаётся свободным.

У решения было две половины. Этот ADR фиксирует вошедшую — форму и lint, — и объявляет, почему другая, включение Dependabot для образов, не вошла (см. Чего этот ADR НЕ делает).

Контекст​

ADR 0159 закрепил 39 ссылок на сторонние образы по digest, с тегом в комментарии рядом (neo4j@sha256:… # 5.26-community; в Dockerfile — на строке выше), повторяя форму actions (uses: x@<sha> # v4). И объявил цену: digest замораживает, и «включение экосистемы docker Dependabot — отдельное решение».

Решение пришло 2026-09-27 — Dependabot docker — с условием: сначала измерить, проходят ли его PR lint закрепления. Измерение (lane раунда 15, которая остановилась на шлюзе, без PR) опрокинуло форму ADR 0159:

  • Dependabot не читает тег в комментарии ни в каком формате. Комментарий, который читает его github-actions, — соглашение ТОЙ экосистемы; у docker нет аналога.
  • Закрепление только по digest обновляется до digest latest (update_checker.rb). pgvector, neo4j и node ушли бы на другую major-версию, а комментарий по-прежнему утверждал бы старый тег, и lint был бы ЗЕЛЁНЫМ — check проверял наличие комментария, а не его правдивость.
  • Дубликаты compose × workflow (pgvector и ollama в services: ci.yml/golden-set-*.yml) заставили бы каждый PR бота рождаться красным по правилу «один тег, два digest» самого ADR 0159: ни одна экосистема не читает services: workflow.
  • imageName CloudNativePG тоже остаётся вне какой-либо экосистемы.

А в репозитории была ссылка, у которой уже была другая форма, по другой причине: imageName CNPG — это postgresql:16.10@sha256:…, потому что webhook оператора ОТКЛОНЯЕТ ссылку только с digest — он читает из тега версию Postgres. То есть форма imagem:tag@sha256: уже была доказана в репозитории Kubernetes, kubeconform и bootstrap k3d.

Решение​

1. Ссылка на сторонний образ становится imagem:tag@sha256:<índice>.

image: neo4j:5.26-community@sha256:22ec5cd0…
FROM node:24.11.1-alpine3.21@sha256:b8f7c905… AS deps

При наличии обоих Docker скачивает по digest: тег — это информация для читающего, а digest решает байты — неизменяемость ADR 0159 остаётся нетронутой. Меняется то, что теперь тег читается ОДНИМ И ТЕМ ЖЕ синтаксисом тем, кто исполняет (Docker), тем, кто обновляет (Dependabot), и тем, кто ревьюит. Digest по-прежнему — digest ИНДЕКСА, никогда не платформы.

39 ссылок трёх деревьев мигрировали без замены единого digest — 29 в docker/ (16 в compose, 13 FROM), 4 в deploy/k8s/ и 6 в .github/workflows/. Комментарии с тегами ушли: ссылка — источник, а одинаковый комментарий был бы второй копией, которая стареет.

2. Lint выводит тег из ссылки и отклоняет то, что ему противоречит. scripts/ci/imagens-pinadas.ts отклоняет, помимо изменяемой ссылки:

  • digest без встроенного тега — включая форму ADR 0159, с тегом только в комментарии, потому что именно её бот обновил бы до latest;
  • комментарий, утверждающий другой тег — комментарий, который является ОДНИМ токеном с цифрой (# 5.26-community, # pg16) и расходится со встроенным тегом. Комментарий по-прежнему разрешён (решение гласит «комментарий вне или равный»); отклоняется то, что остаётся после повышения версии. Проза и отдельное слово без цифры не утверждают версию и не оцениваются;
  • комментарий в конце строки FROM — по-прежнему отклоняется, теперь с собственной причиной: парсер Docker его не принимает (ADR 0159);
  • один и тот же тег с двумя digest — ключом становится nome:tag, прочитанный из ссылки.

Каждое правило доказано мутацией в imagens-pinadas.spec.ts: отключение любого из пяти роняет хотя бы один тест.

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

Форма ADR 0159 начинает отклоняться. Тот, кто скопирует старый пример (из ADR 0159, из старого PR), получит сообщение с требованием поставить тег inline, с объяснением. Runbook (Bumping a third-party image), страница цепочки поставок и CLAUDE.md начинают показывать только новую форму.

Комментарий перестаёт быть обязательным, и это инверсия аргумента ADR 0159, а не ослабление: там он был обязательным, потому что был ЕДИНСТВЕННЫМ местом тега; здесь тег находится в ссылке, и check требует, чтобы он существовал и чтобы ничто рядом ему не противоречило.

Ни одна версия не меняется. Каждый digest тот же, что и раньше; docker build --check проходит на девяти Dockerfile, настоящая сборка (docker/backup/Dockerfile.prod) завершается, docker compose config проходит на затронутых compose, а kubectl kustomize + kubeconform — на overlays.

Чего этот ADR НЕ делает​

Не включает Dependabot для образов, и причина — блокировка, которую решение от 27.09 не покрывало. Решение требовало «шаг, который выравнивает services: workflows в PR бота», по образцу dependabot-justifica-pin.ts. Этому шагу пришлось бы ПУШИТЬ commit, изменяющий .github/workflows/*.yml, в ветку бота, а GITHUB_TOKEN не может этого сделать: у него нет и не может быть через permissions: разрешения workflows, и GitHub отклоняет push («refusing to allow a GitHub App to create or update workflow … without workflows permission»). Задокументировано сообществом GitHub и не измерено здесь запуском. Есть вторая цена, уже известная в репозитории: push, сделанный через GITHUB_TOKEN, не запускает workflow, поэтому commit выравнивания остался бы без checks, пока человек не перезапустит триггер.

Включение docker/docker-compose без этого шага заставило бы рождаться красным каждый PR бота, затрагивающий pgvector или ollama. Существующие пути — и они являются решением сопровождающего, а не этого ADR — перечислены в отчёте AT-139: учётные данные с workflows (GitHub App или токен) только для этого шага; убрать литерал из workflows (image: в services: принимает контекст needs, и предыдущий job читал бы его из compose — несуществующий дубликат не расходится); шаг лишь ДИАГНОСТИРУЕТ, а человек применяет; или ignore бота для двух дублирующихся образов. До тех пор повышение digest — процедура runbook.

Не распространяет dependabot-justifica-pin.ts на закрепление образов. PR бота, затрагивающий docker/**, упёрся бы в drift (config-e-inferencia, block), а решение AT-094 ограничило строку, записываемую ботом, заменой закрепления ACTION. Расширение этого класса — отдельное решение.

Не покрывает того, что уже было вне: собственные образы продукта, интерполированную ссылку, стадию multi-stage и otel-collector-values.yaml Helm (ADR 0159). И остаются на РУЧНОЙ процедуре, с Dependabot или без, два образа, живущих вне трёх деревьев или в ключе, который не читает ни одна экосистема: imageName CNPG и образ случаев golden-set QA (IMAGEM_DO_GOLDEN_SET_QA, константа TypeScript — уже во встроенной форме).

Не создаёт RN. Правило — родственник правила actions и живёт, как и оно, в CLAUDE.md и в docs/explanation/cadeia-de-suprimentos-do-ci.md.