Every contributor and every CI job should run the same Terraform. The way to
get that is a version file in the repository, read by a tool that installs
what it names. I use mise. It reads asdf’s
.tool-versions as well as its own mise.toml, so a repository that pinned
versions for asdf keeps working. The setup is three commands. The decision
worth writing down is which tools to pin at all.
The version file
One line per tool, either in asdf’s format:
terraform 1.9.7or in mise’s own:
[tools]terraform = "1.9.7"mise install reads whichever is present and installs what it names, and
the shell then resolves the pinned binary:
mise install# mise [email protected] ✓ installed
which terraform# ~/.local/share/mise/installs/terraform/1.9.7/bin/terraform
terraform version# Terraform v1.9.7That is the entire setup for a contributor: clone, mise install, done. CI
does the same, so a pipeline and a laptop disagree about a tool version only
when the file changed between them.
One directory, another version
In a monorepo one stack may still need an older Terraform than the rest:
terraform { required_version = "1.9.4"}Two commands cover it. mise use [email protected], run in that directory,
records the version in that directory’s configuration. It applies whenever the
shell is inside it. For a one-off run that should leave nothing behind:
mise x installs the version if needed, runs the command with it on the
path, and changes nothing else.
What not to pin
Pinning is not free. Every pinned tool is a stream of update pull requests
from Renovate or Dependabot. Every merged one makes everyone run
mise install again. So a pin has to buy something. Two examples of tools I
leave out:
The AWS CLI. Its minor releases almost never break anything a pipeline depends on, so a pin trades daily update noise for a guarantee nobody needed. Let it float.
Python, in a repository that is not a Python project. The system version is enough for a helper script. A project that is Python has a Python-native tool (uv, today) that manages interpreters and dependencies together. A second manager for the same interpreter helps nobody.
The tools that earn a pin are the ones whose version changes behaviour a reviewer cannot see in the diff. Terraform, because its state format and plan output depend on the version. Linters and formatters, because their rules do. And anything that generates code that gets committed.
Limitations
mise use changes what terraform resolves to only in a shell where mise is
activated (mise activate in the shell’s startup file). Without activation,
mise x is the reliable form, and a script that assumes activation fails
quietly on a machine without it.
Tools outside mise’s core set are installed through asdf plugins. The quality
of the install is then the quality of that plugin. A plugin that builds from
source turns mise install into a compile.
Two version files in one repository, .tool-versions for asdf users and
mise.toml for mise, drift the moment one is edited without the other. Pick
mise.toml. mise still reads a .tool-versions that is already there, so
the asdf holdouts lose nothing while they move, and no repository needs a
second file.
Comments