Overview
Pinning is easy; keeping pins honest is not. A version that is stated in four places will eventually be four different versions, and a version stated correctly today rots silently — nothing in the build fails when a pinned release stops being the latest stable. Those are two separate problems: consistency (all consumers agree) and currency (the agreed value is still the right one), and they want different mechanisms.
Key points
- Choose a precision deliberately. A major (
25) tracks a series and takes patches for free; an exact version (25.0.4+7) is reproducible but is now a maintenance obligation; a floating tag (latest) is neither. The right answer differs per surface — a library's supported range is not a CI runner's toolchain is not a release image's tag. - One source, many consumers. The same tool version typically appears in the declared project requirement, the dev container, the CI pipeline, and the release image. Name it once and have the rest resolve from that; anything else is four sources pretending to be one fact.
- Single-sourcing is only real if something checks it. A convention that consumers "should" read the registry is not enforcement. Add a test in the fast gate that fails when a consumer and the registry disagree.
- Guard the blind spots, not just the entries. The subtle failure is not a wrong value, it is a new surface that quietly grows a version nobody registered. A sweep that fails on any version-shaped string no registry entry claims catches that; the rule is to extend the registry, never to widen the sweep's exclusions.
- Record deliberate absences too. A surface that floats by construction — a major-only
tag, a
stablechannel, a version read from a project file — should be registered as an absence, so a hardcoded version appearing there later is as loud as a wrong one. - Currency wants to be a scheduled report rather than a gate. An upstream release would otherwise turn unrelated work red — a third party could break your build by shipping. Run the "is each pin still latest stable?" comparison on a schedule; the failing run is the report, naming each stale pin and its current upstream value. The exception is a policy you are actually held to: where a compliance or security regime requires dependencies within a stated age, gating is the point, and the cost of a surprise red build is one you have accepted deliberately.
- A bump is a reviewed change, proved by tests. Automation can tell you a version is stale; it should not decide that the new one works. The bump updates the value and every consumer together, and the existing test suite is what says it is safe.
- Distinguish the two automations. Dependency bots (Dependabot, Renovate) cover the dependencies your package manager resolves. They do not cover versions embedded in templates, Dockerfiles, workflow files, or scaffolded output — those need the registry and the currency report above.
Details
The two problems, and their mechanisms
| Problem | Symptom | Mechanism | Where it runs |
|---|---|---|---|
| Consistency | The dev container ships a JDK the pipeline does not | One registry + a check that consumers match it | The fast gate, on every change |
| Currency | Every pin is internally consistent and nine months old | Compare each entry against its upstream latest stable | A schedule, as a report |
Keeping these apart is the whole trick. Consistency is cheap, deterministic and offline, so it belongs in the gate every change passes. Currency is network-bound and depends on events outside the repository, so gating on it makes a third party able to break your build by shipping a release.
What a registry entry needs
Enough to find every place the value lives, and to ask upstream what the current one is:
- the value itself, and what it is;
- the locations — file patterns plus how to extract the version from each;
- the upstream check — which feed answers "what is latest stable?", or an explicit marker that this entry deliberately floats and is not to be checked.
The locations list is what makes a bump mechanical rather than archaeological, and what lets the consistency check exist at all.
Precision, per surface
| Surface | Usual precision | Why |
|---|---|---|
| Library's declared support | a range or minimum | Consumers pick within it |
| Application dependencies | exact, via lockfile | Reproducible installs; the range lives in the manifest |
| Toolchain requirement | major | Patches are free and wanted; a series is the real intent |
Manager lockfiles (.tool-versions, .sdkmanrc) |
exact | The format demands it — see Version managers |
| Release image base | exact + digest | The one place drift is a security problem |
| CI action / plugin refs | commit SHA | A mutable tag is an arbitrary-code-execution surface |
The supply-chain half
Pinning is not only a reproducibility practice. A mutable reference — a floating tag, a branch name, an unpinned third-party CI action — is a place someone else can change what your build executes. Pin those to immutable identifiers (digests, commit SHAs), and accept the maintenance cost that creates: an unmaintained pin is its own risk, which is exactly why the currency report has to exist alongside.
Related
- Developer environments — the group; the versions here are what its layers provision.
- Version managers — which files take a prefix and which demand an exact version.
- Dev containers — one of the consumers a registry keeps in line.
- CI/CD & delivery engineering — supply-chain hygiene and the pipeline as an attack surface.
- Patterns for tools that generate configuration — the single-source registry as a design pattern.
- Threat modeling — how to reason about the mutable-reference risk.