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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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