Artifacts and Versioning
By the end of this lesson
Produce versioned outputs you can trace back to a commit.
An artifact is the thing your build produces: a container image, a published folder of compiled output, a package. It is what actually gets deployed, and everything after the build stage should be handling one of these rather than handling source code.
Two properties make an artifact useful. It is immutable, meaning its contents never change after it is built. And it is traceable, meaning you can go from something running in production back to the exact commit it was built from, without guessing.
Both properties come from the same decision: build the artifact once, and move that same artifact through your environments.
The terms, with both providers' names where they differ:
- Artifact
- The output of a build, stored somewhere durable. For the employees API it is a container image; for a class library it is a package.
- Registry or feed
- Where artifacts live. Azure Container Registry or Amazon ECR for images; Azure Artifacts or AWS CodeArtifact for packages. A pipeline run's own storage is not a registry — it expires.
- Tag
- A human-readable label pointing at an artifact, such as 1.4.0 or the commit hash. A tag is a pointer, and a pointer can be moved to something else later.
- Digest
- A hash of the artifact's contents, such as sha256 followed by 64 hexadecimal characters. It cannot be repointed, because changing the contents changes the digest. This is the artifact's real identity.
- Build metadata
- The facts about how the artifact was made: commit, pipeline run, build time, version. Embedded in the artifact so the running instance can report it.
- Promotion
- Deploying an artifact that already ran in one environment to the next environment, unchanged. No rebuild is involved, which is the entire point.
Two ways to get code into three environments. They look similar on a diagram and behave very differently.
| Rebuild per environment | Build once and promote | |
|---|---|---|
| What production runs | A build nobody tested. The tested one was a different build | The exact bytes that passed the tests and ran in staging |
| Traceability | Three artifacts per commit, and the deployed one is whichever the last run produced | One artifact per commit, identified by digest, recorded at each promotion |
| Dependency drift | Each build resolves dependencies and base images again. A patched base image between staging and production is silently a different system | Impossible between environments. Whatever was resolved at build time is what ships |
| Time to promote | A full build and test cycle, every time | Seconds. It is a deployment, not a build |
| Failure mode | A production-only failure that reproduces nowhere, because the failing artifact no longer exists | If it worked in staging and fails in production, the difference is configuration or data — a much smaller search space |
| Configuration differences | Often compiled in, which is what forces the rebuild in the first place | Supplied at run time from the environment, so one artifact serves all of them |
| Reasonable when | Almost never for application code. Occasionally unavoidable when a licence or a compliance rule requires a build inside a specific environment | Default |
set -euo pipefail
COMMIT=$(git rev-parse --short HEAD) # 9f4c2ab
VERSION="1.4.0+$COMMIT" # release version plus provenance
IMAGE=anviregistry.azurecr.io/employees-api
# ---- Build stage: once, and only once ----
docker build \
--build-arg BUILD_VERSION="$VERSION" \
--tag "$IMAGE:$COMMIT" .
docker push "$IMAGE:$COMMIT"
# The digest is content-addressed. Record it and pass it down the pipeline.
DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' "$IMAGE:$COMMIT")
echo "$DIGEST" > digest.txt
# ---- Promote: the same bytes, a different environment ----
az containerapp update --name employees-api-staging \
--resource-group employees-staging --image "$DIGEST"
# ...tests pass in staging, so production gets that identical digest.
az containerapp update --name employees-api-prod \
--resource-group employees-prod --image "$DIGEST"- The commit hash as a tag makes the artifact traceable by inspection. Anyone can read the tag on a running service and check out that exact commit, with no pipeline archaeology.
- The version string carries both a release number people can talk about and the commit that produced it. Passing it in as a build argument is how it ends up inside the artifact rather than only in the pipeline's memory.
- Deploying by digest rather than by tag is the guarantee. A tag can be moved — by a later build, by a person, by a mistake — and if it moves between the staging deploy and the production deploy, promotion has quietly turned back into rebuilding.
- Writing the digest to a file lets later stages consume the same identity instead of resolving the tag again. Resolving the tag again is where the difference creeps in.
- The two promotion commands are identical apart from the target. That is the tell-tale sign of a healthy pipeline: promotion is a deployment of a known artifact, with nothing about the artifact decided at that moment.
- On AWS the model is the same — ECR holds images with tags and digests, and services are updated to a specific digest. The commands differ; the reasoning does not.
{
"name": "employees-api",
"version": "1.4.0+9f4c2ab",
"commit": "9f4c2abf1d2e4c7b8a0d5e6f3c1b9a8d7e6f5c4b",
"builtAtUtc": "2025-03-04T09:12:44Z",
"buildRun": "ci-2291",
"imageDigest": "sha256:3d9f8c1b7a4e2f60d5c3b19a8e7f6d5c4b3a2918f7e6d5c4b3a291807f6e5d4c"
}- This is generated at build time and written into the artifact, then returned by a small read-only endpoint. It turns what is running in production from a question into a request.
- The full commit hash is here even though the tag uses the short form, because the short form can collide once a repository is large. If you are about to debug a production issue, you want the unambiguous one.
- The build run identifier links back to the pipeline log, which holds the test results for this artifact. That link is what lets you answer whether a specific build passed the integration suite, months later.
- The digest lets you compare what is deployed against what you intended to promote. Those two disagreeing is rare and is worth being able to detect rather than assume.
- Nothing sensitive belongs in here: no configuration values, no connection details, no internal hostnames. Even so, publishing your version and build time is a small amount of information about your system, so some teams keep this endpoint behind authentication or restrict it to internal callers. That is a reasonable choice, and it should be a decision rather than an oversight.
Versioning rules that keep artifacts traceable. Each one exists because of a specific failure.
- Never reuse a version number. A version identifies one set of bytes forever. Reusing one means two different artifacts answer to the same name, and every later conversation about that version is ambiguous
- Never move a tag once something has been deployed from it. If you need a change, build a new artifact with a new tag. Moving a tag changes what is running for anyone who redeploys
- Do not deploy from latest. It names whatever was pushed most recently, which means a restart can silently pick up a different version than the one you last tested
- Derive the version in the pipeline, from the commit, rather than having a person edit a file at release time. A hand-maintained version number drifts, and it drifts most during the releases where accuracy matters most
- Put the commit in the version string. Semantic versioning covers what changed for consumers; the commit covers which code, and you want both
- Use a prerelease suffix for candidates — 1.5.0-rc.2 — so a candidate cannot be mistaken for the release, and so tools that understand semantic versions order them correctly
- Keep artifacts at least as long as you might need to roll back to them. A 30-day retention policy on the registry quietly means you cannot return to last quarter's release
- Record which artifact is in which environment, somewhere a human can read without opening the pipeline. During an incident, this is the first fact you need and the slowest one to reconstruct
Summary
- An artifact is the deployable output of a build, and it is useful when it is immutable and traceable to a commit
- Build once and promote the same artifact, because a rebuild for production ships something you never tested
- Tags are movable pointers; a digest is content-addressed and is the artifact's real identity
- Embed build metadata so a running instance can report its own version, commit and build run
- Never reuse a version or move a tag, and keep artifacts at least as long as you might need to roll back
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
What could differ between two builds of one commit?
A team builds the same commit twice, an hour apart, for staging and then production. List everything that could differ between the two artifacts.
Show solution
Base images. If the Dockerfile refers to a tag rather than a digest, the underlying image can be republished between the two builds — usually for a security patch, which is a good change arriving at a bad moment.
Dependencies. Any version range, or any restore without a lock file, can resolve differently. A package published in that hour is enough.
Build tooling on the runner. Hosted runners are updated regularly, so the compiler and SDK patch level can change between two runs.
Generated content: timestamps, build numbers, embedded paths. These make the artifacts differ even when the behaviour is identical, which matters because it means you cannot verify they are the same.
A transient failure that a retry hides — a partially restored dependency, a truncated download.
None of these is likely on its own, and that is what makes the habit dangerous. It works for months, and the one time it does not, you are debugging a production artifact that no longer exists and cannot be reproduced.
Try it yourself
Trace what is running
For a service you have deployed, find out which commit it is running without looking at the deployment history. Then find the artifact in its registry and confirm the digest matches.
If you cannot do either, write down what you would add.
Show solution
The usual gaps are that nothing reports its own version, and that the deployed reference is a mutable tag, so the digest cannot be confirmed.
The minimum useful addition is a version endpoint carrying the commit and the build run, and a deployment that names a digest. Together they turn what is running into a fact rather than an inference from timestamps.
This exercise pays off at the worst moment rather than the current one. During an incident, the first question is what changed, and a team that cannot say what is running spends its first twenty minutes on that instead of on the problem.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.