cem_acpt 0.3.3-universal-java-17 → 0.3.5-universal-java-17

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.
@@ -2,9 +2,11 @@
2
2
 
3
3
  require 'concurrent-ruby'
4
4
  require 'English'
5
- require_relative '../logging'
6
5
  require_relative '../rspec_utils'
6
+ require_relative 'logging'
7
7
  require_relative 'runner_result'
8
+ require_relative 'runner_workflow_builder'
9
+ #require_relative 'workflow'
8
10
 
9
11
  module CemAcpt
10
12
  module TestRunner
@@ -28,7 +30,7 @@ module CemAcpt
28
30
  # reporting the results back to the main thread. Runner objects are created
29
31
  # by the RunHandler and then, when started, execute their logic in a thread.
30
32
  class Runner
31
- include CemAcpt::LoggingAsync
33
+ include CemAcpt::TestRunner::Logging
32
34
 
33
35
  attr_reader :node, :node_exists, :run_result
34
36
 
@@ -42,34 +44,34 @@ module CemAcpt
42
44
  @debug_mode = @context.config.debug_mode?
43
45
  @node_inventory = @context.node_inventory
44
46
  @module_pkg_path = @context.module_package_path
47
+ @provision_attempts = 0
48
+ @provision_start_time = nil
45
49
  @node_exists = false
46
50
  @run_result = CemAcpt::TestRunner::RunnerResult.new(@node, debug: @debug_mode)
47
- @completed_steps = []
51
+ @logger = use_logger(nil, stage: 'Runner', node: @node.node_name, test: @node.test_data[:test_name])
48
52
  validate!
49
53
  end
50
54
 
51
- def run_step(step_sym)
52
- send(step_sym)
53
- @completed_steps << step_sym
54
- rescue StandardError => e
55
- err = CemAcpt::TestRunner::RunnerStepError.new(step_sym, e)
56
- step_error_logging(err)
57
- @run_result.from_error(err)
58
- destroy unless step_sym == :destroy
59
- end
60
-
61
55
  # Executes test suite steps
62
56
  def start
63
- async_info("Starting test suite for #{@node.node_name}", log_prefix('RUNNER'))
64
- run_step(:provision)
65
- run_step(:bootstrap)
66
- run_step(:run_tests)
67
- run_step(:destroy)
68
- true
57
+ @logger.info('Starting test suite workflow...')
58
+ @workflow = new_workflow
59
+ @workflow.run
60
+ if @workflow.success?
61
+ @run_result = @workflow.last_result
62
+ @workflow.completed_steps.each do |s|
63
+ @logger.info("Step '#{s.name}' completed successfully")
64
+ end
65
+ true
66
+ else
67
+ @run_result = @workflow.last_error
68
+ step_error_logging(@workflow.last_error)
69
+ false
70
+ end
69
71
  rescue StandardError => e
70
- step_error_logging(e)
72
+ step_error_logging(e, :fatal)
73
+ @run_result = CemAcpt::TestRunner::RunnerResult.new(@node, debug: @debug_mode)
71
74
  @run_result.from_error(e)
72
- destroy
73
75
  end
74
76
 
75
77
  # Checks for failures in the test results.
@@ -81,104 +83,37 @@ module CemAcpt
81
83
 
82
84
  private
83
85
 
84
- def step_error_logging(err)
85
- prefix = err.respond_to?(:step) ? log_prefix(err.step.capitalize) : log_prefix('RUNNER')
86
- fatal_msg = ["runner failed: #{err.message}"]
87
- async_fatal(fatal_msg, prefix)
88
- async_debug("Completed steps: #{@completed_steps}", prefix)
89
- async_debug("Failed runner backtrace:\n#{err.backtrace.join("\n")}", prefix)
90
- async_debug("Failed runner test data: #{@node.test_data}", prefix)
86
+ # Builds a new workflow for the runner
87
+ # @return [CemAcpt::TestRunner::Workflow::Manager] the new workflow
88
+ def new_workflow
89
+ builder = RunnerWorkflowBuilder.new(@node, @context.config, @logger)
90
+ builder.add_provision
91
+ builder.add_sleep(time: 30)
92
+ builder.add_wait_for_node_ssh
93
+ builder.add_check_dnf_automatic
94
+ builder.add_check_rpm_db
95
+ builder.add_save_node_to_inventory(node_inventory: @node_inventory, platform: @platform)
96
+ builder.add_check_for_module_package_path(module_pkg_path: @module_pkg_path)
97
+ builder.add_install_module(module_pkg_path: @module_pkg_path)
98
+ builder.add_check_node_inventory_file(node_inventory: @node_inventory)
99
+ builder.add_run_tests(rspec_opts: rspec_opts, rspec_cmd: CemAcpt::RSpecUtils::Command, run_result: @run_result)
100
+ builder.add_clean_up
101
+ builder.workflow
91
102
  end
92
103
 
93
- def log_prefix(prefix)
94
- "#{prefix}: #{@node.test_data[:test_name]}:"
104
+ def step_error_logging(err, kind = :error)
105
+ msg = if err.respond_to?(:step)
106
+ "runner failed on step '#{err.step}': #{err.message}"
107
+ else
108
+ "runner failed: #{err.message}"
109
+ end
110
+ @logger.send(kind, msg)
111
+ @logger.debug("failed runner backtrace:\n#{err.backtrace.join("\n")}")
112
+ @logger.debug("failed runner test data: #{@node.test_data}")
95
113
  end
96
114
 
97
- # Provisions the node for the acceptance test suite.
98
- def provision
99
- async_info("Provisioning #{@node.node_name}...", log_prefix('PROVISION'))
100
- start_time = Time.now
101
- @node.provision
102
- @node_exists = true
103
- max_retries = 60 # equals 300 seconds because we check every five seconds
104
- until @node.ready?
105
- if max_retries <= 0
106
- async_fatal("Node #{@node.node_name} failed to provision", log_prefix('PROVISION'))
107
- raise CemAcpt::TestRunner::RunnerProvisionError, "Provisioning timed out for node #{@node.node_name}"
108
- end
109
-
110
- async_info("Waiting for #{@node.node_name} to be ready for remote connections...", log_prefix('PROVISION'))
111
- max_retries -= 1
112
- sleep(5)
113
- end
114
- async_info("Node #{@node.node_name} is ready...", log_prefix('PROVISION'))
115
- node_desc = {
116
- test_data: @node.test_data,
117
- platform: @platform,
118
- local_port: @node.local_port,
119
- }.merge(@node.node)
120
- @node_inventory.add(@node.node_name, node_desc)
121
- @node_inventory.save
122
- async_info("Node #{@node.node_name} provisioned in #{Time.now - start_time} seconds", log_prefix('PROVISION'))
123
- end
124
-
125
- # Bootstraps the node for the acceptance test suite. Currently, this
126
- # just uploads and installs the module package.
127
- def bootstrap
128
- async_info("Bootstrapping #{@node.node_name}...", log_prefix('BOOTSTRAP'))
129
- until File.exist?(@module_pkg_path)
130
- async_debug("Waiting for module package #{@module_pkg_path} to exist...", log_prefix('BOOTSTRAP'))
131
- sleep(1)
132
- end
133
- async_info("Installing module package #{@module_pkg_path}...", log_prefix('BOOTSTRAP'))
134
- @node.install_puppet_module_package(@module_pkg_path)
135
- end
136
-
137
- # Runs the acceptance test suite via rspec.
138
- def run_tests
139
- attempts = 0
140
- until File.exist?(@node_inventory.save_file_path)
141
- raise 'Node inventory file not found' if (attempts += 1) > 3
142
-
143
- sleep(1)
144
- end
145
- async_info("Running test #{@node.test_data[:test_name]} on node #{@node.node_name}...", log_prefix('RSPEC'))
146
- @node.run_tests do |cmd_env|
147
- cmd_opts = rspec_opts
148
- cmd_opts.env = cmd_opts.env.merge(cmd_env) if cmd_env
149
- # Documentation format gets logged in real time, JSON file is read after the fact
150
- begin
151
- @rspec_cmd = CemAcpt::RSpecUtils::Command.new(cmd_opts)
152
- @rspec_cmd.execute(pty: false, log_prefix: log_prefix('RSPEC'))
153
- @run_result.from_json_file(cmd_opts.format[:json])
154
- rescue Errno::EIO => e
155
- async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", log_prefix('RSPEC'))
156
- @run_result.from_error(e)
157
- rescue StandardError => e
158
- async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}", log_prefix('RSPEC'))
159
- async_debug("Backtrace:\n#{e.backtrace}", log_prefix('RSPEC'))
160
- @run_result.from_error(e)
161
- end
162
- end
163
- async_info("Tests completed with exit code: #{@run_result.exit_status}", log_prefix('RSPEC'))
164
- end
165
-
166
- # Destroys the node for the acceptance test suite.
167
- def destroy
168
- kill_spec_pty_if_exists
169
- if @context.config.get('no_destroy_nodes')
170
- async_info("Not destroying node #{@node.node_name} because 'no_destroy_nodes' is set to true",
171
- log_prefix('DESTROY'))
172
- else
173
- async_info("Destroying #{@node.node_name}...", log_prefix('DESTROY'))
174
- @node.destroy
175
- @node_exists = false
176
- async_info("Node #{@node.node_name} destroyed successfully", log_prefix('DESTROY'))
177
- end
178
- end
179
-
180
- def kill_spec_pty_if_exists
181
- @rspec_cmd&.kill_pty
115
+ def log_prefix(prefix)
116
+ "#{prefix}: #{@node.test_data[:test_name]}:"
182
117
  end
183
118
 
184
119
  # Validates the runner configuration.
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logging'
4
+ require_relative 'workflow'
5
+
6
+ module CemAcpt
7
+ module TestRunner
8
+ # RunnerWorkflowBuilder builds a workflow for a TestRunner
9
+ # @!attribute [r] workflow
10
+ # @return [CemAcpt::TestRunner::Workflow::Manager] Workflow object
11
+ class RunnerWorkflowBuilder
12
+ include CemAcpt::TestRunner::Logging
13
+
14
+ attr_reader :workflow
15
+
16
+ # @param node [CemAcpt::Platform::Base] Initialized node object
17
+ # @param config [Hash] Context config hash
18
+ # @param logger [CemAcpt::TestRunner::Logging::Logger] Logger object
19
+ def initialize(node, config, logger = nil)
20
+ @node = node
21
+ @logger = use_logger(logger, stage: 'Workflow')
22
+ @workflow = CemAcpt::TestRunner::Workflow::Manager.new(workflow_manager_opts(config, logger))
23
+ @config = config
24
+ end
25
+
26
+ def add_sleep(**kwargs)
27
+ opts = {
28
+ node: @node,
29
+ time: kwargs[:time] || 10,
30
+ retryable: false,
31
+ }
32
+ @workflow.add_step(:sleep, **opts) do |s|
33
+ s.logger.info("sleeping for #{s.opts[:time]} seconds")
34
+ sleep(s.opts[:time])
35
+ end
36
+ end
37
+
38
+ def add_provision(**kwargs)
39
+ opts = {
40
+ node: @node,
41
+ retryable: kwargs[:retryable] || false,
42
+ }
43
+ @workflow.add_step(:provision, **opts) do |s|
44
+ s.opts[:node].provision
45
+ s.opts[:node]
46
+ end
47
+ end
48
+
49
+ def add_wait_for_node_ssh(**kwargs)
50
+ opts = {
51
+ node: @node,
52
+ retryable: kwargs[:retryable] || true,
53
+ retry_delay: kwargs[:retry_delay] || 30,
54
+ retry_max: kwargs[:retry_max] || 10,
55
+ }
56
+ @workflow.add_step(:wait_for_node_ssh, **opts) do |s|
57
+ unless s.opts[:node].ready?
58
+ raise "wait_for_node_ssh timed out for node #{s.opts[:node].node_name}"
59
+ end
60
+ s.opts[:node]
61
+ end
62
+ end
63
+
64
+ def add_check_dnf_automatic(**kwargs)
65
+ opts = {
66
+ node: @node,
67
+ retryable: kwargs[:retryable] || true,
68
+ retry_delay: kwargs[:retry_delay] || 30,
69
+ retry_workflow_on_fail: kwargs[:retry_workflow_on_fail] || true,
70
+ }
71
+ @workflow.add_step(:check_dnf_automatic, **opts) do |s|
72
+ unless s.opts[:node].dnf_automatic_success?
73
+ raise "dnf_automatic failed on node #{s.opts[:node].node_name}"
74
+ end
75
+ s.opts[:node]
76
+ end
77
+ end
78
+
79
+ def add_check_rpm_db(**kwargs)
80
+ opts = {
81
+ node: @node,
82
+ retryable: kwargs[:retryable] || true,
83
+ retry_delay: kwargs[:retry_delay] || 30,
84
+ retry_workflow_on_fail: kwargs[:retry_workflow_on_fail] || true,
85
+ }
86
+ @workflow.add_step(:rpm_db_check, **opts) do |s|
87
+ unless s.opts[:node].rpm_db_check_success?
88
+ raise "rpm_db_check failed on node #{s.opts[:node].node_name}"
89
+ end
90
+ s.opts[:node]
91
+ end
92
+ end
93
+
94
+ def add_save_node_to_inventory(node_inventory:, platform:, **kwargs)
95
+ opts = {
96
+ node: @node,
97
+ platform: platform,
98
+ node_inventory: node_inventory,
99
+ retryable: kwargs[:retryable] || false,
100
+ }
101
+ @workflow.add_step(:save_node_to_inventory, **opts) do |s|
102
+ node_desc = {
103
+ test_data: s.opts[:node].test_data,
104
+ platform: s.opts[:platform],
105
+ local_port: s.opts[:node].local_port,
106
+ }.merge(s.opts[:node].node)
107
+ s.opts[:node_inventory].add(s.opts[:node].node_name, node_desc)
108
+ s.opts[:node_inventory].save
109
+ s.logger.info("node #{s.opts[:node_name]} saved to inventory")
110
+ s.opts[:node_inventory]
111
+ end
112
+ end
113
+
114
+ def add_check_for_module_package_path(module_pkg_path:, **kwargs)
115
+ opts = {
116
+ module_pkg_path: module_pkg_path,
117
+ retryable: kwargs[:retryable] || true,
118
+ retry_delay: kwargs[:retry_delay] || 30,
119
+ }
120
+ @workflow.add_step(:check_for_module_package_path, **opts) do |s|
121
+ unless File.exist?(s.opts[:module_pkg_path])
122
+ raise "module package #{s.opts[:module_pkg_path]} does not exist"
123
+ end
124
+ s.opts[:module_pkg_path]
125
+ end
126
+ end
127
+
128
+ def add_install_module(module_pkg_path:, **kwargs)
129
+ opts = {
130
+ node: @node,
131
+ module_pkg_path: module_pkg_path,
132
+ retryable: kwargs[:retryable] || true,
133
+ retry_delay: kwargs[:retry_delay] || 60,
134
+ }
135
+ @workflow.add_step(:bootstrap, **opts) do |s|
136
+ s.opts[:node].install_puppet_module_package(s.opts[:module_pkg_path])
137
+ s.opts[:node]
138
+ end
139
+ end
140
+
141
+ def add_check_node_inventory_file(node_inventory:, **kwargs)
142
+ opts = {
143
+ ni_file_path: node_inventory.save_file_path,
144
+ retryable: kwargs[:retryable] || true,
145
+ retry_max: kwargs[:retry_max] || 60,
146
+ retry_delay: kwargs[:retry_delay] || 5,
147
+ }
148
+ @workflow.add_step(:check_node_inventory_file, **opts) do |s|
149
+ unless File.exist?(s.opts[:ni_file_path])
150
+ raise "node inventory file #{s.opts[:ni_file_path]} not found"
151
+ end
152
+ s.opts[:ni_file_path]
153
+ end
154
+ end
155
+
156
+ def add_run_tests(rspec_opts:, rspec_cmd:, run_result:, **kwargs)
157
+ opts = {
158
+ node: @node,
159
+ rspec_opts: rspec_opts,
160
+ rspec_cmd: rspec_cmd,
161
+ run_result: run_result,
162
+ retryable: kwargs[:retryable] || false,
163
+ }
164
+ @workflow.add_step(:run_tests, **opts) do |s|
165
+ s.opts[:node].run_tests do |cmd_env|
166
+ cmd_opts = s.opts[:rspec_opts].dup
167
+ cmd_opts.env = cmd_opts.env.merge(cmd_env) if cmd_env
168
+ rspec_cmd = s.opts[:rspec_cmd].new(cmd_opts)
169
+ run_result = s.opts[:run_result].dup
170
+ begin
171
+ rspec_cmd.execute(pty: false, log_prefix: "RSPEC: #{@node.test_data[:test_name]}")
172
+ run_result.from_json_file(cmd_opts.format[:json])
173
+ rescue Errno::EIO => e
174
+ s.logger.error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}")
175
+ run_result.from_error(e)
176
+ rescue StandardError => e
177
+ s.logger.error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}")
178
+ s.logger.debug(e.backtrace.join("\n"))
179
+ run_result.from_error(e)
180
+ end
181
+ end
182
+ run_result
183
+ end
184
+ end
185
+
186
+ def add_clean_up(force: false, **kwargs)
187
+ opts = {
188
+ node: @node,
189
+ config: @config,
190
+ force: force,
191
+ retryable: kwargs[:retryable] || false,
192
+ }
193
+ @workflow.add_step(:clean_up, **opts) do |s|
194
+ if !force && s.opts[:config].get('no_destroy_nodes')
195
+ s.logger.info("not destroying node #{s.opts[:node].node_name} because 'no_destroy_nodes' is set to true")
196
+ else
197
+ s.logger.info("destroying node #{s.opts[:node].node_name}")
198
+ s.opts[:node].destroy
199
+ s.logger.info("node #{s.opts[:node].node_name} destroyed")
200
+ end
201
+ end
202
+ end
203
+
204
+ private
205
+
206
+ def workflow_manager_opts(config, logger = nil)
207
+ {
208
+ retry_max: config.get('workflow.retry_max') || 3,
209
+ retry_delay: config.get('workflow.retry_delay') || 0,
210
+ ignore_failures: config.get('workflow.ignore_failures') || false,
211
+ raise_on_fail: config.get('workflow.raise_on_fail') || true,
212
+ logger: logger,
213
+ }
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../logging'
4
+ require_relative 'step'
5
+
6
+ module CemAcpt
7
+ module TestRunner
8
+ module Workflow
9
+ # Manager is a class that manages how steps in a workflow are executed.
10
+ # @!attribute [r] completed_steps
11
+ # @return [Array<Step>] The steps that have been completed
12
+ # @!attribute [r] last_error
13
+ # @return [Exception, nil] The last error that occurred, if any
14
+ # @!attribute [r] last_result
15
+ # @return [Any] The last result that occurred, if any
16
+ # @!attribute [r] retry_max
17
+ # @return [Integer] The maximum number of workflow runs to attempt before failing. Default: 3
18
+ # @!attribute [r] retry_delay
19
+ # @return [Integer] The number of seconds to wait between workflow runs. Default: 0
20
+ # @!attribute [r] steps
21
+ # @return [Array<Step>] The steps that are part of this workflow
22
+ class Manager
23
+ include CemAcpt::TestRunner::Logging
24
+
25
+ attr_reader :completed_steps, :last_error, :last_result, :retry_max, :retry_delay, :steps
26
+
27
+ # @param [Hash] opts The options to create a new workflow manager with
28
+ # @option opts [Integer] :retry_max The maximum number of workflow runs to attempt before failing. Default: 3
29
+ # @option opts [Integer] :retry_delay The number of seconds to wait between workflow runs. Default: 0
30
+ # @option opts [Boolean] :ignore_failures Whether to ignore Step failures and continue to the next step. Default: false
31
+ # @option opts [Boolean] :raise_on_fail Whether to raise an exception when a Step fails. Default: true
32
+ def initialize(**opts)
33
+ @retry_max = opts[:retry_max] || 3
34
+ @retry_delay = opts[:retry_delay] || 0
35
+ @ignore_failures = opts[:ignore_failures] || false
36
+ @raise_on_fail = opts[:raise_on_fail] || true
37
+ @logger = use_logger(opts[:logger], stage: 'Workflow')
38
+ @steps = []
39
+ @workflow_runs = 0
40
+ @last_error = nil
41
+ @last_result = nil
42
+ @completed_steps = []
43
+ end
44
+
45
+ # Add a step to the workflow. Steps can be named anything, but if the step name is :clean_up,
46
+ # it will be run at the end of the workflow, regardless of the order it was added, and it's
47
+ # output will not be saved as the last_result. Additionally, the :clean_up step will be run
48
+ # even if the workflow fails.
49
+ # @param [Symbol] name The name of the step
50
+ # @param [Hash] kwargs The keyword arguments to pass to the step
51
+ # @param [Proc] block The block to pass to the step
52
+ # @yieldparam [Step] step The step that was added. This is passed to the block.
53
+ def add_step(name, **kwargs, &block)
54
+ raise ArgumentError, 'name must be a Symbol' unless name.is_a?(Symbol)
55
+
56
+ step_name = if @steps.any? { |step| step.name == name }
57
+ "#{name}_#{@steps.length}".to_sym
58
+ else
59
+ name
60
+ end
61
+ step = Step.new(step_name, **kwargs.merge(logger: @logger), &block)
62
+ @steps << StepState.new(step, @steps.length, **kwargs.merge(logger: @logger))
63
+ end
64
+
65
+ # Run the workflow
66
+ def run
67
+ @workflow_runs += 1
68
+ @completed_steps = []
69
+ @steps.each do |step|
70
+ next if step.name == :clean_up
71
+
72
+ result = step.run
73
+ handle_result(step, result)
74
+ end
75
+ ensure
76
+ clean_up
77
+ end
78
+
79
+ # Whether the workflow has completed successfully
80
+ def success?
81
+ (@completed_steps.length == @steps.length) && @completed_steps.none? { |step| step.failed? }
82
+ end
83
+
84
+ # @return [Boolean] Whether to ignore failures and continue to the next step
85
+ def ignore_failures?
86
+ @ignore_failures
87
+ end
88
+
89
+ # @return [Boolean] Whether to raise an exception when a step fails
90
+ def raise_on_fail?
91
+ @raise_on_fail
92
+ end
93
+
94
+ private
95
+
96
+ def new_logger(logger)
97
+ if logger.nil?
98
+ logger
99
+ else
100
+ @logger = logger.dup
101
+ @logger.add_prefix_parts(stage: 'Workflow')
102
+ end
103
+ end
104
+
105
+ def log(level, msg)
106
+ if @logger
107
+ @logger.send(level, msg)
108
+ else
109
+ send("async_#{level}".to_sym, msg)
110
+ end
111
+ end
112
+
113
+ def log_debug(msg)
114
+ log(:debug, msg)
115
+ end
116
+
117
+ def log_info(msg)
118
+ log(:info, msg)
119
+ end
120
+
121
+ def log_warn(msg)
122
+ log(:warn, msg)
123
+ end
124
+
125
+ def log_error(msg)
126
+ log(:error, msg)
127
+ end
128
+
129
+ def log_fatal(msg)
130
+ log(:fatal, msg)
131
+ end
132
+
133
+ # Handles the result of a step
134
+ # @param [Step] step The step that was run
135
+ # @param [Object] result The result of the step
136
+ # @raise [StandardError] If the step failed and the workflow is not configured to ignore failures
137
+ def handle_result(step, result)
138
+ @last_result = result unless step.name == :clean_up # Don't overwrite the last result with the clean_up step
139
+ case result
140
+ when :retry_workflow
141
+ log_warn("step '#{step.name}' failed and requested a workflow retry")
142
+ log_debug("step '#{step.name}' failed with error: #{step.last_error.message}")
143
+ log_debug("step '#{step.name}' failed with error: #{step.last_error.backtrace.join("\n")}")
144
+ @last_error = step.last_error
145
+ retry_workflow
146
+ when :fail
147
+ log_warn("step '#{step.name}' failed")
148
+ log_debug(step.last_error.message)
149
+ log_debug(step.last_error.backtrace.join("\n"))
150
+ @last_error = step.last_error
151
+ if ignore_failures?
152
+ log_warn("ignoring failure of step '#{step.name}'")
153
+ @completed_steps << step
154
+ else
155
+ log_error("failed with error: #{@last_error.message}")
156
+ raise @last_error
157
+ end
158
+ else
159
+ log_info("step '#{step.name}' succeeded")
160
+ log_debug("step '#{step.name}' returned: #{result}")
161
+ @completed_steps << step
162
+ end
163
+ end
164
+
165
+ # Retries the workflow if it is retryable
166
+ # @raise [StandardError] If the workflow is not retryable or has exceeded the maximum number of retries
167
+ def retry_workflow
168
+ if @workflow_runs < @retry_max
169
+ log_info("Retrying workflow (attempt #{@workflow_runs + 1} of #{@retry_max})")
170
+ sleep @retry_delay if @retry_delay > 0
171
+ clean_up
172
+ run
173
+ else
174
+ log_fatal('Workflow is not retryable or has exceeded the maximum number of retries')
175
+ raise @last_error
176
+ end
177
+ end
178
+
179
+ # Adds a default clean_up step if one is not defined
180
+ # @return [Step] The default clean_up step
181
+ def default_clean_up
182
+ add_step(:clean_up) do
183
+ log_info('No clean_up step defined, skipping')
184
+ true
185
+ end
186
+ @steps.last
187
+ end
188
+
189
+ # Runs the clean_up step
190
+ def clean_up
191
+ cleanup_step = @steps.find { |step| step.name == :clean_up } || default_clean_up
192
+ result = cleanup_step.run
193
+ handle_result(cleanup_step, result)
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end