Git, CI/CD & GitOpsCIV · CI/CD Anti-PatternsDependencyPinning
Mutable dependencies — the floating-tag contract
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
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 installwithout a lockfile, orpackage.jsonwith^and~ranges, pulls the version that satisfies the range at the moment of install. - Unpinned base images.
FROM node:20resolves to whatever the registry serves fornode:20at pull time. - Terraform module references by branch. A module
block with
source = "git::...//modules/vpc?ref=main"pulls from the tip ofmain. The branch is mutable. - Action references by mutable tag.
uses: third-party/action@v3resolves 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 cieverywhere CI runs. Lockfile committed; range updates in a separate dependency-update PR with review.pip install --require-hashes -r requirements.lock.terraform init -lockfile=readonlyin 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
- Lockfiles are committed, lockfiles are consulted.
npm ci,pip install --require-hashes,terraform init -lockfile=readonly. - Container images are pinned by digest.
- 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
Q1. A CI pipeline runs `npm install` in production builds. The lockfile is committed. What changes between yesterday's build and today's?
Q2. A Dockerfile that uses `FROM node:20` is not reproducible because the major-version tag can move to different bytes.
Q3. Name the four floating-reference shapes that turn a reproducible build into a moving target.
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.