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,388 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "change_set"
|
|
4
|
+
require_relative "changelog_file"
|
|
5
|
+
require_relative "version_rb_file"
|
|
6
|
+
|
|
7
|
+
module Toys
|
|
8
|
+
module Release
|
|
9
|
+
##
|
|
10
|
+
# Represents a particular releasable component in the release system
|
|
11
|
+
#
|
|
12
|
+
class Component
|
|
13
|
+
##
|
|
14
|
+
# Factory method
|
|
15
|
+
#
|
|
16
|
+
# @param repo_settings [Toys::Release::RepoSettings] the repo settings
|
|
17
|
+
# @param name [String] The component name
|
|
18
|
+
# @param environment_utils [Toys::Release::EnvironmentUtils] env utils
|
|
19
|
+
#
|
|
20
|
+
def self.build(repo_settings, name, environment_utils)
|
|
21
|
+
settings = repo_settings.component_settings(name)
|
|
22
|
+
if settings.type == "gem"
|
|
23
|
+
GemComponent.new(repo_settings, settings, environment_utils)
|
|
24
|
+
else
|
|
25
|
+
Component.new(repo_settings, settings, environment_utils)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @private
|
|
30
|
+
def initialize(repo_settings, settings, environment_utils)
|
|
31
|
+
@repo_settings = repo_settings
|
|
32
|
+
@settings = settings
|
|
33
|
+
@utils = environment_utils
|
|
34
|
+
@changelog_file = ChangelogFile.new(changelog_path(from: :absolute), @utils)
|
|
35
|
+
@version_rb_file = VersionRbFile.new(version_rb_path(from: :absolute), @utils, @settings.version_constant)
|
|
36
|
+
@coordination_group = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# @return [Toys::Release::ComponentSettings] The component settings
|
|
41
|
+
#
|
|
42
|
+
attr_reader :settings
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# @return [Toys::Release::ChangelogFile] The changelog file in this
|
|
46
|
+
# component
|
|
47
|
+
#
|
|
48
|
+
attr_reader :changelog_file
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# @return [Toys::Release::VersionRbFile] The version.rb file in this
|
|
52
|
+
# component
|
|
53
|
+
#
|
|
54
|
+
attr_reader :version_rb_file
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# @return [Array<Component>] The coordination group containing this
|
|
58
|
+
# component. If this component is not coordinated, it will be part of
|
|
59
|
+
# a one-element coordination group.
|
|
60
|
+
#
|
|
61
|
+
attr_reader :coordination_group
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @return [String] The type of the component, either `component` or `gem`.
|
|
65
|
+
#
|
|
66
|
+
def type
|
|
67
|
+
settings.type
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# @return [String] The name of the component, e.g. the gem name.
|
|
72
|
+
#
|
|
73
|
+
def name
|
|
74
|
+
settings.name
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# Change the working directory to the component directory.
|
|
79
|
+
#
|
|
80
|
+
def cd(&block)
|
|
81
|
+
::Dir.chdir(directory(from: :absolute), &block)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Returns the directory path. It can be returned either as a relative path
|
|
86
|
+
# from the context directory or an absolute path.
|
|
87
|
+
#
|
|
88
|
+
# @param from [:context,:absolute] From where (defaults to `:context`)
|
|
89
|
+
# @return [String] The directory path
|
|
90
|
+
#
|
|
91
|
+
def directory(from: :context)
|
|
92
|
+
case from
|
|
93
|
+
when :context
|
|
94
|
+
settings.directory
|
|
95
|
+
when :absolute
|
|
96
|
+
::File.expand_path(settings.directory, @utils.context_directory)
|
|
97
|
+
else
|
|
98
|
+
raise ArgumentError, "Unknown from value: #{from.inspect}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Returns the path to a given file. It can be returned as a relative path
|
|
104
|
+
# from the component directory, a relative path from the context
|
|
105
|
+
# directory, or an absolute path.
|
|
106
|
+
#
|
|
107
|
+
# @param from [:directory,:context,:absolute] From where (defaults to
|
|
108
|
+
# `:directory`)
|
|
109
|
+
# @return [String] The path to the file
|
|
110
|
+
#
|
|
111
|
+
def file_path(path, from: :directory)
|
|
112
|
+
case from
|
|
113
|
+
when :directory
|
|
114
|
+
path
|
|
115
|
+
when :context
|
|
116
|
+
::File.join(directory, path)
|
|
117
|
+
when :absolute
|
|
118
|
+
::File.expand_path(path, directory(from: :absolute))
|
|
119
|
+
else
|
|
120
|
+
raise ArgumentError, "Unknown from value: #{from.inspect}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# Returns the path to the changelog. It can be returned as a relative
|
|
126
|
+
# path from the component directory, a relative path from the context
|
|
127
|
+
# directory, or an absolute path.
|
|
128
|
+
#
|
|
129
|
+
# @param from [:directory,:context,:absolute] From where (defaults to
|
|
130
|
+
# `:directory`)
|
|
131
|
+
# @return [String] The path to the changelog
|
|
132
|
+
#
|
|
133
|
+
def changelog_path(from: :directory)
|
|
134
|
+
file_path(settings.changelog_path, from: from)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Returns the path to the version.rb. It can be returned as a relative
|
|
139
|
+
# path from the component directory, a relative path from the context
|
|
140
|
+
# directory, or an absolute path.
|
|
141
|
+
#
|
|
142
|
+
# @param from [:directory,:context,:absolute] From where (defaults to
|
|
143
|
+
# `:directory`)
|
|
144
|
+
# @return [String] The path to the `version.rb` file
|
|
145
|
+
#
|
|
146
|
+
def version_rb_path(from: :directory)
|
|
147
|
+
file_path(settings.version_rb_path, from: from)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# Validates the component and reports any errors.
|
|
152
|
+
#
|
|
153
|
+
def validate
|
|
154
|
+
@utils.accumulate_errors("Component \"#{name}\" failed validation") do
|
|
155
|
+
path = directory(from: :absolute)
|
|
156
|
+
@utils.error("Missing directory #{path} for #{name}") unless ::File.directory?(path)
|
|
157
|
+
@utils.error("Missing changelog #{changelog_file.path} for #{name}") unless changelog_file.exists?
|
|
158
|
+
@utils.error("Missing version #{version_rb_file.path} for #{name}") unless version_rb_file.exists?
|
|
159
|
+
version_constant = settings.version_constant.join("::")
|
|
160
|
+
unless version_rb_file.eval_version
|
|
161
|
+
@utils.error("#{version_rb_file.path} for #{name} didn't define #{version_constant}")
|
|
162
|
+
end
|
|
163
|
+
yield if block_given?
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
##
|
|
168
|
+
# Returns the version of the latest release tag on the given branch.
|
|
169
|
+
#
|
|
170
|
+
# @param ref [String] The branch name or head ref. Optional. Defaults to
|
|
171
|
+
# the current HEAD.
|
|
172
|
+
# @return [Gem::Version,nil] The version, or nil if no release tags found.
|
|
173
|
+
#
|
|
174
|
+
def latest_tag_version(ref: nil)
|
|
175
|
+
ref ||= "HEAD"
|
|
176
|
+
last_version = nil
|
|
177
|
+
@utils.capture(["git", "tag", "--merged", ref], e: true).split("\n").each do |tag|
|
|
178
|
+
match = %r{^#{name}/v(\d+\.\d+\.\d+(?:\.\w+)*)$}.match(tag)
|
|
179
|
+
next unless match
|
|
180
|
+
version = ::Gem::Version.new(match[1])
|
|
181
|
+
last_version = version if !last_version || version > last_version
|
|
182
|
+
end
|
|
183
|
+
last_version
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
##
|
|
187
|
+
# Returns the latest release tag on the given branch.
|
|
188
|
+
#
|
|
189
|
+
# @param ref [String] The branch name or head ref. Optional. Defaults to
|
|
190
|
+
# the current HEAD.
|
|
191
|
+
# @return [String,nil] The tag, or nil if no release tags found.
|
|
192
|
+
#
|
|
193
|
+
def latest_tag(ref: nil)
|
|
194
|
+
version_tag(latest_tag_version(ref: ref))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
##
|
|
198
|
+
# Returns the tag for the given version.
|
|
199
|
+
#
|
|
200
|
+
# @param version [::Gem::Version,nil]
|
|
201
|
+
# @return [String] The tag, for a version
|
|
202
|
+
# @return [nil] if the given version is nil.
|
|
203
|
+
#
|
|
204
|
+
def version_tag(version)
|
|
205
|
+
version ? "#{name}/v#{version}" : nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
##
|
|
209
|
+
# Gets the current version from the changelog.
|
|
210
|
+
#
|
|
211
|
+
# @param at [String,nil] An optional committish
|
|
212
|
+
# @return [::Gem::Version,nil] The current version
|
|
213
|
+
#
|
|
214
|
+
def current_changelog_version(at: nil)
|
|
215
|
+
if at
|
|
216
|
+
path = changelog_path(from: :context)
|
|
217
|
+
content = @utils.capture(["git", "show", "#{at}:#{path}"], e: true)
|
|
218
|
+
return ChangelogFile.current_version_from_content(content)
|
|
219
|
+
end
|
|
220
|
+
changelog_file.current_version
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
##
|
|
224
|
+
# Gets the current version from the version constant.
|
|
225
|
+
#
|
|
226
|
+
# @param at [String,nil] An optional committish
|
|
227
|
+
# @return [::Gem::Version,nil] The current version
|
|
228
|
+
#
|
|
229
|
+
def current_constant_version(at: nil)
|
|
230
|
+
if at
|
|
231
|
+
path = version_rb_path(from: :context)
|
|
232
|
+
content = @utils.capture(["git", "show", "#{at}:#{path}"], e: true)
|
|
233
|
+
return VersionRbFile.current_version_from_content(content)
|
|
234
|
+
end
|
|
235
|
+
version_rb_file.current_version
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
##
|
|
239
|
+
# Verify the given version matches the current version from the changelog
|
|
240
|
+
# and version constant. Reports any errors found.
|
|
241
|
+
#
|
|
242
|
+
# @param version [String,::Gem::Version] The claimed version
|
|
243
|
+
#
|
|
244
|
+
def verify_version(version)
|
|
245
|
+
@utils.accumulate_errors("Requested #{name} version #{version} doesn't match existing files.") do
|
|
246
|
+
changelog_version = changelog_file.current_version
|
|
247
|
+
if version.to_s != changelog_version.to_s
|
|
248
|
+
@utils.error("#{changelog_file.path} reports version #{changelog_version}.")
|
|
249
|
+
end
|
|
250
|
+
constant_version = version_rb_file.current_version
|
|
251
|
+
if version.to_s != constant_version.to_s
|
|
252
|
+
@utils.error("#{version_rb_file.path} reports version #{constant_version}.")
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
##
|
|
258
|
+
# Returns a list of commit messages, since the given committish, that are
|
|
259
|
+
# relevant to this component.
|
|
260
|
+
#
|
|
261
|
+
# @param from [String,nil] The starting point, defaults to the last
|
|
262
|
+
# release tag. Set to nil explicitly to use the first commit.
|
|
263
|
+
# @param to [String] The endpoint. Defaults to HEAD.
|
|
264
|
+
# @return [ChangeSet]
|
|
265
|
+
#
|
|
266
|
+
def make_change_set(from: :default, to: nil)
|
|
267
|
+
to ||= "HEAD"
|
|
268
|
+
from = latest_tag(ref: to) if from == :default
|
|
269
|
+
commits = from ? "#{from}..#{to}" : to
|
|
270
|
+
changeset = ChangeSet.new(@repo_settings)
|
|
271
|
+
shas = @utils.capture(["git", "log", commits, "--format=%H"], e: true).split("\n")
|
|
272
|
+
shas.reverse_each do |sha|
|
|
273
|
+
message = touched_message(sha)
|
|
274
|
+
changeset.add_message(sha, message) if message
|
|
275
|
+
end
|
|
276
|
+
changeset.finish
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
##
|
|
280
|
+
# Run bundler
|
|
281
|
+
#
|
|
282
|
+
def bundle
|
|
283
|
+
cd do
|
|
284
|
+
exec_result = @utils.exec(["bundle", "install"])
|
|
285
|
+
@utils.error("Bundle install failed for #{name}.") unless exec_result.success?
|
|
286
|
+
end
|
|
287
|
+
self
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
##
|
|
291
|
+
# Checks if the given sha touches this component. If so, returns the
|
|
292
|
+
# commit message, otherwise returns nil.
|
|
293
|
+
#
|
|
294
|
+
# @return [String] if the given commit touches this component
|
|
295
|
+
# @return [nil] if the given commit does not touch this component
|
|
296
|
+
#
|
|
297
|
+
def touched_message(sha)
|
|
298
|
+
dir = settings.directory
|
|
299
|
+
dir = "#{dir}/" unless dir.end_with?("/")
|
|
300
|
+
|
|
301
|
+
message = @utils.capture(["git", "log", sha, "--max-count=1", "--format=%B"], e: true)
|
|
302
|
+
return message if dir == "./" || /(^|\n)touch-component: #{name}(\s|$)/i.match?(message)
|
|
303
|
+
|
|
304
|
+
result = @utils.exec(["git", "rev-parse", "#{sha}^"], out: :capture, err: :null)
|
|
305
|
+
parent_sha =
|
|
306
|
+
if result.success?
|
|
307
|
+
result.captured_out.strip
|
|
308
|
+
else
|
|
309
|
+
@utils.capture(["git", "hash-object", "-t", "tree", "--stdin"], in: :null).strip
|
|
310
|
+
end
|
|
311
|
+
files = @utils.capture(["git", "diff", "--name-only", "#{parent_sha}..#{sha}"], e: true)
|
|
312
|
+
files.split("\n").each do |file|
|
|
313
|
+
return message if (file.start_with?(dir) ||
|
|
314
|
+
settings.include_globs.any? { |pattern| ::File.fnmatch?(pattern, file) }) &&
|
|
315
|
+
settings.exclude_globs.none? { |pattern| ::File.fnmatch?(pattern, file) }
|
|
316
|
+
end
|
|
317
|
+
nil
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# @private
|
|
321
|
+
attr_writer :coordination_group
|
|
322
|
+
|
|
323
|
+
# @private
|
|
324
|
+
def eql?(other)
|
|
325
|
+
name == other.name
|
|
326
|
+
end
|
|
327
|
+
alias == eql?
|
|
328
|
+
|
|
329
|
+
# @private
|
|
330
|
+
def hash
|
|
331
|
+
name.hash
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
##
|
|
336
|
+
# Subclass for Gem components
|
|
337
|
+
#
|
|
338
|
+
class GemComponent < Component
|
|
339
|
+
##
|
|
340
|
+
# Returns the path to the gemspec. It can be returned as a relative path
|
|
341
|
+
# from the component directory, a relative path from the context
|
|
342
|
+
# directory, or an absolute path.
|
|
343
|
+
#
|
|
344
|
+
# @param from [:directory,:context,:absolute] From where (defaults to
|
|
345
|
+
# `:directory`)
|
|
346
|
+
# @return [String] The path to the gemspec file
|
|
347
|
+
#
|
|
348
|
+
def gemspec_path(from: :directory)
|
|
349
|
+
file_path("#{name}.gemspec", from: from)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
##
|
|
353
|
+
# Validates the component and reports any errors.
|
|
354
|
+
# Includes both errors from the base class and gem-specific errors.
|
|
355
|
+
#
|
|
356
|
+
def validate
|
|
357
|
+
super do
|
|
358
|
+
path = gemspec_path(from: :absolute)
|
|
359
|
+
@utils.error("Missing gemspec #{path} for #{name}") unless ::File.file?(path)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
##
|
|
364
|
+
# Return a list of released versions
|
|
365
|
+
#
|
|
366
|
+
# @return [Array<::Gem::Version>]
|
|
367
|
+
#
|
|
368
|
+
def released_versions
|
|
369
|
+
content = @utils.capture(["gem", "info", "-r", "-a", name], e: true)
|
|
370
|
+
match = /#{name} \(([\w., ]+)\)/.match(content)
|
|
371
|
+
return [] unless match
|
|
372
|
+
match[1].split(/,\s+/).map { |str| ::Gem::Version.new(str) }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
##
|
|
376
|
+
# Determines if a version has been released
|
|
377
|
+
#
|
|
378
|
+
# @param version [::Gem::Version,String] The version to check
|
|
379
|
+
# @return [boolean] Whether the version has been released
|
|
380
|
+
#
|
|
381
|
+
def version_released?(version)
|
|
382
|
+
cmd = ["gem", "search", name, "--exact", "--remote", "--version", version.to_s]
|
|
383
|
+
content = @utils.capture(cmd)
|
|
384
|
+
content.include?("#{name} (#{version})")
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toys
|
|
4
|
+
module Release
|
|
5
|
+
##
|
|
6
|
+
# An error raised by the release system
|
|
7
|
+
#
|
|
8
|
+
class ReleaseError < ::StandardError
|
|
9
|
+
##
|
|
10
|
+
# Create a ReleaseError
|
|
11
|
+
# @private
|
|
12
|
+
#
|
|
13
|
+
def initialize(message, more_messages)
|
|
14
|
+
super(message)
|
|
15
|
+
@more_messages = more_messages
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @return [Array<String>] Any secondary error messages
|
|
20
|
+
#
|
|
21
|
+
attr_reader :more_messages
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @return [Array<String>] All messages including the primary and secondary
|
|
25
|
+
#
|
|
26
|
+
def all_messages
|
|
27
|
+
[message] + more_messages
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Utilities for running release scripts
|
|
33
|
+
#
|
|
34
|
+
class EnvironmentUtils
|
|
35
|
+
##
|
|
36
|
+
# Create script utilities
|
|
37
|
+
#
|
|
38
|
+
def initialize(tool_context,
|
|
39
|
+
in_github_action: nil,
|
|
40
|
+
on_error_option: nil)
|
|
41
|
+
@in_github_action = !::ENV["GITHUB_ACTIONS"].nil? if in_github_action.nil?
|
|
42
|
+
@on_error_option = on_error_option || :exit
|
|
43
|
+
@tool_context = tool_context
|
|
44
|
+
@logger = tool_context.logger
|
|
45
|
+
@error_list = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @return [Toys::Context] The Toys tool context
|
|
50
|
+
#
|
|
51
|
+
attr_reader :tool_context
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @return [Logger] The current Toys logger
|
|
55
|
+
#
|
|
56
|
+
attr_reader :logger
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# @return [:nothing,:raise,:exit] What to do on error
|
|
60
|
+
#
|
|
61
|
+
attr_reader :on_error_option
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @return [boolean] Whether we are running in a GitHub action
|
|
65
|
+
#
|
|
66
|
+
def in_github_action?
|
|
67
|
+
@in_github_action
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# @return [String] Absolute path to the context directory
|
|
72
|
+
#
|
|
73
|
+
def context_directory
|
|
74
|
+
tool_context.context_directory
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# Log a message at INFO level
|
|
79
|
+
#
|
|
80
|
+
# @param message [String] Message to log
|
|
81
|
+
#
|
|
82
|
+
def log(message)
|
|
83
|
+
logger.info(message)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Report a fatal error.
|
|
88
|
+
#
|
|
89
|
+
# @param message [String] Message to report
|
|
90
|
+
# @param more_messages [Array<String>] Additional secondary messages
|
|
91
|
+
#
|
|
92
|
+
def error(message, *more_messages)
|
|
93
|
+
if @error_list
|
|
94
|
+
@error_list << message
|
|
95
|
+
more_messages.each { |msg| @error_list << msg }
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
if in_github_action? && !::ENV["TOYS_RELEASER_TESTING"]
|
|
99
|
+
loc = caller_locations(1).first
|
|
100
|
+
puts("::error file=#{loc.path},line=#{loc.lineno}::#{message}")
|
|
101
|
+
else
|
|
102
|
+
tool_context.puts(message, :red, :bold)
|
|
103
|
+
end
|
|
104
|
+
more_messages.each { |m| tool_context.puts(m, :red) }
|
|
105
|
+
case on_error_option
|
|
106
|
+
when :raise
|
|
107
|
+
raise ReleaseError.new(message, more_messages)
|
|
108
|
+
when :exit
|
|
109
|
+
sleep(1) if in_github_action?
|
|
110
|
+
tool_context.exit(1)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# Accumulate any errors within the block. If any were present, then
|
|
116
|
+
# emit them all at once, prefaced with the given main message.
|
|
117
|
+
#
|
|
118
|
+
# @param main_message [String,nil] An initial message to emit if there are
|
|
119
|
+
# errors. Omit if nil.
|
|
120
|
+
#
|
|
121
|
+
def accumulate_errors(main_message = nil)
|
|
122
|
+
previous_list = @error_list
|
|
123
|
+
@error_list = []
|
|
124
|
+
result = yield
|
|
125
|
+
current_list = @error_list
|
|
126
|
+
@error_list = previous_list
|
|
127
|
+
unless current_list.empty?
|
|
128
|
+
current_list.unshift(main_message) if main_message
|
|
129
|
+
error(*current_list)
|
|
130
|
+
end
|
|
131
|
+
result
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
##
|
|
135
|
+
# Accumulate any errors within the block and return the messages instead
|
|
136
|
+
# of raising an exception or exiting. If no errors happened, returns the
|
|
137
|
+
# empty array.
|
|
138
|
+
#
|
|
139
|
+
# @param errors [Array<String>,nil] If an array, append any errors to it
|
|
140
|
+
# in place, otherwise execute the block and let errors bubble through.
|
|
141
|
+
# @return [Object] The block's result if success
|
|
142
|
+
# @return [nil] if errors happened
|
|
143
|
+
#
|
|
144
|
+
def capture_errors(errors = nil)
|
|
145
|
+
return yield unless errors
|
|
146
|
+
previous_option = on_error_option
|
|
147
|
+
@on_error_option = :raise
|
|
148
|
+
yield
|
|
149
|
+
rescue ReleaseError => e
|
|
150
|
+
errors.concat(e.all_messages)
|
|
151
|
+
nil
|
|
152
|
+
ensure
|
|
153
|
+
@on_error_option = previous_option
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
##
|
|
157
|
+
# Report a recoverable warning.
|
|
158
|
+
#
|
|
159
|
+
# @param message [String] Message to report
|
|
160
|
+
# @param more_messages [Array<String>] Additional secondary messages
|
|
161
|
+
#
|
|
162
|
+
def warning(message, *more_messages)
|
|
163
|
+
if in_github_action? && !::ENV["TOYS_RELEASER_TESTING"]
|
|
164
|
+
loc = caller_locations(1).first
|
|
165
|
+
puts("::warning file=#{loc.path},line=#{loc.lineno}::#{message}")
|
|
166
|
+
else
|
|
167
|
+
tool_context.puts(message, :yellow, :bold)
|
|
168
|
+
end
|
|
169
|
+
more_messages.each { |m| tool_context.puts(m, :yellow) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
##
|
|
173
|
+
# Run an external command.
|
|
174
|
+
#
|
|
175
|
+
# @param cmd [Array<String>] The command
|
|
176
|
+
# @param opts [Hash] Extra options
|
|
177
|
+
# @return [Toys::Utils::Exec::Result]
|
|
178
|
+
#
|
|
179
|
+
def exec(cmd, **opts, &block)
|
|
180
|
+
modify_exec_opts(opts, cmd)
|
|
181
|
+
tool_context.exec(cmd, **opts, &block)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
##
|
|
185
|
+
# Run an external command and return its output.
|
|
186
|
+
#
|
|
187
|
+
# @param cmd [Array<String>] The command
|
|
188
|
+
# @param opts [Hash] Extra options
|
|
189
|
+
# @return [String] The output
|
|
190
|
+
#
|
|
191
|
+
def capture(cmd, **opts, &block)
|
|
192
|
+
modify_exec_opts(opts, cmd)
|
|
193
|
+
tool_context.capture(cmd, **opts, &block)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
##
|
|
197
|
+
# Run an external Ruby script.
|
|
198
|
+
#
|
|
199
|
+
# @param code [String] The Ruby code
|
|
200
|
+
# @param opts [Hash] Extra options
|
|
201
|
+
# @return [Toys::Utils::Exec::Result]
|
|
202
|
+
#
|
|
203
|
+
def ruby(code, **opts, &block)
|
|
204
|
+
opts[:in] = [:string, code]
|
|
205
|
+
modify_exec_opts(opts, "ruby")
|
|
206
|
+
tool_context.ruby([], **opts, &block)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
##
|
|
210
|
+
# Run an external Ruby script and return its output.
|
|
211
|
+
#
|
|
212
|
+
# @param code [String] The Ruby code
|
|
213
|
+
# @param opts [Hash] Extra options
|
|
214
|
+
# @return [String] The output
|
|
215
|
+
#
|
|
216
|
+
def capture_ruby(code, **opts, &block)
|
|
217
|
+
opts[:in] = [:string, code]
|
|
218
|
+
modify_exec_opts(opts, "ruby")
|
|
219
|
+
tool_context.capture_ruby([], **opts, &block)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
##
|
|
223
|
+
# Run an external toys tool.
|
|
224
|
+
#
|
|
225
|
+
# @param cmd [Array<String>] The tool and its parameters
|
|
226
|
+
# @param opts [Hash] Extra options
|
|
227
|
+
# @return [Toys::Utils::Exec::Result]
|
|
228
|
+
#
|
|
229
|
+
def exec_separate_tool(cmd, **opts, &block)
|
|
230
|
+
modify_exec_opts(opts, cmd)
|
|
231
|
+
tool_context.exec_separate_tool(cmd, **opts, &block)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def modify_exec_opts(opts, cmd)
|
|
237
|
+
if opts.delete(:e) || opts.delete(:exit_on_nonzero_status)
|
|
238
|
+
opts[:result_callback] ||=
|
|
239
|
+
proc do |r|
|
|
240
|
+
error("Command failed with exit code #{r.exit_code}: #{cmd.inspect}") if r.error?
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|