tomo 0.18.0 → 1.1.2

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/lib/tomo/cli.rb +1 -3
  4. data/lib/tomo/cli/common_options.rb +4 -12
  5. data/lib/tomo/cli/deploy_options.rb +2 -7
  6. data/lib/tomo/cli/parser.rb +1 -6
  7. data/lib/tomo/cli/project_options.rb +1 -3
  8. data/lib/tomo/cli/rules.rb +4 -22
  9. data/lib/tomo/cli/rules/switch.rb +1 -5
  10. data/lib/tomo/cli/rules/value_switch.rb +1 -2
  11. data/lib/tomo/cli/rules_evaluator.rb +3 -13
  12. data/lib/tomo/cli/usage.rb +1 -3
  13. data/lib/tomo/commands/default.rb +1 -5
  14. data/lib/tomo/commands/run.rb +1 -3
  15. data/lib/tomo/configuration.rb +5 -11
  16. data/lib/tomo/configuration/dsl/error_formatter.rb +0 -4
  17. data/lib/tomo/configuration/dsl/hosts_and_settings.rb +1 -2
  18. data/lib/tomo/configuration/plugins_registry.rb +1 -2
  19. data/lib/tomo/configuration/unknown_environment_error.rb +1 -4
  20. data/lib/tomo/console.rb +2 -8
  21. data/lib/tomo/console/menu.rb +1 -2
  22. data/lib/tomo/host.rb +1 -2
  23. data/lib/tomo/plugin/bundler/tasks.rb +8 -8
  24. data/lib/tomo/plugin/core/tasks.rb +3 -16
  25. data/lib/tomo/plugin/env/tasks.rb +2 -7
  26. data/lib/tomo/plugin/git.rb +0 -3
  27. data/lib/tomo/plugin/git/tasks.rb +4 -16
  28. data/lib/tomo/plugin/nodenv/tasks.rb +1 -3
  29. data/lib/tomo/plugin/puma.rb +0 -3
  30. data/lib/tomo/plugin/puma/systemd/service.erb +1 -1
  31. data/lib/tomo/plugin/puma/tasks.rb +6 -15
  32. data/lib/tomo/plugin/rails/helpers.rb +1 -1
  33. data/lib/tomo/plugin/rails/tasks.rb +2 -4
  34. data/lib/tomo/plugin/testing.rb +1 -3
  35. data/lib/tomo/remote.rb +1 -3
  36. data/lib/tomo/runtime.rb +3 -6
  37. data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +1 -4
  38. data/lib/tomo/runtime/execution_plan.rb +1 -4
  39. data/lib/tomo/runtime/explanation.rb +1 -7
  40. data/lib/tomo/runtime/settings_interpolation.rb +1 -3
  41. data/lib/tomo/runtime/settings_required_error.rb +1 -3
  42. data/lib/tomo/runtime/task_runner.rb +2 -7
  43. data/lib/tomo/runtime/unknown_task_error.rb +1 -4
  44. data/lib/tomo/script.rb +1 -5
  45. data/lib/tomo/shell_builder.rb +5 -10
  46. data/lib/tomo/ssh/child_process.rb +6 -13
  47. data/lib/tomo/ssh/connection.rb +3 -16
  48. data/lib/tomo/ssh/connection_validator.rb +1 -4
  49. data/lib/tomo/ssh/executable_error.rb +1 -2
  50. data/lib/tomo/ssh/options.rb +2 -5
  51. data/lib/tomo/task_api.rb +4 -15
  52. data/lib/tomo/testing.rb +0 -2
  53. data/lib/tomo/testing/Dockerfile +1 -3
  54. data/lib/tomo/testing/connection.rb +1 -6
  55. data/lib/tomo/testing/docker_image.rb +4 -17
  56. data/lib/tomo/testing/local.rb +1 -3
  57. data/lib/tomo/testing/mock_plugin_tester.rb +27 -4
  58. data/lib/tomo/testing/mocked_exit_error.rb +1 -1
  59. data/lib/tomo/testing/ubuntu_setup.sh +1 -2
  60. data/lib/tomo/version.rb +1 -1
  61. metadata +13 -29
  62. data/lib/tomo/testing/docker_plugin_tester.rb +0 -39
  63. data/lib/tomo/testing/plugin_tester.rb +0 -33
@@ -2,11 +2,7 @@ module Tomo
2
2
  class Script
3
3
  attr_reader :script
4
4
 
5
- def initialize(script,
6
- echo: true,
7
- pty: false,
8
- raise_on_error: true,
9
- silent: false)
5
+ def initialize(script, echo: true, pty: false, raise_on_error: true, silent: false)
10
6
  @script = script
11
7
  @echo = echo
12
8
  @pty = pty
@@ -3,8 +3,9 @@ require "shellwords"
3
3
  module Tomo
4
4
  class ShellBuilder
5
5
  def self.raw(string)
6
- string.define_singleton_method(:shellescape) { string }
7
- string
6
+ string.dup.tap do |raw_string|
7
+ raw_string.define_singleton_method(:shellescape) { string }
8
+ end
8
9
  end
9
10
 
10
11
  def initialize
@@ -45,9 +46,7 @@ module Tomo
45
46
  end
46
47
 
47
48
  def build(*command, default_chdir: nil)
48
- if @chdir.empty? && default_chdir
49
- return chdir(default_chdir) { build(*command) }
50
- end
49
+ return chdir(default_chdir) { build(*command) } if @chdir.empty? && default_chdir
51
50
 
52
51
  command_string = command_to_string(*command)
53
52
  modifiers = [cd_chdir, unset_env, export_env, set_umask].compact.flatten
@@ -97,11 +96,7 @@ module Tomo
97
96
  def set_umask
98
97
  return if @umask.nil?
99
98
 
100
- umask_value = if @umask.is_a?(Integer)
101
- @umask.to_s(8).rjust(4, "0")
102
- else
103
- @umask
104
- end
99
+ umask_value = @umask.is_a?(Integer) ? @umask.to_s(8).rjust(4, "0") : @umask
105
100
  "umask #{umask_value.to_s.shellescape}"
106
101
  end
107
102
  end
@@ -30,27 +30,20 @@ module Tomo
30
30
  end
31
31
 
32
32
  def result
33
- Result.new(
34
- exit_status: exit_status,
35
- stdout: stdout_buffer.string,
36
- stderr: stderr_buffer.string
37
- )
33
+ Result.new(exit_status: exit_status, stdout: stdout_buffer.string, stderr: stderr_buffer.string)
38
34
  end
39
35
 
40
36
  private
41
37
 
42
- attr_reader :command, :exit_status, :on_data,
43
- :stdout_buffer, :stderr_buffer
38
+ attr_reader :command, :exit_status, :on_data, :stdout_buffer, :stderr_buffer
44
39
 
45
40
  def start_io_thread(source, buffer)
46
41
  new_thread_inheriting_current_vars do
47
- begin
48
- while (line = source.gets)
49
- on_data&.call(line)
50
- buffer << line
51
- end
52
- rescue IOError # rubocop:disable Lint/SuppressedException
42
+ while (line = source.gets)
43
+ on_data&.call(line)
44
+ buffer << line
53
45
  end
46
+ rescue IOError # rubocop:disable Lint/SuppressedException
54
47
  end
55
48
  end
56
49
 
@@ -6,12 +6,7 @@ module Tomo
6
6
  module SSH
7
7
  class Connection
8
8
  def self.dry_run(host, options)
9
- new(
10
- host,
11
- options,
12
- exec_proc: proc { CLI.exit },
13
- child_proc: proc { Result.empty_success }
14
- )
9
+ new(host, options, exec_proc: proc { CLI.exit }, child_proc: proc { Result.empty_success })
15
10
  end
16
11
 
17
12
  attr_reader :host
@@ -38,9 +33,7 @@ module Tomo
38
33
  result = child_proc.call(*ssh_args, on_data: handle_data)
39
34
  logger.script_end(script, result)
40
35
 
41
- if result.failure? && script.raise_on_error?
42
- raise_run_error(script, ssh_args, result)
43
- end
36
+ raise_run_error(script, ssh_args, result) if result.failure? && script.raise_on_error?
44
37
 
45
38
  result
46
39
  end
@@ -69,13 +62,7 @@ module Tomo
69
62
  end
70
63
 
71
64
  def raise_run_error(script, ssh_args, result)
72
- ScriptError.raise_with(
73
- result.output,
74
- host: host,
75
- result: result,
76
- script: script,
77
- ssh_args: ssh_args
78
- )
65
+ ScriptError.raise_with(result.output, host: host, result: result, script: script, ssh_args: ssh_args)
79
66
  end
80
67
  end
81
68
  end
@@ -27,10 +27,7 @@ module Tomo
27
27
  end
28
28
 
29
29
  def assert_valid_connection!
30
- script = Script.new(
31
- "echo hi",
32
- silent: !Tomo.debug?, echo: false, raise_on_error: false
33
- )
30
+ script = Script.new("echo hi", silent: !Tomo.debug?, echo: false, raise_on_error: false)
34
31
  res = connection.ssh_subprocess(script, verbose: Tomo.debug?)
35
32
  raise_connection_failure(res) if res.exit_status == 255
36
33
  raise_unknown_error(res) if res.failure? || res.stdout.chomp != "hi"
@@ -7,8 +7,7 @@ module Tomo
7
7
  hint = if executable.to_s.include?("/")
8
8
  "Is the ssh binary properly installed in this location?"
9
9
  else
10
- "Is #{yellow(executable)} installed and in your "\
11
- "#{blue('$PATH')}?"
10
+ "Is #{yellow(executable)} installed and in your #{blue('$PATH')}?"
12
11
  end
13
12
 
14
13
  <<~ERROR
@@ -20,8 +20,7 @@ module Tomo
20
20
  freeze
21
21
  end
22
22
 
23
- # rubocop:disable Metrics/AbcSize
24
- def build_args(host, script, control_path, verbose)
23
+ def build_args(host, script, control_path, verbose) # rubocop:disable Metrics/AbcSize
25
24
  args = [verbose ? "-v" : ["-o", "LogLevel=ERROR"]]
26
25
  args << "-A" if forward_agent
27
26
  args << connect_timeout_option
@@ -34,13 +33,11 @@ module Tomo
34
33
 
35
34
  [executable, args, script.to_s].flatten
36
35
  end
37
- # rubocop:enable Metrics/AbcSize
38
36
 
39
37
  private
40
38
 
41
39
  attr_writer :executable
42
- attr_accessor :connect_timeout, :extra_opts, :forward_agent,
43
- :reuse_connections, :strict_host_key_checking
40
+ attr_accessor :connect_timeout, :extra_opts, :forward_agent, :reuse_connections, :strict_host_key_checking
44
41
 
45
42
  def control_opts(path, verbose)
46
43
  opts = [
@@ -9,11 +9,7 @@ module Tomo
9
9
  def_delegators :context, :paths, :settings
10
10
 
11
11
  def die(reason)
12
- Runtime::TaskAbortedError.raise_with(
13
- reason,
14
- task: context.current_task,
15
- host: remote.host
16
- )
12
+ Runtime::TaskAbortedError.raise_with(reason, task: context.current_task, host: remote.host)
17
13
  end
18
14
 
19
15
  def dry_run?
@@ -26,13 +22,9 @@ module Tomo
26
22
 
27
23
  def merge_template(path)
28
24
  working_path = paths.tomo_config_file&.dirname
29
- if working_path && path.start_with?(".")
30
- path = File.expand_path(path, working_path)
31
- end
25
+ path = File.expand_path(path, working_path) if working_path && path.start_with?(".")
32
26
 
33
- unless File.file?(path)
34
- Runtime::TemplateNotFoundError.raise_with(path: path)
35
- end
27
+ Runtime::TemplateNotFoundError.raise_with(path: path) unless File.file?(path)
36
28
  template = IO.read(path)
37
29
  ERB.new(template).result(binding)
38
30
  end
@@ -49,10 +41,7 @@ module Tomo
49
41
  missing = names.flatten.select { |sett| settings[sett].nil? }
50
42
  return if missing.empty?
51
43
 
52
- Runtime::SettingsRequiredError.raise_with(
53
- settings: missing,
54
- task: context.current_task
55
- )
44
+ Runtime::SettingsRequiredError.raise_with(settings: missing, task: context.current_task)
56
45
  end
57
46
  alias require_settings require_setting
58
47
  end
@@ -6,14 +6,12 @@ module Tomo
6
6
  autoload :CLITester, "tomo/testing/cli_tester"
7
7
  autoload :Connection, "tomo/testing/connection"
8
8
  autoload :DockerImage, "tomo/testing/docker_image"
9
- autoload :DockerPluginTester, "tomo/testing/docker_plugin_tester"
10
9
  autoload :HostExtensions, "tomo/testing/host_extensions"
11
10
  autoload :Local, "tomo/testing/local"
12
11
  autoload :LogCapturing, "tomo/testing/log_capturing"
13
12
  autoload :MockedExecError, "tomo/testing/mocked_exec_error"
14
13
  autoload :MockedExitError, "tomo/testing/mocked_exit_error"
15
14
  autoload :MockPluginTester, "tomo/testing/mock_plugin_tester"
16
- autoload :PluginTester, "tomo/testing/plugin_tester"
17
15
  autoload :RemoteExtensions, "tomo/testing/remote_extensions"
18
16
  autoload :SSHExtensions, "tomo/testing/ssh_extensions"
19
17
 
@@ -1,10 +1,8 @@
1
- FROM ubuntu:18.04
1
+ FROM ubuntu:20.04
2
2
  WORKDIR /provision
3
3
  COPY ./tomo_test_ed25519.pub /root/.ssh/authorized_keys
4
4
  COPY ./ubuntu_setup.sh ./
5
5
  RUN ./ubuntu_setup.sh
6
- COPY ./custom_setup.sh ./
7
- RUN ./custom_setup.sh
8
6
  COPY ./systemctl.rb /usr/local/bin/systemctl
9
7
  RUN chmod a+x /usr/local/bin/systemctl
10
8
  EXPOSE 22
@@ -2,12 +2,7 @@ module Tomo
2
2
  module Testing
3
3
  class Connection < Tomo::SSH::Connection
4
4
  def initialize(host, options)
5
- super(
6
- host,
7
- options,
8
- exec_proc: proc { raise MockedExecError },
9
- child_proc: method(:mock_child_process)
10
- )
5
+ super(host, options, exec_proc: proc { raise MockedExecError }, child_proc: method(:mock_child_process))
11
6
  end
12
7
 
13
8
  def ssh_exec(script)
@@ -22,13 +22,8 @@ module Tomo
22
22
  end
23
23
  @running_images = []
24
24
 
25
- attr_accessor :setup_script
26
25
  attr_reader :host
27
26
 
28
- def initialize
29
- @setup_script = "#!/bin/bash\n"
30
- end
31
-
32
27
  def build_and_run
33
28
  raise "Already running!" if frozen?
34
29
 
@@ -74,19 +69,13 @@ module Tomo
74
69
  attr_reader :container_id, :image_id, :private_key_path
75
70
 
76
71
  def pull_base_image_if_needed
77
- images = Local.capture('docker images --format "{{.ID}}" ubuntu:18.04')
78
- Local.capture("docker pull ubuntu:18.04") if images.strip.empty?
72
+ images = Local.capture('docker images --format "{{.ID}}" ubuntu:20.04')
73
+ Local.capture("docker pull ubuntu:20.04") if images.strip.empty?
79
74
  end
80
75
 
81
76
  def set_up_private_key
82
- @private_key_path = File.join(
83
- Dir.tmpdir,
84
- "tomo_test_ed25519_#{SecureRandom.hex(8)}"
85
- )
86
- FileUtils.cp(
87
- File.expand_path("tomo_test_ed25519", __dir__),
88
- private_key_path
89
- )
77
+ @private_key_path = File.join(Dir.tmpdir, "tomo_test_ed25519_#{SecureRandom.hex(8)}")
78
+ FileUtils.cp(File.expand_path("tomo_test_ed25519", __dir__), private_key_path)
90
79
  FileUtils.chmod(0o600, private_key_path)
91
80
  end
92
81
 
@@ -121,8 +110,6 @@ module Tomo
121
110
  FILES_TO_COPY.each do |file|
122
111
  FileUtils.cp(File.expand_path(file, __dir__), build_dir)
123
112
  end
124
- IO.write(File.join(build_dir, "custom_setup.sh"), setup_script)
125
- FileUtils.chmod(0o755, File.join(build_dir, "custom_setup.sh"))
126
113
  end
127
114
 
128
115
  def build_dir
@@ -41,9 +41,7 @@ module Tomo
41
41
  progress(command_str) do
42
42
  output, status = Open3.capture2e(*command)
43
43
 
44
- if raise_on_error && !status.success?
45
- raise "Command failed: #{command_str}\n#{output}"
46
- end
44
+ raise "Command failed: #{command_str}\n#{output}" if raise_on_error && !status.success?
47
45
 
48
46
  output
49
47
  end
@@ -1,10 +1,29 @@
1
1
  module Tomo
2
2
  module Testing
3
- class MockPluginTester < PluginTester
3
+ class MockPluginTester
4
+ include LogCapturing
5
+
4
6
  def initialize(*plugin_names, settings: {}, release: {})
5
- host = Host.parse("testing@host")
6
- host.release.merge!(release)
7
- super(*plugin_names, settings: settings, host: host)
7
+ @host = Host.parse("testing@host")
8
+ @host.release.merge!(release)
9
+ config = Configuration.new
10
+ config.hosts << @host
11
+ config.plugins.push(*plugin_names, "testing")
12
+ config.settings[:application] = "testing"
13
+ config.settings.merge!(settings)
14
+ @runtime = config.build_runtime
15
+ end
16
+
17
+ def call_helper(helper, *args, **kwargs)
18
+ run_task("testing:call_helper", helper, args, kwargs)
19
+ host.helper_values.pop
20
+ end
21
+
22
+ def run_task(task, *args)
23
+ capturing_logger_output do
24
+ runtime.run!(task, *args, privileged: false)
25
+ nil
26
+ end
8
27
  end
9
28
 
10
29
  def executed_script
@@ -21,6 +40,10 @@ module Tomo
21
40
  host.mock(script, **kwargs)
22
41
  self
23
42
  end
43
+
44
+ private
45
+
46
+ attr_reader :host, :runtime
24
47
  end
25
48
  end
26
49
  end
@@ -9,7 +9,7 @@ module Tomo
9
9
  end
10
10
 
11
11
  def success?
12
- status == true || status == 0 # rubocop:disable Style/NumericPredicate
12
+ status == true || status == 0
13
13
  end
14
14
  end
15
15
  end
@@ -17,8 +17,7 @@ touch /var/lib/systemd/linger/deployer
17
17
 
18
18
  # Packages needed for ruby, etc.
19
19
  apt-get -y update
20
- apt-get -y install build-essential zlib1g-dev libssl-dev libreadline-dev \
21
- git-core curl locales libsqlite3-dev
20
+ apt-get -y install build-essential zlib1g-dev libssl-dev libreadline-dev git-core curl locales libsqlite3-dev
22
21
 
23
22
  apt-get -y install tzdata \
24
23
  -o DPkg::options::="--force-confdef" \
@@ -1,3 +1,3 @@
1
1
  module Tomo
2
- VERSION = "0.18.0".freeze
2
+ VERSION = "1.1.2".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tomo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Brictson
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-01-23 00:00:00.000000000 Z
11
+ date: 2020-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.4'
69
- - !ruby/object:Gem::Dependency
70
- name: minitest-hooks
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.5'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.5'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: minitest-reporters
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -114,42 +100,42 @@ dependencies:
114
100
  requirements:
115
101
  - - '='
116
102
  - !ruby/object:Gem::Version
117
- version: 0.79.0
103
+ version: 0.85.1
118
104
  type: :development
119
105
  prerelease: false
120
106
  version_requirements: !ruby/object:Gem::Requirement
121
107
  requirements:
122
108
  - - '='
123
109
  - !ruby/object:Gem::Version
124
- version: 0.79.0
110
+ version: 0.85.1
125
111
  - !ruby/object:Gem::Dependency
126
112
  name: rubocop-minitest
127
113
  requirement: !ruby/object:Gem::Requirement
128
114
  requirements:
129
115
  - - '='
130
116
  - !ruby/object:Gem::Version
131
- version: 0.5.1
117
+ version: 0.9.0
132
118
  type: :development
133
119
  prerelease: false
134
120
  version_requirements: !ruby/object:Gem::Requirement
135
121
  requirements:
136
122
  - - '='
137
123
  - !ruby/object:Gem::Version
138
- version: 0.5.1
124
+ version: 0.9.0
139
125
  - !ruby/object:Gem::Dependency
140
126
  name: rubocop-performance
141
127
  requirement: !ruby/object:Gem::Requirement
142
128
  requirements:
143
129
  - - '='
144
130
  - !ruby/object:Gem::Version
145
- version: 1.5.2
131
+ version: 1.6.1
146
132
  type: :development
147
133
  prerelease: false
148
134
  version_requirements: !ruby/object:Gem::Requirement
149
135
  requirements:
150
136
  - - '='
151
137
  - !ruby/object:Gem::Version
152
- version: 1.5.2
138
+ version: 1.6.1
153
139
  description: Tomo is a feature-rich deployment tool that contains everything you need
154
140
  to deploy a basic Rails app out of the box. It has an opinionated, production-tested
155
141
  set of defaults, but is easily extensible via a well-documented plugin system. Unlike
@@ -292,14 +278,12 @@ files:
292
278
  - lib/tomo/testing/cli_tester.rb
293
279
  - lib/tomo/testing/connection.rb
294
280
  - lib/tomo/testing/docker_image.rb
295
- - lib/tomo/testing/docker_plugin_tester.rb
296
281
  - lib/tomo/testing/host_extensions.rb
297
282
  - lib/tomo/testing/local.rb
298
283
  - lib/tomo/testing/log_capturing.rb
299
284
  - lib/tomo/testing/mock_plugin_tester.rb
300
285
  - lib/tomo/testing/mocked_exec_error.rb
301
286
  - lib/tomo/testing/mocked_exit_error.rb
302
- - lib/tomo/testing/plugin_tester.rb
303
287
  - lib/tomo/testing/remote_extensions.rb
304
288
  - lib/tomo/testing/ssh_extensions.rb
305
289
  - lib/tomo/testing/systemctl.rb
@@ -316,7 +300,7 @@ metadata:
316
300
  source_code_uri: https://github.com/mattbrictson/tomo
317
301
  homepage_uri: https://tomo-deploy.com/
318
302
  documentation_uri: https://tomo-deploy.com/
319
- post_install_message:
303
+ post_install_message:
320
304
  rdoc_options: []
321
305
  require_paths:
322
306
  - lib
@@ -324,15 +308,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
324
308
  requirements:
325
309
  - - ">="
326
310
  - !ruby/object:Gem::Version
327
- version: 2.4.0
311
+ version: 2.5.0
328
312
  required_rubygems_version: !ruby/object:Gem::Requirement
329
313
  requirements:
330
314
  - - ">="
331
315
  - !ruby/object:Gem::Version
332
316
  version: '0'
333
317
  requirements: []
334
- rubygems_version: 3.1.2
335
- signing_key:
318
+ rubygems_version: 3.1.4
319
+ signing_key:
336
320
  specification_version: 4
337
321
  summary: A friendly CLI for deploying Rails apps ✨
338
322
  test_files: []