Skip to content

Instantly share code, notes, and snippets.

@knu
Last active July 19, 2026 15:29
Show Gist options
  • Select an option

  • Save knu/86c3d1a2c4cbcf02b1d6306fdffd8ded to your computer and use it in GitHub Desktop.

Select an option

Save knu/86c3d1a2c4cbcf02b1d6306fdffd8ded to your computer and use it in GitHub Desktop.
Zero-downtime mise upgrade
#!/bin/sh
set -e
config=$HOME/.config/mise/config.toml
usage() {
echo "Usage: $0 [-p config] [tool...]"
}
current_tools() {
printf %s "$outdated" | jq -r \
'to_entries[] | select(.value.current != null) | "\(.key)@\(.value.current)"'
}
latest_tools() {
printf %s "$outdated" | jq -r \
'to_entries[] | "\(.key)@\(.value.latest)"'
}
new_latest_tools() {
latest_tools | while IFS= read -r spec; do
tool=${spec%@*}
version=${spec##*@}
if ! mise ls --json --installed "$tool" |
jq -e --arg version "$version" 'any(.[]; .version == $version)' >/dev/null; then
printf '%s\n' "$spec"
fi
done
}
pin_installed_paths() {
# Build specs from local data only; mise outdated cannot be used here
# because it reports current=null for a tool whose requested version is
# resolved but not installed, and it may itself invoke the credential
# command before any pinning takes effect.
installed_specs=$(
mise ls --json | jq -r '
to_entries[]
| .key as $tool
| (.value | map(select(.installed))) as $installed
| (($installed | map(select(.active)) | last) // ($installed | last))
| select(. != null)
| "\($tool)@\(.version)"
'
)
[ -n "$installed_specs" ] || return 0
# shellcheck disable=SC2016,SC2086
PATH=$(mise exec $installed_specs -- sh -c 'printf %s "$PATH"')
export PATH
}
cleanup_new_latest_tools() {
[ -n "$new_latest_specs" ] || return 0
# shellcheck disable=SC2086
mise uninstall -y $new_latest_specs
}
while getopts "p:" opt; do
case $opt in
p)
config=$OPTARG
;;
\?)
usage
exit 1
;;
esac
done
shift $((OPTIND - 1))
config=$(realpath "$config")
config_bak=${config%.toml}.bak.toml
if [ -f "$config_bak" ]; then
echo "$config_bak exists; upgrade seems to be in progress."
exit 1
fi
cd "$(dirname "$config")"
# Prefer installed tool bins for every mise invocation below, so commands
# spawned by mise (e.g. settings.gitlab.credential_command running glab)
# never resolve to a shim that would auto-install the very tool being
# upgraded and recurse.
pin_installed_paths
# Get outdated tools.
outdated=$(mise outdated --json "$@")
if [ "$outdated" = '{}' ]; then
echo Nothing to upgrade.
exit
fi
current_specs=$(current_tools)
latest_specs=$(latest_tools)
new_latest_specs=$(new_latest_tools)
# Pin the tools to the currently installed versions while new versions are being installed.
cp -p "$config" "$config_bak"
if [ -n "$current_specs" ]; then
# shellcheck disable=SC2086
mise use --path "$config" $current_specs
fi
# Install the latest versions. If this fails, remove any partial latest
# installs before restoring the config.
# shellcheck disable=SC2086
if mise install $latest_specs; then
mv "$config_bak" "$config"
else
ret=$?
if cleanup_new_latest_tools; then
mv "$config_bak" "$config"
else
echo "Upgrade cleanup failed; $config remains pinned to the previous versions." >&2
fi
exit "$ret"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment