Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCIV · CI/CD Anti-PatternsDependencyPinning

Mutable dependencies — the floating-tag contract

Intermediate⏱ ~23 mingit

What you'll learn

  • Identify the four floating-reference shapes that turn a reproducible build into a moving target
  • Explain how a yanked or substituted upstream version rewrites a build that was clean yesterday
  • Distinguish a lockfile-pinned build from a tag-resolved build and the audit difference between them

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A mutable dependency is a contract that resolves to whoever shows up next. The build script says pip install requests, the registry answers with whatever is tagged latest, and the artifact contains that version. Six hours later, somebody yanks the release, pushes a different version under the same tag, or re-uploads the same tag with compromised bytes - and the next build produces a different artifact from the same script. The script has not changed. The bytes have.

Four shapes the violation takes

  • Floating tags in package managers. npm install without a lockfile, or package.json with ^ and ~ ranges, pulls the version that satisfies the range at the moment of install.
  • Unpinned base images. FROM node:20 resolves to whatever the registry serves for node:20 at pull time.
  • Terraform module references by branch. A module block with source = "git::...//modules/vpc?ref=main" pulls from the tip of main. The branch is mutable.
  • Action references by mutable tag. uses: third-party/action@v3 resolves to whatever the maintainer has pushed.
    flowchart LR
        A["npm install without lockfile"] --> E["Resolves at install time"]
        B["FROM node:20"] --> E
        C["git ref=main"] --> E
        D["uses: action@v3"] --> E
        E --> F["Different bytes per job"]
        F --> G["Audit trail breaks"]
        F --> H["Supply-chain compromise survives rebuild"]

All four converge: the bytes the pipeline consumed are not the bytes the team approved.

Why a lockfile is not the same as a tag

A tag is a name. A lockfile is a list of names and the bytes those names resolved to. The package-lock.json, the requirements.lock, the .terraform.lock.hcl, the resolved action version - each is a record of the exact bytes the build consumed. A tag-resolved build asks the registry “what is requests today?”; a lockfile-resolved build asks “give me requests==2.31.0 and verify its sha256”. The registry answers with the version or fails the build.

What pinning looks like

  • npm ci everywhere CI runs. Lockfile committed; range updates in a separate dependency-update PR with review.
  • pip install --require-hashes -r requirements.lock.
  • terraform init -lockfile=readonly in plan.
  • FROM node:20.11.1@sha256:... in Dockerfiles. The digest is the bytes; the tag is a label for humans.
  • uses: actions/checkout@v4.1.7@sha256:... for third-party actions.

Production discipline

  1. Lockfiles are committed, lockfiles are consulted. npm ci, pip install --require-hashes, terraform init -lockfile=readonly.
  2. Container images are pinned by digest.
  3. Actions are pinned by SHA.

Cross-course references

  • This course, Part XLV (ArtifactImmutability) - digest pinning and the immutable-identity principle.
  • This course, Part XXXVI (RepoArch) - repository layout for dependency declaration.
  • Linux for Production Sysadmins, Part XII (RepoSecurity) - apt/dnf repository trust.

Quiz

Knowledge check · 4 questions

  1. Q1. A CI pipeline runs `npm install` in production builds. The lockfile is committed. What changes between yesterday's build and today's?

  2. Q2. A Dockerfile that uses `FROM node:20` is not reproducible because the major-version tag can move to different bytes.

  3. Q3. Name the four floating-reference shapes that turn a reproducible build into a moving target.

  4. Q4. Diagnose a supply-chain incident caused by a mutable dependency and recommend the discipline that prevents it.

    A Docker base image is compromised; the maintainer's account pushed a backdoored version under the same :stable tag. The CI pipeline uses FROM debian:stable-slim for every job; no digest pin.

Passing score: 75%. Answers are checked in this browser.