diff --git a/README.md b/README.md index 8481e532..56d26116 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ MRSK deploys web apps anywhere from bare metal to cloud VMs using Docker with ze Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I +Join us on Discord: https://discord.gg/DQETs3Pm + ## Installation Install MRSK globally with `gem install mrsk`. Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this: diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 73268f50..328af480 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -9,42 +9,58 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base end desc "deploy", "Deploy app to servers" + option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def deploy + invoke_options = options.without(:skip_push) + runtime = print_runtime do say "Ensure curl and Docker are installed...", :magenta - invoke "mrsk:cli:server:bootstrap" + invoke "mrsk:cli:server:bootstrap", [], invoke_options say "Log into image registry...", :magenta - invoke "mrsk:cli:registry:login" + invoke "mrsk:cli:registry:login", [], invoke_options - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:deliver" + if options[:skip_push] + say "Pull app image...", :magenta + invoke "mrsk:cli:build:pull", [], invoke_options + else + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:deliver", [], invoke_options + end say "Ensure Traefik is running...", :magenta - invoke "mrsk:cli:traefik:boot" + invoke "mrsk:cli:traefik:boot", [], invoke_options say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform" + invoke "mrsk:cli:healthcheck:perform", [], invoke_options - invoke "mrsk:cli:app:boot" + invoke "mrsk:cli:app:boot", [], invoke_options say "Prune old containers and images...", :magenta - invoke "mrsk:cli:prune:all" + invoke "mrsk:cli:prune:all", [], invoke_options end audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] end desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" + option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def redeploy + invoke_options = options.without(:skip_push) + runtime = print_runtime do - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:deliver" + if options[:skip_push] + say "Pull app image...", :magenta + invoke "mrsk:cli:build:pull", [], invoke_options + else + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:deliver", [], invoke_options + end say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform" + invoke "mrsk:cli:healthcheck:perform", [], invoke_options - invoke "mrsk:cli:app:boot" + invoke "mrsk:cli:app:boot", [], invoke_options end audit_broadcast "Redeployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] @@ -119,8 +135,10 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base puts "Binstub already exists in bin/mrsk (remove first to create a new one)" else puts "Adding MRSK to Gemfile and bundle..." - `bundle add mrsk` - `bundle binstubs mrsk` + run_locally do + execute :bundle, :add, :mrsk + execute :bundle, :binstubs, :mrsk + end puts "Created binstub file in bin/mrsk" end end diff --git a/lib/mrsk/commands/traefik.rb b/lib/mrsk/commands/traefik.rb index 67ab448a..b9ff19c1 100644 --- a/lib/mrsk/commands/traefik.rb +++ b/lib/mrsk/commands/traefik.rb @@ -56,7 +56,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base private def cmd_option_args if args = config.traefik["args"] - optionize args + optionize args, with: "=" else [] end diff --git a/lib/mrsk/configuration/role.rb b/lib/mrsk/configuration/role.rb index cc56be3d..f3e20e9a 100644 --- a/lib/mrsk/configuration/role.rb +++ b/lib/mrsk/configuration/role.rb @@ -73,8 +73,9 @@ class Mrsk::Configuration::Role "traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)", "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"], "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s", - "traefik.http.middlewares.#{config.service}.retry.attempts" => "5", - "traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms" + "traefik.http.middlewares.#{config.service}-retry.retry.attempts" => "5", + "traefik.http.middlewares.#{config.service}-retry.retry.initialinterval" => "500ms", + "traefik.http.routers.#{config.service}.middlewares" => "#{config.service}-retry@docker" } else {} diff --git a/lib/mrsk/utils.rb b/lib/mrsk/utils.rb index d8860f1a..1e763250 100644 --- a/lib/mrsk/utils.rb +++ b/lib/mrsk/utils.rb @@ -24,8 +24,14 @@ module Mrsk::Utils end # Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option. - def optionize(args) - args.collect { |(key, value)| [ "--#{key}", value == true ? nil : escape_shell_value(value) ] }.flatten.compact + def optionize(args, with: nil) + options = if with + args.collect { |(key, value)| value == true ? "--#{key}" : "--#{key}#{with}#{escape_shell_value(value)}" } + else + args.collect { |(key, value)| [ "--#{key}", value == true ? nil : escape_shell_value(value) ] } + end + + options.flatten.compact end # Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 9f5640e4..f72741dd 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -1,9 +1,80 @@ require_relative "cli_test_case" class CliMainTest < CliTestCase - test "version" do - version = stdouted { Mrsk::Cli::Main.new.version } - assert_equal Mrsk::VERSION, version + test "setup" do + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap") + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:accessory:boot", [ "all" ]) + Mrsk::Cli::Main.any_instance.expects(:deploy) + + run_command("setup") + end + + test "deploy" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => false } + + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options) + + run_command("deploy").tap do |output| + assert_match /Ensure curl and Docker are installed/, output + assert_match /Log into image registry/, output + assert_match /Build and push app image/, output + assert_match /Ensure Traefik is running/, output + assert_match /Ensure app can pass healthcheck/, output + assert_match /Prune old containers and images/, output + end + end + + test "deploy with skip_push" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => true } + + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options) + + run_command("deploy", "--skip_push").tap do |output| + assert_match /Ensure curl and Docker are installed/, output + assert_match /Log into image registry/, output + assert_match /Pull app image/, output + assert_match /Ensure Traefik is running/, output + assert_match /Ensure app can pass healthcheck/, output + assert_match /Prune old containers and images/, output + end + end + + test "redeploy" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => false} + + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + + run_command("redeploy").tap do |output| + assert_match /Build and push app image/, output + assert_match /Ensure app can pass healthcheck/, output + end + end + + test "redeploy with skip_push" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => true } + + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + + run_command("redeploy", "--skip_push").tap do |output| + assert_match /Pull app image/, output + assert_match /Ensure app can pass healthcheck/, output + end end test "rollback bad version" do @@ -25,6 +96,95 @@ class CliMainTest < CliTestCase end end + test "details" do + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:details") + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:details") + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:accessory:details", [ "all" ]) + + run_command("details") + end + + test "audit" do + run_command("audit").tap do |output| + assert_match /tail -n 50 mrsk-app-audit.log on 1.1.1.1/, output + assert_match /App Host: 1.1.1.1/, output + assert_match /tail -n 50 mrsk-app-audit.log on 1.1.1.2/, output + assert_match /App Host: 1.1.1.2/, output + end + end + + test "config" do + run_command("config").tap do |output| + config = YAML.load(output) + + assert_equal ["web"], config[:roles] + assert_equal ["1.1.1.1", "1.1.1.2"], config[:hosts] + assert_equal "999", config[:version] + assert_equal "dhh/app", config[:repository] + assert_equal "dhh/app:999", config[:absolute_image] + assert_equal "app-999", config[:service_with_version] + end + end + + test "init" do + Pathname.any_instance.expects(:exist?).returns(false).twice + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:cp_r) + + run_command("init").tap do |output| + assert_match /Created configuration file in config\/deploy.yml/, output + assert_match /Created \.env file/, output + end + end + + test "init with existing config" do + Pathname.any_instance.expects(:exist?).returns(true).twice + + run_command("init").tap do |output| + assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output + end + end + + test "init with bundle option" do + Pathname.any_instance.expects(:exist?).returns(false).times(3) + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:cp_r) + + run_command("init", "--bundle").tap do |output| + assert_match /Created configuration file in config\/deploy.yml/, output + assert_match /Created \.env file/, output + assert_match /Adding MRSK to Gemfile and bundle/, output + assert_match /bundle add mrsk/, output + assert_match /bundle binstubs mrsk/, output + assert_match /Created binstub file in bin\/mrsk/, output + end + end + + test "init with bundle option and existing binstub" do + Pathname.any_instance.expects(:exist?).returns(true).times(3) + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:cp_r) + + run_command("init", "--bundle").tap do |output| + assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output + assert_match /Binstub already exists in bin\/mrsk \(remove first to create a new one\)/, output + end + end + + test "envify" do + File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>") + File.expects(:write).with(".env", "HELLO=world", perm: 0600) + + run_command("envify") + end + + test "envify with destination" do + File.expects(:read).with(".env.staging.erb").returns("HELLO=<%= 'world' %>") + File.expects(:write).with(".env.staging", "HELLO=world", perm: 0600) + + run_command("envify", "-d", "staging") + end + test "remove with confirmation" do run_command("remove", "-y").tap do |output| assert_match /docker container stop traefik/, output @@ -49,6 +209,11 @@ class CliMainTest < CliTestCase end end + test "version" do + version = stdouted { Mrsk::Cli::Main.new.version } + assert_equal Mrsk::VERSION, version + end + private def run_command(*command) stdouted { Mrsk::Cli::Main.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 80592ea6..b04d0b97 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", @app.run.join(" ") end @@ -22,7 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:volumes] = ["/local/path:/container/path" ] assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", @app.run.join(" ") end @@ -30,7 +30,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "path" => "/healthz" } assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", @app.run.join(" ") end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index 97310623..5ca51e13 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -4,18 +4,18 @@ class CommandsTraefikTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + traefik: { "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } end test "run" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format \"json\" --metrics.prometheus.buckets \"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["host_port"] = "8080" assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format \"json\" --metrics.prometheus.buckets \"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 1017221f..0f83d3df 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "special label args for web" do - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\""], @config.role(:web).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app.middlewares=\"app-retry@docker\"" ], @config.role(:web).label_args end test "custom labels" do @@ -66,7 +66,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] } }) - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\"" ], config.role(:beta).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app.middlewares=\"app-retry@docker\"" ], config.role(:beta).label_args end test "env overwritten by role" do diff --git a/test/test_helper.rb b/test/test_helper.rb index 4c381df2..3704e8e2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,7 +9,18 @@ require "mrsk" ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] +# Applies to remote commands only. SSHKit.config.backend = SSHKit::Backend::Printer +# Ensure local commands use the printer backend too. +# See https://github.com/capistrano/sshkit/blob/master/lib/sshkit/dsl.rb#L9 +module SSHKit + module DSL + def run_locally(&block) + SSHKit::Backend::Printer.new(SSHKit::Host.new(:local), &block).run + end + end +end + class ActiveSupport::TestCase end