mise, in ogni repository
Nel 2021 scrivevo di asdf + direnv. Oggi uso mise: un solo strumento per versioni, variabili e task, uguale sul portatile, in CI e sui bastion. E andrebbe messo in ogni repository.

Nel 2021 avevo scritto un articolo su asdf + direnv: come avere la versione giusta di ogni strumento da riga di comando, cartella per cartella, senza impazzire.
Il problema non è cambiato. Un progetto vuole kubectl 1.35, quello accanto la 1.31, la CI ne ha una terza e sul bastion c'è quella che qualcuno ha installato a mano due anni fa.
Lo strumento invece è cambiato: oggi uso mise. E secondo me dovrebbe stare in ogni repository.
Cos'è
mise è un solo binario che fa tre lavori:
- le versioni degli strumenti: node, go, kubectl, opentofu, ognuno alla versione che dice il progetto (quello che faceva asdf);
- le variabili d'ambiente per cartella: entri e ci sono, esci e spariscono (quello che faceva direnv);
- i task: i comandi del progetto, con un nome e una descrizione (quello che di solito finiva in un Makefile).
Tutto sta in un file, mise.toml, nella radice del repository.
Perché al posto di asdf + direnv
asdf + direnv funzionava, ma erano due strumenti, due file (.tool-versions e .envrc) e un plugin da installare per ogni strumento. In più asdf passa da uno shim per ogni comando: ogni kubectl che lanci è prima uno script che decide quale kubectl lanciare.
mise fa le stesse cose da solo:
- un file invece di due, e se trova un
.tool-versionslo legge lo stesso, quindi si passa senza traumi; - di default niente shim: quando entri in una cartella aggiorna il
PATH, e i comandi partono diretti; - oltre ai plugin di asdf sa prendere gli strumenti direttamente dalle release di GitHub, dai pacchetti Go, npm, pipx e cargo, senza plugin da scrivere né da mantenere;
- è veloce, e non te ne accorgi nemmeno.
Un mise.toml vero
Nell'organizzazione GitHub della SIGHUP Distribution di ReeVo, a cui contribuisco, mise è ovunque: c'è un mise.toml in quasi tutti i repository, da furyctl ai moduli della distribuzione. L'ha portato un mio collega, e non siamo più tornati indietro.
Questo è un pezzo di quello di furyctl, accorciato:
[tools]
go = "1.26.5"
golangci-lint = "2.12.2"
goreleaser = "2.15.4"
# Strumenti presi da una release GitHub e da un pacchetto Go, senza plugin
"ubi:mvdan/gofumpt" = "v0.6.0"
"go:github.com/onsi/ginkgo/v2/ginkgo" = "v2.19.0"
[env]
CGO_ENABLED = 0
[tasks.lint]
description = "run linting with golangci-lint"
run = "golangci-lint -v run --timeout=10m --config=.rules/.golangci.yml ./..."
[tasks.test-unit]
description = "run unit tests"
run = "go test -v -tags=unit ./..."
[tasks.build]
description = "build with goreleaser (snapshot)"
run = "goreleaser release --verbose --snapshot --clean"Chi clona il repository fa:
mise install # scarica gli strumenti, alle versioni giuste
mise tasks # cosa si può fare qui
mise run test-unitE basta. Niente README con "installa queste dieci cose, in queste versioni, in quest'ordine".
In locale
Una riga nella configurazione della shell:
eval "$(mise activate zsh)"Da lì entri in una cartella e hai gli strumenti di quel progetto, entri in un'altra e hai i suoi. Anche questo sito funziona così: un mise.toml con node e pnpm, e i task per sviluppo, build e validazione.
In CI
Qui mise dà il meglio: la pipeline usa gli stessi strumenti, alle stesse versioni, e lancia gli stessi task che lancio io sul portatile. Se mise run test-unit passa da me, in CI fa esattamente la stessa cosa.
Lì usiamo Drone, con un'immagine che ha solo mise preinstallato. Il primo step installa gli strumenti, gli altri li usano. Questa è la pipeline di furyctl, accorciata:
volumes:
- name: mise-cache
host:
path: /root/mise_data_dir
steps:
- name: install-tools
image: quay.io/sighup/mise:v2026.6.14
environment:
MISE_DATA_DIR: /mise-data
MISE_GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
volumes:
- name: mise-cache
path: /mise-data
commands:
- mise install
- name: lint
image: quay.io/sighup/mise:v2026.6.14
depends_on: [install-tools]
environment:
MISE_DATA_DIR: /mise-data
volumes:
- name: mise-cache
path: /mise-data
commands:
- eval "$(mise activate bash --shims)"
- mise run lintIl volume e il token non sono lì per caso: ci torno tra poco. Su GitHub Actions c'è anche un'azione già pronta, jdx/mise-action, che fa tutto in un passo.
Sui bastion
Sui bastion remoti spesso non sei root, e quello che c'è installato è quello che c'è. mise si installa nella home, senza root, con un comando:
curl https://mise.run | shCloni il repository, mise install, e hai gli stessi kubectl, furyctl e opentofu del portatile. Niente più "sul bastion c'è una versione vecchia, quindi il comando lì è diverso".
Dove stare attenti
mise non è magia, e un paio di cose le abbiamo imparate sbattendoci contro.
I limiti di GitHub. Tanti strumenti arrivano dalle release di GitHub, e mise usa le API di GitHub anche per scoprire quali versioni esistono. Senza autenticazione il limite è basso: in CI, o da un bastion che esce dallo stesso IP di tanti altri, ci si arriva in fretta e l'installazione fallisce. La soluzione è dare a mise un token GitHub, in MISE_GITHUB_TOKEN o GITHUB_TOKEN.
La cache in CI. Senza accorgimenti ogni job riparte da zero e scarica tutto a ogni run: go, kubectl, python, e via così. Conviene un volume condiviso tra i job per la cartella di mise (MISE_DATA_DIR), così le run che usano gli stessi strumenti li trovano già lì. Ma se più pipeline girano in parallelo sullo stesso worker, due mise install che scrivono nella stessa cartella si pestano i piedi. In altri repository dell'organizzazione l'install passa da un flock, così chi arriva secondo aspetta il suo turno:
commands:
- "(flock -w 1800 9 || exit 1; mise install) 9>/mise-data/.mise-install.lock"Uno standard, in ogni repository
Per questo secondo me mise andrebbe messo in ogni repository, anche nei più piccoli:
- "da me funziona" smette di essere una scusa: le versioni sono scritte nel repository, uguali per tutti;
- la documentazione si esegue:
mise tasksdice cosa si può fare, e i task sono i comandi veri, non un README che invecchia; - la CI è uguale al portatile: stessi strumenti, stessi comandi;
- si parte in due comandi:
git cloneemise install; - e serve anche a me, tra sei mesi, quando mi sarò dimenticato tutto.
È anche una delle prime cose che guardo quando apro un repository per capire se è tenuto bene, come raccontavo in Chi preme il bottone.
Ciao asdf, ciao direnv: grazie di tutto. 🙂