toys-release 0.1.1 → 0.3.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
- require_relative "semver"
5
+ require "toys/release/semver"
6
6
 
7
7
  module Toys
8
8
  module Release
@@ -14,16 +14,31 @@ module Toys
14
14
  ScopeInfo = ::Struct.new(:semver, :header)
15
15
 
16
16
  ##
17
- # Create a CommitTagSettings from either a tag name string (which will
18
- # default to patch releases) or a hash with fields.
17
+ # Create an empty settings for an unknown tag
19
18
  #
20
- def initialize(input)
19
+ # @param tag [String] Conventional commit tag
20
+ # @return [CommitTagSettings]
21
+ #
22
+ def self.empty(tag)
23
+ new({"tag" => tag, "header" => nil}, [])
24
+ end
25
+
26
+ ##
27
+ # @private
28
+ # Create a CommitTagSettings from an input hash.
29
+ #
30
+ def initialize(info, errors)
31
+ @tag = info.delete("tag").to_s
32
+ errors << "Commit tag missing : #{info}" if @tag.empty?
33
+ @header = info.fetch("header", @tag.upcase) || :hidden
34
+ info.delete("header")
35
+ @semver = load_semver(info.delete("semver"), errors)
21
36
  @scopes = {}
22
- case input
23
- when ::String
24
- init_from_string(input)
25
- when ::Hash
26
- init_from_hash(input)
37
+ info.delete("scopes")&.each do |scope_info|
38
+ load_scope(scope_info, errors)
39
+ end
40
+ info.each_key do |key|
41
+ errors << "Unknown key #{key.inspect} in configuration of tag #{@tag.inspect}"
27
42
  end
28
43
  end
29
44
 
@@ -70,86 +85,27 @@ module Toys
70
85
  end
71
86
  end
72
87
 
73
- ##
74
- # Make specified modifications to the settings
75
- #
76
- # @param input [Hash] Modifications
77
- #
78
- def modify(input)
79
- if input.key?("header") || input.key?("label")
80
- @header = input.fetch("header", input["label"]) || :hidden
81
- end
82
- if input.key?("semver")
83
- @semver = load_semver(input["semver"])
84
- end
85
- input["scopes"]&.each do |key, value|
86
- if value.nil?
87
- @scopes.delete(key)
88
- else
89
- scope_info = load_scope(key, value)
90
- @scopes[key] = scope_info if scope_info
91
- end
92
- end
93
- end
94
-
95
88
  private
96
89
 
97
- def init_from_string(input)
98
- @tag = input
99
- @header = @tag.upcase
100
- @semver = Semver::PATCH
101
- end
102
-
103
- def init_from_hash(input)
104
- if input.size == 1
105
- key = input.keys.first
106
- value = input.values.first
107
- if value.is_a?(::Hash)
108
- @tag = key
109
- load_hash(value)
110
- elsif key == "tag"
111
- @tag = value
112
- @header = @tag.upcase
113
- @semver = Semver::PATCH
114
- else
115
- @tag = key
116
- @header = @tag.upcase
117
- @semver = load_semver(value)
118
- end
119
- else
120
- @tag = input["tag"]
121
- raise "tag missing in #{input}" unless @tag
122
- load_hash(input)
90
+ def load_scope(info, errors)
91
+ scope = info.delete("scope").to_s
92
+ errors << "Commit tag scope missing under tag #{@tag.inspect} : #{info}" if scope.empty?
93
+ scope_semver = load_semver(info.delete("semver"), errors, scope) if info.key?("semver")
94
+ scope_header = info.fetch("header", :inherit) || :hidden
95
+ info.delete("header")
96
+ scope_header = nil if scope_header == :inherit
97
+ @scopes[scope] = ScopeInfo.new(scope_semver, scope_header)
98
+ info.each_key do |key|
99
+ errors << "Unknown key #{key.inspect} in configuration of tag \"#{@tag}(#{scope})\""
123
100
  end
124
101
  end
125
102
 
126
- def load_hash(input)
127
- @header = input.fetch("header", input.fetch("label", @tag.upcase)) || :hidden
128
- @semver = load_semver(input.fetch("semver", "patch"))
129
- input["scopes"]&.each do |key, value|
130
- scope_info = load_scope(key, value)
131
- @scopes[key] = scope_info if scope_info
132
- end
133
- end
134
-
135
- def load_scope(key, value)
136
- case value
137
- when ::String
138
- semver = load_semver(value, key)
139
- ScopeInfo.new(semver, nil)
140
- when ::Hash
141
- semver = load_semver(value["semver"], key) if value.key?("semver")
142
- header = value.fetch("header", value.fetch("label", :inherit)) || :hidden
143
- header = nil if header == :inherit
144
- ScopeInfo.new(semver, header)
145
- end
146
- end
147
-
148
- def load_semver(value, scope = nil)
103
+ def load_semver(value, errors, scope = nil)
149
104
  result = Semver.for_name(value || "none")
150
105
  unless result
151
106
  tag = scope ? "#{@tag}(#{scope})" : @tag
152
- raise "Unknown semver: #{value} for tag #{tag}"
107
+ errors << "Unknown semver: #{value} for tag #{tag}"
108
+ result = Semver::NONE
153
109
  end
154
110
  result
155
111
  end
@@ -160,6 +116,7 @@ module Toys
160
116
  #
161
117
  class ComponentSettings
162
118
  ##
119
+ # @private
163
120
  # Create a ComponentSettings from input data structures
164
121
  #
165
122
  # @param info [Hash] Nested hash input
@@ -167,13 +124,13 @@ module Toys
167
124
  # components
168
125
  #
169
126
  def initialize(repo_settings, info, has_multiple_components)
170
- @name = info["name"]
171
- @type = info["type"] || "component"
172
-
127
+ @name = info.delete("name").to_s
173
128
  read_path_info(info, has_multiple_components)
174
129
  read_file_modification_info(info)
175
130
  read_gh_pages_info(repo_settings, info, has_multiple_components)
176
131
  read_steps_info(repo_settings, info)
132
+ read_commit_tag_info(repo_settings, info)
133
+ check_problems(repo_settings, info)
177
134
  end
178
135
 
179
136
  ##
@@ -181,12 +138,6 @@ module Toys
181
138
  #
182
139
  attr_reader :name
183
140
 
184
- ##
185
- # @return [String] The type of component. Default is `"component"`.
186
- # Subclasses may define other types.
187
- #
188
- attr_reader :type
189
-
190
141
  ##
191
142
  # @return [String] The directory within the repo in which the component
192
143
  # is located
@@ -245,6 +196,24 @@ module Toys
245
196
  #
246
197
  attr_reader :steps
247
198
 
199
+ ##
200
+ # @return [Array<CommitTagSettings>] The conventional commit types
201
+ # recognized as release-triggering, along with information on the
202
+ # change they map to.
203
+ #
204
+ attr_reader :commit_tags
205
+
206
+ ##
207
+ # @return [String] Header for breaking changes in a changelog
208
+ #
209
+ attr_reader :breaking_change_header
210
+
211
+ ##
212
+ # @return [String] Notice displayed in the changelog when there are
213
+ # otherwise no significant updates in the release
214
+ #
215
+ attr_reader :no_significant_updates_notice
216
+
248
217
  ##
249
218
  # @return [StepSettings,nil] The unique step with the given name
250
219
  #
@@ -252,41 +221,69 @@ module Toys
252
221
  steps.find { |t| t.name == name }
253
222
  end
254
223
 
224
+ ##
225
+ # Look up the settings for the given named tag.
226
+ #
227
+ # @param tag [String] Conventional commit tag to look up
228
+ # @return [CommitTagSettings] The commit tag settings for the given tag
229
+ #
230
+ def commit_tag_named(tag)
231
+ commit_tags.find { |elem| elem.tag == tag } || CommitTagSettings.empty(tag)
232
+ end
233
+
255
234
  private
256
235
 
257
236
  def read_path_info(info, has_multiple_components)
258
- @directory = info["directory"] || (has_multiple_components ? name : ".")
259
- @include_globs = Array(info["include_globs"])
260
- @exclude_globs = Array(info["exclude_globs"])
237
+ @directory = info.delete("directory") || (has_multiple_components ? name : ".")
238
+ @include_globs = Array(info.delete("include_globs"))
239
+ @exclude_globs = Array(info.delete("exclude_globs"))
261
240
  end
262
241
 
263
242
  def read_file_modification_info(info)
264
- segments = info["name"].split("-")
243
+ segments = @name.split("-")
265
244
  name_path = segments.join("/")
266
- @version_rb_path = info["version_rb_path"] || "lib/#{name_path}/version.rb"
267
- @version_constant = info["version_constant"] ||
245
+ @version_rb_path = info.delete("version_rb_path") || "lib/#{name_path}/version.rb"
246
+ @version_constant = info.delete("version_constant") ||
268
247
  (segments.map { |seg| camelize(seg) } + ["VERSION"])
269
248
  @version_constant = @version_constant.split("::") if @version_constant.is_a?(::String)
270
- @changelog_path = info["changelog_path"] || "CHANGELOG.md"
249
+ @changelog_path = info.delete("changelog_path") || "CHANGELOG.md"
271
250
  end
272
251
 
273
252
  def read_gh_pages_info(repo_settings, info, has_multiple_components)
274
- @gh_pages_directory = info["gh_pages_directory"] || (has_multiple_components ? name : ".")
275
- @gh_pages_version_var = info["gh_pages_version_var"] ||
276
- (has_multiple_components ? "version_#{name}".tr("-", "_") : "version")
277
253
  @gh_pages_enabled = info.fetch("gh_pages_enabled") do |_key|
278
254
  repo_settings.gh_pages_enabled ||
279
255
  info.key?("gh_pages_directory") ||
280
256
  info.key?("gh_pages_version_var")
281
257
  end
258
+ info.delete("gh_pages_enabled")
259
+ @gh_pages_directory = info.delete("gh_pages_directory") || (has_multiple_components ? name : ".")
260
+ @gh_pages_version_var = info.delete("gh_pages_version_var") ||
261
+ (has_multiple_components ? "version_#{name}".tr("-", "_") : "version")
282
262
  end
283
263
 
284
264
  def read_steps_info(repo_settings, info)
285
- @steps = info["steps"] ? repo_settings.read_steps(info["steps"]) : repo_settings.default_steps(@type)
286
- @steps = repo_settings.modify_steps(@steps, info["modify_steps"] || [])
287
- @steps = repo_settings.prepend_steps(@steps, info["prepend_steps"] || [])
288
- @steps = repo_settings.append_steps(@steps, info["append_steps"] || [])
289
- @steps = repo_settings.delete_steps(@steps, info["delete_steps"] || [])
265
+ @steps =
266
+ if info.key?("steps")
267
+ repo_settings.read_steps(info.delete("steps"))
268
+ else
269
+ repo_settings.steps.map(&:deep_copy)
270
+ end
271
+ @steps = repo_settings.modify_steps(@steps, info.delete("modify_steps") || [])
272
+ @steps = repo_settings.prepend_steps(@steps, info.delete("prepend_steps") || [])
273
+ @steps = repo_settings.append_steps(@steps, info.delete("append_steps") || [])
274
+ @steps = repo_settings.delete_steps(@steps, info.delete("delete_steps") || [])
275
+ end
276
+
277
+ def read_commit_tag_info(repo_settings, info)
278
+ @commit_tags =
279
+ if info.key?("commit_tags")
280
+ repo_settings.read_commit_tags(info.delete("commit_tags"))
281
+ else
282
+ repo_settings.commit_tags.dup
283
+ end
284
+ @breaking_change_header = info.delete("breaking_change_header") || repo_settings.breaking_change_header
285
+ @no_significant_updates_notice =
286
+ info.delete("no_significant_updates_notice") || repo_settings.no_significant_updates_notice
290
287
  end
291
288
 
292
289
  def camelize(str)
@@ -296,16 +293,173 @@ module Toys
296
293
  .gsub(/_+/, "_")
297
294
  .gsub(/(?:^|_)([a-zA-Z])/) { ::Regexp.last_match(1).upcase }
298
295
  end
296
+
297
+ def check_problems(repo_settings, info)
298
+ info.each_key do |key|
299
+ repo_settings.errors << "Unknown key #{key.inspect} in component #{@name.inspect}"
300
+ end
301
+ repo_settings.errors << 'Component is missing required key "name"' if @name.empty?
302
+ end
303
+ end
304
+
305
+ ##
306
+ # Configuration of input settings for a step.
307
+ # An input declares a dependency on a step, and copies any files output by
308
+ # that dependency.
309
+ #
310
+ class InputSettings
311
+ ##
312
+ # @private
313
+ # Construct input settings
314
+ #
315
+ # @param info [Hash,String] Config data
316
+ #
317
+ def initialize(info, errors, containing_step_name)
318
+ @step_name = @dest = @source_path = @dest_path = nil
319
+ case info
320
+ when ::String
321
+ @step_name = info
322
+ @dest = "component"
323
+ when ::Hash
324
+ @step_name = info.delete("name").to_s
325
+ if @step_name.empty?
326
+ errors << "Missing required key \"name\" in input for step #{containing_step_name.inspect}"
327
+ end
328
+ @dest = info.delete("dest")
329
+ if @dest == false
330
+ @dest = "none"
331
+ elsif @dest.nil?
332
+ @dest = "component"
333
+ end
334
+ @source_path = info.delete("source_path")
335
+ @dest_path = info.delete("dest_path")
336
+ @collisions = info.delete("collisions") || "error"
337
+ info.each_key do |key|
338
+ errors << "Unknown key #{key.inspect} in input for step #{containing_step_name.inspect}"
339
+ end
340
+ end
341
+ end
342
+
343
+ ##
344
+ # @return [String] Name of the step to copy data from.
345
+ #
346
+ attr_reader :step_name
347
+
348
+ ##
349
+ # @return [String,false] Where to copy data to. Possible values are
350
+ # "component", "repo_root", "output", "temp", and "none". If "none",
351
+ # no copying is performed and this input declares a dependency only.
352
+ #
353
+ attr_reader :dest
354
+
355
+ ##
356
+ # @return [String,nil] Path in the source to copy from. Can be a path to
357
+ # a file or a directory. If nil, copy everything from the input.
358
+ #
359
+ attr_reader :source_path
360
+
361
+ ##
362
+ # @return [String,nil] Path in the destination to copy to, relative to
363
+ # the destination. If nil, uses the source path.
364
+ #
365
+ attr_reader :dest_path
366
+
367
+ ##
368
+ # @return [String] What to do if a collision occurs. Possible values are
369
+ # "error", "replace", and "keep".
370
+ #
371
+ attr_reader :collisions
372
+
373
+ ##
374
+ # @return [Hash] the hash representation
375
+ #
376
+ def to_h
377
+ {
378
+ "name" => step_name,
379
+ "dest" => dest,
380
+ "source_path" => source_path,
381
+ "dest_path" => dest_path,
382
+ "collisions" => collisions,
383
+ }
384
+ end
385
+ end
386
+
387
+ ##
388
+ # Configuration of output info for a step.
389
+ # An output automatically copies files from the repo directory to this
390
+ # step's output where they can be imported by another step.
391
+ #
392
+ class OutputSettings
393
+ ##
394
+ # @private
395
+ # Construct output settings
396
+ #
397
+ # @param info [Hash,String] Config data
398
+ #
399
+ def initialize(info, errors, containing_step_name)
400
+ @source = @source_path = @dest_path = nil
401
+ case info
402
+ when ::String
403
+ @source_path = info
404
+ @source = "component"
405
+ when ::Hash
406
+ @source = info.delete("source") || "component"
407
+ @source_path = info.delete("source_path")
408
+ @dest_path = info.delete("dest_path")
409
+ @collisions = info.delete("collisions") || "error"
410
+ info.each_key do |key|
411
+ errors << "Unknown key #{key.inspect} in output for step #{containing_step_name.inspect}"
412
+ end
413
+ end
414
+ end
415
+
416
+ ##
417
+ # @return [String] Where to copy data from. Possible values are
418
+ # "component", "repo_root", and "temp".
419
+ #
420
+ attr_reader :source
421
+
422
+ ##
423
+ # @return [String,nil] Path to copy from, relative to the source. Can be
424
+ # a file or a directory. If nil, copy everything in the source.
425
+ #
426
+ attr_reader :source_path
427
+
428
+ ##
429
+ # @return [String,nil] Path in the step's output to copy to.
430
+ # If nil, uses the source path.
431
+ #
432
+ attr_reader :dest_path
433
+
434
+ ##
435
+ # @return [String] What to do if a collision occurs. Possible values are
436
+ # "error", "replace", and "keep".
437
+ #
438
+ attr_reader :collisions
439
+
440
+ ##
441
+ # @return [Hash] the hash representation
442
+ #
443
+ def to_h
444
+ {
445
+ "source" => source,
446
+ "source_path" => source_path,
447
+ "dest_path" => dest_path,
448
+ "collisions" => collisions,
449
+ }
450
+ end
299
451
  end
300
452
 
301
453
  ##
454
+ # @private
302
455
  # Configuration of a step
303
456
  #
304
457
  class StepSettings
305
- def initialize(name, type, options)
306
- @name = name
307
- @type = type
308
- @options = options
458
+ ##
459
+ # Create a StepSettings
460
+ #
461
+ def initialize(info, errors)
462
+ from_h(info.dup, errors)
309
463
  end
310
464
 
311
465
  ##
@@ -318,18 +472,66 @@ module Toys
318
472
  #
319
473
  attr_reader :type
320
474
 
475
+ ##
476
+ # @return [boolean] Whether this step is explicitly requested
477
+ #
478
+ def requested?
479
+ @requested
480
+ end
481
+
482
+ ##
483
+ # @return [Array<InputSettings>] Inputs for this step
484
+ #
485
+ attr_reader :inputs
486
+
487
+ ##
488
+ # @return [Array<OutputSettings>] Extra outputs for this step
489
+ #
490
+ attr_reader :outputs
491
+
321
492
  ##
322
493
  # @return [Hash{String=>Object}] Options for this step
323
494
  #
324
495
  attr_reader :options
325
496
 
497
+ ##
498
+ # @return [Hash] the hash representation
499
+ #
500
+ def to_h
501
+ {
502
+ "name" => name,
503
+ "type" => type,
504
+ "run" => requested?,
505
+ "inputs" => inputs.map(&:to_h),
506
+ "outputs" => outputs.map(&:to_h),
507
+ }.merge(RepoSettings.deep_copy(options))
508
+ end
509
+
326
510
  ##
327
511
  # Make a deep copy
328
512
  #
329
513
  # @return [StepSettings] A deep copy
330
514
  #
331
515
  def deep_copy
332
- StepSettings.new(name, type, RepoSettings.deep_copy(options))
516
+ StepSettings.new(to_h, [])
517
+ end
518
+
519
+ ##
520
+ # @private
521
+ # Initialize the step from the given hash.
522
+ # The hash will be deconstructed in place.
523
+ #
524
+ def from_h(info, errors)
525
+ @type = info.delete("type") || info["name"] || "noop"
526
+ @name = info.delete("name") || "_anon_#{@type}_#{object_id}"
527
+ @requested = info.delete("run") ? true : false
528
+ @inputs = Array(info.delete("inputs")).map do |input_info|
529
+ InputSettings.new(input_info, errors, @name)
530
+ end
531
+ @outputs = Array(info.delete("outputs")).map do |output_info|
532
+ OutputSettings.new(output_info, errors, @name)
533
+ end
534
+ @options = info
333
535
  end
334
536
  end
335
537
 
@@ -368,6 +570,7 @@ module Toys
368
570
  end
369
571
 
370
572
  ##
573
+ # @private
371
574
  # Create a repo configuration object.
372
575
  #
373
576
  # @param info [Hash] Configuration hash read from JSON.
@@ -375,14 +578,14 @@ module Toys
375
578
  def initialize(info)
376
579
  @warnings = []
377
580
  @errors = []
378
- @default_component_name = nil
379
581
  read_global_info(info)
582
+ read_required_checks_info(info)
380
583
  read_label_info(info)
381
- read_commit_lint_info(info)
382
- read_commit_tag_info(info)
584
+ read_default_commit_tag_info(info)
383
585
  read_default_step_info(info)
384
586
  read_component_info(info)
385
587
  read_coordination_info(info)
588
+ check_global_problems(info)
386
589
  end
387
590
 
388
591
  ##
@@ -417,11 +620,6 @@ module Toys
417
620
  #
418
621
  attr_reader :git_user_email
419
622
 
420
- ##
421
- # @return [String] The name of the default component to release
422
- #
423
- attr_reader :default_component_name
424
-
425
623
  ##
426
624
  # @return [Array<Array<String>>] An array of groups of component names
427
625
  # whose releases should be coordinated.
@@ -435,12 +633,6 @@ module Toys
435
633
  #
436
634
  attr_reader :required_checks_regexp
437
635
 
438
- ##
439
- # @return [Regexp,nil] A regular expression identifying all the
440
- # release-related GitHub checks
441
- #
442
- attr_reader :release_jobs_regexp
443
-
444
636
  ##
445
637
  # @return [Numeric] The number of seconds that releases will wait for
446
638
  # checks to complete.
@@ -453,23 +645,18 @@ module Toys
453
645
  attr_reader :gh_pages_enabled
454
646
 
455
647
  ##
456
- # @return [Array<String>] The merge strategies allowed when linting
457
- # commit messages.
648
+ # @return [Array<CommitTagSettings>] The conventional commit types
649
+ # recognized as release-triggering, along with information on the
650
+ # change they map to.
458
651
  #
459
- attr_reader :commit_lint_merge
652
+ attr_reader :commit_tags
460
653
 
461
654
  ##
462
- # @return [Array<String>] The allowed conventional commit types when
463
- # linting commit messages.
655
+ # Get the build step pipeline
464
656
  #
465
- attr_reader :commit_lint_allowed_types
466
-
467
- ##
468
- # @return [Hash{String=>CommitTagSettings}] The conventional commit types
469
- # recognized as release-triggering, along with the type of change they
470
- # map to.
657
+ # @return [Array<StepSettings>] Step pipeline
471
658
  #
472
- attr_reader :release_commit_tags
659
+ attr_reader :steps
473
660
 
474
661
  ##
475
662
  # @return [String] Header for breaking changes in a changelog
@@ -477,7 +664,8 @@ module Toys
477
664
  attr_reader :breaking_change_header
478
665
 
479
666
  ##
480
- # @return [String] No significant updates notice
667
+ # @return [String] Notice displayed in the changelog when there are
668
+ # otherwise no significant updates in the release
481
669
  #
482
670
  attr_reader :no_significant_updates_notice
483
671
 
@@ -506,6 +694,16 @@ module Toys
506
694
  #
507
695
  attr_reader :release_branch_prefix
508
696
 
697
+ ##
698
+ # Look up the settings for the given named tag.
699
+ #
700
+ # @param tag [String] Conventional commit tag to look up
701
+ # @return [CommitTagSettings] The commit tag settings for the given tag
702
+ #
703
+ def commit_tag_named(tag)
704
+ commit_tags.find { |elem| elem.tag == tag } || CommitTagSettings.empty(tag)
705
+ end
706
+
509
707
  ##
510
708
  # @return [String] The owner of the repo
511
709
  #
@@ -535,21 +733,6 @@ module Toys
535
733
  @enable_release_automation
536
734
  end
537
735
 
538
- ##
539
- # @return [boolean] Whether conventional commit linting errors should fail
540
- # GitHub checks.
541
- #
542
- def commit_lint_fail_checks?
543
- @commit_lint_fail_checks
544
- end
545
-
546
- ##
547
- # @return [boolean] Whether to perform conventional commit linting.
548
- #
549
- def commit_lint_active?
550
- @commit_lint_active
551
- end
552
-
553
736
  ##
554
737
  # @return [Array<String>] A list of all component names.
555
738
  #
@@ -575,34 +758,17 @@ module Toys
575
758
  @components[name]
576
759
  end
577
760
 
578
- ##
579
- # Get the default step pipeline settings for a component type
580
- #
581
- # @param component_type [String] Type of component
582
- # @return [Array<StepSettings>] Step pipeline
583
- #
584
- def default_steps(component_type)
585
- (@default_steps[component_type] || @default_steps["component"]).map(&:deep_copy)
586
- end
587
-
588
761
  # @private
589
762
  def read_steps(info)
590
- steps = []
591
- info.each do |step_info|
592
- step_info = step_info.dup
593
- name = step_info.delete("name")
594
- type = step_info.delete("type")
595
- if type
596
- steps << StepSettings.new(name, type, step_info)
597
- else
598
- @errors << "No step type provided for step #{name.inspect}"
599
- end
600
- end
601
- steps
763
+ Array(info).map { |step_info| StepSettings.new(step_info, @errors) }
602
764
  end
603
765
 
604
766
  # @private
605
- def modify_steps(steps, modifications)
767
+ def modify_steps(steps, modifications) # rubocop:disable Metrics/MethodLength
768
+ unless modifications.is_a?(::Array)
769
+ @errors << "modify_steps expected an array of modification dictionaries"
770
+ return steps
771
+ end
606
772
  modifications.each do |mod_data|
607
773
  mod_name = mod_data.delete("name")
608
774
  mod_type = mod_data.delete("type")
@@ -610,14 +776,15 @@ module Toys
610
776
  steps.each do |step|
611
777
  next if (mod_name && step.name != mod_name) || (mod_type && step.type != mod_type)
612
778
  count += 1
613
- opts = step.options
779
+ modified_info = step.to_h
614
780
  mod_data.each do |key, value|
615
781
  if value.nil?
616
- opts.delete(key)
782
+ modified_info.delete(key)
617
783
  else
618
- opts[key] = value
784
+ modified_info[key] = value
619
785
  end
620
786
  end
787
+ step.from_h(modified_info, @errors)
621
788
  end
622
789
  if count.zero?
623
790
  @errors << "Unable to find step to modify for name=#{mod_name.inspect} and type=#{mod_type.inspect}."
@@ -628,86 +795,112 @@ module Toys
628
795
 
629
796
  # @private
630
797
  def prepend_steps(steps, info)
631
- pre_steps = read_steps(info)
632
- pre_steps + steps
798
+ before = []
799
+ insert = []
800
+ after = steps
801
+ case info
802
+ when ::Hash
803
+ if (before_name = info["before"])
804
+ before_index = steps.find_index { |step| step.name == before_name }
805
+ if before_index
806
+ before = steps[...before_index]
807
+ after = steps[before_index..]
808
+ else
809
+ @errors << "Unable to find step named #{before_name} in prepend_steps.before"
810
+ end
811
+ end
812
+ if (steps_info = info["steps"]).is_a?(::Array)
813
+ insert = read_steps(steps_info)
814
+ else
815
+ @errors << "steps expected in prepend_steps"
816
+ end
817
+ when ::Array
818
+ insert = read_steps(info)
819
+ else
820
+ @errors << "prepend_steps expected a hash or array"
821
+ end
822
+ before + insert + after
633
823
  end
634
824
 
635
825
  # @private
636
826
  def append_steps(steps, info)
637
- post_steps = read_steps(info)
638
- steps + post_steps
827
+ before = steps
828
+ insert = []
829
+ after = []
830
+ case info
831
+ when ::Hash
832
+ if (after_name = info["after"])
833
+ after_index = steps.find_index { |step| step.name == after_name }
834
+ if after_index
835
+ before = steps[..after_index]
836
+ after = steps[(after_index + 1)..]
837
+ else
838
+ @errors << "Unable to find step named #{after_name} in append_steps.after"
839
+ end
840
+ end
841
+ if (steps_info = info["steps"]).is_a?(::Array)
842
+ insert = read_steps(steps_info)
843
+ else
844
+ @errors << "steps expected in append_steps"
845
+ end
846
+ when ::Array
847
+ insert = read_steps(info)
848
+ else
849
+ @errors << "append_steps expected a hash or array"
850
+ end
851
+ before + insert + after
639
852
  end
640
853
 
641
854
  # @private
642
855
  def delete_steps(steps, info)
643
- info.each do |del_name|
644
- index = steps.find_index { |step| step.name == del_name }
645
- if index
646
- steps.delete_at(index)
647
- else
648
- @errors << "Unable to find step named #{del_name} to delete."
856
+ if info.is_a?(::Array)
857
+ info.each do |del_name|
858
+ index = steps.find_index { |step| step.name == del_name }
859
+ if index
860
+ steps.delete_at(index)
861
+ else
862
+ @errors << "Unable to find step named #{del_name} to delete."
863
+ end
649
864
  end
865
+ else
866
+ @errors << "delete_steps expected an array of names"
650
867
  end
651
868
  steps
652
869
  end
653
870
 
871
+ # @private
872
+ def read_commit_tags(info)
873
+ Array(info).map { |tag_info| CommitTagSettings.new(tag_info, @errors) }
874
+ end
875
+
654
876
  private
655
877
 
656
878
  DEFAULT_MAIN_BRAMCH = "main"
657
879
  private_constant :DEFAULT_MAIN_BRAMCH
658
880
 
659
- DEFAULT_RELEASE_COMMIT_TAGS = [
660
- {
661
- "tag" => "feat",
662
- "header" => "ADDED",
663
- "semver" => "minor",
664
- }.freeze,
665
- {
666
- "tag" => "fix",
667
- "header" => "FIXED",
668
- }.freeze,
669
- "docs",
670
- ].freeze
671
- private_constant :DEFAULT_RELEASE_COMMIT_TAGS
672
-
673
- DEFAULT_STEPS = {
674
- "component" => [
675
- {
676
- "name" => "github-release",
677
- "type" => "GitHubRelease",
678
- }.freeze,
679
- ].freeze,
680
- "gem" => [
681
- {
682
- "name" => "bundle",
683
- "type" => "Bundle",
684
- }.freeze,
685
- {
686
- "name" => "build-gem",
687
- "type" => "BuildGem",
688
- }.freeze,
689
- {
690
- "name" => "build-yard",
691
- "type" => "BuildYard",
692
- "require_gh_pages_enabled" => true,
693
- }.freeze,
694
- {
695
- "name" => "github-release",
696
- "type" => "GitHubRelease",
697
- }.freeze,
698
- {
699
- "name" => "release-gem",
700
- "type" => "ReleaseGem",
701
- "input" => "build-gem",
702
- }.freeze,
703
- {
704
- "name" => "push-gh-pages",
705
- "type" => "PushGhPages",
706
- "input" => "build-yard",
707
- }.freeze,
708
- ].freeze,
709
- }.freeze
710
- private_constant :DEFAULT_STEPS
881
+ DEFAULT_COMMIT_TAGS_YAML = <<~STRING
882
+ - tag: feat
883
+ semver: minor
884
+ header: ADDED
885
+ - tag: fix
886
+ semver: patch
887
+ header: FIXED
888
+ - tag: docs
889
+ semver: patch
890
+ STRING
891
+ private_constant :DEFAULT_COMMIT_TAGS_YAML
892
+
893
+ DEFAULT_STEPS_YAML = <<~STRING
894
+ - name: bundle
895
+ - name: build_gem
896
+ - name: build_yard
897
+ - name: release_github
898
+ - name: release_gem
899
+ source: build_gem
900
+ - name: push_gh_pages
901
+ source: build_yard
902
+ STRING
903
+ private_constant :DEFAULT_STEPS_YAML
711
904
 
712
905
  DEFAULT_BREAKING_CHANGE_HEADER = "BREAKING CHANGE"
713
906
  private_constant :DEFAULT_BREAKING_CHANGE_HEADER
@@ -728,114 +921,71 @@ module Toys
728
921
  private_constant :DEFAULT_RELEASE_COMPLETE_LABEL
729
922
 
730
923
  def read_global_info(info)
731
- @main_branch = info["main_branch"] || DEFAULT_MAIN_BRAMCH
732
- @repo_path = info["repo"]
733
- @signoff_commits = info["signoff_commits"] ? true : false
734
- @gh_pages_enabled = info["gh_pages_enabled"] ? true : false
735
- @enable_release_automation = info["enable_release_automation"] != false
736
- required_checks = info["required_checks"]
737
- @required_checks_regexp = required_checks == false ? nil : ::Regexp.new(required_checks.to_s)
738
- @required_checks_timeout = info["required_checks_timeout"] || 900
739
- @release_jobs_regexp = ::Regexp.new(info["release_jobs_regexp"] || "^release-")
740
- @release_branch_prefix = info["release_branch_prefix"] || "release"
741
- @git_user_name = info["git_user_name"]
742
- @git_user_email = info["git_user_email"]
743
- @errors << "Repo key missing from releases.yml" unless @repo_path
924
+ @main_branch = info.delete("main_branch") || DEFAULT_MAIN_BRAMCH
925
+ @repo_path = info.delete("repo")
926
+ @signoff_commits = info.delete("signoff_commits") ? true : false
927
+ @gh_pages_enabled = info.delete("gh_pages_enabled") ? true : false
928
+ @enable_release_automation = info.delete("enable_release_automation") != false
929
+ @release_branch_prefix = info.delete("release_branch_prefix") || "release"
930
+ @git_user_name = info.delete("git_user_name")
931
+ @git_user_email = info.delete("git_user_email")
932
+ end
933
+
934
+ def read_required_checks_info(info)
935
+ required_checks = info.delete("required_checks")
936
+ @required_checks_regexp =
937
+ case required_checks
938
+ when false
939
+ nil
940
+ when true
941
+ //
942
+ else
943
+ ::Regexp.new(required_checks.to_s)
944
+ end
945
+ @required_checks_timeout = info.delete("required_checks_timeout") || 900
744
946
  end
745
947
 
746
948
  def read_label_info(info)
747
- @release_pending_label = info["release_pending_label"] || DEFAULT_RELEASE_PENDING_LABEL
748
- @release_error_label = info["release_error_label"] || DEFAULT_RELEASE_ERROR_LABEL
749
- @release_aborted_label = info["release_aborted_label"] || DEFAULT_RELEASE_ABORTED_LABEL
750
- @release_complete_label = info["release_complete_label"] || DEFAULT_RELEASE_COMPLETE_LABEL
751
- end
752
-
753
- def read_commit_lint_info(info)
754
- info = info["commit_lint"]
755
- @commit_lint_active = !info.nil?
756
- info = {} unless info.is_a?(::Hash)
757
- @commit_lint_fail_checks = info["fail_checks"] ? true : false
758
- @commit_lint_merge = Array(info["merge"] || ["squash", "merge", "rebase"])
759
- @commit_lint_allowed_types = info["allowed_types"]
760
- if @commit_lint_allowed_types
761
- @commit_lint_allowed_types = Array(@commit_lint_allowed_types).map(&:downcase)
762
- end
949
+ @release_pending_label = info.delete("release_pending_label") || DEFAULT_RELEASE_PENDING_LABEL
950
+ @release_error_label = info.delete("release_error_label") || DEFAULT_RELEASE_ERROR_LABEL
951
+ @release_aborted_label = info.delete("release_aborted_label") || DEFAULT_RELEASE_ABORTED_LABEL
952
+ @release_complete_label = info.delete("release_complete_label") || DEFAULT_RELEASE_COMPLETE_LABEL
763
953
  end
764
954
 
765
- def read_commit_tag_info(info)
766
- @release_commit_tags = read_commit_tag_info_set(info["release_commit_tags"] || DEFAULT_RELEASE_COMMIT_TAGS)
767
- info["modify_release_commit_tags"]&.each do |tag, data|
768
- if data.nil?
769
- @release_commit_tags.delete(tag)
770
- elsif (tag_settings = @release_commit_tags[tag])
771
- tag_settings.modify(data)
772
- end
773
- end
774
- @release_commit_tags = read_commit_tag_info_set(info["prepend_release_commit_tags"]).merge(@release_commit_tags)
775
- @release_commit_tags.merge!(read_commit_tag_info_set(info["append_release_commit_tags"]))
776
- @breaking_change_header = info["breaking_change_header"] || DEFAULT_BREAKING_CHANGE_HEADER
777
- @no_significant_updates_notice = info["no_significant_updates_notice"] || DEFAULT_NO_SIGNIFICANT_UPDATES_NOTICE
955
+ def read_default_commit_tag_info(info)
956
+ @commit_tags = read_commit_tags(info.delete("commit_tags") || ::YAML.load(DEFAULT_COMMIT_TAGS_YAML))
957
+ @breaking_change_header = info.delete("breaking_change_header") || DEFAULT_BREAKING_CHANGE_HEADER
958
+ @no_significant_updates_notice =
959
+ info.delete("no_significant_updates_notice") || DEFAULT_NO_SIGNIFICANT_UPDATES_NOTICE
778
960
  end
779
961
 
780
- def read_commit_tag_info_set(input)
781
- input.to_h do |value|
782
- settings = CommitTagSettings.new(value)
783
- [settings.tag, settings]
784
- end
785
- end
786
-
787
- def read_default_step_info(info) # rubocop:disable Metrics/AbcSize
788
- default_step_data = info["default_steps"] || DEFAULT_STEPS
789
- @default_steps = {}
790
- default_step_data.each do |key, data|
791
- @default_steps[key] = read_steps(data)
792
- end
793
- (info["modify_default_steps"] || {}).each do |key, data|
794
- @default_steps[key] = modify_steps(@default_steps[key], data)
795
- end
796
- (info["append_default_steps"] || {}).each do |key, data|
797
- @default_steps[key] = append_steps(@default_steps[key], data)
798
- end
799
- (info["prepend_default_steps"] || {}).each do |key, data|
800
- @default_steps[key] = prepend_steps(@default_steps[key], data)
801
- end
802
- (info["delete_default_steps"] || {}).each do |key, data|
803
- @default_steps[key] = delete_steps(@default_steps[key], data)
804
- end
962
+ def read_default_step_info(info)
963
+ @steps = read_steps(info.delete("steps") || ::YAML.load(DEFAULT_STEPS_YAML))
964
+ @steps = modify_steps(@steps, info.delete("modify_steps") || [])
965
+ @steps = prepend_steps(@steps, info.delete("prepend_steps") || [])
966
+ @steps = append_steps(@steps, info.delete("append_steps") || [])
967
+ @steps = delete_steps(@steps, info.delete("delete_steps") || [])
805
968
  end
806
969
 
807
970
  def read_component_info(info)
808
971
  @components = {}
809
- @default_component_name = nil
810
- @has_multiple_components = (info["components"]&.size.to_i + info["gems"]&.size.to_i) > 1
811
- info["gems"]&.each do |component_info|
812
- component_info["type"] = "gem"
813
- read_component_settings(component_info)
814
- end
815
- info["components"]&.each do |component_info|
816
- read_component_settings(component_info)
972
+ component_info_array = Array(info.delete("components")) + Array(info.delete("gems"))
973
+ @has_multiple_components = component_info_array.size > 1
974
+ component_info_array.each do |component_info|
975
+ component = ComponentSettings.new(self, component_info, @has_multiple_components)
976
+ if component.name.empty?
977
+ @errors << "A component is missing a name"
978
+ elsif @components[component.name]
979
+ @errors << "Duplicate component #{component.name.inspect}"
980
+ else
981
+ @components[component.name] = component
982
+ end
817
983
  end
818
984
  @errors << "No components found" if @components.empty?
819
985
  end
820
986
 
821
- def read_component_settings(component_info)
822
- component = ComponentSettings.new(self, component_info, @has_multiple_components)
823
- if component.name.empty?
824
- @errors << "A component is missing a name"
825
- elsif @components[component.name]
826
- @errors << "Duplicate component #{component.name.inspect}"
827
- else
828
- @components[component.name] = component
829
- @default_component_name ||= component.name
830
- end
831
- end
832
-
833
987
  def read_coordination_info(info)
834
- if info["coordinate_versions"]
835
- @coordination_groups = [@components.keys]
836
- return
837
- end
838
- @coordination_groups = Array(info["coordination_groups"])
988
+ @coordination_groups = Array(info.delete("coordination_groups"))
839
989
  @coordination_groups = [@coordination_groups] if @coordination_groups.first.is_a?(::String)
840
990
  seen = {}
841
991
  @coordination_groups.each do |group|
@@ -849,6 +999,18 @@ module Toys
849
999
  end
850
1000
  end
851
1001
  end
1002
+ if info.delete("coordinate_versions") && @coordination_groups.empty?
1003
+ @coordination_groups = [@components.keys]
1004
+ end
1005
+ end
1006
+
1007
+ def check_global_problems(info)
1008
+ info.each_key do |key|
1009
+ @errors << "Unknown top level key #{key.inspect} in releases.yml"
1010
+ end
1011
+ @errors << 'Required key "repo" missing from releases.yml' unless @repo_path
1012
+ @errors << 'Required key "git_user_name" missing from releases.yml' unless @git_user_name
1013
+ @errors << 'Required key "git_user_email" missing from releases.yml' unless @git_user_email
852
1014
  end
853
1015
  end
854
1016
  end