Skip to content

ADR-009: Version Pinning Strategy for Tool Management

Status: Accepted Date: 2026-02-04 Deciders: Project maintainers Related: v1.8.0 Build Reproducibility, ROADMAP.md Batch 4

Context

Our dotfiles repository uses mise to manage CLI tools, languages, and development utilities. Currently, all tools use "latest" versions (except node which uses "lts"). This provides simplicity and ensures we stay current, but creates challenges:

  1. Non-Deterministic Builds: CI runs and local environments may resolve to different tool versions over time
  2. Breaking Changes: Automatic updates can introduce breaking changes without warning
  3. Debugging Difficulty: Inconsistent tool versions across environments complicate troubleshooting
  4. Security Audit Trail: No clear record of which versions were validated/approved

We evaluated three approaches to version management:

Option A: Full Reproducibility (mise.lock)

Generate and commit mise.lock with exact versions of all tools.

Pros: - Complete determinism - exact same versions everywhere - Full audit trail of tool changes - Easy rollback to known-good state

Cons: - 40+ Renovate PRs per week (one per tool update) - Large repository bloat from lockfile churn - Maintenance burden reviewing minor updates - Delayed security patches (requires PR + merge)

Option B: Pin Critical Tools Only (Chosen)

Pin language runtimes and critical dependencies to major.minor versions; keep utilities at "latest".

Pros: - Balanced reproducibility for build-critical tools - Manageable update volume (~5 PRs/month) - Security patches auto-apply (patch versions update freely) - Development tools stay current

Cons: - Partial reproducibility (utilities may drift) - Requires judgment on what qualifies as "critical" - Some non-determinism in CI over time

Option C: Document "Latest" Philosophy

Keep current approach, document trade-offs clearly.

Pros: - Zero maintenance overhead - Always current with latest features/fixes - Simple mental model

Cons: - CI failures from tool updates hard to diagnose - No reproducibility guarantees - Breaking changes surprise developers

Decision

We choose Option B: Pin Critical Tools Only.

What Gets Pinned

Critical Tools (Pinned to major.minor): - node: lts (already pinned - Node.js LTS releases) - python: 3.13 (stable branch, security updates auto-apply) - rust: 1.93 (stable channel, patch updates allowed)

Rationale: These tools affect build reproducibility, compilation, and runtime behavior. Pinning ensures consistent environments across development, CI, and production deployments.

Non-Critical Tools (Stay "latest"): - CLI utilities: bat, eza, fd, ripgrep, fzf, etc. - Development tools: lazygit, neovim, glow - Linters/formatters: shellcheck, shfmt, yamllint

Rationale: These tools don't affect build artifacts or runtime behavior. Keeping them current provides latest features/fixes without reproducibility concerns.

Version Update Policy

  1. Patch versions (x.y.Z): Auto-update immediately
  2. Handled by mise's version resolution
  3. Security patches applied without PR

  4. Minor versions (x.Y.z): Renovate PRs, automerge if CI passes

  5. Configured in .github/renovate.json
  6. Example: python 3.13.x → 3.14.x

  7. Major versions (X.y.z): Manual review required

  8. Breaking changes expected
  9. Requires testing and documentation updates

Renovate Configuration

Update .github/renovate.json to: - Group pinned tool updates together - Automerge minor/patch for pinned tools if CI passes - Separate major version updates for manual review

Consequences

Positive

Reproducible Builds: Critical language tools have consistent versions ✅ Manageable Updates: ~5 PRs/month vs 40+ with full lockfile ✅ Security Responsive: Patch versions auto-apply via mise resolution ✅ Developer Experience: Utilities stay current with latest features ✅ CI Reliability: Fewer surprise failures from tool updates ✅ Audit Trail: Git history shows when critical tools were updated

Negative

⚠️ Partial Non-Determinism: CLI utilities may drift slightly between environments ⚠️ Judgment Calls: Requires ongoing decisions about what's "critical" ⚠️ Mixed Strategy: Developers must remember which tools are pinned

Mitigation Strategies

  1. Document pinned vs unpinned in mise.toml comments
  2. CI validation ensures pinned versions work together
  3. Doctor task can warn about unexpected version mismatches
  4. Re-evaluate annually which tools qualify as critical

Implementation

  1. Update mise.toml:

    # Pinned for reproducibility
    python = "3.13"  # Stable branch (3.13.x patches auto-update)
    rust = "1.93"    # Stable channel (1.93.x patches auto-update)
    node = { version = "lts", postinstall = "corepack enable" }  # Already pinned
    
    # Keep at latest for freshness
    neovim = "latest"
    "github:sharkdp/bat" = "latest"
    # ... etc
    

  2. Update version policy comment in mise.toml header

  3. Test CI validates pinned versions

  4. Monitor Renovate PR volume over next month

Alternatives Considered

  • Pin Go/Lua/Zig if added: Future languages should follow same pinning pattern
  • mise.lock for CI only: Complexity outweighs benefits (two config systems)
  • Pin by ecosystem: All npm tools pinned, all cargo tools unpinned (rejected: too coarse)

References

Review Schedule

Next Review: 2027-02-04 (1 year) Triggers for Early Review: - Adding new language runtimes - CI failure rate increases >10% due to tool updates - Team feedback indicates pinning strategy is too loose/strict