toys-release 0.1.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.md +21 -0
  5. data/README.md +87 -0
  6. data/docs/guide.md +7 -0
  7. data/lib/toys/release/version.rb +11 -0
  8. data/lib/toys-release.rb +23 -0
  9. data/toys/.data/templates/gh-pages-404.html.erb +25 -0
  10. data/toys/.data/templates/gh-pages-empty.html.erb +11 -0
  11. data/toys/.data/templates/gh-pages-gitignore.erb +1 -0
  12. data/toys/.data/templates/gh-pages-index.html.erb +15 -0
  13. data/toys/.data/templates/release-hook-on-closed.yml.erb +34 -0
  14. data/toys/.data/templates/release-hook-on-open.yml.erb +30 -0
  15. data/toys/.data/templates/release-hook-on-push.yml.erb +32 -0
  16. data/toys/.data/templates/release-perform.yml.erb +46 -0
  17. data/toys/.data/templates/release-request.yml.erb +37 -0
  18. data/toys/.data/templates/release-retry.yml.erb +42 -0
  19. data/toys/.lib/toys/release/artifact_dir.rb +70 -0
  20. data/toys/.lib/toys/release/change_set.rb +259 -0
  21. data/toys/.lib/toys/release/changelog_file.rb +136 -0
  22. data/toys/.lib/toys/release/component.rb +388 -0
  23. data/toys/.lib/toys/release/environment_utils.rb +246 -0
  24. data/toys/.lib/toys/release/performer.rb +346 -0
  25. data/toys/.lib/toys/release/pull_request.rb +154 -0
  26. data/toys/.lib/toys/release/repo_settings.rb +855 -0
  27. data/toys/.lib/toys/release/repository.rb +661 -0
  28. data/toys/.lib/toys/release/request_logic.rb +217 -0
  29. data/toys/.lib/toys/release/request_spec.rb +188 -0
  30. data/toys/.lib/toys/release/semver.rb +112 -0
  31. data/toys/.lib/toys/release/steps.rb +580 -0
  32. data/toys/.lib/toys/release/version_rb_file.rb +91 -0
  33. data/toys/.toys.rb +5 -0
  34. data/toys/_onclosed.rb +113 -0
  35. data/toys/_onopen.rb +158 -0
  36. data/toys/_onpush.rb +57 -0
  37. data/toys/create-labels.rb +115 -0
  38. data/toys/gen-gh-pages.rb +146 -0
  39. data/toys/gen-settings.rb +46 -0
  40. data/toys/gen-workflows.rb +70 -0
  41. data/toys/perform.rb +152 -0
  42. data/toys/request.rb +162 -0
  43. data/toys/retry.rb +133 -0
  44. metadata +106 -0
@@ -0,0 +1,855 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ require_relative "semver"
6
+
7
+ module Toys
8
+ module Release
9
+ ##
10
+ # How to handle a conventional commit tag
11
+ #
12
+ class CommitTagSettings
13
+ # @private
14
+ ScopeInfo = ::Struct.new(:semver, :header)
15
+
16
+ ##
17
+ # Create a CommitTagSettings from either a tag name string (which will
18
+ # default to patch releases) or a hash with fields.
19
+ #
20
+ def initialize(input)
21
+ @scopes = {}
22
+ case input
23
+ when ::String
24
+ init_from_string(input)
25
+ when ::Hash
26
+ init_from_hash(input)
27
+ end
28
+ end
29
+
30
+ ##
31
+ # @return [String] The conventional commit tag being described
32
+ #
33
+ attr_reader :tag
34
+
35
+ ##
36
+ # Return the semver type for this tag and scope.
37
+ #
38
+ # @param scope [String,nil] The scope, or nil for no scope
39
+ # @return [Toys::Release::Semver] The semver type
40
+ #
41
+ def semver(scope = nil)
42
+ @scopes[scope]&.semver || @semver
43
+ end
44
+
45
+ ##
46
+ # Return a header describing this type of change in a changelog.
47
+ #
48
+ # @param scope [String,nil] The scope, or nil for no scope
49
+ # @return [String] The header
50
+ # @return [:hidden] if this type of change should not appear in the
51
+ # changelog
52
+ #
53
+ def header(scope = nil)
54
+ @scopes[scope]&.header || @header
55
+ end
56
+
57
+ ##
58
+ # Return an array of all headers used by this tag
59
+ #
60
+ # @return [Array<String>]
61
+ #
62
+ def all_headers
63
+ @all_headers ||= begin
64
+ result = []
65
+ result << @header unless @header == :hidden
66
+ @scopes.each_value do |scope_info|
67
+ result << scope_info.header unless scope_info.header.nil? || scope_info.header == :hidden
68
+ end
69
+ result.uniq
70
+ end
71
+ end
72
+
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
+ private
96
+
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)
123
+ end
124
+ end
125
+
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)
149
+ result = Semver.for_name(value || "none")
150
+ unless result
151
+ tag = scope ? "#{@tag}(#{scope})" : @tag
152
+ raise "Unknown semver: #{value} for tag #{tag}"
153
+ end
154
+ result
155
+ end
156
+ end
157
+
158
+ ##
159
+ # Configuration of a single component
160
+ #
161
+ class ComponentSettings
162
+ ##
163
+ # Create a ComponentSettings from input data structures
164
+ #
165
+ # @param info [Hash] Nested hash input
166
+ # @param has_multiple_components [boolean] Whether there are other
167
+ # components
168
+ #
169
+ def initialize(repo_settings, info, has_multiple_components)
170
+ @name = info["name"]
171
+ @type = info["type"] || "component"
172
+
173
+ read_path_info(info, has_multiple_components)
174
+ read_file_modification_info(info)
175
+ read_gh_pages_info(repo_settings, info, has_multiple_components)
176
+ read_steps_info(repo_settings, info)
177
+ end
178
+
179
+ ##
180
+ # @return [String] The name of the component
181
+ #
182
+ attr_reader :name
183
+
184
+ ##
185
+ # @return [String] The type of component. Default is `"component"`.
186
+ # Subclasses may define other types.
187
+ #
188
+ attr_reader :type
189
+
190
+ ##
191
+ # @return [String] The directory within the repo in which the component
192
+ # is located
193
+ #
194
+ attr_reader :directory
195
+
196
+ ##
197
+ # @return [Array<String>] Additional globs that should be checked for
198
+ # changes
199
+ #
200
+ attr_reader :include_globs
201
+
202
+ ##
203
+ # @return [Array<String>] Globs that should be ignored when checking for
204
+ # changes
205
+ #
206
+ attr_reader :exclude_globs
207
+
208
+ ##
209
+ # @return [String] Path to the changelog relative to the component's
210
+ # directory
211
+ #
212
+ attr_reader :changelog_path
213
+
214
+ ##
215
+ # @return [String] Path to version.rb relative to the component's
216
+ # directory
217
+ #
218
+ attr_reader :version_rb_path
219
+
220
+ ##
221
+ # @return [Array<String>] The constant used to define the version, as an
222
+ # array representing the module path
223
+ #
224
+ attr_reader :version_constant
225
+
226
+ ##
227
+ # @return [boolean] Whether gh-pages publication is enabled.
228
+ #
229
+ attr_reader :gh_pages_enabled
230
+
231
+ ##
232
+ # @return [String] The directory within the gh_pages branch where the
233
+ # reference documentation should be built
234
+ #
235
+ attr_reader :gh_pages_directory
236
+
237
+ ##
238
+ # @return [String] The name of the Javascript variable representing this
239
+ # gem's version in gh_pages
240
+ #
241
+ attr_reader :gh_pages_version_var
242
+
243
+ ##
244
+ # @return [Array<StepSettings>] A list of build steps.
245
+ #
246
+ attr_reader :steps
247
+
248
+ ##
249
+ # @return [StepSettings,nil] The unique step with the given name
250
+ #
251
+ def step_named(name)
252
+ steps.find { |t| t.name == name }
253
+ end
254
+
255
+ private
256
+
257
+ 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"])
261
+ end
262
+
263
+ def read_file_modification_info(info)
264
+ segments = info["name"].split("-")
265
+ name_path = segments.join("/")
266
+ @version_rb_path = info["version_rb_path"] || "lib/#{name_path}/version.rb"
267
+ @version_constant = info["version_constant"] ||
268
+ (segments.map { |seg| camelize(seg) } + ["VERSION"])
269
+ @version_constant = @version_constant.split("::") if @version_constant.is_a?(::String)
270
+ @changelog_path = info["changelog_path"] || "CHANGELOG.md"
271
+ end
272
+
273
+ 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
+ @gh_pages_enabled = info.fetch("gh_pages_enabled") do |_key|
278
+ repo_settings.gh_pages_enabled ||
279
+ info.key?("gh_pages_directory") ||
280
+ info.key?("gh_pages_version_var")
281
+ end
282
+ end
283
+
284
+ 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"] || [])
290
+ end
291
+
292
+ def camelize(str)
293
+ str.to_s
294
+ .sub(/^_/, "")
295
+ .sub(/_$/, "")
296
+ .gsub(/_+/, "_")
297
+ .gsub(/(?:^|_)([a-zA-Z])/) { ::Regexp.last_match(1).upcase }
298
+ end
299
+ end
300
+
301
+ ##
302
+ # Configuration of a step
303
+ #
304
+ class StepSettings
305
+ def initialize(name, type, options)
306
+ @name = name
307
+ @type = type
308
+ @options = options
309
+ end
310
+
311
+ ##
312
+ # @return [String] Name of this step
313
+ #
314
+ attr_reader :name
315
+
316
+ ##
317
+ # @return [String] Type of step
318
+ #
319
+ attr_reader :type
320
+
321
+ ##
322
+ # @return [Hash{String=>Object}] Options for this step
323
+ #
324
+ attr_reader :options
325
+
326
+ ##
327
+ # Make a deep copy
328
+ #
329
+ # @return [StepSettings] A deep copy
330
+ #
331
+ def deep_copy
332
+ StepSettings.new(name, type, RepoSettings.deep_copy(options))
333
+ end
334
+ end
335
+
336
+ ##
337
+ # Full repo configuration
338
+ #
339
+ class RepoSettings
340
+ ##
341
+ # Load repo settings from the current environment.
342
+ #
343
+ # @param environment_utils [Toys::Release::EnvrionmentUtils]
344
+ # @return [Toys::Release::RepoSettings]
345
+ #
346
+ def self.load_from_environment(environment_utils)
347
+ file_path = environment_utils.tool_context.find_data("releases.yml")
348
+ environment_utils.error("Unable to find releases.yml data file") unless file_path
349
+ info = ::YAML.load_file(file_path)
350
+ settings = RepoSettings.new(info)
351
+ errors = settings.errors
352
+ environment_utils.error("Errors while loading releases.yml", *errors) unless errors.empty?
353
+ settings
354
+ end
355
+
356
+ ##
357
+ # Basic deep copy tool that will handle nested arrays and hashes
358
+ #
359
+ def self.deep_copy(obj)
360
+ case obj
361
+ when ::Hash
362
+ obj.transform_values { |v| deep_copy(v) }
363
+ when ::Array
364
+ obj.map { |v| deep_copy(v) }
365
+ else
366
+ obj.dup
367
+ end
368
+ end
369
+
370
+ ##
371
+ # Create a repo configuration object.
372
+ #
373
+ # @param info [Hash] Configuration hash read from JSON.
374
+ #
375
+ def initialize(info)
376
+ @warnings = []
377
+ @errors = []
378
+ @default_component_name = nil
379
+ read_global_info(info)
380
+ read_label_info(info)
381
+ read_commit_lint_info(info)
382
+ read_commit_tag_info(info)
383
+ read_default_step_info(info)
384
+ read_component_info(info)
385
+ read_coordination_info(info)
386
+ end
387
+
388
+ ##
389
+ # @return[Array<String>] Non-fatal warnings detected when loading the
390
+ # settings, or the empty array if there were no warnings.
391
+ #
392
+ attr_reader :warnings
393
+
394
+ ##
395
+ # @return[Array<String>] Fatal errors detected when loading the settings,
396
+ # or the empty array if there were no errors.
397
+ #
398
+ attr_reader :errors
399
+
400
+ ##
401
+ # @return [String] The repo path in the form `owner/repo`.
402
+ #
403
+ attr_reader :repo_path
404
+
405
+ ##
406
+ # @return [String] The name of the main branch (typically `main`)
407
+ #
408
+ attr_reader :main_branch
409
+
410
+ ##
411
+ # @return [String] The name of a git user to use for commits
412
+ #
413
+ attr_reader :git_user_name
414
+
415
+ ##
416
+ # @return [String] The email of a git user to use for commits
417
+ #
418
+ attr_reader :git_user_email
419
+
420
+ ##
421
+ # @return [String] The name of the default component to release
422
+ #
423
+ attr_reader :default_component_name
424
+
425
+ ##
426
+ # @return [Array<Array<String>>] An array of groups of component names
427
+ # whose releases should be coordinated.
428
+ #
429
+ attr_reader :coordination_groups
430
+
431
+ ##
432
+ # @return [Regexp,nil] A regular expression identifying all the GitHub
433
+ # checks that must pass before a release will take place, or nil to
434
+ # ignore GitHub checks
435
+ #
436
+ attr_reader :required_checks_regexp
437
+
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
+ ##
445
+ # @return [Numeric] The number of seconds that releases will wait for
446
+ # checks to complete.
447
+ #
448
+ attr_reader :required_checks_timeout
449
+
450
+ ##
451
+ # @return [boolean] Whether gh-pages publication is enabled.
452
+ #
453
+ attr_reader :gh_pages_enabled
454
+
455
+ ##
456
+ # @return [Array<String>] The merge strategies allowed when linting
457
+ # commit messages.
458
+ #
459
+ attr_reader :commit_lint_merge
460
+
461
+ ##
462
+ # @return [Array<String>] The allowed conventional commit types when
463
+ # linting commit messages.
464
+ #
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.
471
+ #
472
+ attr_reader :release_commit_tags
473
+
474
+ ##
475
+ # @return [String] Header for breaking changes in a changelog
476
+ #
477
+ attr_reader :breaking_change_header
478
+
479
+ ##
480
+ # @return [String] No significant updates notice
481
+ #
482
+ attr_reader :no_significant_updates_notice
483
+
484
+ ##
485
+ # @return [String] GitHub label applied for pending release
486
+ #
487
+ attr_reader :release_pending_label
488
+
489
+ ##
490
+ # @return [String] GitHub label applied for release in error state
491
+ #
492
+ attr_reader :release_error_label
493
+
494
+ ##
495
+ # @return [String] GitHub label applied for aborted release
496
+ #
497
+ attr_reader :release_aborted_label
498
+
499
+ ##
500
+ # @return [String] GitHub label applied for completed release
501
+ #
502
+ attr_reader :release_complete_label
503
+
504
+ ##
505
+ # @return [String] Prefix for release branches
506
+ #
507
+ attr_reader :release_branch_prefix
508
+
509
+ ##
510
+ # @return [String] The owner of the repo
511
+ #
512
+ def repo_owner
513
+ repo_path.split("/").first
514
+ end
515
+
516
+ ##
517
+ # @return [String] The name of the repo
518
+ #
519
+ def repo_name
520
+ repo_path.split("/").last
521
+ end
522
+
523
+ ##
524
+ # @return [boolean] Whether to signoff release commits
525
+ #
526
+ def signoff_commits?
527
+ @signoff_commits
528
+ end
529
+
530
+ ##
531
+ # @return [boolean] Whether the automation should perform releases in
532
+ # response to release pull requests being merged.
533
+ #
534
+ def enable_release_automation?
535
+ @enable_release_automation
536
+ end
537
+
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
+ ##
554
+ # @return [Array<String>] A list of all component names.
555
+ #
556
+ def all_component_names
557
+ @components.keys
558
+ end
559
+
560
+ ##
561
+ # @return [Array<ComponentSettings>] A list of all component settings.
562
+ #
563
+ def all_component_settings
564
+ @components.values
565
+ end
566
+
567
+ ##
568
+ # Get the settings for a single component.
569
+ #
570
+ # @param name [String] Name of a component.
571
+ # @return [ComponentSettings,nil] The component settings for the given
572
+ # name, or nil if the name is not found.
573
+ #
574
+ def component_settings(name)
575
+ @components[name]
576
+ end
577
+
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
+ # @private
589
+ 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
602
+ end
603
+
604
+ # @private
605
+ def modify_steps(steps, modifications)
606
+ modifications.each do |mod_data|
607
+ mod_name = mod_data.delete("name")
608
+ mod_type = mod_data.delete("type")
609
+ count = 0
610
+ steps.each do |step|
611
+ next if (mod_name && step.name != mod_name) || (mod_type && step.type != mod_type)
612
+ count += 1
613
+ opts = step.options
614
+ mod_data.each do |key, value|
615
+ if value.nil?
616
+ opts.delete(key)
617
+ else
618
+ opts[key] = value
619
+ end
620
+ end
621
+ end
622
+ if count.zero?
623
+ @errors << "Unable to find step to modify for name=#{mod_name.inspect} and type=#{mod_type.inspect}."
624
+ end
625
+ end
626
+ steps
627
+ end
628
+
629
+ # @private
630
+ def prepend_steps(steps, info)
631
+ pre_steps = read_steps(info)
632
+ pre_steps + steps
633
+ end
634
+
635
+ # @private
636
+ def append_steps(steps, info)
637
+ post_steps = read_steps(info)
638
+ steps + post_steps
639
+ end
640
+
641
+ # @private
642
+ 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."
649
+ end
650
+ end
651
+ steps
652
+ end
653
+
654
+ private
655
+
656
+ DEFAULT_MAIN_BRAMCH = "main"
657
+ private_constant :DEFAULT_MAIN_BRAMCH
658
+
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
711
+
712
+ DEFAULT_BREAKING_CHANGE_HEADER = "BREAKING CHANGE"
713
+ private_constant :DEFAULT_BREAKING_CHANGE_HEADER
714
+
715
+ DEFAULT_NO_SIGNIFICANT_UPDATES_NOTICE = "No significant updates."
716
+ private_constant :DEFAULT_NO_SIGNIFICANT_UPDATES_NOTICE
717
+
718
+ DEFAULT_RELEASE_PENDING_LABEL = "release: pending"
719
+ private_constant :DEFAULT_RELEASE_PENDING_LABEL
720
+
721
+ DEFAULT_RELEASE_ERROR_LABEL = "release: error"
722
+ private_constant :DEFAULT_RELEASE_ERROR_LABEL
723
+
724
+ DEFAULT_RELEASE_ABORTED_LABEL = "release: aborted"
725
+ private_constant :DEFAULT_RELEASE_ABORTED_LABEL
726
+
727
+ DEFAULT_RELEASE_COMPLETE_LABEL = "release: complete"
728
+ private_constant :DEFAULT_RELEASE_COMPLETE_LABEL
729
+
730
+ 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
744
+ end
745
+
746
+ 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
763
+ end
764
+
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
778
+ end
779
+
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
805
+ end
806
+
807
+ def read_component_info(info)
808
+ @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)
817
+ end
818
+ @errors << "No components found" if @components.empty?
819
+ end
820
+
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
+ 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"])
839
+ @coordination_groups = [@coordination_groups] if @coordination_groups.first.is_a?(::String)
840
+ seen = {}
841
+ @coordination_groups.each do |group|
842
+ group.each do |member|
843
+ if !@components.key?(member)
844
+ @errors << "Unrecognized component #{member.inspect} listed in a coordination group"
845
+ elsif seen.key?(member)
846
+ @errors << "Component #{member.inspect} is in multiple coordination groups"
847
+ else
848
+ seen[member] = true
849
+ end
850
+ end
851
+ end
852
+ end
853
+ end
854
+ end
855
+ end