Mise
Mise ⧉ (pronounced "meez") is a tool that can pin dev tool versions via
a single mise.toml config file. Every developer then has the correct version of each tool installed.
We do not have to manage them through pyproject.toml.
Installation
On your own machine, install it however you like. The installer script is fine here:
curl https://mise.run | sh
Not in a build path
An installer script is an unpinned input with a live network dependency
(BUILD-11). CI installs Mise with the pinned jdx/mise-action, through .github/actions/set-up-mise. The
builder image installs it from the release binary at a pinned version, with a mirrorable URL.
Neither pipes mise.run.
Then activate it in your shell. Add one of these to your shell config:
# bash (~/.bashrc)
eval "$(~/.local/bin/mise activate bash)"
# zsh (~/.zshrc)
eval "$(~/.local/bin/mise activate zsh)"
Restart your shell or source the config file.
Activation is required, not optional: without it mise installs the pinned tools
but never places them on PATH, and shell completion reports
usage CLI not found.
Upgrade Mise to latest version
mise self-update upgrades your local binary. The version the repository targets is
min_version in the root mise.toml. The version that regenerates mise.lock is the
github:jdx/mise pin in [tools]. Your local one is neither.
Installing tools
Inside the root of the monorepo, simply run:
mise trust mise.toml
mise install
During CI runs, MISE_YES=1 is set which allows is to skip the trust step
Running tasks
Tasks are defined per project and run with mise run. Run a bare task from
inside a project directory, or address any project from anywhere with the
//<project>:<task> syntax:
mise run //croesus:install # one project
mise run '//...:lint' # every project that defines 'lint'
mise tasks --all # list every task
Shared task definitions live in mise/includes/ (the task bodies) and
mise/scripts/ (the bash they call), not in mise/tasks/. See below. Every task is catalogued in
Mise tasks. The rules governing them are build rules.
Adding tools
Add the pin to [tools] in the root mise.toml and commit the mise.lock change with it.
The mise-lock pre-commit hook regenerates the lockfile, not you. mise lock output is
version-dependent, so regeneration runs through the github:jdx/mise pin in [tools], not
whatever Mise is on your PATH. A lockfile written by a different Mise is a diff nobody can review.
CI must allowlist the full github:jdx/mise spec in MISE_ENABLE_TOOLS, or the pin silently does
nothing.
A tool the build needs MUST be pinned here, not assumed from the ambient shell. The Makefiles made
that assumption, which is why the builder image had to bootstrap protoc separately.
The empty mise/tasks/ directory
mise/tasks/ is one of mise's file-task directories: any file placed under it becomes a task of the
config root it sits in. At the repository root that would define phantom root tasks such as
//:build and //:lint. Those match wildcards, so mise '//...:lint' would try to lint the
repository root, where there is no go.mod. It then fails in a task nobody wrote. The directory
MUST therefore stay empty. Shared task definitions live in mise/includes/ and mise/scripts/
instead.
The directory is kept in git by a hidden .tombstone.md, not a .gitkeep. A visible file makes
mise warn on every mise tasks. Marking it executable would create exactly the phantom root task
described above. A dotfile is ignored outright, and .tombstone.md additionally documents the trap.
Do not add visible files there and do not chmod +x anything in it.
Config roots
A project is visible to Mise only if it is listed in [monorepo].config_roots in the root
mise.toml and has a mise.toml of its own. The list is explicit, never globbed: iris/ and
others hold directories that are not task projects, and a listed root with no config file warns on
every invocation. Everyone learns to ignore that warning.
A project missing from config_roots is matched by no wildcard. Not linted, not tested, not built
by any sweep, and nothing says so. See BUILD-6.