📝
· 17 min de lecture

Mise : un seul outil pour tes runtimes et tes CLI

Un seul outil pour remplacer nvm, tfenv, pyenv... et installer ses CLI versionnés par projet, avec lockfile à la clé.

Tu bosses sur ton projet principal en Node 22, et tu dois dépanner un vieux service coincé en Node 18. Tu switch entre les deux dix fois par jour. Ou tu veux tester le nouveau Node 26 dont tout le monde parle, juste cinq minutes, sans casser ton install actuelle. Ou encore : tu rejoins un nouveau projet et le README liste sept outils CLI à installer dans les bonnes versions avant même de pouvoir lancer les tests.

La réponse classique à ces situations, c’est nvm. Et pyenv. Et tfenv, rbenv, jenv, sdkman… Un outil par langage, chacun avec sa syntaxe, son fichier de config (.nvmrc, .python-version, .terraform-version), son shell hook qui ralentit ton terminal. Tu finis avec une demi-douzaine de gestionnaires de versions qui font tous la même chose, mais chacun à sa manière.

Et ce n’est que la moitié du problème. À côté des langages, il y a tous ces outils en ligne de commande dont tes projets dépendent : golangci-lint, ripgrep, terraform-docs, kubectl, helm… Là, plus de version manager dédié. On dégaine brew install et on passe à la suite. C’est-à-dire qu’on les installe en global, en une seule version, partagée par tous tes projets. Le jour où un projet a besoin de golangci-lint@1.55 et un autre de @2.0, c’est cassé.

Mise résout tout ça d’un coup. Un seul binaire pour remplacer la galaxie des *env, mais aussi pour gérer les CLI à la place de brew, avec une vraie config par projet. Tester une autre version de Node devient mise use node@26, et tu peux revenir en arrière aussi vite. Je suis passé d’un mélange asdf + brew à Mise il y a quelques mois, et je ne reviendrai pas en arrière. L’unification de la config à elle seule justifie déjà la migration. Le reste, c’est du bonus.

TL;DR : je raconte les trois trucs qui m’ont fait basculer sur Mise : une vraie config par projet, des backends multiples pour installer n’importe quel CLI, et un lockfile. Bonus : tout ça fonctionne aussi sur la CI en une seule ligne de YAML.

Installer Mise

L’installation tient en une ligne :

curl https://mise.run | sh

Si tu n’es pas fan du curl | sh, brew install mise, apt, winget ou cargo install font aussi le job. La doc liste toutes les méthodes.

Puis on active le hook dans son shell. Pour zsh :

echo 'eval "$(mise activate zsh)"' >> ~/.zshrc

Pour les autres shells (bash, fish, nushell…) ou pour Windows, la doc officielle d’installation a tout. Elle est très bien faite et toujours à jour.

Config globale

Le point de départ, c’est un ~/.config/mise/config.toml qui liste les outils qui seront disponibles partout. Pour démarrer, ça peut tenir en trois lignes :

[tools]
node = "26"
go = "1.24"

Pour mettre tout ça en place : mise install télécharge les outils, et au prochain shell node --version te répond v26.x. Pour ajouter un outil sans éditer le fichier à la main : mise use -g terraform@1.11.

Voilà à quoi ressemble ma vraie config globale en pratique, avec un peu plus de matière :

[tools]
node = { version = "26", postinstall = "corepack enable" }
go = "1.24"
terraform = "1.11"
"npm:prettier" = "latest"
"github:goreleaser/goreleaser-pro" = "latest"
golangci-lint = "latest"
awscli = "latest"

[settings]
lockfile = true
auto_install = true
idiomatic_version_file_enable_tools = ["node"]

Quelques points méritent un commentaire :

  • La syntaxe table pour aller plus loin. Pour node, j’écris { version = "26", postinstall = "corepack enable" } plutôt qu’une simple chaîne. La forme table permet d’ajouter des options par outil. Ici, un hook postinstall déclenche corepack enable après chaque install de Node, ce qui me donne pnpm et yarn sans rien faire de plus.
  • Plusieurs backends dès le global. Mon [tools] mélange déjà core (le node nu), npm: et github:. C’est la feature qui change la donne par rapport à nvm, on y revient dans la section dédiée.
  • L’auto-install. Avec auto_install = true, Mise installe l’outil tout seul la première fois que tu l’invoques. Plus besoin de penser à mise install après avoir édité le toml : il s’occupe de tout au prochain appel.
  • Le support des fichiers historiques. idiomatic_version_file_enable_tools = ["node"] réactive la lecture du .nvmrc (les fichiers de ce genre sont ignorés par défaut depuis quelques versions). Pratique pour cohabiter avec des repos qui n’ont pas encore migré.

Le dernier setting, lockfile = true, on en parle plus loin dans la section dédiée. Il assure que tout le monde se retrouve sur la même version résolue, et sur le même binaire pour les backends qui supportent les checksums. (Pour un repo partagé, c’est plus défensif de mettre ce setting dans le mise.toml du projet plutôt qu’en global, j’y reviens plus bas.)

Vérifier que tout marche

Une fois mise install lancé, deux commandes pour confirmer que ta config est bien prise en compte :

mise ls          # liste les versions actives et leur source
mise which node  # affiche le chemin du binaire résolu

Si mise which node te répond quelque chose comme ~/.local/share/mise/installs/node/26.x.y/bin/node, tu es bon. Sinon, vérifie que le shell hook (mise activate) est bien chargé dans ton .zshrc ou équivalent.

Override par projet

À la racine d’un projet, un mise.toml (ou .mise.toml) override le global, outil par outil. La résolution se fait par merge avec le global : ce qui n’est pas redéfini reste hérité.

[tools]
node = "18"

Dans cet exemple, depuis ~/dev/vieux-service/, node --version retourne 18.x. Mais Terraform, Go et Prettier restent ceux du global (26 / 1.24 / latest). Tu ne dupliques pas la config globale dans chaque repo, juste les exceptions. Un mise ls te montre exactement ce qui est résolu et d’où ça vient :

Sortie de mise ls montrant les outils actifs, leur version et leur source (global ou projet)

On voit bien dans la colonne Source que node vient du mise.toml du projet courant, pendant que tout le reste est hérité du global.

Pour ajouter au toml du projet courant : mise use node@18 (sans -g).

Avant d’aller plus loin, trois commandes à connaître :

  • mise install node@26 télécharge la version, sans rien modifier d’autre.
  • mise exec node@26 -- node script.js exécute une commande avec cette version, ponctuellement, sans toucher au mise.toml.
  • mise use node@26 ajoute l’entrée au mise.toml du projet (et mise use --rm node retire l’entrée).

Concrètement, pour tester Node 26 cinq minutes sans casser ton install (le scénario de l’intro) : mise install node@26 puis mise exec node@26 -- node --version. La bascule prend trois secondes, et tu n’as rien à nettoyer ensuite. Si finalement tu veux la garder pour ce projet, mise use node@26 la fixe dans le toml.

Le modèle de confiance

La première fois que tu entres dans un dépôt avec un mise.toml, Mise te demande de le trust. C’est une sécurité : ton mise.toml peut contenir des variables d’env, des tâches, des hooks qui s’exécutent en entrant dans le dossier. Mise ne va pas exécuter ça à l’aveugle sur un repo cloné depuis l’internet.

Pour éviter le mise trust à chaque clonage dans ton workspace :

[settings]
trusted_config_paths = ["~/dev"]

Tout ce qui est sous ~/dev/ devient implicitement de confiance. Pratique pour tes propres projets, à ne surtout pas faire sur ~/Downloads/.

Sous le capot : PATH et shims

Cette sous-section est surtout utile le jour où tu te demandes pourquoi ton IDE ou un script externe n’utilise pas la bonne version. Mise a deux façons de rediriger node, go, terraform… vers la bonne version : modifier ton $PATH à chaque cd (c’est ce que fait mise activate, le hook installé plus haut), ou exposer des shims, des petits wrappers dans ~/.local/share/mise/shims/ qui résolvent la version à chaque appel. Le PATH suffit dans ton shell. Les shims sont là pour ce qui démarre hors shell interactif : ton IDE, un cron, une appli macOS.

C’est d’ailleurs le truc qui m’a piégé au début. Mon IDE continuait à utiliser le Node système alors que node --version dans le terminal me retournait bien la 26. Normal, l’IDE démarre sans charger ton .zshrc, donc sans mise activate. La solution est d’ajouter ~/.local/share/mise/shims au PATH système (launchctl setenv PATH sur macOS, ou la config de ton IDE). Une fois fait, plus aucune surprise. Pour les détails, la doc shims est très bien faite.

Les backends : installer (presque) tout

C’est là que Mise sort du lot des version managers classiques. Plutôt que d’avoir une liste figée d’outils gérés, Mise s’appuie sur plusieurs backends, qui sont autant de sources d’installation. Les principaux :

  • core : les runtimes intégrés (node, python, go, ruby, java…). Syntaxe nue : node = "22".
  • aqua : un registre curé d’outils CLI, avec vérification de signature (Cosign, SLSA, attestations GitHub) faite nativement par Mise. À privilégier quand l’outil y est présent.
  • github : n’importe quelle release GitHub. Mise détecte automatiquement le bon asset pour ta plateforme.
  • npm, cargo, pipx, go : pour les outils packagés dans un écosystème de langage.
  • asdf / vfox : fallback historique via plugins, quand rien d’autre ne couvre l’outil.

Tu as peut-être remarqué que dans mon config.toml global, certains outils ont un préfixe ("npm:prettier", "github:goreleaser/goreleaser-pro") et d’autres non (golangci-lint, awscli, terraform). Quand tu écris un nom court sans préfixe, Mise consulte son registre intégré qui mappe ce nom vers un backend par défaut, selon un ordre de priorité (aqua et github en haut, asdf et vfox en bas). En pratique, tu n’écris le préfixe que quand tu veux forcer un backend précis ou quand l’outil n’est pas dans le registre.

Pour savoir si l’outil que tu cherches est géré par Mise et via quel backend, jette un œil à mise-versions.jdx.dev. Le site indexe environ un millier d’outils avec leurs versions, leurs statistiques de téléchargement et leur backend de référence.

Le registre mise-versions.jdx.dev avec ses 996 outils, classés par popularité, et la répartition par backend

La syntaxe pour spécifier un backend est backend:identifiant comme clé dans [tools]. Voici un exemple qui montre le mélange :

[tools]
node = "26"
python = "3.13"
"aqua:golangci/golangci-lint" = "1.55"
"cargo:ripgrep" = "latest"
"npm:prettier" = "3.3"
"pipx:black" = "24.0"

Le collègue qui clone ton repo lance mise install et il a exactement les mêmes versions de Node, de golangci-lint et de prettier que toi. Plus de « ça marche chez moi parce que j’ai brew install ça il y a six mois ». Plus de README qui liste les outils à installer à la main, dans l’ordre.

La règle simple pour choisir un backend : aqua si l’outil y est, github sinon, le reste en dernier recours.

Aqua te donne gratuitement la vérification de signature (Cosign, SLSA, attestations GitHub). github:owner/repo fait le job sans cérémonie sur n’importe quelle release GitHub. Le backend ubi: existe encore par souci de compatibilité, mais la doc pousse à migrer vers github:.

Pour les cas plus pointus, la doc maintient un tableau comparatif complet des backends sur la vitesse, la sécurité, le support Windows, les attestations et d’autres critères.

Le lockfile : la vraie reproductibilité

C’est exactement le même principe qu’un pnpm-lock.yaml ou un package-lock.json, mais appliqué à tes outils plutôt qu’à tes dépendances Node. Ton mise.toml dit « Node 26 », pareil qu’un package.json qui dit "react": "^18". Mais Node 26.0.0 et 26.1.0 ne sont pas le même binaire, exactement comme React 18.0.0 et 18.3.1 ne sont pas le même code. Si tu veux que ton collègue installe la même version que toi (idéalement avec le même binaire au checksum près, on verra que ça dépend du backend), il te faut un lockfile pour tes outils, au même titre que pour tes deps.

Le lockfile (mise.lock) fige la version exacte, le checksum du binaire et l’URL de téléchargement, par plateforme. Il est stable, mais désactivé par défaut. C’est pour ça que je l’avais activé tout en haut dans la section settings du global :

[settings]
lockfile = true

Comme évoqué plus haut, je le mets en global parce que c’est mon standard sur tous mes repos. Pour un projet partagé, c’est plus défensif de le déplacer dans le mise.toml du projet et de le commit : tous les contributeurs tournent alors sur le même comportement sans avoir à recopier la config globale.

Une fois cette option active, chaque mise install ou mise use met à jour mise.lock, que tu commit avec ton mise.toml. Aperçu du format généré :

[[tools.node]]
version = "26.1.0"
backend = "core:node"

[tools.node.platforms.linux-x64]
checksum = "sha256:a6c2..."
url = "https://nodejs.org/dist/v26.1.0/node-v26.1.0-linux-x64.tar.xz"

Un point à connaître : tous les backends ne figent pas la même chose. aqua, github et http figent tout (version, checksum, URL). ubi et vfox figent partiellement. asdf, npm, cargo, pipx ne figent que la version. C’est mieux que rien, mais ça n’a pas la garantie au bit près. Bon à avoir en tête au moment de choisir un backend pour un outil critique.

En CI : une seule action installe tout

C’est sur la CI que la combinaison mise.toml + mise.lock montre sa vraie valeur. Pour s’en convaincre, regarde à quoi ressemble un workflow GitHub Actions classique qui veut Go, Terraform et golangci-lint dans des versions précises. Même quand chaque outil a sa propre action officielle, tu finis par les empiler :

steps:
  - uses: actions/setup-go@v5
    with:
      go-version: "1.24"

  - uses: hashicorp/setup-terraform@v3
    with:
      terraform_version: "1.11"

  - uses: golangci/golangci-lint-action@v6
    with:
      version: "v1.55"

Trois étapes, trois actions à connaître (et à mettre à jour quand elles bumpent), trois versions hardcodées qui ne sont plus celles de ton mise.toml. À chaque changement de version, c’est deux fichiers à toucher au lieu d’un. Et ajoute un quatrième outil dans le projet, c’est une étape de plus à écrire.

L’action officielle jdx/mise-action remplace tout ça par une seule ligne :

steps:
  - uses: jdx/mise-action@v4

Cette étape installe Mise, lit ton mise.toml, respecte ton mise.lock, installe tous les outils déclarés, et met en cache les binaires via le cache GitHub Actions (activé par défaut). Pour un workflow lint + test, tu passes d’une chaîne d’étapes hétérogènes à une seule, qui pose exactement les mêmes versions que celles que tes collègues ont en local (au bit près sur les backends qui figent le checksum).

Si tu veux durcir d’un cran, ajoute locked = true dans tes settings (ou MISE_LOCKED=1 en variable d’env sur le runner). mise install échoue alors si aucune entrée de lockfile n’existe pour la plateforme courante. Pratique pour t’assurer que personne n’a oublié de commit mise.lock après avoir ajouté un outil.

Variables d’env et hooks par projet

Mise gère aussi les variables d’env et les hooks par projet. Ensemble, ça couvre une bonne partie des usages de direnv et des scripts shell d’init qu’on traîne dans nos repos.

[env]
NODE_ENV = "development"
DATABASE_URL = "postgres://localhost:5432/myapp"
_.file = ".env"
_.path = ["./node_modules/.bin", "./scripts"]

[hooks]
enter = "echo 'Bienvenue sur le projet'"

À chaque cd dans le dossier, Mise charge les variables du [env], lit ton .env s’il existe (_.file), étend le PATH avec les dossiers que tu lui passes (_.path), et exécute le hook enter. Combiné avec le postinstall qu’on a vu plus haut sur node = { version = "26", postinstall = "corepack enable" }, ça remplace pas mal de scripts d’init faits maison.

Pour plus de détails (templates, hooks cd / preinstall, etc.), la doc env et la doc hooks couvrent tout.

Tout tient dans trois fichiers

Aujourd’hui, mon workflow d’install d’outils tient dans trois fichiers : un ~/.config/mise/config.toml global pour mes outils par défaut, un mise.toml par projet pour ce qui s’en écarte, un mise.lock committé pour garantir les versions exactes. Plus de nvm, plus de pyenv, plus de CLI installées à moitié via brew, plus de README qui liste sept outils à poser à la main avant de pouvoir lancer les tests. Sur la CI, une seule ligne (jdx/mise-action@v4) réinstalle exactement la même chose que ce que tes collègues ont en local.

L’écosystème ne s’arrête pas là : le même auteur, jdx, maintient aussi fnox dans la même veine, pour la gestion de secrets (chiffré dans git, dans un cloud provider, ou les deux). Ce sera pour un autre article.

Trois fichiers, plus rien à expliquer à un nouveau dev. C’est exactement ce que je voulais.

Si tu constates une coquille dans l’article ou si tu souhaites être au courant des prochains, viens me trouver sur Bluesky ou Twitter.