ruby_raider 1.1.4 → 2.0.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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/integration.yml +4 -6
  3. data/.github/workflows/reek.yml +6 -5
  4. data/.github/workflows/release.yml +175 -0
  5. data/.github/workflows/rubocop.yml +7 -6
  6. data/.github/workflows/system_tests.yml +83 -0
  7. data/.gitignore +1 -1
  8. data/.rubocop.yml +24 -0
  9. data/README.md +3 -1
  10. data/RELEASE.md +412 -0
  11. data/RELEASE_QUICK_GUIDE.md +77 -0
  12. data/bin/release +186 -0
  13. data/lib/adopter/adopt_menu.rb +150 -0
  14. data/lib/adopter/converters/base_converter.rb +85 -0
  15. data/lib/adopter/converters/identity_converter.rb +56 -0
  16. data/lib/adopter/migration_plan.rb +75 -0
  17. data/lib/adopter/migrator.rb +96 -0
  18. data/lib/adopter/plan_builder.rb +278 -0
  19. data/lib/adopter/project_analyzer.rb +256 -0
  20. data/lib/adopter/project_detector.rb +159 -0
  21. data/lib/commands/adopt_commands.rb +43 -0
  22. data/lib/generators/automation/templates/account.tt +9 -5
  23. data/lib/generators/automation/templates/appium_caps.tt +60 -6
  24. data/lib/generators/automation/templates/home.tt +4 -4
  25. data/lib/generators/automation/templates/login.tt +61 -4
  26. data/lib/generators/automation/templates/page.tt +13 -7
  27. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -4
  28. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -1
  29. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -4
  30. data/lib/generators/automation/templates/partials/visit_method.tt +11 -1
  31. data/lib/generators/automation/templates/pdp.tt +1 -1
  32. data/lib/generators/cucumber/templates/env.tt +6 -4
  33. data/lib/generators/cucumber/templates/partials/capybara_env.tt +20 -0
  34. data/lib/generators/cucumber/templates/partials/capybara_world.tt +6 -0
  35. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +2 -2
  36. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -3
  37. data/lib/generators/cucumber/templates/steps.tt +2 -2
  38. data/lib/generators/cucumber/templates/world.tt +5 -3
  39. data/lib/generators/generator.rb +14 -2
  40. data/lib/generators/helper_generator.rb +16 -3
  41. data/lib/generators/infrastructure/github_generator.rb +6 -0
  42. data/lib/generators/infrastructure/templates/github.tt +11 -7
  43. data/lib/generators/infrastructure/templates/github_appium.tt +108 -0
  44. data/lib/generators/infrastructure/templates/gitlab.tt +5 -2
  45. data/lib/generators/invoke_generators.rb +1 -0
  46. data/lib/generators/menu_generator.rb +2 -0
  47. data/lib/generators/minitest/minitest_generator.rb +23 -0
  48. data/lib/generators/minitest/templates/test.tt +93 -0
  49. data/lib/generators/rspec/templates/spec.tt +12 -10
  50. data/lib/generators/template_renderer/partial_cache.rb +116 -0
  51. data/lib/generators/template_renderer/partial_resolver.rb +103 -0
  52. data/lib/generators/template_renderer/template_error.rb +50 -0
  53. data/lib/generators/template_renderer.rb +90 -0
  54. data/lib/generators/templates/common/config.tt +2 -2
  55. data/lib/generators/templates/common/gemfile.tt +15 -3
  56. data/lib/generators/templates/common/partials/web_config.tt +1 -1
  57. data/lib/generators/templates/common/read_me.tt +3 -1
  58. data/lib/generators/templates/helpers/allure_helper.tt +2 -2
  59. data/lib/generators/templates/helpers/browser_helper.tt +1 -0
  60. data/lib/generators/templates/helpers/capybara_helper.tt +28 -0
  61. data/lib/generators/templates/helpers/driver_helper.tt +1 -1
  62. data/lib/generators/templates/helpers/partials/allure_imports.tt +3 -1
  63. data/lib/generators/templates/helpers/partials/allure_requirements.tt +3 -1
  64. data/lib/generators/templates/helpers/partials/appium_driver.tt +46 -0
  65. data/lib/generators/templates/helpers/partials/axe_driver.tt +10 -0
  66. data/lib/generators/templates/helpers/partials/browserstack_config.tt +13 -0
  67. data/lib/generators/templates/helpers/partials/driver_and_options.tt +6 -114
  68. data/lib/generators/templates/helpers/partials/quit_driver.tt +3 -1
  69. data/lib/generators/templates/helpers/partials/screenshot.tt +3 -1
  70. data/lib/generators/templates/helpers/partials/selenium_driver.tt +25 -0
  71. data/lib/generators/templates/helpers/spec_helper.tt +17 -4
  72. data/lib/generators/templates/helpers/test_helper.tt +26 -0
  73. data/lib/generators/templates/helpers/visual_spec_helper.tt +1 -1
  74. data/lib/ruby_raider.rb +5 -0
  75. data/lib/version +1 -1
  76. data/spec/adopter/adopt_menu_spec.rb +176 -0
  77. data/spec/adopter/converters/identity_converter_spec.rb +145 -0
  78. data/spec/adopter/migration_plan_spec.rb +113 -0
  79. data/spec/adopter/migrator_spec.rb +277 -0
  80. data/spec/adopter/plan_builder_spec.rb +298 -0
  81. data/spec/adopter/project_analyzer_spec.rb +337 -0
  82. data/spec/adopter/project_detector_spec.rb +295 -0
  83. data/spec/generators/fixtures/templates/test.tt +1 -0
  84. data/spec/generators/fixtures/templates/test_partial.tt +1 -0
  85. data/spec/generators/template_renderer_spec.rb +298 -0
  86. data/spec/integration/commands/scaffolding_commands_spec.rb +2 -2
  87. data/spec/integration/commands/utility_commands_spec.rb +2 -2
  88. data/spec/integration/end_to_end_spec.rb +325 -0
  89. data/spec/integration/generators/automation_generator_spec.rb +11 -11
  90. data/spec/integration/generators/common_generator_spec.rb +40 -40
  91. data/spec/integration/generators/cucumber_generator_spec.rb +7 -7
  92. data/spec/integration/generators/github_generator_spec.rb +8 -8
  93. data/spec/integration/generators/gitlab_generator_spec.rb +8 -8
  94. data/spec/integration/generators/helpers_generator_spec.rb +73 -35
  95. data/spec/integration/generators/minitest_generator_spec.rb +70 -0
  96. data/spec/integration/generators/rspec_generator_spec.rb +7 -7
  97. data/spec/integration/settings_helper.rb +1 -1
  98. data/spec/integration/spec_helper.rb +20 -2
  99. data/spec/system/capybara_spec.rb +42 -0
  100. data/spec/system/selenium_spec.rb +19 -17
  101. data/spec/system/support/system_test_helper.rb +35 -0
  102. data/spec/system/watir_spec.rb +19 -17
  103. metadata +46 -16
  104. data/.github/workflows/push_gem.yml +0 -37
  105. data/.github/workflows/selenium.yml +0 -22
  106. data/.github/workflows/watir.yml +0 -22
  107. data/lib/generators/automation/templates/partials/android_caps.tt +0 -17
  108. data/lib/generators/automation/templates/partials/cross_platform_caps.tt +0 -25
  109. data/lib/generators/automation/templates/partials/ios_caps.tt +0 -18
  110. data/lib/generators/automation/templates/partials/selenium_account.tt +0 -9
  111. data/lib/generators/automation/templates/partials/selenium_login.tt +0 -34
  112. data/lib/generators/automation/templates/partials/watir_account.tt +0 -7
  113. data/lib/generators/automation/templates/partials/watir_login.tt +0 -32
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+ require_relative 'project_analyzer'
5
+ require_relative 'plan_builder'
6
+ require_relative 'migrator'
7
+
8
+ module Adopter
9
+ # :reek:TooManyMethods { enabled: false }
10
+ class AdoptMenu
11
+ WEB_AUTOMATIONS = %w[selenium capybara watir].freeze
12
+ TEST_FRAMEWORKS = %w[rspec cucumber minitest].freeze
13
+ CI_PLATFORMS = %w[github gitlab].freeze
14
+
15
+ def initialize
16
+ @prompt = TTY::Prompt.new
17
+ end
18
+
19
+ def run
20
+ source_path = ask_source_path
21
+ detection = preview_detection(source_path)
22
+ target_automation = ask_target_automation(detection[:automation])
23
+ target_framework = ask_target_framework(detection[:framework])
24
+ output_path = ask_output_path
25
+ ci_platform = ask_ci_platform(detection[:ci_platform])
26
+
27
+ execute_adoption(
28
+ source_path: source_path,
29
+ output_path: output_path,
30
+ target_automation: target_automation,
31
+ target_framework: target_framework,
32
+ ci_platform: ci_platform
33
+ )
34
+ end
35
+
36
+ # Programmatic entry point for raider_desktop and CLI --parameters
37
+ # :reek:LongParameterList { enabled: false }
38
+ def self.adopt(params)
39
+ validate_params!(params)
40
+
41
+ analysis = ProjectAnalyzer.new(params[:source_path]).analyze
42
+ plan = PlanBuilder.new(analysis, params).build
43
+ results = Migrator.new(plan).execute
44
+
45
+ { plan: plan, results: results }
46
+ end
47
+
48
+ def self.validate_params!(params)
49
+ raise ArgumentError, 'source_path is required' unless params[:source_path]
50
+ raise ArgumentError, 'output_path is required' unless params[:output_path]
51
+ raise ArgumentError, 'target_automation is required' unless params[:target_automation]
52
+ raise ArgumentError, 'target_framework is required' unless params[:target_framework]
53
+
54
+ unless WEB_AUTOMATIONS.include?(params[:target_automation])
55
+ raise ArgumentError, "target_automation must be one of: #{WEB_AUTOMATIONS.join(', ')}"
56
+ end
57
+
58
+ return if TEST_FRAMEWORKS.include?(params[:target_framework])
59
+
60
+ raise ArgumentError, "target_framework must be one of: #{TEST_FRAMEWORKS.join(', ')}"
61
+ end
62
+
63
+ private
64
+
65
+ def ask_source_path
66
+ path = @prompt.ask('Enter the path to your existing test project:') do |q|
67
+ q.required true
68
+ q.validate ->(input) { Dir.exist?(input) }
69
+ q.messages[:valid?] = 'Directory does not exist: %{value}'
70
+ end
71
+ File.expand_path(path)
72
+ end
73
+
74
+ def preview_detection(source_path)
75
+ detection = ProjectDetector.detect(source_path)
76
+ @prompt.say("\nDetected project settings:")
77
+ @prompt.say(" Automation: #{detection[:automation] || 'unknown'}")
78
+ @prompt.say(" Framework: #{detection[:framework] || 'unknown'}")
79
+ @prompt.say(" Browser: #{detection[:browser] || 'not detected'}")
80
+ @prompt.say(" URL: #{detection[:url] || 'not detected'}")
81
+ @prompt.say(" CI: #{detection[:ci_platform] || 'none'}")
82
+ @prompt.say('')
83
+ detection
84
+ end
85
+
86
+ def ask_target_automation(detected)
87
+ default = WEB_AUTOMATIONS.include?(detected) ? detected : nil
88
+ @prompt.select('Select target automation framework:', WEB_AUTOMATIONS.map(&:capitalize),
89
+ default: default&.capitalize) do |menu|
90
+ menu.choice :Quit, -> { exit }
91
+ end.downcase
92
+ end
93
+
94
+ def ask_target_framework(detected)
95
+ default = TEST_FRAMEWORKS.include?(detected) ? detected : nil
96
+ @prompt.select('Select target test framework:', TEST_FRAMEWORKS.map(&:capitalize),
97
+ default: default&.capitalize) do |menu|
98
+ menu.choice :Quit, -> { exit }
99
+ end.downcase
100
+ end
101
+
102
+ def ask_output_path
103
+ @prompt.ask('Enter the output path for the new Raider project:') do |q|
104
+ q.required true
105
+ end
106
+ end
107
+
108
+ def ask_ci_platform(detected)
109
+ choices = [{ name: 'Github Actions', value: 'github' },
110
+ { name: 'Gitlab CI/CD', value: 'gitlab' },
111
+ { name: 'No CI', value: nil }]
112
+ default = case detected
113
+ when 'github' then 'Github Actions'
114
+ when 'gitlab' then 'Gitlab CI/CD'
115
+ end
116
+ @prompt.select('Configure CI/CD?', choices, default: default)
117
+ end
118
+
119
+ def execute_adoption(params)
120
+ @prompt.say("\nAdopting project...")
121
+ result = self.class.adopt(params)
122
+ print_results(result[:plan], result[:results])
123
+ rescue MobileProjectError => e
124
+ @prompt.error(e.message)
125
+ rescue StandardError => e
126
+ @prompt.error("Adoption failed: #{e.message}")
127
+ end
128
+
129
+ def print_results(plan, results)
130
+ @prompt.say("\nAdoption complete!")
131
+ @prompt.say(" Pages converted: #{results[:pages]}")
132
+ @prompt.say(" Tests converted: #{results[:tests]}")
133
+ @prompt.say(" Features copied: #{results[:features]}")
134
+ @prompt.say(" Steps converted: #{results[:steps]}")
135
+
136
+ unless results[:errors].empty?
137
+ @prompt.warn("\nErrors:")
138
+ results[:errors].each { |e| @prompt.warn(" - #{e}") }
139
+ end
140
+
141
+ unless plan.warnings.empty?
142
+ @prompt.warn("\nWarnings:")
143
+ plan.warnings.each { |w| @prompt.warn(" - #{w}") }
144
+ end
145
+
146
+ @prompt.say("\nOutput: #{plan.output_path}")
147
+ @prompt.say("Run: cd #{plan.output_path} && bundle install")
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Adopter
4
+ module Converters
5
+ class BaseConverter
6
+ RAIDER_BASE_CLASSES = %w[Page BasePage AbstractPage].freeze
7
+
8
+ def convert_page(content, _page_info)
9
+ content
10
+ end
11
+
12
+ def convert_test(content, _test_info)
13
+ content
14
+ end
15
+
16
+ private
17
+
18
+ def ensure_frozen_string_literal(content)
19
+ return content if content.include?('frozen_string_literal')
20
+
21
+ "# frozen_string_literal: true\n\n#{content}"
22
+ end
23
+
24
+ def update_base_class(content, target_base = 'Page')
25
+ content.gsub(/class\s+(\w+)\s*<\s*([\w:]+)/) do
26
+ class_name = ::Regexp.last_match(1)
27
+ current_base = ::Regexp.last_match(2)
28
+ if raider_compatible_base?(current_base)
29
+ "class #{class_name} < #{target_base}"
30
+ else
31
+ "class #{class_name} < #{current_base}"
32
+ end
33
+ end
34
+ end
35
+
36
+ def raider_compatible_base?(base_class)
37
+ RAIDER_BASE_CLASSES.include?(base_class) || base_class.include?('Page')
38
+ end
39
+
40
+ def update_require_paths(content, target_automation)
41
+ result = content.dup
42
+ result = update_page_requires(result)
43
+ result = update_helper_requires(result, target_automation)
44
+ result
45
+ end
46
+
47
+ def update_page_requires(content)
48
+ content.gsub(%r{require_relative\s+['"]\.*/(?:pages?|page_objects)/(\w+)['"]}) do
49
+ page_name = ::Regexp.last_match(1)
50
+ "require_relative '../page_objects/pages/#{page_name}'"
51
+ end
52
+ end
53
+
54
+ def update_helper_requires(content, target_automation)
55
+ case target_automation
56
+ when 'capybara'
57
+ content.gsub(%r{require_relative\s+['"]\.*/(?:helpers?|support)/(?:driver|browser)_?\w*['"]},
58
+ "require_relative '../helpers/capybara_helper'")
59
+ when 'watir'
60
+ content.gsub(%r{require_relative\s+['"]\.*/(?:helpers?|support)/(?:driver|capybara)_?\w*['"]},
61
+ "require_relative '../helpers/browser_helper'")
62
+ else
63
+ content.gsub(%r{require_relative\s+['"]\.*/(?:helpers?|support)/(?:browser|capybara)_?\w*['"]},
64
+ "require_relative '../helpers/driver_helper'")
65
+ end
66
+ end
67
+
68
+ def remove_driver_arg(content)
69
+ content.gsub(/(\w+)\.new\(\s*(?:driver|browser|page)\s*\)/, '\1.new')
70
+ end
71
+
72
+ def swap_driver_arg(content, target_arg)
73
+ content.gsub(/(\w+)\.new\(\s*(?:driver|browser|page)\s*\)/, "\\1.new(#{target_arg})")
74
+ end
75
+
76
+ def driver_arg_for(target_automation)
77
+ case target_automation
78
+ when 'watir' then 'browser'
79
+ when 'capybara' then nil
80
+ else 'driver'
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_converter'
4
+
5
+ module Adopter
6
+ module Converters
7
+ class IdentityConverter < BaseConverter
8
+ def initialize(target_automation)
9
+ super()
10
+ @target_automation = target_automation
11
+ end
12
+
13
+ def convert_page(content, _page_info)
14
+ result = content.dup
15
+ result = ensure_frozen_string_literal(result)
16
+ result = update_base_class(result)
17
+ result = add_page_require(result)
18
+ result = update_page_instantiation(result)
19
+ result
20
+ end
21
+
22
+ def convert_test(content, _test_info)
23
+ result = content.dup
24
+ result = ensure_frozen_string_literal(result)
25
+ result = update_require_paths(result, @target_automation)
26
+ result = update_page_instantiation(result)
27
+ result
28
+ end
29
+
30
+ def convert_step(content)
31
+ result = content.dup
32
+ result = ensure_frozen_string_literal(result)
33
+ result = update_require_paths(result, @target_automation)
34
+ result = update_page_instantiation(result)
35
+ result
36
+ end
37
+
38
+ private
39
+
40
+ def add_page_require(content)
41
+ return content if content.include?("require_relative '../abstract/page'")
42
+
43
+ content.sub(/^(class\s)/, "require_relative '../abstract/page'\n\n\\1")
44
+ end
45
+
46
+ def update_page_instantiation(content)
47
+ arg = driver_arg_for(@target_automation)
48
+ if arg
49
+ swap_driver_arg(content, arg)
50
+ else
51
+ remove_driver_arg(content)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Adopter
4
+ class MigrationPlan
5
+ attr_reader :source_path, :output_path, :target_automation, :target_framework,
6
+ :ci_platform, :skeleton_structure, :converted_pages, :converted_tests,
7
+ :converted_features, :converted_steps, :gemfile_additions,
8
+ :config_overrides, :warnings, :manual_actions
9
+
10
+ # :reek:LongParameterList { enabled: false }
11
+ def initialize(attrs = {})
12
+ @source_path = attrs[:source_path]
13
+ @output_path = attrs[:output_path]
14
+ @target_automation = attrs[:target_automation]
15
+ @target_framework = attrs[:target_framework]
16
+ @ci_platform = attrs[:ci_platform]
17
+ @skeleton_structure = attrs[:skeleton_structure] || {}
18
+ @converted_pages = attrs[:converted_pages] || []
19
+ @converted_tests = attrs[:converted_tests] || []
20
+ @converted_features = attrs[:converted_features] || []
21
+ @converted_steps = attrs[:converted_steps] || []
22
+ @gemfile_additions = attrs[:gemfile_additions] || []
23
+ @config_overrides = attrs[:config_overrides] || {}
24
+ @warnings = attrs[:warnings] || []
25
+ @manual_actions = attrs[:manual_actions] || []
26
+ end
27
+
28
+ def to_h
29
+ {
30
+ source_path:,
31
+ output_path:,
32
+ target_automation:,
33
+ target_framework:,
34
+ ci_platform:,
35
+ skeleton_structure:,
36
+ converted_pages: converted_pages.map(&:to_h),
37
+ converted_tests: converted_tests.map(&:to_h),
38
+ converted_features: converted_features.map(&:to_h),
39
+ converted_steps: converted_steps.map(&:to_h),
40
+ gemfile_additions:,
41
+ config_overrides:,
42
+ warnings:,
43
+ manual_actions:
44
+ }
45
+ end
46
+
47
+ def to_json(*args)
48
+ require 'json'
49
+ to_h.to_json(*args)
50
+ end
51
+
52
+ def summary
53
+ {
54
+ pages: converted_pages.length,
55
+ tests: converted_tests.length,
56
+ features: converted_features.length,
57
+ steps: converted_steps.length,
58
+ custom_gems: gemfile_additions.length,
59
+ warnings: warnings.length,
60
+ manual_actions: manual_actions.length
61
+ }
62
+ end
63
+ end
64
+
65
+ ConvertedFile = Struct.new(:output_path, :content, :source_file, :conversion_notes, keyword_init: true) do
66
+ def to_h
67
+ {
68
+ output_path:,
69
+ content:,
70
+ source_file:,
71
+ conversion_notes:
72
+ }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ require_relative '../generators/invoke_generators'
6
+
7
+ module Adopter
8
+ class Migrator
9
+ attr_reader :plan, :results
10
+
11
+ def initialize(plan)
12
+ @plan = plan
13
+ @results = { pages: 0, tests: 0, features: 0, steps: 0, errors: [] }
14
+ end
15
+
16
+ def execute
17
+ generate_skeleton
18
+ write_converted_pages
19
+ write_converted_tests
20
+ write_converted_features
21
+ write_converted_steps
22
+ merge_gemfile
23
+ apply_config_overrides
24
+ results
25
+ end
26
+
27
+ private
28
+
29
+ # Phase A: Generate a standard Raider project skeleton
30
+ def generate_skeleton
31
+ InvokeGenerators.generate_framework(plan.skeleton_structure)
32
+ end
33
+
34
+ # Phase B: Overlay converted user files on top of the skeleton
35
+
36
+ def write_converted_pages
37
+ plan.converted_pages.each do |file|
38
+ write_file(file)
39
+ @results[:pages] += 1
40
+ end
41
+ end
42
+
43
+ def write_converted_tests
44
+ plan.converted_tests.each do |file|
45
+ write_file(file)
46
+ @results[:tests] += 1
47
+ end
48
+ end
49
+
50
+ def write_converted_features
51
+ plan.converted_features.each do |file|
52
+ write_file(file)
53
+ @results[:features] += 1
54
+ end
55
+ end
56
+
57
+ def write_converted_steps
58
+ plan.converted_steps.each do |file|
59
+ write_file(file)
60
+ @results[:steps] += 1
61
+ end
62
+ end
63
+
64
+ def write_file(converted_file)
65
+ FileUtils.mkdir_p(File.dirname(converted_file.output_path))
66
+ File.write(converted_file.output_path, converted_file.content)
67
+ rescue StandardError => e
68
+ @results[:errors] << "Failed to write #{converted_file.output_path}: #{e.message}"
69
+ end
70
+
71
+ def merge_gemfile
72
+ return if plan.gemfile_additions.empty?
73
+
74
+ gemfile_path = "#{plan.output_path}/Gemfile"
75
+ return unless File.exist?(gemfile_path)
76
+
77
+ content = File.read(gemfile_path)
78
+ additions = plan.gemfile_additions.reject { |gem| content.include?("'#{gem}'") }
79
+ return if additions.empty?
80
+
81
+ gem_lines = additions.map { |gem| "gem '#{gem}'" }.join("\n")
82
+ File.write(gemfile_path, "#{content}\n# Gems from source project\n#{gem_lines}\n")
83
+ end
84
+
85
+ def apply_config_overrides
86
+ return if plan.config_overrides.empty?
87
+
88
+ config_path = "#{plan.output_path}/config/config.yml"
89
+ return unless File.exist?(config_path)
90
+
91
+ config = YAML.safe_load(File.read(config_path), permitted_classes: [Symbol]) || {}
92
+ plan.config_overrides.each { |key, value| config[key.to_s] = value }
93
+ File.write(config_path, YAML.dump(config))
94
+ end
95
+ end
96
+ end