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.
- checksums.yaml +7 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +21 -0
- data/README.md +87 -0
- data/docs/guide.md +7 -0
- data/lib/toys/release/version.rb +11 -0
- data/lib/toys-release.rb +23 -0
- data/toys/.data/templates/gh-pages-404.html.erb +25 -0
- data/toys/.data/templates/gh-pages-empty.html.erb +11 -0
- data/toys/.data/templates/gh-pages-gitignore.erb +1 -0
- data/toys/.data/templates/gh-pages-index.html.erb +15 -0
- data/toys/.data/templates/release-hook-on-closed.yml.erb +34 -0
- data/toys/.data/templates/release-hook-on-open.yml.erb +30 -0
- data/toys/.data/templates/release-hook-on-push.yml.erb +32 -0
- data/toys/.data/templates/release-perform.yml.erb +46 -0
- data/toys/.data/templates/release-request.yml.erb +37 -0
- data/toys/.data/templates/release-retry.yml.erb +42 -0
- data/toys/.lib/toys/release/artifact_dir.rb +70 -0
- data/toys/.lib/toys/release/change_set.rb +259 -0
- data/toys/.lib/toys/release/changelog_file.rb +136 -0
- data/toys/.lib/toys/release/component.rb +388 -0
- data/toys/.lib/toys/release/environment_utils.rb +246 -0
- data/toys/.lib/toys/release/performer.rb +346 -0
- data/toys/.lib/toys/release/pull_request.rb +154 -0
- data/toys/.lib/toys/release/repo_settings.rb +855 -0
- data/toys/.lib/toys/release/repository.rb +661 -0
- data/toys/.lib/toys/release/request_logic.rb +217 -0
- data/toys/.lib/toys/release/request_spec.rb +188 -0
- data/toys/.lib/toys/release/semver.rb +112 -0
- data/toys/.lib/toys/release/steps.rb +580 -0
- data/toys/.lib/toys/release/version_rb_file.rb +91 -0
- data/toys/.toys.rb +5 -0
- data/toys/_onclosed.rb +113 -0
- data/toys/_onopen.rb +158 -0
- data/toys/_onpush.rb +57 -0
- data/toys/create-labels.rb +115 -0
- data/toys/gen-gh-pages.rb +146 -0
- data/toys/gen-settings.rb +46 -0
- data/toys/gen-workflows.rb +70 -0
- data/toys/perform.rb +152 -0
- data/toys/request.rb +162 -0
- data/toys/retry.rb +133 -0
- 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
|