rgoussu@goussu: ~/library/platform/developer-environments
~/library/platform/developer-environments cat version-pinning-and-currency.md

Version pinning & currency

# Pinning tool and dependency versions without letting them rot — single-sourcing the value across its consumers, choosing a precision, and detecting drift from latest stable without gating unrelated work.

Conceptsaved 2026-08-20 #platform#tooling#dependencies#reproducibility#supply-chain#maintenance

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 stable channel, 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