capistrano-mise 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bc195e84e9c69c1058bfbb7899a7335e8b580ae9504e02c9c5568db1962ceb34
4
+ data.tar.gz: 8f711f539d0897768e79de6cfe86dae6e9e29968615e11b2b6f29286cefdf1b8
5
+ SHA512:
6
+ metadata.gz: b0e6bc50bc4480368a5a2d82b848862dcbd722a6ff232726829d27a046eaa6919ff850bf635ec18e5cad9964f3c5cf8993accf4ccba1f16d16720f41ce5694ba
7
+ data.tar.gz: 45037703949301e25d04a3ab70aa7125b0badbad64f78a3675296e8653044dbeb50d4e4584d810706858c48e6c3748c1f6fc0346073d8552cea25eb7983b111e
data/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-09-18
11
+
12
+ First release.
13
+
14
+ ### Added
15
+ - Wraps the commands Capistrano runs on a host with `mise exec --`, pushed onto the
16
+ front of SSHKit's per-command prefix list so that a prefix another plugin
17
+ contributed — `capistrano-bundler`'s `bundle exec` — still applies.
18
+ - `mise:check`, after `deploy:check`, which stops the deploy if mise is not
19
+ executable where `:mise_path` says it is.
20
+ - `mise:install`, after `deploy:updating`, which runs `mise install` in the new
21
+ release. It installs the tools mise manages, not mise itself.
22
+ - Settings: `:mise_path` (default `$HOME/.local/bin/mise`), `:mise_map_bins`
23
+ (default `%w[rake gem bundle ruby rails]`) and `:mise_roles` (default `:all`).
24
+ - `MISE_EXEC_AUTO_INSTALL=0`, so a missing tool surfaces as a failure from
25
+ `mise:install` rather than as a silent multi-minute build inside an unrelated
26
+ command.
27
+ - A warning when a release carries an idiomatic version file such as
28
+ `.ruby-version` but no mise config of its own, since mise does not read those
29
+ unless you opt in per tool.
30
+ - A clear error when `capistrano/mise` is required before `capistrano/deploy`,
31
+ in place of rake's "Don't know how to build task 'deploy:check'".
32
+
33
+ ### Design notes
34
+ - There is deliberately no setting naming a version. The mise config that shipped
35
+ with the revision is the only source of truth; use mise's own per-environment
36
+ config files to vary it per host.
37
+ - Commands that do not reach the host through Capistrano — a systemd unit's
38
+ `ExecStart`, a crontab entry — are outside this plugin's reach and need their
39
+ own arrangement. The README has the shapes that work.
40
+ - `mise exec` rather than shims or shell activation. Capistrano's SSH connection
41
+ gets a non-interactive, non-login shell, which reads no profile at all, and
42
+ mise's activation recomputes the environment when a prompt is displayed — which
43
+ never happens here. `mise exec` with an absolute path needs neither, and unlike
44
+ shims it does not require writing to a shared `PATH`.
45
+
46
+ ### Internal
47
+ - The shell fragments live in `Capistrano::Mise::Commands`, apart from the rake
48
+ file, which cannot be loaded outside a Capfile. They are covered by mutation
49
+ testing at 100%, enforced in CI.
50
+ - Tasks are covered at the seam that matters for a Capistrano plugin: the
51
+ commands a task sends to a host, recorded through a stub SSHKit backend.
52
+ - Settings are read outside the `on` block. Inside one, `self` is the SSHKit
53
+ backend, and the Capistrano DSL is reachable there only because
54
+ `capistrano/setup` includes it into `Object`.
55
+
56
+ [Unreleased]: https://github.com/7a6163/capistrano-mise/compare/v0.1.0...HEAD
57
+ [0.1.0]: https://github.com/7a6163/capistrano-mise/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zac
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # capistrano-mise
2
+
3
+ Runs Capistrano's remote commands through [mise](https://mise.jdx.dev), so they resolve
4
+ their tool versions from the mise config that shipped with the revision being deployed.
5
+
6
+ ```ruby
7
+ # Gemfile
8
+ gem "capistrano-mise", github: "7a6163/capistrano-mise", require: false
9
+ ```
10
+
11
+ ```ruby
12
+ # Capfile — after capistrano/deploy, which defines the tasks this hooks
13
+ require "capistrano/deploy"
14
+ require "capistrano/mise"
15
+ ```
16
+
17
+ That is the whole setup. There is no version to configure: `mise.toml` or `.tool-versions`
18
+ in your repository already says which versions to run, and that file is in the release
19
+ directory by the time anything runs. A setting naming the version would be a second
20
+ source of truth that can silently disagree with the repository, so there isn't one.
21
+
22
+ **mise must already be installed on your hosts.** Installing it is provisioning's job, the
23
+ same way `capistrano-rbenv` does not install rbenv.
24
+
25
+ ## What it does
26
+
27
+ - Prefixes the mapped commands with `mise exec --`, so `bundle install` becomes
28
+ `$HOME/.local/bin/mise exec -- bundle install`. The prefix goes in *front* of any prefix
29
+ another plugin added, so `capistrano-bundler`'s `bundle exec` still applies.
30
+ - Adds `mise:check` after `deploy:check`, which fails the deploy if mise is not executable
31
+ where it is expected.
32
+ - Adds `mise:install` after `deploy:updating`, which runs `mise install` in the new release
33
+ so the versions that revision declares are present before anything tries to use them.
34
+ This installs the *tools* mise manages, not mise itself — same as the CLI command it
35
+ is named after.
36
+ - Sets `MISE_EXEC_AUTO_INSTALL=0`. Mise installs missing tools on demand by default, which
37
+ would bury a multi-minute Ruby build inside whichever command happened to run first;
38
+ `mise:install` owns installation instead, as a step you can see.
39
+
40
+ ## Settings
41
+
42
+ | Setting | Default | |
43
+ |---|---|---|
44
+ | `: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. |
45
+ | `:mise_map_bins` | `%w[rake gem bundle ruby rails]` | Which commands to run through mise. |
46
+ | `:mise_roles` | `:all` | Which roles this applies to. |
47
+
48
+ `:mise_map_bins` is Ruby-only on purpose. Adding `node` here when your repo does not declare
49
+ node means `mise exec -- node` quietly falls back to whatever node is on the host's `PATH` —
50
+ worse than not mapping it. Add a tool here once the repo declares it:
51
+
52
+ ```ruby
53
+ set :mise_map_bins, fetch(:mise_map_bins) + %w[node npm yarn]
54
+ ```
55
+
56
+ ## What it does not do
57
+
58
+ Capistrano can only wrap the commands it runs itself. Anything else on the host that runs your
59
+ application — a systemd unit's `ExecStart`, a crontab entry, a login shell — never passes
60
+ through Capistrano and is unaffected by this plugin. **A deploy can succeed completely while
61
+ every one of those still runs the wrong Ruby.**
62
+
63
+ Each needs its own arrangement:
64
+
65
+ ```ini
66
+ # systemd: call mise explicitly, with an absolute path
67
+ ExecStart=/home/deploy/.local/bin/mise exec -- bundle exec puma -C /path/to/puma.rb
68
+ ```
69
+
70
+ ```sh
71
+ # login shells (which is what `bash -l -c` in a crontab gets): put mise's shims on PATH,
72
+ # in ~/.profile or /etc/profile.d/mise.sh
73
+ export PATH="$HOME/.local/share/mise/shims:$PATH"
74
+ ```
75
+
76
+ Use shims rather than `eval "$(mise activate bash)"` there: activation recomputes the
77
+ environment when a prompt is displayed, and a cron job never displays one.
78
+
79
+ ## Gotcha: `.ruby-version` alone is not enough
80
+
81
+ Mise does not read `.ruby-version`, `.nvmrc` and friends unless you opt in per tool, so a
82
+ repository carrying only a `.ruby-version` resolves to no version at all. `mise:install` warns
83
+ when it sees this. Either add a `mise.toml` / `.tool-versions`, or opt in on the host:
84
+
85
+ ```sh
86
+ mise settings add idiomatic_version_file_enable_tools ruby
87
+ ```
88
+
89
+ ## Migrating from capistrano-rbenv or capistrano-rvm
90
+
91
+ Swap the `require`, then move the version declaration out of your deploy config and into the
92
+ repository — `set :rvm_ruby_version, "3.3.12"` becomes a line in `mise.toml` or
93
+ `.tool-versions`. This is the only part that is not mechanical, and it is deliberate.
94
+
95
+ If your systemd unit was generated by `capistrano3-puma`, its `ExecStart` contains whatever
96
+ prefix your old plugin contributed (`/usr/local/rvm/bin/rvm 3.3.12 do …`) baked in at the time
97
+ it was written. Update it by hand, and delete any `Environment="PATH=…"` line pointing into the
98
+ old version manager's directories — `mise exec` sets up the child's `PATH` itself, and those
99
+ directories are about to stop existing.
100
+
101
+ ## Releasing
102
+
103
+ Bump `lib/capistrano/mise/version.rb`, commit, then tag:
104
+
105
+ ```sh
106
+ git tag v0.1.0 && git push origin v0.1.0
107
+ ```
108
+
109
+ That fires `.github/workflows/release.yml`, which publishes to RubyGems (over OIDC — there
110
+ is no API key stored anywhere), to GitHub Packages, and as a GitHub Release with the `.gem`
111
+ attached.
112
+
113
+ ## License
114
+
115
+ MIT.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capistrano
4
+ module Mise
5
+ # The shell fragments this plugin sends to a host. Kept apart from the rake
6
+ # file so they can be loaded, read and mutated on their own: requiring the
7
+ # rake file outside a Capfile is not possible, because it hooks tasks that
8
+ # only exist once capistrano/deploy has been loaded.
9
+ module Commands
10
+ # Every filename mise looks for in a single directory, from mise's
11
+ # configuration docs. mise also searches parent directories and a global
12
+ # config, so a release holding none of these can still resolve a version.
13
+ CONFIG_FILENAMES = %w[
14
+ mise.toml mise.local.toml .mise.toml .mise.local.toml
15
+ mise/config.toml .mise/config.toml
16
+ .config/mise.toml .config/mise/config.toml
17
+ .tool-versions
18
+ ].freeze
19
+
20
+ # mise reads idiomatic version files only when opted in per tool, so a
21
+ # release carrying this and nothing else resolves no version at all.
22
+ IDIOMATIC_VERSION_FILE = ".ruby-version"
23
+
24
+ # extend self, not module_function: module_function copies each method onto
25
+ # the module's singleton, so a mutation to the instance method would leave
26
+ # the copy that callers actually reach untouched, and mutant would report
27
+ # coverage this module does not have.
28
+ extend self
29
+
30
+ # Goes in front of a mapped command. `--` ends mise's own arguments, so a
31
+ # command that starts with a dash is not read as one of them.
32
+ def exec_prefix(mise_path)
33
+ "#{mise_path} exec --"
34
+ end
35
+
36
+ def executable(path)
37
+ "[ -x #{path} ]"
38
+ end
39
+
40
+ def any_exist(paths)
41
+ "[ #{paths.map { |path| "-e #{path}" }.join(' -o ')} ]"
42
+ end
43
+
44
+ # Spelled out against the release rather than run inside SSHKit's `within`:
45
+ # SSHKit hands a string command containing whitespace straight to the shell,
46
+ # with no cd and no env, so a relative probe would silently inspect the SSH
47
+ # login directory instead.
48
+ def config_present(release_path)
49
+ any_exist(CONFIG_FILENAMES.map { |name| File.join(release_path, name) })
50
+ end
51
+
52
+ def idiomatic_version_file_present(release_path)
53
+ any_exist([File.join(release_path, IDIOMATIC_VERSION_FILE)])
54
+ end
55
+
56
+ def missing_binary(path, host)
57
+ "mise is not executable at #{path} on #{host}. Install mise, " \
58
+ "or set :mise_path if it is installed somewhere else on this host."
59
+ end
60
+
61
+ def idiomatic_version_file_warning
62
+ "this revision has #{IDIOMATIC_VERSION_FILE} but no mise config of its own. mise does " \
63
+ "not read idiomatic version files unless you opt in, so unless a parent directory or " \
64
+ "the global config supplies one, no Ruby version resolves here. Add a mise.toml or " \
65
+ ".tool-versions, or run: mise settings add idiomatic_version_file_enable_tools ruby"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capistrano
4
+ module Mise
5
+ VERSION = "0.1.0".freeze
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capistrano/mise/version"
4
+
5
+ load File.expand_path("../tasks/mise.rake", __FILE__)
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capistrano/mise/commands"
4
+
5
+ commands = Capistrano::Mise::Commands
6
+
7
+ namespace :mise do
8
+ desc "Check that mise is usable on the target hosts"
9
+ task :check do
10
+ # Read outside the `on` block: inside it, self is the SSHKit backend, and the
11
+ # Capistrano DSL is only reachable there because capistrano/setup includes it
12
+ # into Object. These are settings, not per-host values.
13
+ path = fetch(:mise_path)
14
+
15
+ on release_roles(fetch(:mise_roles)) do |host|
16
+ next if test commands.executable(path)
17
+
18
+ error commands.missing_binary(path, host)
19
+ exit 1
20
+ end
21
+ end
22
+
23
+ desc "Install the tool versions this revision declares"
24
+ task :install do
25
+ release = release_path
26
+
27
+ on release_roles(fetch(:mise_roles)) do
28
+ unless test commands.config_present(release)
29
+ warn commands.idiomatic_version_file_warning if test commands.idiomatic_version_file_present(release)
30
+ end
31
+
32
+ within release do
33
+ execute :mise, :install
34
+ end
35
+ end
36
+ end
37
+
38
+ # Runs after the stage task so that stage-level `set` calls have been applied.
39
+ task :map_bins do
40
+ # mise installs missing tools on demand by default, which would bury a Ruby
41
+ # build inside whichever command happened to run first. mise:install owns that.
42
+ SSHKit.config.default_env["MISE_EXEC_AUTO_INSTALL"] = "0"
43
+
44
+ SSHKit.config.command_map[:mise] = fetch(:mise_path)
45
+
46
+ # unshift, so a command already carrying a prefix (capistrano-bundler's
47
+ # `bundle exec`) keeps it and gains mise in front rather than losing it.
48
+ prefix = commands.exec_prefix(fetch(:mise_path))
49
+ fetch(:mise_map_bins).uniq.each do |command|
50
+ SSHKit.config.command_map.prefix[command.to_sym].unshift(prefix)
51
+ end
52
+ end
53
+ end
54
+
55
+ Capistrano::DSL.stages.each do |stage|
56
+ after stage, "mise:map_bins"
57
+ end
58
+
59
+ # These hooks need their tasks to exist, so this must be required after
60
+ # capistrano/deploy. Without the guard the failure is "Don't know how to build
61
+ # task 'deploy:check'", which names nothing you would think to look at.
62
+ unless Rake::Task.task_defined?("deploy:check")
63
+ raise "capistrano/mise must be required after capistrano/deploy in your Capfile."
64
+ end
65
+
66
+ after "deploy:check", "mise:check"
67
+
68
+ # deploy:updating, not deploy:updated: capistrano-bundler hooks `before
69
+ # deploy:updated`, and hooks on the same task fire in registration order, which
70
+ # depends on Capfile require order. Hooking the earlier task wins regardless.
71
+ # The release is already populated by then, because git:create_release runs off
72
+ # deploy:new_release_path, a prerequisite of deploy:updating.
73
+ after "deploy:updating", "mise:install"
74
+
75
+ namespace :load do
76
+ task :defaults do
77
+ set :mise_path, "$HOME/.local/bin/mise"
78
+ set :mise_roles, fetch(:mise_roles, :all)
79
+ set :mise_map_bins, %w[rake gem bundle ruby rails]
80
+ end
81
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capistrano-mise
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Zac
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: capistrano
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ description: Wraps the commands Capistrano runs on a host with `mise exec`, so they
55
+ resolve their tool versions from the mise config that shipped with the revision.
56
+ email:
57
+ - 579103+7a6163@users.noreply.github.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - Rakefile
66
+ - lib/capistrano/mise.rb
67
+ - lib/capistrano/mise/commands.rb
68
+ - lib/capistrano/mise/version.rb
69
+ - lib/capistrano/tasks/mise.rake
70
+ homepage: https://github.com/7a6163/capistrano-mise
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/7a6163/capistrano-mise
75
+ source_code_uri: https://github.com/7a6163/capistrano-mise
76
+ changelog_uri: https://github.com/7a6163/capistrano-mise/blob/main/CHANGELOG.md
77
+ github_repo: ssh://github.com/7a6163/capistrano-mise
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '3.1'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 4.0.20
93
+ specification_version: 4
94
+ summary: Run Capistrano's remote commands through mise
95
+ test_files: []