discharger 0.2.10 → 0.2.12

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -26
  3. data/Rakefile +3 -1
  4. data/lib/discharger/setup_runner/command_factory.rb +56 -0
  5. data/lib/discharger/setup_runner/command_registry.rb +62 -0
  6. data/lib/discharger/setup_runner/commands/asdf_command.rb +61 -0
  7. data/lib/discharger/setup_runner/commands/base_command.rb +185 -0
  8. data/lib/discharger/setup_runner/commands/brew_command.rb +26 -0
  9. data/lib/discharger/setup_runner/commands/bundler_command.rb +25 -0
  10. data/lib/discharger/setup_runner/commands/config_command.rb +51 -0
  11. data/lib/discharger/setup_runner/commands/custom_command.rb +41 -0
  12. data/lib/discharger/setup_runner/commands/database_command.rb +111 -0
  13. data/lib/discharger/setup_runner/commands/docker_command.rb +100 -0
  14. data/lib/discharger/setup_runner/commands/env_command.rb +42 -0
  15. data/lib/discharger/setup_runner/commands/git_command.rb +45 -0
  16. data/lib/discharger/setup_runner/commands/yarn_command.rb +45 -0
  17. data/lib/discharger/setup_runner/condition_evaluator.rb +130 -0
  18. data/lib/discharger/setup_runner/configuration.rb +71 -0
  19. data/lib/discharger/setup_runner/runner.rb +111 -0
  20. data/lib/discharger/setup_runner/version.rb +7 -0
  21. data/lib/discharger/setup_runner.rb +55 -0
  22. data/lib/discharger/task.rb +2 -0
  23. data/lib/discharger/version.rb +1 -1
  24. data/lib/discharger.rb +1 -0
  25. data/lib/generators/discharger/install/install_generator.rb +14 -0
  26. data/lib/generators/discharger/install/templates/setup +36 -0
  27. data/lib/generators/discharger/install/templates/setup.yml +60 -0
  28. metadata +47 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 123fc77a1effa1f7cd70e68b9056cfb9d9491069edafca3939b40c092cf580fd
4
- data.tar.gz: 9800b64540311c15e0805d835e6f137e39b5751a932ee3106406743b21353d8e
3
+ metadata.gz: 0556c953dac93a704d652f69824bd24da384f42c204487340512beafde360f95
4
+ data.tar.gz: f3224ed2f8bc399bc7a9e33014334843058a4e44540fc8a14e34fa47091611d5
5
5
  SHA512:
6
- metadata.gz: f840bfaf4c5db5c4321263d968d7fda42388d978fa49571bf78be219b64afcd52b9e06e870fe6d33fdd54b417104d1646dbbed4bf77ab42c154ff851d68da96c
7
- data.tar.gz: 6dc97ddbfe29459973e3d2eaee4fa299c644112b5d0a3c3c65d566015c7018ba3ae4c0da0265781fc3d6785c0bbfe38bca0bdacbede5584646cafdaa09dff8bc
6
+ metadata.gz: 33ace7f51a07a0c126ba03285cde6695bdb5b38a84b796ca1588a16695dac532f9222340ca683ed5b2b7ae5e61d1abc3704f444db15ee2ba73a60f6e2e95f6a4
7
+ data.tar.gz: f0c3821be4631d3a804a71f68df53a56b7788f7d56598361d540027b7118f79466ba8e82a082ddac1bd2c124c8e4002f37e5f5cb3aa8da62ffbe7a4da97ee842
data/README.md CHANGED
@@ -1,26 +1,80 @@
1
1
  # Discharger
2
- Code supporting tasks that discharge code for deployment.
3
2
 
4
- ## Usage
3
+ A Ruby gem that provides Rake tasks for managing code deployment workflows with automated versioning, changelog management, and Slack notifications.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "discharger"
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ $ bundle install
16
+ ```
17
+
18
+ Then run the install generator:
19
+ ```bash
20
+ $ rails generate discharger:install
21
+ ```
5
22
 
6
- Add `require "discharger/task"` to your Rakefile.
23
+ Or install it yourself as:
24
+ ```bash
25
+ $ gem install discharger
26
+ ```
7
27
 
8
- Then build the discharger task
28
+ ## Usage
29
+
30
+ Add `require "discharger/task"` to your Rakefile, then configure the discharger task:
9
31
 
10
32
  ```ruby
11
33
  require "discharger/task"
12
34
 
13
35
  Discharger::Task.create do |task|
36
+ # Version management
14
37
  task.version_file = "config/application.rb"
15
- task.release_message_channel = "#some-slack-channel"
16
38
  task.version_constant = "MyApp::VERSION"
17
- task.app_name = "My App name"
39
+
40
+ # Slack integration
41
+ task.release_message_channel = "#some-slack-channel"
42
+ task.app_name = "My App Name"
43
+
44
+ # Git integration
18
45
  task.commit_identifier = -> { `git rev-parse HEAD`.strip }
19
46
  task.pull_request_url = "https://github.com/SOFware/some-app"
47
+
48
+ # Changelog management (optional)
49
+ task.fragment_directory = "changelog.d" # Directory for changelog fragments
50
+ end
51
+ ```
52
+
53
+ ### Changelog Fragments
54
+
55
+ Discharger supports changelog fragment management through the `fragment_directory` setting. When set, Discharger will look for changelog fragments in the specified directory and automatically combine them into the main changelog during releases.
56
+
57
+ ```ruby
58
+ Discharger::Task.create do |task|
59
+ # ... other configuration ...
60
+ task.fragment_directory = "changelog.d" # Default: nil (disabled)
20
61
  end
21
62
  ```
22
63
 
23
- It will make Rake tasks available to push code to branches and notify Slack channels.
64
+ With fragments enabled, you can create individual changelog files in the `changelog.d/` directory:
65
+
66
+ ```
67
+ changelog.d/
68
+ ├── 123-fix-login-bug.md
69
+ ├── 124-add-user-profile.md
70
+ └── 125-update-dependencies.md
71
+ ```
72
+
73
+ Each fragment file should contain the changelog entry for a specific change or feature.
74
+
75
+ ## Available Tasks
76
+
77
+ The gem creates several Rake tasks for managing your deployment workflow:
24
78
 
25
79
  ```bash
26
80
  $ rake -T release
@@ -32,33 +86,28 @@ rake release:slack[text,channel,emoji] # Send a message to Slack
32
86
  rake release:stage # ---------- STEP 2 ----------
33
87
  ```
34
88
 
35
- ## Installation
36
- Add this line to your application's Gemfile:
89
+ ### Workflow Steps
37
90
 
38
- ```ruby
39
- gem "discharger"
40
- ```
91
+ 1. **Prepare** (`rake release:prepare`): Create a new branch to prepare the release, update the changelog, and bump the version
92
+ 2. **Stage** (`rake release:stage`): Update the staging branch and create a PR to production
93
+ 3. **Release** (`rake release`): Release the current version to production by tagging and pushing to the production branch
41
94
 
42
- And then execute:
43
- ```bash
44
- $ bundle
45
- ```
95
+ ## Contributing
46
96
 
47
- Then run the install generator:
48
- ```bash
49
- $ rails generate discharger:install
50
- ```
97
+ This gem is managed with [Reissue](https://github.com/SOFware/reissue).
51
98
 
52
- Or install it yourself as:
53
- ```bash
54
- $ gem install discharger
55
- ```
99
+ ### Releasing
56
100
 
57
- ## Contributing
101
+ Releases are automated via GitHub Actions:
58
102
 
59
- This gem is managed with [Reissue](https://github.com/SOFware/reissue).
103
+ 1. Go to Actions "Prepare Release" → Run workflow
104
+ 2. Select version type (major, minor, patch, or custom)
105
+ 3. Review the created PR with version bumps and changelog updates
106
+ 4. Add the `approved-release` label and merge
107
+ 5. The gem will be automatically published to RubyGems.org
60
108
 
61
109
  Bug reports and pull requests are welcome on GitHub.
62
110
 
63
111
  ## License
112
+
64
113
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -7,7 +7,9 @@ require "reissue/gem"
7
7
 
8
8
  Reissue::Task.create :reissue do |task|
9
9
  task.version_file = "lib/discharger/version.rb"
10
- task.commit = true
10
+ task.commit = !ENV["GITHUB_ACTIONS"]
11
+ task.commit_finalize = !ENV["GITHUB_ACTIONS"]
12
+ task.push_finalize = :branch
11
13
  end
12
14
 
13
15
  Rake::TestTask.new(:test) do |t|
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command_registry"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ class CommandFactory
8
+ attr_reader :config, :app_root, :logger
9
+
10
+ def initialize(config, app_root, logger)
11
+ @config = config
12
+ @app_root = app_root
13
+ @logger = logger
14
+ end
15
+
16
+ def create_command(name)
17
+ command_class = CommandRegistry.get(name)
18
+ return nil unless command_class
19
+
20
+ command_class.new(config, app_root, logger)
21
+ rescue => e
22
+ logger&.warn "Failed to create command #{name}: #{e.message}"
23
+ nil
24
+ end
25
+
26
+ def create_all_commands
27
+ commands = []
28
+
29
+ # Create built-in commands from steps
30
+ if config.steps.any?
31
+ config.steps.each do |step|
32
+ command = create_command(step)
33
+ commands << command if command
34
+ end
35
+ else
36
+ # If no steps specified, create all registered commands
37
+ CommandRegistry.names.each do |name|
38
+ command = create_command(name)
39
+ commands << command if command
40
+ end
41
+ end
42
+
43
+ # Create custom commands
44
+ if config.respond_to?(:custom_steps) && config.custom_steps.any?
45
+ require_relative "commands/custom_command"
46
+ config.custom_steps.each do |step_config|
47
+ command = Commands::CustomCommand.new(config, app_root, logger, step_config)
48
+ commands << command
49
+ end
50
+ end
51
+
52
+ commands
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discharger
4
+ module SetupRunner
5
+ class CommandRegistry
6
+ class << self
7
+ def register(name, command_class)
8
+ commands[name.to_s] = command_class
9
+ end
10
+
11
+ def get(name)
12
+ commands[name.to_s]
13
+ end
14
+
15
+ def all
16
+ commands.values
17
+ end
18
+
19
+ def names
20
+ commands.keys
21
+ end
22
+
23
+ def clear
24
+ commands.clear
25
+ end
26
+
27
+ def load_commands
28
+ # Load base command first
29
+ require_relative "commands/base_command"
30
+
31
+ # Load all command files from the commands directory
32
+ commands_dir = File.expand_path("commands", __dir__)
33
+ Dir.glob(File.join(commands_dir, "*_command.rb")).each do |file|
34
+ require file
35
+ end
36
+
37
+ # Auto-register commands based on naming convention
38
+ Commands.constants.each do |const_name|
39
+ next unless const_name.to_s.end_with?("Command")
40
+
41
+ command_class = Commands.const_get(const_name)
42
+ next unless command_class < Commands::BaseCommand
43
+ next if command_class == Commands::BaseCommand
44
+
45
+ # Convert class name to command name (e.g., AsdfCommand -> asdf)
46
+ command_name = const_name.to_s.sub(/Command$/, "").gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
47
+ register(command_name, command_class)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def commands
54
+ @commands ||= {}
55
+ end
56
+ end
57
+
58
+ # Load and register all built-in commands
59
+ load_commands
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module Commands
8
+ class AsdfCommand < BaseCommand
9
+ def execute
10
+ log "Install tool-versions dependencies via ASDF"
11
+
12
+ unless system_quiet("which asdf")
13
+ log "asdf not installed. Run `brew install asdf` if you want bin/setup to ensure versions are up-to-date"
14
+ return
15
+ end
16
+
17
+ tools_versions_file = File.join(app_root, ".tool-versions")
18
+ return log("No .tool-versions file found") unless File.exist?(tools_versions_file)
19
+
20
+ dependencies = File.read(tools_versions_file).split("\n")
21
+ installables = []
22
+
23
+ # Check for nodejs plugin
24
+ unless system_quiet("asdf plugin list | grep nodejs")
25
+ node_deps = dependencies.select { |item| item.match?(/node/) }
26
+ if node_deps.any?
27
+ ask_to_install "asdf to manage Node JS" do
28
+ installables.concat(node_deps)
29
+ system! "asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git"
30
+ end
31
+ end
32
+ end
33
+
34
+ # Check for ruby plugin
35
+ unless system_quiet("asdf plugin list | grep ruby")
36
+ ruby_deps = dependencies.select { |item| item.match?(/ruby/) }
37
+ if ruby_deps.any?
38
+ ask_to_install "asdf to manage Ruby" do
39
+ installables.concat(ruby_deps)
40
+ system! "asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git"
41
+ end
42
+ end
43
+ end
44
+
45
+ # Install all versions
46
+ installables.each do |name_version|
47
+ system! "asdf install #{name_version}"
48
+ end
49
+ end
50
+
51
+ def can_execute?
52
+ File.exist?(File.join(app_root, ".tool-versions"))
53
+ end
54
+
55
+ def description
56
+ "Install tool versions with asdf"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discharger
4
+ module SetupRunner
5
+ module Commands
6
+ class BaseCommand
7
+ attr_reader :config, :app_root, :logger
8
+
9
+ def initialize(config, app_root, logger)
10
+ @config = config
11
+ @app_root = app_root
12
+ @logger = logger
13
+ end
14
+
15
+ def execute
16
+ raise NotImplementedError, "#{self.class} must implement #execute"
17
+ end
18
+
19
+ def can_execute?
20
+ true
21
+ end
22
+
23
+ def description
24
+ class_name = self.class.name || "AnonymousCommand"
25
+ class_name.demodulize.underscore.humanize
26
+ end
27
+
28
+ protected
29
+
30
+ def log(message, emoji: nil)
31
+ return unless logger
32
+ class_name = self.class.name || "AnonymousCommand"
33
+ prefix = emoji ? "#{emoji} " : ""
34
+ logger.info "#{prefix}[#{class_name.demodulize}] #{message}"
35
+ end
36
+
37
+ def with_spinner(message)
38
+ if ENV["CI"] || ENV["NO_SPINNER"] || !$stdout.tty? || ENV["QUIET_SETUP"]
39
+ result = yield
40
+ # Handle error case when spinner is disabled
41
+ if result.is_a?(Hash) && !result[:success] && result[:raise_error] != false
42
+ raise result[:error]
43
+ end
44
+ return result
45
+ end
46
+
47
+ require "rainbow"
48
+ spinner_chars = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏]
49
+ spinner_thread = nil
50
+ stop_spinner = false
51
+
52
+ begin
53
+ # Print initial message
54
+ print Rainbow("◯ #{message}").cyan
55
+ $stdout.flush
56
+
57
+ # Start spinner in background thread
58
+ spinner_thread = Thread.new do
59
+ i = 0
60
+ until stop_spinner
61
+ print "\r#{Rainbow(spinner_chars[i % spinner_chars.length]).cyan} #{Rainbow(message).cyan}"
62
+ $stdout.flush
63
+ sleep 0.1
64
+ i += 1
65
+ end
66
+ end
67
+
68
+ # Execute the block
69
+ result = yield
70
+
71
+ # Stop spinner
72
+ stop_spinner = true
73
+ spinner_thread&.join(0.1)
74
+
75
+ # Clear line and print result
76
+ if result.is_a?(Hash)
77
+ if result[:success]
78
+ puts "\r#{Rainbow(result[:message] || "✓").green} #{message}"
79
+ else
80
+ puts "\r#{Rainbow(result[:message] || "✗").red} #{message}"
81
+ raise result[:error] if result[:error] && result[:raise_error] != false
82
+ end
83
+ else
84
+ puts "\r#{Rainbow("✓").green} #{message}"
85
+ end
86
+
87
+ result
88
+ rescue
89
+ stop_spinner = true
90
+ spinner_thread&.join(0.1)
91
+ puts "\r#{Rainbow("✗").red} #{message}"
92
+ raise
93
+ ensure
94
+ stop_spinner = true
95
+ spinner_thread&.kill if spinner_thread&.alive?
96
+ end
97
+ end
98
+
99
+ def simple_action(message)
100
+ return yield if ENV["CI"] || ENV["NO_SPINNER"] || !$stdout.tty? || ENV["QUIET_SETUP"]
101
+
102
+ require "rainbow"
103
+ print Rainbow(" → #{message}...").cyan
104
+ $stdout.flush
105
+
106
+ begin
107
+ result = yield
108
+ puts Rainbow(" ✓").green
109
+ result
110
+ rescue
111
+ puts Rainbow(" ✗").red
112
+ raise
113
+ end
114
+ end
115
+
116
+ def system!(*args)
117
+ require "open3"
118
+ command_str = args.join(" ")
119
+
120
+ # Create a more readable message for the spinner
121
+ spinner_message = if command_str.length > 80
122
+ if args.first.is_a?(Hash)
123
+ # Skip env hash in display
124
+ cmd_args = args[1..]
125
+ base_cmd = cmd_args.take(3).join(" ")
126
+ else
127
+ base_cmd = args.take(3).join(" ")
128
+ end
129
+ "Executing #{base_cmd}..."
130
+ else
131
+ "Executing #{command_str}"
132
+ end
133
+
134
+ result = with_spinner(spinner_message) do
135
+ stdout, stderr, status = Open3.capture3(*args)
136
+
137
+ if status.success?
138
+ # Log output if there is any (for debugging)
139
+ logger&.debug("Output: #{stdout}") if stdout && !stdout.empty?
140
+ {success: true, message: "✓"}
141
+ elsif args.first.to_s.include?("docker")
142
+ logger&.debug("Error: #{stderr}") if stderr && !stderr.empty?
143
+ {success: false, message: "✗ (Docker command failed)", raise_error: false}
144
+ else
145
+ {success: false, message: "✗", error: "#{command_str} failed: #{stderr}"}
146
+ end
147
+ end
148
+
149
+ # Handle the case when spinner is disabled
150
+ if result.is_a?(Hash) && !result[:success] && result[:raise_error] != false
151
+ raise result[:error]
152
+ end
153
+
154
+ result
155
+ end
156
+
157
+ def system_quiet(*args)
158
+ require "open3"
159
+ stdout, _stderr, status = Open3.capture3(*args)
160
+ logger&.debug("Quietly executed #{args.join(" ")} - success: #{status.success?}")
161
+ logger&.debug("Output: #{stdout}") if stdout && !stdout.empty? && logger
162
+ status.success?
163
+ end
164
+
165
+ def ask_to_install(description)
166
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
167
+ puts "You do not currently use #{description}.\n ===> If you want to, type Y\nOtherwise hit any key to ignore."
168
+ end
169
+ if gets.chomp == "Y"
170
+ yield
171
+ end
172
+ end
173
+
174
+ def proceed_with(task)
175
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
176
+ puts "Proceed with #{task}?\n ===> Type Y to proceed\nOtherwise hit any key to ignore."
177
+ end
178
+ if gets.chomp == "Y"
179
+ yield
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module Commands
8
+ class BrewCommand < BaseCommand
9
+ def execute
10
+ proceed_with "brew bundle" do
11
+ log "Ensuring brew dependencies"
12
+ system! "brew bundle"
13
+ end
14
+ end
15
+
16
+ def can_execute?
17
+ File.exist?("Brewfile")
18
+ end
19
+
20
+ def description
21
+ "Install Homebrew dependencies"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module Commands
8
+ class BundlerCommand < BaseCommand
9
+ def execute
10
+ log "Installing dependencies"
11
+ system! "gem install bundler --conservative"
12
+ system_quiet("bundle check") || system!("bundle install")
13
+ end
14
+
15
+ def can_execute?
16
+ File.exist?("Gemfile")
17
+ end
18
+
19
+ def description
20
+ "Install Ruby dependencies"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+ require "fileutils"
5
+
6
+ module Discharger
7
+ module SetupRunner
8
+ module Commands
9
+ class ConfigCommand < BaseCommand
10
+ def execute
11
+ log "Ensuring configuration files are present"
12
+
13
+ # Copy database.yml if needed
14
+ database_yml = File.join(app_root, "config/database.yml")
15
+ database_yml_example = File.join(app_root, "config/database.yml.example")
16
+
17
+ if !File.exist?(database_yml) && File.exist?(database_yml_example)
18
+ FileUtils.cp(database_yml_example, database_yml)
19
+ log "Copied config/database.yml.example to config/database.yml"
20
+ end
21
+
22
+ # Copy Procfile.dev to Procfile if needed
23
+ procfile = File.join(app_root, "Procfile")
24
+ procfile_dev = File.join(app_root, "Procfile.dev")
25
+
26
+ if !File.exist?(procfile) && File.exist?(procfile_dev)
27
+ FileUtils.cp(procfile_dev, procfile)
28
+ log "Copied Procfile.dev to Procfile"
29
+ end
30
+
31
+ # Copy any other example config files
32
+ Dir.glob(File.join(app_root, "config/**/*.example")).each do |example_file|
33
+ config_file = example_file.sub(/\.example$/, "")
34
+ unless File.exist?(config_file)
35
+ FileUtils.cp(example_file, config_file)
36
+ log "Copied #{example_file.sub(app_root + "/", "")} to #{config_file.sub(app_root + "/", "")}"
37
+ end
38
+ end
39
+ end
40
+
41
+ def can_execute?
42
+ true
43
+ end
44
+
45
+ def description
46
+ "Setup configuration files"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../condition_evaluator"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module Commands
8
+ class CustomCommand < BaseCommand
9
+ attr_reader :step_config
10
+
11
+ def initialize(config, app_root, logger, step_config)
12
+ super(config, app_root, logger)
13
+ @step_config = step_config
14
+ end
15
+
16
+ def execute
17
+ command = step_config["command"]
18
+ description = step_config["description"] || command
19
+ condition = step_config["condition"]
20
+
21
+ # Check condition if provided using safe evaluator
22
+ if condition && !ConditionEvaluator.evaluate(condition)
23
+ log "Skipping #{description} (condition not met)"
24
+ return
25
+ end
26
+
27
+ log "Running: #{description}"
28
+ system!(command)
29
+ end
30
+
31
+ def can_execute?
32
+ step_config["command"].present?
33
+ end
34
+
35
+ def description
36
+ step_config["description"] || "Custom command: #{step_config["command"]}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end