Delivery Pipeline · Build & artifact management

Build & artifact management

This page covers the handoff between "code that compiles" and "artifact that's safe to run in production": what makes a build reproducible, how semantic versioning turns a version number into a machine-readable contract, and why an artifact repository — not a fresh build per environment — is the system of record every pipeline stage should pull from. The throughline is one rule: build once, promote everywhere. By the end, you should be able to explain why rebuilding "the same" artifact per environment is a bug, not a best practice.

☺ Explain it like I'm 10

Think about how a shipping container works. It gets packed and sealed at the factory, inspected once, and stamped with a tracking number. From there it moves through every port and checkpoint as the exact same sealed box — nobody unpacks it and repacks a "new" container at each stop, they just add a new stamp to the manifest saying it cleared that checkpoint. An artifact repository works the same way: you build the software once, seal it under an immutable version identifier, and every environment from staging to production gets that same sealed thing, with a new tag added each time it clears a gate — never a freshly packed box.

Reproducible builds: same input, same output

A reproducible build is one where compiling the same source at the same commit, with the same toolchain, produces byte-for-byte identical output every time — same hash, regardless of who ran it or when. That sounds obvious until you look at what quietly breaks it: embedded build timestamps, filesystem-iteration order leaking into a hash map's serialization, absolute paths baked into debug symbols, or a dependency resolver that picks up a newer patch release because a version range wasn't pinned. The Reproducible Builds project (driven largely out of Debian, tracking this since around 2013) treats it as a security property, not just a QA nicety: if two independent parties rebuild a package from the same source and get an identical hash, that's proof nothing was altered between source and binary — a supply-chain guarantee code review alone can't give you.

Practically, reproducibility comes from a short list of disciplines: pin every dependency to an exact version via a lockfile (package-lock.json, poetry.lock, Cargo.lock — not a floating range), build inside a hermetic image pinned by digest rather than a mutable tag, normalize or strip timestamps (SOURCE_DATE_EPOCH is the de facto environment variable most toolchains now honor), and sort any iteration order that affects output. None of this is exotic — it's the same discipline infrastructure as code applies to environments, applied one layer earlier, to the build step itself.

Semantic versioning

Semantic Versioning (semver.org, originally proposed by Tom Preston-Werner) gives a version number of the form MAJOR.MINOR.PATCH a contract, not just an incrementing counter: bump MAJOR on a backwards-incompatible change to the public API, MINOR when you add backwards-compatible functionality, and PATCH for a backwards-compatible bug fix. A consumer who sees a MINOR bump knows they can upgrade without touching their own code; a MAJOR bump is the signal to actually read the changelog before moving the pin. Pre-release and build-metadata suffixes extend the scheme — 2.4.0-rc.1 for a release candidate, 2.4.0+build.417 for metadata that doesn't affect precedence.

The value of semver is that it's machine-readable, not just a human convention. Dependency resolvers (npm, Cargo, pip's ~= compatible-release operator) use the MAJOR boundary to decide what an unpinned upgrade is allowed to touch — a caret range like ^2.4.0 means "anything up to but not including 3.0.0." That same machine-readability is what lets tooling compute the version automatically instead of a human guessing it: semantic-release and similar tools read the Conventional Commits type of every commit since the last tag (fix → patch, feat → minor, a BREAKING CHANGE footer → major) — the convention covered on version control & branching — and cut the next tag without anyone typing a version number by hand.

Artifact repositories: the system of record for build outputs

Once a build produces something deployable — a container image, a compiled binary, a language package, a Helm chart — that output needs somewhere authoritative to live, and that's what an artifact repository is: a versioned, addressable store that every later pipeline stage (CI/CD, staging, prod) pulls from instead of rebuilding from source. Docker registries (Docker Hub, Amazon ECR, Google Artifact Registry, GitHub Container Registry) hold container images, addressed by name-plus-tag or, more precisely, by content-addressable digest (sha256:...). Language-specific registries do the same job for packages: the npm registry, PyPI, RubyGems, and Maven Central (often mirrored through a self-hosted proxy) for Java. General-purpose managers like JFrog Artifactory and Sonatype Nexus sit in front of several of these formats at once, adding retention policies, access control, and one audit trail for "who pulled what, when."

What makes a registry a system of record rather than a file server is immutability: once app:1.4.2 is pushed, that exact byte content stays behind that name-and-tag pair forever — you don't overwrite it, you publish app:1.4.3. A registry that allows tag mutation undermines the whole guarantee, because "which bytes actually ran in prod last Tuesday" stops being answerable from the tag alone; see containers & orchestration for how orchestrators resolve images by digest specifically to close that gap.

"Build once, promote everywhere"

The principle that ties reproducible builds, semver, and artifact repositories together: build an artifact exactly once, and promote that literal artifact through every environment — dev, staging, prod — rather than rebuilding from source at each stage. The artifact that passed integration tests in staging is, byte for byte, the artifact that runs in production; nothing gets recompiled, re-linked, or re-resolved in between. This closes a real gap: even a build you believe is reproducible is only a belief until proven, and rebuilding per environment means a dependency resolver could silently pick up a newer transitive version between the staging build and the prod build, a base image could get repatched underneath you, or a compiler flag could differ — any of which makes "it passed staging" stop meaning anything about what's actually running in prod.

The naming and tagging convention below shows the shape of it: tag the artifact once with something immutable — usually the git commit SHA — push it to the registry, and promote it by adding new tags to that same digest as it clears each gate, never by building a new one.

# 1. Build once, tag with the immutable identifier: the git commit SHA
docker build -t registry.internal/checkout-svc:7f3a9c2 .
docker push registry.internal/checkout-svc:7f3a9c2

# 2. CI deploys that exact image to staging and runs the test suite
docker pull registry.internal/checkout-svc:7f3a9c2
# ... integration tests pass ...

# 3. Promote by adding a tag to the SAME image — never rebuild
docker tag registry.internal/checkout-svc:7f3a9c2 registry.internal/checkout-svc:staging-verified
docker push registry.internal/checkout-svc:staging-verified

# 4. Prod deploy pulls the immutable SHA tag, not a floating one
docker tag registry.internal/checkout-svc:7f3a9c2 registry.internal/checkout-svc:prod
docker push registry.internal/checkout-svc:prod

# checkout-svc:7f3a9c2 is the artifact of record — :staging-verified and
# :prod are pointers to it, not separate builds

Only the tags move; the image digest referenced by checkout-svc:7f3a9c2, :staging-verified, and :prod is identical throughout, which is exactly what a promotion audit needs to answer "is prod running what we tested" with a diff instead of a guess. This is also the deployment half of what deployment strategies covers — blue-green and canary rollouts assume there is one known-good artifact to route traffic toward, not a fresh build per rollout stage.

⚠ Watch out

A floating tag like :latest — or rebuilding "the same" image per environment — quietly breaks build-once-promote-everywhere even when every other practice on this page is followed correctly, because the tag no longer identifies one immutable set of bytes: "which image is in prod" stops being answerable without SSHing in and checking a digest by hand. Pin deploys to the immutable SHA tag or the digest itself, never to a tag that gets re-pushed.

SBOMs: a starting point for supply-chain visibility

A Software Bill of Materials (SBOM) is a machine-readable manifest — typically in CycloneDX or SPDX format — listing every dependency an artifact contains, direct and transitive, down to exact versions. Tools like Syft or Trivy can generate one from a container image or build output as part of the same pipeline step that produces the artifact itself, and the SBOM gets attached to the artifact in the registry alongside it, so "what's actually in this build" is answerable without re-scanning source later. Generating an SBOM at build time is close to table stakes now — a growing set of supply-chain frameworks and procurement requirements expect one to exist per release.

That's the scope of this page: SBOMs as an artifact-time practice, not a security program. Actually consuming one — scanning it against a vulnerability database, gating a promotion on the result, signing and attesting provenance (frameworks like SLSA, tools like cosign) — is deeper supply-chain security territory this course treats as a DevSecOps concern, not a build-and-artifact-management one.

✓ Checkpoint

1. What makes a build "reproducible," and why does the Reproducible Builds project treat that as a security property rather than just a QA nicety? 2. Under semantic versioning, what specifically triggers a MAJOR bump versus a MINOR bump, and how do tools like semantic-release compute this automatically? 3. What does "build once, promote everywhere" actually forbid, and why does rebuilding per environment undermine it even if each individual build is reproducible? 4. What does an SBOM contain, and where does this page draw the line between "artifact-time practice" and "DevSecOps"?

Check your answers
  1. A reproducible build produces byte-for-byte identical output from the same source, commit, and toolchain, regardless of who builds it or when. The Reproducible Builds project treats this as security-relevant because if independent parties can rebuild from source and match hashes, that's proof nothing was altered between source and binary — a supply-chain guarantee code review alone can't provide.
  2. MAJOR bumps on a backwards-incompatible change to the public API; MINOR bumps on backwards-compatible added functionality; PATCH on a backwards-compatible fix. Tools like semantic-release read the Conventional Commits type of every commit since the last tag (feat/fix/a BREAKING CHANGE footer) and compute the next version and changelog from that convention automatically.
  3. It forbids rebuilding the artifact from source at any stage after the first — the same digest that passed staging must be the one deployed to prod. Rebuilding per environment reopens the door to a dependency resolver picking up a newer transitive version, a repatched base image, or a different compiler flag between builds, so "it passed staging" no longer guarantees anything about what's actually running in prod.
  4. An SBOM is a machine-readable manifest (CycloneDX/SPDX) listing every direct and transitive dependency and version in an artifact, generated at build time by tools like Syft. This page treats generating one as the artifact-time practice; scanning it for vulnerabilities, gating promotions on it, and signing/attesting provenance (SLSA, cosign) is scoped to DevSecOps, not covered here.