Last active
July 19, 2026 15:29
-
-
Save knu/86c3d1a2c4cbcf02b1d6306fdffd8ded to your computer and use it in GitHub Desktop.
Zero-downtime mise upgrade
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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