The Git SHA identifies source history. The container digest identifies the built artifact.
They are related, they are not interchangeable, and most container traceability problems come from treating one as a substitute for the other.
What each one proves
Section titled “What each one proves”A Git SHA is a hash over a commit: its tree, its parents, its author and its message. Given one, you can retrieve exactly the source that existed at that point.
It says nothing about what was built from it. The same commit built twice can produce different images — a floating base image, an unpinned package, a build timestamp. The commit is fixed; what a build makes of it is not.
An image digest is a hash over the image manifest, which references the config and layers. Given one, you have exactly that image content.
It says nothing about where it came from. A digest is a fingerprint of bytes, and nothing in it records a repository, a commit or a builder.
| Question | Git SHA | Digest |
|---|---|---|
| What source produced this? | Yes | No |
| What exactly is running? | No | Yes |
| Is this the same as what I tested? | Not sufficient | Yes |
| Can I read the code? | Yes | No |
| Immutable? | Yes | Yes |
The two “no” answers in the first column are why a SHA alone is insufficient for deployment, and the two in the second are why a digest alone is insufficient for investigation.
The one-to-many relationship
Section titled “The one-to-many relationship”The property that makes this more than pedantry.
One commit can produce many digests. Rebuild after a base image update: same commit, different digest. Build for amd64 and arm64: same commit, two digests. Build on Tuesday and Thursday with an unpinned package: potentially different digests.
One digest comes from one build, which used one commit — but the digest itself does not record which. Two identical builds from different commits would even produce the same digest, if the commits differ only in files that do not affect the image.
So the mapping is one-to-many in one direction and lossy in the other unless somebody records it. That recording is the entire subject of this lesson.
Recording the link
Section titled “Recording the link”Three mechanisms, in increasing order of strength.
OCI labels
Section titled “OCI labels”Baked into the image config, travelling with the artifact.
ARG GIT_SHAARG GIT_REFARG BUILD_DATEARG SOURCE_URL
LABEL org.opencontainers.image.revision="$GIT_SHA" \ org.opencontainers.image.source="$SOURCE_URL" \ org.opencontainers.image.version="$GIT_REF" \ org.opencontainers.image.created="$BUILD_DATE"The standard annotation keys worth using:
| Key | Contains |
|---|---|
org.opencontainers.image.revision | The full commit SHA |
org.opencontainers.image.source | The repository URL |
org.opencontainers.image.version | Version or ref |
org.opencontainers.image.created | Build timestamp, RFC 3339 |
org.opencontainers.image.title | Human-readable name |
org.opencontainers.image.description | What it is |
docker/metadata-action populates these from the Git context automatically, so this is mostly free.
Strength: travels with the image, readable from a running container, no external system needed.
Limit: a label is a string somebody set at build time. It can say anything, including something untrue — either by mistake, when a build passes the wrong value, or deliberately. It is a record, not evidence, and the distinction matters as soon as anybody has a reason to care whether the record is accurate.
The SHA tag
Section titled “The SHA tag”Covered in tagging. Makes the image findable by commit, which is the reverse lookup the label does not give you.
Strength: answers “which image came from this commit?” — the lookup you need when a fix has been merged and you want to know whether its image exists yet.
Limit: tags are mutable, and a rebuild reassigns it.
Provenance attestations
Section titled “Provenance attestations”A signed statement binding the digest to the source repository, commit, workflow and builder.
- uses: docker/build-push-action@v7 with: provenance: true sbom: trueStrength: cryptographically verifiable and produced by the build system rather than asserted by the Dockerfile. This is the mechanism that turns a claim into evidence.
Limit: requires verification to be worth anything. An attestation nobody checks is a file. See artifact attestations for the verification side.
Full SHA, not short
Section titled “Full SHA, not short”Short SHAs collide. Git itself lengthens abbreviations as repositories grow, precisely because 7 characters stops being unique.
For a label it barely matters — you can resolve a short SHA against the repository. For a tag it matters a great deal: two commits sharing a 7-character prefix produce the same tag, and the second push silently overwrites the first. Now sha-a1b2c3d is an image from a commit that is not the one you would resolve.
type=sha,prefix=sha-,format=longThe tag is unreadable and nobody types it. Automation reads it, and automation is indifferent.
The reverse lookups
Section titled “The reverse lookups”Two directions, two mechanisms, and you want both.
Digest to commit: read the image’s org.opencontainers.image.revision label, or verify its provenance attestation. This is the incident question — something is running, what is it?
Commit to digest: look up the sha-<commit> tag, or query the registry for images whose provenance references that commit. This is the deployment question — I have a fix, where is its image?
A workflow that records only one direction leaves you doing manual archaeology in the other, usually at a bad moment.
Using the pair in practice
Section titled “Using the pair in practice”Four situations where having both identities changes what you can do.
An incident at 2am. A service is misbehaving. The pod reports a digest. Read its revision label, and you have a commit — which gives you the diff against the previous deployment, the pull request that introduced it, and the person who wrote it. Without the label you have a hash and a registry lookup that tells you which tags it carries, which may be none if a retention policy pruned them.
A security advisory against a dependency. You need to know which running images contain it. Query the registry for images whose SBOM lists the package, take their digests, and match those against what your clusters report. The commit is how you then work out which branches need the fix.
A rollback. You need the previous digest, and you need to know which commit it corresponds to so you can tell what you are giving up. Both identities, from the deployment history.
An audit. “Show that everything in production was built from reviewed code.” That is: digest → attestation → repository and commit → pull request → approval. Every link is mechanical if the identities were recorded, and impossible to reconstruct if they were not.
The common thread is that these are all questions asked under time pressure by somebody who was not involved. The identity recording is cheap at build time and expensive to substitute for afterwards.
Where the chain breaks
Section titled “Where the chain breaks”The failure modes, in rough order of frequency.
The image was built locally. No label populated from CI, possibly a dirty working tree, no attestation. This is why only CI should push.
The label was set but the working tree was dirty. The recorded SHA describes committed content; the image contains that plus whatever was uncommitted. The label is confidently wrong, which is worse than absent. A build step that fails when git status --porcelain is non-empty prevents it.
The commit was on a branch that was force-pushed or deleted. The SHA is recorded and no longer resolves. Rare, and it happens with rebased feature branches whose builds outlived them.
The repository was renamed or moved. org.opencontainers.image.source points at a URL that no longer exists. The commit is still valid if you know where to look.
A retention policy removed the tags. The digest still works; the commit-to-image lookup is gone. Labels survive this because they are inside the image, which is a good argument for not relying solely on tags.
Nothing was recorded at all. The default state for a build that used no metadata action and no labels, which is a surprising proportion of production images.
What neither identity tells you
Section titled “What neither identity tells you”Worth being explicit, because both are sometimes over-trusted.
Neither says the image is safe. A digest identifies content; it does not evaluate it. A vulnerable image has a perfectly good digest.
Neither says the build was clean. A commit does not record whether the build machine was compromised. A digest records the result of whatever happened. Provenance attestations address this partially — they record which builder ran — and even then, trusting the attestation means trusting the builder.
Neither says the source is complete. A build that fetched a script from the internet and executed it produced an image whose content is not derivable from the commit alone. The commit is accurate, the digest is accurate, and the relationship between them is not reproducible — which means anybody trying to verify the image by rebuilding it will get a different answer and have no way to tell whether that is benign.
Neither says the running container matches. A container started from an image can be modified at runtime — kubectl exec, a mutating admission webhook, an init process writing files. The digest describes what was pulled, not what is currently in the filesystem.
That last one is a genuine gap and the reason runtime attestation is a separate discipline from build provenance.
Beyond the commit: what else is worth recording
Section titled “Beyond the commit: what else is worth recording”The commit is the anchor. A few other facts pay for themselves.
The workflow run. A URL to the CI run that produced the image. It has the logs, the test results and the timing. Attestations carry this; a custom label works too.
The build parameters. Which target, which build arguments, which platforms. Two images from the same commit with different build arguments are different artifacts and nothing else distinguishes them.
Whether the tree was clean. A boolean, or refuse to build at all. Knowing an image came from a dirty tree is far better than not knowing.
The base image digest. Recorded at build time, this answers “which base is this running?” without re-deriving it — useful when an advisory lands against a base image and you need to know which of your images inherited it.
What not to record: anything that changes on every build without meaning. A build timestamp with second precision makes otherwise identical builds produce different digests, which destroys your ability to compare them. If you record a timestamp, consider deriving it from the commit.
The general test: would this fact help somebody who found this image and knows nothing else? The commit passes. The build number of your CI system, on its own, does not.
Verifying rather than trusting
Section titled “Verifying rather than trusting”Labels are self-reported. Making the chain trustworthy is a separate step, and it is worth knowing what it involves.
Verification means checking an attestation against the expected source repository and workflow, using the build system’s signing identity. The attestations lesson covers the mechanics.
Where to verify: at admission, ideally. A policy that rejects images without valid provenance from your organisation’s repositories is a control; a verification step in CI that the deploying pipeline could skip is a check.
What verification actually establishes: that this digest was produced by that workflow, in that repository, from that commit. It does not establish that the code was good, that the review was thorough, or that the build was not subverted in a way the builder itself could not detect.
Why it still matters: it closes the specific gap where somebody pushes an image built somewhere else under your organisation’s name. Without verification, the only thing stopping that is registry write permissions — which is a real control and a narrower one than it appears once you count how many identities hold it.
Start by looking. Before building policy, check whether the images currently running in your environments have provenance at all. For most organisations adopting this, the first finding is that a meaningful fraction do not, and working out where those came from is the actual first task.
Common mistakes
Section titled “Common mistakes”Short SHA tags. Collisions, and the second build wins silently.
Assuming a SHA tag guarantees the commit. It is a mutable label; the digest is the guarantee.
Recording only the SHA. No way to tell which of several builds is running.
Recording only the digest. No way to find the source.
Rebuilding for each environment. Same commit, different digests, and the testing proved nothing about the artifact that shipped.
Treating labels as evidence. They are strings set at build time. Attestations are the evidence.
Omitting org.opencontainers.image.source. It is what links a package to its repository on GHCR.
Building from a dirty working tree. The recorded SHA describes committed content; the image contains something else.
Mental model
Section titled “Mental model”A commit is what you wrote. A digest is what you shipped. The label and the attestation are how anybody proves which produced which.
Both identities are necessary and neither is sufficient. The engineering task is recording the relationship at the moment it exists, because nothing can reconstruct it afterwards.
What you learned
Section titled “What you learned”- A Git SHA identifies source; a digest identifies built content; neither implies the other
- One commit can produce many digests — rebuilds, platforms, unpinned inputs
- OCI labels travel with the image;
org.opencontainers.image.revisionis the key one - SHA tags give you commit-to-image lookup; labels give you image-to-commit
- Use full SHAs in tags; short prefixes collide and silently overwrite
- Labels are records; provenance attestations are evidence
- Neither identity tells you the image is safe, the build was clean, or the running container is unmodified
Exercise
Section titled “Exercise”Use a disposable directory with local Docker and a Git repository. No registry credentials needed.
-
Create a repository with a Dockerfile that accepts
ARG GIT_SHAand setsorg.opencontainers.image.revision. -
Build with
--build-arg GIT_SHA=$(git rev-parse HEAD). Read the label back withdocker inspect. -
Build again from the same commit without changing anything. Compare the two digests. Predict: identical?
-
Change the base image to a floating tag, wait for it to be updated upstream, and rebuild from the same commit. Predict: same digest?
-
Make an uncommitted change and build again, still passing the committed SHA. Inspect the label. Predict: does the image match the commit it claims?
-
Tag two builds from different commits using only the first 7 characters of each SHA — pick two commits whose prefixes you shorten to collide artificially. Predict: what happens to the first tag?
-
Delete the directory and the images.
Related lessons
Section titled “Related lessons”The GitOps and infrastructure repository templates are in the Professional Toolkit.