CI/CD and DevOps path · Trust the artifact · Topic 5 of 13

Reproducible builds

Not started

A build is **reproducible** when anyone who takes the same source, the same build instructions and the same tool versions gets bit-for-bit the same output. It sounds academic, but it is what makes "build once, promote everywhere" safe, lets you rebuild a two-year-old release for a hotfix, and lets a second, independent build check that the first one wasn't tampered with.

Why it matters: If two builds of one commit produce different files, you can't tell a harmless difference from a tampered build, and you can't rebuild an old release with confidence.

  1. 1Pin every input

    Lockfiles for dependencies, exact versions for compilers and tools, base images by digest. A floating input makes a floating output.

  2. 2Remove the noise

    Timestamps, file ordering, absolute paths, usernames and random IDs leak into archives and binaries. Fix them or strip them.

  3. 3Identity by hash

    A reproducible artifact is named by the digest of its bytes. Anyone can rebuild it and check they get the same digest.

1

How it works

The explanation, with examples you can run.

Where differences come from

Most non-reproducibility comes from two places.

Floating inputs. A version range in package.json, a FROM node:24 tag, apt-get install without versions, or a "latest" compiler on the build machine. Each can resolve to something new without any change in your repository. Fix them with lockfiles (package-lock.json, go.sum, poetry.lock), installers that respect them (npm ci, pip install --require-hashes), base images pinned by digest, and toolchains pinned in the repository (.nvmrc, go.mod's toolchain, a pinned builder image).

Noise in the output. Build timestamps baked into binaries or archive entries, files added in whatever order the filesystem returns them, absolute paths of the build directory, the username, random identifiers. The Reproducible Builds project's convention is the SOURCE_DATE_EPOCH environment variable: tools that support it use that fixed time, usually the commit time, instead of "now".

Seeing it

This toy build packs two files into an "archive" and hashes it. With the current time and filesystem order in the archive, every build gets a new digest. With a fixed time and sorted entries, the digest is stable:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

In a pipeline

  • Build in a pinned, clean environment: a container image by digest or a hosted runner, never a long-lived machine with leftovers.
  • Install from the lockfile only and fail if it would change.
  • Set SOURCE_DATE_EPOCH from the commit time (git log -1 --format=%ct).
  • Record the artifact's digest and deploy by digest.

How far to go

Perfect reproducibility can take real effort, especially for native code. Even partway helps: pinned inputs alone remove most "it built differently in production" surprises. Full reproducibility is what lets independent builders verify each other, the strongest check against a compromised build machine.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A Dockerfile starts with `FROM node:24`. Why can the same commit build different images next month?
  2. 2.Two builds of the same commit produce tarballs with different hashes. The files inside are identical. What is the most likely cause?
  3. 3.What does `npm ci` do that `npm install` does not?

0 of 3 right so far. Get all three to finish this topic.