capistrano-mise
Runs Capistrano's remote commands through mise, so they resolve their tool versions from the mise config that shipped with the revision being deployed.
# Gemfile
gem "capistrano-mise", github: "7a6163/capistrano-mise", require: false# Capfile — after capistrano/deploy, which defines the tasks this hooks
require "capistrano/deploy"
require "capistrano/mise"That is the whole setup. There is no version to configure: mise.toml or .tool-versions
in your repository already says which versions to run, and that file is in the release
directory by the time anything runs. A setting naming the version would be a second
source of truth that can silently disagree with the repository, so there isn't one.
mise must already be installed on your hosts. Installing it is provisioning's job, the
same way capistrano-rbenv does not install rbenv.
What it does
- Prefixes the mapped commands with
mise exec --, sobundle installbecomes$HOME/.local/bin/mise exec -- bundle install. The prefix goes in front of any prefix another plugin added, socapistrano-bundler'sbundle execstill applies. - Adds
mise:checkafterdeploy:check, which fails the deploy if mise is not executable where it is expected. - Adds
mise:installafterdeploy:updating, which runsmise installin the new release so the versions that revision declares are present before anything tries to use them. This installs the tools mise manages, not mise itself — same as the CLI command it is named after. - Sets
MISE_EXEC_AUTO_INSTALL=0. Mise installs missing tools on demand by default, which would bury a multi-minute Ruby build inside whichever command happened to run first;mise:installowns installation instead, as a step you can see.
Settings
| Setting | Default | |
|---|---|---|
:mise_path |
$HOME/.local/bin/mise |
Absolute path to the mise binary. A host provisioned from a package manager has it at /usr/bin/mise or /usr/local/bin/mise and must say so. |
:mise_map_bins |
%w[rake gem bundle ruby rails] |
Which commands to run through mise. |
:mise_roles |
:all |
Which roles mise:check and mise:install run on. See the note below. |
:mise_roles does not limit the mise exec prefix. SSHKit keeps one command map
for every host, so once mise:map_bins has run, bundle, rake and the rest go through
mise wherever they run. Narrowing :mise_roles only narrows where mise is checked for and
installed into. Every host that runs a mapped command needs mise, whatever its role.
capistrano-rbenv's :rbenv_roles behaves the same way, for the same reason.
:mise_map_bins is Ruby-only on purpose. Adding node here when your repo does not declare
node means mise exec -- node quietly falls back to whatever node is on the host's PATH —
worse than not mapping it. Add a tool here once the repo declares it:
set :mise_map_bins, fetch(:mise_map_bins) + %w[node npm yarn]What it does not do
Capistrano can only wrap the commands it runs itself. Anything else on the host that runs your
application — a systemd unit's ExecStart, a crontab entry, a login shell — never passes
through Capistrano and is unaffected by this plugin. A deploy can succeed completely while
every one of those still runs the wrong Ruby.
Each needs its own arrangement:
# systemd: call mise explicitly, with an absolute path
ExecStart=/home/deploy/.local/bin/mise exec -- bundle exec puma -C /path/to/puma.rb# login shells (which is what `bash -l -c` in a crontab gets): put mise's shims on PATH,
# in ~/.profile or /etc/profile.d/mise.sh
export PATH="$HOME/.local/share/mise/shims:$PATH"Use shims rather than eval "$(mise activate bash)" there: activation recomputes the
environment when a prompt is displayed, and a cron job never displays one.
Gotcha: .ruby-version alone is not enough
Mise does not read .ruby-version, .nvmrc and friends unless you opt in per tool, so a
repository carrying only a .ruby-version resolves to no version at all. mise:install warns
when it sees this. Either add a mise.toml / .tool-versions, or opt in on the host:
mise settings add idiomatic_version_file_enable_tools rubyMigrating from capistrano-rbenv or capistrano-rvm
Swap the require, then move the version declaration out of your deploy config and into the
repository — set :rvm_ruby_version, "3.3.12" becomes a line in mise.toml or
.tool-versions. This is the only part that is not mechanical, and it is deliberate.
If your systemd unit was generated by capistrano3-puma, its ExecStart contains whatever
prefix your old plugin contributed (/usr/local/rvm/bin/rvm 3.3.12 do …) baked in at the time
it was written. Update it by hand, and delete any Environment="PATH=…" line pointing into the
old version manager's directories — mise exec sets up the child's PATH itself, and those
directories are about to stop existing.
Releasing
Bump lib/capistrano/mise/version.rb, commit, then tag:
git tag v0.1.0 && git push origin v0.1.0That fires .github/workflows/release.yml, which publishes to RubyGems (over OIDC — there
is no API key stored anywhere), to GitHub Packages, and as a GitHub Release with the .gem
attached.
License
MIT.