roast-ai 0.1.6 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +1 -1
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +20 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +11 -6
- data/README.md +225 -13
- data/bin/roast +27 -0
- data/docs/INSTRUMENTATION.md +42 -1
- data/docs/ITERATION_SYNTAX.md +119 -0
- data/examples/conditional/README.md +161 -0
- data/examples/conditional/check_condition/prompt.md +1 -0
- data/examples/conditional/simple_workflow.yml +15 -0
- data/examples/conditional/workflow.yml +23 -0
- data/examples/dot_notation/README.md +37 -0
- data/examples/dot_notation/workflow.yml +44 -0
- data/examples/exit_on_error/README.md +50 -0
- data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
- data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
- data/examples/exit_on_error/workflow.yml +19 -0
- data/examples/grading/workflow.yml +5 -1
- data/examples/iteration/IMPLEMENTATION.md +88 -0
- data/examples/iteration/README.md +68 -0
- data/examples/iteration/analyze_complexity/prompt.md +22 -0
- data/examples/iteration/generate_recommendations/prompt.md +21 -0
- data/examples/iteration/generate_report/prompt.md +129 -0
- data/examples/iteration/implement_fix/prompt.md +25 -0
- data/examples/iteration/prioritize_issues/prompt.md +24 -0
- data/examples/iteration/prompts/analyze_file.md +28 -0
- data/examples/iteration/prompts/generate_summary.md +24 -0
- data/examples/iteration/prompts/update_report.md +29 -0
- data/examples/iteration/prompts/write_report.md +22 -0
- data/examples/iteration/read_file/prompt.md +9 -0
- data/examples/iteration/select_next_issue/prompt.md +25 -0
- data/examples/iteration/simple_workflow.md +39 -0
- data/examples/iteration/simple_workflow.yml +58 -0
- data/examples/iteration/update_fix_count/prompt.md +26 -0
- data/examples/iteration/verify_fix/prompt.md +29 -0
- data/examples/iteration/workflow.yml +42 -0
- data/examples/openrouter_example/workflow.yml +2 -2
- data/examples/workflow_generator/README.md +27 -0
- data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
- data/examples/workflow_generator/get_user_input/prompt.md +14 -0
- data/examples/workflow_generator/info_from_roast.rb +22 -0
- data/examples/workflow_generator/workflow.yml +35 -0
- data/lib/roast/errors.rb +9 -0
- data/lib/roast/factories/api_provider_factory.rb +61 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
- data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
- data/lib/roast/helpers/prompt_loader.rb +50 -1
- data/lib/roast/resources/base_resource.rb +7 -0
- data/lib/roast/resources.rb +6 -6
- data/lib/roast/tools/ask_user.rb +40 -0
- data/lib/roast/tools/cmd.rb +1 -1
- data/lib/roast/tools/search_file.rb +1 -1
- data/lib/roast/tools/update_files.rb +413 -0
- data/lib/roast/tools.rb +12 -1
- data/lib/roast/value_objects/api_token.rb +49 -0
- data/lib/roast/value_objects/step_name.rb +39 -0
- data/lib/roast/value_objects/workflow_path.rb +77 -0
- data/lib/roast/value_objects.rb +5 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/api_configuration.rb +61 -0
- data/lib/roast/workflow/base_iteration_step.rb +165 -0
- data/lib/roast/workflow/base_step.rb +4 -24
- data/lib/roast/workflow/base_workflow.rb +76 -73
- data/lib/roast/workflow/command_executor.rb +88 -0
- data/lib/roast/workflow/conditional_executor.rb +50 -0
- data/lib/roast/workflow/conditional_step.rb +96 -0
- data/lib/roast/workflow/configuration.rb +35 -158
- data/lib/roast/workflow/configuration_loader.rb +78 -0
- data/lib/roast/workflow/configuration_parser.rb +13 -248
- data/lib/roast/workflow/context_path_resolver.rb +43 -0
- data/lib/roast/workflow/dot_access_hash.rb +198 -0
- data/lib/roast/workflow/each_step.rb +86 -0
- data/lib/roast/workflow/error_handler.rb +97 -0
- data/lib/roast/workflow/expression_utils.rb +36 -0
- data/lib/roast/workflow/file_state_repository.rb +3 -2
- data/lib/roast/workflow/interpolator.rb +34 -0
- data/lib/roast/workflow/iteration_executor.rb +85 -0
- data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
- data/lib/roast/workflow/output_handler.rb +35 -0
- data/lib/roast/workflow/output_manager.rb +77 -0
- data/lib/roast/workflow/parallel_executor.rb +49 -0
- data/lib/roast/workflow/repeat_step.rb +75 -0
- data/lib/roast/workflow/replay_handler.rb +123 -0
- data/lib/roast/workflow/resource_resolver.rb +77 -0
- data/lib/roast/workflow/session_manager.rb +6 -2
- data/lib/roast/workflow/state_manager.rb +97 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +205 -0
- data/lib/roast/workflow/step_executor_factory.rb +47 -0
- data/lib/roast/workflow/step_executor_registry.rb +79 -0
- data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
- data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
- data/lib/roast/workflow/step_finder.rb +97 -0
- data/lib/roast/workflow/step_loader.rb +154 -0
- data/lib/roast/workflow/step_orchestrator.rb +45 -0
- data/lib/roast/workflow/step_runner.rb +23 -0
- data/lib/roast/workflow/step_type_resolver.rb +117 -0
- data/lib/roast/workflow/workflow_context.rb +60 -0
- data/lib/roast/workflow/workflow_executor.rb +90 -209
- data/lib/roast/workflow/workflow_initializer.rb +112 -0
- data/lib/roast/workflow/workflow_runner.rb +87 -0
- data/lib/roast/workflow.rb +3 -0
- data/lib/roast.rb +96 -3
- data/roast.gemspec +3 -1
- data/schema/workflow.json +85 -0
- metadata +112 -4
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/conditional_executor"
|
4
|
+
require "roast/workflow/step_executor_factory"
|
5
|
+
require "roast/workflow/step_type_resolver"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Workflow
|
9
|
+
# Coordinates the execution of different types of steps
|
10
|
+
#
|
11
|
+
# This class is responsible for routing steps to their appropriate executors
|
12
|
+
# based on the step type. It acts as a central dispatcher that determines
|
13
|
+
# which execution strategy to use for each step.
|
14
|
+
#
|
15
|
+
# Current Architecture:
|
16
|
+
# - WorkflowExecutor.execute_steps still handles basic routing for backward compatibility
|
17
|
+
# - This coordinator is used by WorkflowExecutor.execute_step for named steps
|
18
|
+
# - Some step types (parallel) use the StepExecutorFactory pattern
|
19
|
+
# - Other step types use direct execution methods
|
20
|
+
#
|
21
|
+
# TODO: Future refactoring should move all execution logic from WorkflowExecutor
|
22
|
+
# to this coordinator and use the factory pattern consistently for all step types.
|
23
|
+
class StepExecutorCoordinator
|
24
|
+
def initialize(context:, dependencies:)
|
25
|
+
@context = context
|
26
|
+
@dependencies = dependencies
|
27
|
+
end
|
28
|
+
|
29
|
+
# Execute a list of steps
|
30
|
+
def execute_steps(workflow_steps)
|
31
|
+
workflow_steps.each do |step|
|
32
|
+
case step
|
33
|
+
when Hash
|
34
|
+
execute(step)
|
35
|
+
when Array
|
36
|
+
execute(step)
|
37
|
+
when String
|
38
|
+
execute(step)
|
39
|
+
# Handle pause after string steps
|
40
|
+
if @context.workflow.pause_step_name == step
|
41
|
+
Kernel.binding.irb # rubocop:disable Lint/Debugger
|
42
|
+
end
|
43
|
+
else
|
44
|
+
step_orchestrator.execute_step(step)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Execute a single step (alias for compatibility)
|
50
|
+
def execute_step(step, options = {})
|
51
|
+
execute(step, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Execute a step based on its type
|
55
|
+
# @param step [String, Hash, Array] The step to execute
|
56
|
+
# @param options [Hash] Execution options
|
57
|
+
# @return [Object] The result of the step execution
|
58
|
+
def execute(step, options = {})
|
59
|
+
step_type = StepTypeResolver.resolve(step, @context)
|
60
|
+
|
61
|
+
case step_type
|
62
|
+
when StepTypeResolver::COMMAND_STEP
|
63
|
+
# Command steps should also go through interpolation
|
64
|
+
execute_string_step(step, options)
|
65
|
+
when StepTypeResolver::GLOB_STEP
|
66
|
+
execute_glob_step(step)
|
67
|
+
when StepTypeResolver::ITERATION_STEP
|
68
|
+
execute_iteration_step(step)
|
69
|
+
when StepTypeResolver::CONDITIONAL_STEP
|
70
|
+
execute_conditional_step(step)
|
71
|
+
when StepTypeResolver::HASH_STEP
|
72
|
+
execute_hash_step(step)
|
73
|
+
when StepTypeResolver::PARALLEL_STEP
|
74
|
+
# Use factory for parallel steps
|
75
|
+
executor = StepExecutorFactory.for(step, workflow_executor)
|
76
|
+
executor.execute(step)
|
77
|
+
when StepTypeResolver::STRING_STEP
|
78
|
+
execute_string_step(step, options)
|
79
|
+
else
|
80
|
+
execute_standard_step(step, options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
attr_reader :context, :dependencies
|
87
|
+
|
88
|
+
def workflow_executor
|
89
|
+
dependencies[:workflow_executor]
|
90
|
+
end
|
91
|
+
|
92
|
+
def interpolator
|
93
|
+
dependencies[:interpolator]
|
94
|
+
end
|
95
|
+
|
96
|
+
def command_executor
|
97
|
+
dependencies[:command_executor]
|
98
|
+
end
|
99
|
+
|
100
|
+
def iteration_executor
|
101
|
+
dependencies[:iteration_executor]
|
102
|
+
end
|
103
|
+
|
104
|
+
def conditional_executor
|
105
|
+
dependencies[:conditional_executor]
|
106
|
+
end
|
107
|
+
|
108
|
+
def step_orchestrator
|
109
|
+
dependencies[:step_orchestrator]
|
110
|
+
end
|
111
|
+
|
112
|
+
def error_handler
|
113
|
+
dependencies[:error_handler]
|
114
|
+
end
|
115
|
+
|
116
|
+
def execute_command_step(step, options)
|
117
|
+
exit_on_error = options.fetch(:exit_on_error, true)
|
118
|
+
resource_type = @context.resource_type
|
119
|
+
|
120
|
+
error_handler.with_error_handling(step, resource_type: resource_type) do
|
121
|
+
$stderr.puts "Executing: #{step} (Resource type: #{resource_type || "unknown"})"
|
122
|
+
|
123
|
+
output = command_executor.execute(step, exit_on_error: exit_on_error)
|
124
|
+
|
125
|
+
# Add to transcript
|
126
|
+
workflow = context.workflow
|
127
|
+
workflow.transcript << {
|
128
|
+
user: "I just executed the following command: ```\n#{step}\n```\n\nHere is the output:\n\n```\n#{output}\n```",
|
129
|
+
}
|
130
|
+
workflow.transcript << { assistant: "Noted, thank you." }
|
131
|
+
|
132
|
+
output
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def execute_glob_step(step)
|
137
|
+
Dir.glob(step).join("\n")
|
138
|
+
end
|
139
|
+
|
140
|
+
def execute_iteration_step(step)
|
141
|
+
name = step.keys.first
|
142
|
+
command = step[name]
|
143
|
+
|
144
|
+
case name
|
145
|
+
when "repeat"
|
146
|
+
iteration_executor.execute_repeat(command)
|
147
|
+
when "each"
|
148
|
+
validate_each_step!(step)
|
149
|
+
iteration_executor.execute_each(step)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def execute_conditional_step(step)
|
154
|
+
conditional_executor.execute_conditional(step)
|
155
|
+
end
|
156
|
+
|
157
|
+
def execute_hash_step(step)
|
158
|
+
name, command = step.to_a.flatten
|
159
|
+
interpolated_name = interpolator.interpolate(name)
|
160
|
+
|
161
|
+
if command.is_a?(Hash)
|
162
|
+
execute_steps([command])
|
163
|
+
else
|
164
|
+
interpolated_command = interpolator.interpolate(command)
|
165
|
+
exit_on_error = context.exit_on_error?(interpolated_name)
|
166
|
+
|
167
|
+
# Execute the command directly using the appropriate executor
|
168
|
+
result = execute(interpolated_command, { exit_on_error: exit_on_error })
|
169
|
+
context.workflow.output[interpolated_name] = result
|
170
|
+
result
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def execute_string_step(step, options = {})
|
175
|
+
# Check for glob before interpolation
|
176
|
+
if StepTypeResolver.glob_step?(step, context)
|
177
|
+
return execute_glob_step(step)
|
178
|
+
end
|
179
|
+
|
180
|
+
interpolated_step = interpolator.interpolate(step)
|
181
|
+
|
182
|
+
if StepTypeResolver.command_step?(interpolated_step)
|
183
|
+
# Command step - execute directly, preserving any passed options
|
184
|
+
exit_on_error = options.fetch(:exit_on_error, true)
|
185
|
+
execute_command_step(interpolated_step, { exit_on_error: exit_on_error })
|
186
|
+
else
|
187
|
+
exit_on_error = options.fetch(:exit_on_error, context.exit_on_error?(step))
|
188
|
+
execute_standard_step(interpolated_step, { exit_on_error: exit_on_error })
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def execute_standard_step(step, options)
|
193
|
+
exit_on_error = options.fetch(:exit_on_error, true)
|
194
|
+
step_orchestrator.execute_step(step, exit_on_error: exit_on_error)
|
195
|
+
end
|
196
|
+
|
197
|
+
def validate_each_step!(step)
|
198
|
+
unless step.key?("as") && step.key?("steps")
|
199
|
+
raise WorkflowExecutor::ConfigurationError,
|
200
|
+
"Invalid 'each' step format. 'as' and 'steps' must be at the same level as 'each'"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/step_executor_registry"
|
4
|
+
require "roast/workflow/step_executors/hash_step_executor"
|
5
|
+
require "roast/workflow/step_executors/parallel_step_executor"
|
6
|
+
require "roast/workflow/step_executors/string_step_executor"
|
7
|
+
|
8
|
+
module Roast
|
9
|
+
module Workflow
|
10
|
+
# Factory for creating step executors - now delegates to registry
|
11
|
+
class StepExecutorFactory
|
12
|
+
class << self
|
13
|
+
# Method to ensure default executors are registered
|
14
|
+
def ensure_defaults_registered
|
15
|
+
return if @defaults_registered
|
16
|
+
|
17
|
+
StepExecutorRegistry.register(Hash, StepExecutors::HashStepExecutor)
|
18
|
+
StepExecutorRegistry.register(Array, StepExecutors::ParallelStepExecutor)
|
19
|
+
StepExecutorRegistry.register(String, StepExecutors::StringStepExecutor)
|
20
|
+
|
21
|
+
@defaults_registered = true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initialize on first use
|
26
|
+
ensure_defaults_registered
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# Delegate to the registry for backward compatibility
|
30
|
+
def for(step, workflow_executor)
|
31
|
+
ensure_defaults_registered
|
32
|
+
StepExecutorRegistry.for(step, workflow_executor)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Allow registration of new executors
|
36
|
+
def register(klass, executor_class)
|
37
|
+
StepExecutorRegistry.register(klass, executor_class)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Allow registration with custom matchers
|
41
|
+
def register_with_matcher(matcher, executor_class)
|
42
|
+
StepExecutorRegistry.register_with_matcher(matcher, executor_class)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Registry pattern for step executors - eliminates case statements
|
6
|
+
# and follows Open/Closed Principle
|
7
|
+
class StepExecutorRegistry
|
8
|
+
class UnknownStepTypeError < StandardError; end
|
9
|
+
|
10
|
+
@executors = {}
|
11
|
+
@type_matchers = []
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Register an executor for a specific class
|
15
|
+
# @param klass [Class] The class to match
|
16
|
+
# @param executor_class [Class] The executor class to use
|
17
|
+
def register(klass, executor_class)
|
18
|
+
@executors[klass] = executor_class
|
19
|
+
end
|
20
|
+
|
21
|
+
# Register an executor with a custom matcher
|
22
|
+
# @param matcher [Proc] A proc that returns true if the step matches
|
23
|
+
# @param executor_class [Class] The executor class to use
|
24
|
+
def register_with_matcher(matcher, executor_class)
|
25
|
+
@type_matchers << { matcher: matcher, executor_class: executor_class }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Find the appropriate executor for a step
|
29
|
+
# @param step [Object] The step to find an executor for
|
30
|
+
# @param workflow_executor [WorkflowExecutor] The workflow executor instance
|
31
|
+
# @return [Object] An instance of the appropriate executor
|
32
|
+
def for(step, workflow_executor)
|
33
|
+
executor_class = find_executor_class(step)
|
34
|
+
|
35
|
+
unless executor_class
|
36
|
+
raise UnknownStepTypeError, "No executor registered for step type: #{step.class} (#{step.inspect})"
|
37
|
+
end
|
38
|
+
|
39
|
+
executor_class.new(workflow_executor)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Clear all registrations (useful for testing)
|
43
|
+
def clear!
|
44
|
+
@executors.clear
|
45
|
+
@type_matchers.clear
|
46
|
+
# Reset the factory's defaults flag if it's defined
|
47
|
+
if defined?(StepExecutorFactory)
|
48
|
+
StepExecutorFactory.instance_variable_set(:@defaults_registered, false)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get all registered executors (useful for debugging)
|
53
|
+
def registered_executors
|
54
|
+
@executors.dup
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def find_executor_class(step)
|
60
|
+
# First check exact class matches
|
61
|
+
executor_class = @executors[step.class]
|
62
|
+
return executor_class if executor_class
|
63
|
+
|
64
|
+
# Then check custom matchers
|
65
|
+
matcher_entry = @type_matchers.find { |entry| entry[:matcher].call(step) }
|
66
|
+
return matcher_entry[:executor_class] if matcher_entry
|
67
|
+
|
68
|
+
# Finally check inheritance chain
|
69
|
+
step.class.ancestors.each do |ancestor|
|
70
|
+
executor_class = @executors[ancestor]
|
71
|
+
return executor_class if executor_class
|
72
|
+
end
|
73
|
+
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
module StepExecutors
|
6
|
+
class BaseStepExecutor
|
7
|
+
def initialize(workflow_executor)
|
8
|
+
@workflow_executor = workflow_executor
|
9
|
+
@workflow = workflow_executor.workflow
|
10
|
+
@config_hash = workflow_executor.config_hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(step)
|
14
|
+
raise NotImplementedError, "Subclasses must implement execute"
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
attr_reader :workflow_executor, :workflow, :config_hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/step_executors/base_step_executor"
|
4
|
+
require "roast/workflow/step_runner"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
module StepExecutors
|
9
|
+
class HashStepExecutor < BaseStepExecutor
|
10
|
+
def execute(step)
|
11
|
+
# execute a command and store the output in a variable
|
12
|
+
name, command = step.to_a.flatten
|
13
|
+
|
14
|
+
# Interpolate variable name if it contains {{}}
|
15
|
+
interpolated_name = workflow_executor.interpolate(name)
|
16
|
+
|
17
|
+
if command.is_a?(Hash)
|
18
|
+
step_runner.execute_steps([command])
|
19
|
+
else
|
20
|
+
# Interpolate command value
|
21
|
+
interpolated_command = workflow_executor.interpolate(command)
|
22
|
+
|
23
|
+
# Check if this step has exit_on_error configuration
|
24
|
+
step_config = config_hash[interpolated_name]
|
25
|
+
exit_on_error = step_config.is_a?(Hash) ? step_config.fetch("exit_on_error", true) : true
|
26
|
+
|
27
|
+
workflow.output[interpolated_name] = step_runner.execute_step(interpolated_command, exit_on_error: exit_on_error)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def step_runner
|
34
|
+
@step_runner ||= StepRunner.new(coordinator)
|
35
|
+
end
|
36
|
+
|
37
|
+
def coordinator
|
38
|
+
workflow_executor.step_executor_coordinator
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/step_executors/base_step_executor"
|
4
|
+
require "roast/workflow/step_runner"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Workflow
|
8
|
+
module StepExecutors
|
9
|
+
class ParallelStepExecutor < BaseStepExecutor
|
10
|
+
def execute(steps)
|
11
|
+
# run steps in parallel, don't proceed until all are done
|
12
|
+
threads = steps.map do |sub_step|
|
13
|
+
Thread.new do
|
14
|
+
# Each thread needs its own isolated execution context
|
15
|
+
Thread.current[:step] = sub_step
|
16
|
+
Thread.current[:result] = nil
|
17
|
+
Thread.current[:error] = nil
|
18
|
+
|
19
|
+
begin
|
20
|
+
# Execute the single step in this thread
|
21
|
+
step_runner.execute_steps([sub_step])
|
22
|
+
Thread.current[:result] = :success
|
23
|
+
rescue => e
|
24
|
+
Thread.current[:error] = e
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Wait for all threads to complete
|
30
|
+
threads.each(&:join)
|
31
|
+
|
32
|
+
# Check for errors in any thread
|
33
|
+
threads.each_with_index do |thread, _index|
|
34
|
+
if thread[:error]
|
35
|
+
raise thread[:error]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
:success
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def step_runner
|
45
|
+
@step_runner ||= StepRunner.new(coordinator)
|
46
|
+
end
|
47
|
+
|
48
|
+
def coordinator
|
49
|
+
workflow_executor.step_executor_coordinator
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/workflow/step_executors/base_step_executor"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Workflow
|
7
|
+
module StepExecutors
|
8
|
+
class StringStepExecutor < BaseStepExecutor
|
9
|
+
def execute(step)
|
10
|
+
# Interpolate any {{}} expressions before executing the step
|
11
|
+
interpolated_step = workflow_executor.interpolate(step)
|
12
|
+
|
13
|
+
# For command steps, check if there's an exit_on_error configuration
|
14
|
+
# We need to extract the step name to look up configuration
|
15
|
+
if interpolated_step.starts_with?("$(")
|
16
|
+
# This is a direct command without a name, so exit_on_error defaults to true
|
17
|
+
workflow_executor.execute_step(interpolated_step)
|
18
|
+
else
|
19
|
+
# Check if this step has exit_on_error configuration
|
20
|
+
step_config = config_hash[step]
|
21
|
+
exit_on_error = step_config.is_a?(Hash) ? step_config.fetch("exit_on_error", true) : true
|
22
|
+
|
23
|
+
workflow_executor.execute_step(interpolated_step, exit_on_error: exit_on_error)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Finds step indices within workflow step arrays
|
6
|
+
# Handles various step formats: strings, hashes, parallel arrays
|
7
|
+
class StepFinder
|
8
|
+
attr_reader :steps
|
9
|
+
|
10
|
+
def initialize(steps = nil)
|
11
|
+
@steps = steps || []
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def find_index(steps_array, step_name)
|
16
|
+
new(steps_array).find_index(step_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Find the index of a step in the workflow steps array
|
21
|
+
# @param step_name [String] The name of the step to find
|
22
|
+
# @param steps_array [Array, nil] Optional steps array to search in
|
23
|
+
# @return [Integer, nil] The index of the step, or nil if not found
|
24
|
+
def find_index(step_name, steps_array = nil)
|
25
|
+
search_array = steps_array || @steps
|
26
|
+
# First, try direct search
|
27
|
+
index = find_by_direct_search(search_array, step_name)
|
28
|
+
return index if index
|
29
|
+
|
30
|
+
# Fall back to extracted name search
|
31
|
+
find_by_extracted_name(search_array, step_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Extract the name from a step definition
|
35
|
+
# @param step [String, Hash, Array] The step definition
|
36
|
+
# @return [String, Array] The step name(s)
|
37
|
+
def extract_name(step)
|
38
|
+
case step
|
39
|
+
when String
|
40
|
+
step
|
41
|
+
when Hash
|
42
|
+
step.keys.first
|
43
|
+
when Array
|
44
|
+
# For arrays, extract names from all contained steps
|
45
|
+
step.map { |s| extract_name(s) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def find_by_direct_search(steps_array, step_name)
|
52
|
+
steps_array.each_with_index do |step, index|
|
53
|
+
case step
|
54
|
+
when Hash
|
55
|
+
# Could be {name: command} or {name: {substeps}}
|
56
|
+
step_key = step.keys.first
|
57
|
+
return index if step_key == step_name
|
58
|
+
when Array
|
59
|
+
# This is a parallel step container, search inside it
|
60
|
+
if contains_step?(step, step_name)
|
61
|
+
return index
|
62
|
+
end
|
63
|
+
when String
|
64
|
+
return index if step == step_name
|
65
|
+
end
|
66
|
+
end
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_by_extracted_name(steps_array, step_name)
|
71
|
+
steps_array.each_with_index do |step, index|
|
72
|
+
name = extract_name(step)
|
73
|
+
if name.is_a?(Array)
|
74
|
+
# For arrays (parallel steps), check if target is in the array
|
75
|
+
return index if name.flatten.include?(step_name)
|
76
|
+
elsif name == step_name
|
77
|
+
return index
|
78
|
+
end
|
79
|
+
end
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def contains_step?(parallel_steps, step_name)
|
84
|
+
parallel_steps.any? do |substep|
|
85
|
+
case substep
|
86
|
+
when Hash
|
87
|
+
substep.keys.first == step_name
|
88
|
+
when String
|
89
|
+
substep == step_name
|
90
|
+
else
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|