ADR 0178 — The third-party image tag lives INSIDE the reference, before the digest
Status
Accepted. 2026-09-29 (AT-139, story HS-024, epic EP-016). On top of ADR 0159, which remains valid in everything that is not the reference's FORM: index digest, the three trees, what the check does not cover and why. Maintainer decision of 2026-09-27, written in the AT-139 note, which called it "ADR 0171" — a number that was reserved and never written; this ADR is that one, and 0171 remains vacant.
The decision had two halves. This ADR records the one that went in — the form and the lint — and declares why the other, enabling Dependabot for images, did not go in (see What this ADR does NOT do).
Context
ADR 0159 pinned the 39 third-party image references by digest, with the tag in
a comment beside it (neo4j@sha256:… # 5.26-community; in a Dockerfile, on the
line above), mirroring the form of actions (uses: x@<sha> # v4). And it
declared the price: a digest freezes, and "enabling Dependabot's docker
ecosystem is a separate decision".
The decision came on 2026-09-27 — Dependabot docker — with a condition: first
measure whether its PRs pass the pin lint. The measurement (round 15 lane, which
stopped at the gate, with no PR) knocked down ADR 0159's form:
- Dependabot does not read the tag in a comment, in any format. The comment
its
github-actionsreads is a convention of THAT ecosystem;dockerhas no equivalent. - A digest-only pin is updated to the
latestdigest (update_checker.rb). pgvector, neo4j and node would go to another major, with the comment still asserting the old tag and the lint GREEN — the check verified the comment's presence, not its truth. - The compose × workflow duplicates (pgvector and ollama in the
services:ofci.yml/golden-set-*.yml) would make every bot PR be born red by ADR 0159's own "same tag, two digests" rule: no ecosystem reads workflowservices:. - CloudNativePG's
imageNamealso stays outside any ecosystem.
And there was a reference in the repository that already had the other form,
for another reason: CNPG's imageName is postgresql:16.10@sha256:… because
the operator's webhook REFUSES a digest-only reference — it reads the Postgres
version from the tag. That is, the imagem:tag@sha256: form was already proven
in the repository by Kubernetes, by kubeconform and by the k3d bootstrap.
Decision
1. The third-party image reference becomes imagem:tag@sha256:<índice>.
image: neo4j:5.26-community@sha256:22ec5cd0…
FROM node:24.11.1-alpine3.21@sha256:b8f7c905… AS deps
With both present, Docker pulls by the digest: the tag is information for the reader, and the digest decides the bytes — ADR 0159's immutability stays intact. What changes is that the tag is now read through the SAME syntax by whoever executes (Docker), whoever updates (Dependabot) and whoever reviews. The digest is still the INDEX one, never a platform's.
The 39 references of the three trees migrated without changing any digest — 29
in docker/ (16 in compose, 13 FROM), 4 in deploy/k8s/ and 6 in
.github/workflows/. The tag comments went away: the reference is the source,
and an equal comment would be a second copy to go stale.
2. The lint derives the tag from the reference and fails what contradicts
it. scripts/ci/imagens-pinadas.ts fails, besides the mutable reference:
- digest without an inline tag — including ADR 0159's form, with the tag
only in the comment, because it is the one the bot would update to
latest; - a comment asserting another tag — a comment that is ONE token with a
digit (
# 5.26-community,# pg16) and diverges from the inline tag. A comment is still allowed (the decision says "comment out or equal"); what fails is the leftover of a version bump. Prose and a loose word without a digit do not assert a version and are not judged; - a comment at the end of the
FROMline — still failed, now with its own reason: Docker's parser does not accept it (ADR 0159); - the same tag with two digests — the key becomes
nome:tagread from the reference.
Each rule is proven by mutation in imagens-pinadas.spec.ts: turning off any of
the five breaks at least one test.
Consequences
ADR 0159's form now fails. Whoever copies an old example (from ADR 0159,
from an old PR) gets the message telling them to put the tag inline, with the
reason. The runbook (Bumping a third-party image), the supply chain page and
CLAUDE.md now show only the new form.
The comment stops being mandatory, and that is the inversion of ADR 0159's argument, not a loosening: there it was mandatory because it was the ONLY place for the tag; here the tag is in the reference, and what the check requires is that it exists and that nothing beside it contradicts it.
No version changes. Every digest is the same as before;
docker build --check passes on the nine Dockerfiles, a real build
(docker/backup/Dockerfile.prod) completes, docker compose config passes on
the touched composes and kubectl kustomize + kubeconform on the overlays.
What this ADR does NOT do
It does not enable Dependabot for images, and the reason is a blocker the
27/09 decision did not cover. The decision called for "a step that aligns the
workflows' services: in the bot's PR", in the mould of
dependabot-justifica-pin.ts. That step would have to PUSH a commit changing
.github/workflows/*.yml to the bot's branch, and GITHUB_TOKEN cannot do
that: it does not have, nor can it receive through permissions:, the
workflows permission, and GitHub refuses the push ("refusing to allow a
GitHub App to create or update workflow … without workflows permission").
Documented by the GitHub community and not measured here by execution. There is
a second cost, already known in the repository: a push made by GITHUB_TOKEN
does not trigger workflows, so the alignment commit would stay without checks
until a human re-triggered it.
Enabling docker/docker-compose without the step would make every bot PR
touching pgvector or ollama be born red. The existing paths — which are the
maintainer's decision, not this ADR's — are in the AT-139 report: a credential
with workflows (GitHub App or token) only for that step; removing the literal
from the workflows (the image: of a services: accepts the needs context,
and a previous job would read it from the compose — a duplicate that does not
exist does not diverge); the step only DIAGNOSING and a human applying it; or a
bot ignore for the two duplicated images. Until then, bumping a digest is the
runbook procedure.
It does not extend dependabot-justifica-pin.ts to image pins. A bot PR
touching docker/** would hit the drift (config-e-inferencia, block), and
AT-094's decision limited the bot-written line to ACTION pin swaps. Widening that
class is a separate decision.
It does not cover what was already out: the product's own images,
interpolated references, multi-stage stages and Helm's
otel-collector-values.yaml (ADR 0159). And two images stay in the MANUAL
procedure, with or without Dependabot, because they live outside the three trees
or in a key no ecosystem reads: CNPG's imageName and the image of the QA
golden-set cases (IMAGEM_DO_GOLDEN_SET_QA, a TypeScript constant — already in
inline form).
It does not create an RN. The rule is a sibling of the actions one and
lives, like it, in CLAUDE.md and in
docs/explanation/cadeia-de-suprimentos-do-ci.md.