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,580 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Toys
|
|
6
|
+
module Release
|
|
7
|
+
##
|
|
8
|
+
# A namespace for steps that can be used in a pipeline
|
|
9
|
+
#
|
|
10
|
+
module Steps
|
|
11
|
+
##
|
|
12
|
+
# Entrypoint for running a step.
|
|
13
|
+
#
|
|
14
|
+
# @param type [String] Name of the step class
|
|
15
|
+
# @param name [String,nil] An optional unique name for the step
|
|
16
|
+
# @param options [Hash{String=>String}] Options to pass to the step
|
|
17
|
+
# @param repository [Toys::Release::Repository]
|
|
18
|
+
# @param component [Toys::Release::Component] The component to release
|
|
19
|
+
# @param version [Gem::Version] The version to release
|
|
20
|
+
# @param artifact_dir [Toys::Release::ArtifactDir]
|
|
21
|
+
# @param dry_run [boolean] Whether to do a dry run release
|
|
22
|
+
# @param git_remote [String] The git remote to push gh-pages to
|
|
23
|
+
#
|
|
24
|
+
# @return [:continue] if the step finished and the next step should run
|
|
25
|
+
# @return [:abort] if the pipeline should be aborted
|
|
26
|
+
#
|
|
27
|
+
def self.run(type:, name:, options:,
|
|
28
|
+
repository:, component:, version:, performer_result:,
|
|
29
|
+
artifact_dir:, dry_run:, git_remote:)
|
|
30
|
+
step_class = nil
|
|
31
|
+
begin
|
|
32
|
+
step_class = const_get(type)
|
|
33
|
+
rescue ::NameError
|
|
34
|
+
repository.utils.error("Unknown step type: #{type}")
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
step = step_class.new(repository: repository, component: component, version: version,
|
|
38
|
+
artifact_dir: artifact_dir, dry_run: dry_run, git_remote: git_remote,
|
|
39
|
+
name: name, options: options, performer_result: performer_result)
|
|
40
|
+
begin
|
|
41
|
+
step.run
|
|
42
|
+
:continue
|
|
43
|
+
rescue StepExit
|
|
44
|
+
:continue
|
|
45
|
+
rescue AbortingExit
|
|
46
|
+
:abort
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Internal exception signaling that the step should end immediately but
|
|
52
|
+
# the pipeline should continue.
|
|
53
|
+
# @private
|
|
54
|
+
#
|
|
55
|
+
class StepExit < ::StandardError
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# Internal exception signaling that the step should end immediately and
|
|
60
|
+
# the pipeline should be aborted.
|
|
61
|
+
# @private
|
|
62
|
+
#
|
|
63
|
+
class AbortingExit < ::StandardError
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Base class for steps
|
|
68
|
+
#
|
|
69
|
+
class Base
|
|
70
|
+
##
|
|
71
|
+
# Construct a base step.
|
|
72
|
+
# @private
|
|
73
|
+
#
|
|
74
|
+
def initialize(repository:, component:, version:, performer_result:,
|
|
75
|
+
artifact_dir:, dry_run:, git_remote:, name:, options:)
|
|
76
|
+
@repository = repository
|
|
77
|
+
@component = component
|
|
78
|
+
@release_version = version
|
|
79
|
+
@performer_result = performer_result
|
|
80
|
+
@artifact_dir = artifact_dir
|
|
81
|
+
@dry_run = dry_run
|
|
82
|
+
@git_remote = git_remote || "origin"
|
|
83
|
+
@utils = repository.utils
|
|
84
|
+
@repo_settings = repository.settings
|
|
85
|
+
@component_settings = component.settings
|
|
86
|
+
@name = name
|
|
87
|
+
@options = options
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Get the option with the given key.
|
|
92
|
+
#
|
|
93
|
+
# @param key [String] Option name to fetch
|
|
94
|
+
# @param required [boolean] Whether to exit with an error if the option
|
|
95
|
+
# is not set. Defaults to false, which instead returns the default.
|
|
96
|
+
# @param default [Object] Default value to return if the option is not
|
|
97
|
+
# set and required is set to false.
|
|
98
|
+
#
|
|
99
|
+
# @return [Object] The option value
|
|
100
|
+
#
|
|
101
|
+
def option(key, required: false, default: nil)
|
|
102
|
+
value = @options[key]
|
|
103
|
+
if !value.nil?
|
|
104
|
+
value
|
|
105
|
+
elsif required
|
|
106
|
+
exit_step("Missing option: #{key.inspect} for step #{self.class} (name = #{name.inspect})")
|
|
107
|
+
else
|
|
108
|
+
default
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
# Exit the step immediately. If an error message is given, it is added
|
|
114
|
+
# to the error stream.
|
|
115
|
+
# Raises an error and not return.
|
|
116
|
+
#
|
|
117
|
+
# @param error_message [String] Optional error message
|
|
118
|
+
# @param abort_pipeline [boolean] Whether to abort the pipeline.
|
|
119
|
+
# Default is false.
|
|
120
|
+
#
|
|
121
|
+
def exit_step(error_message = nil, abort_pipeline: false)
|
|
122
|
+
utils.error(error_message) if error_message
|
|
123
|
+
if abort_pipeline
|
|
124
|
+
raise AbortingExit
|
|
125
|
+
else
|
|
126
|
+
raise StepExit
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
##
|
|
131
|
+
# Get the path to an artifact directory for this step.
|
|
132
|
+
#
|
|
133
|
+
# @param name [String] Optional name that can be used to point to the
|
|
134
|
+
# same directory from multiple steps. If not specified, the step
|
|
135
|
+
# name is used.
|
|
136
|
+
#
|
|
137
|
+
def artifact_dir(name = nil)
|
|
138
|
+
@artifact_dir.get(name || self.name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
##
|
|
142
|
+
# Run any pre-tool configured using the `"pre_tool"` option.
|
|
143
|
+
# The option value must be an array of strings representing the command.
|
|
144
|
+
#
|
|
145
|
+
def pre_tool
|
|
146
|
+
cmd = option("pre_tool")
|
|
147
|
+
return unless cmd
|
|
148
|
+
utils.log("Running pre-build tool...")
|
|
149
|
+
result = utils.exec_separate_tool(cmd, out: [:child, :err])
|
|
150
|
+
unless result.success?
|
|
151
|
+
exit_step("Pre-build tool failed: #{cmd}. Check the logs for details.")
|
|
152
|
+
end
|
|
153
|
+
utils.log("Completed pre-build tool.")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
##
|
|
157
|
+
# Run any pre-command configured using the `"pre_command"` option.
|
|
158
|
+
# The option value must be an array of strings representing the command.
|
|
159
|
+
#
|
|
160
|
+
def pre_command
|
|
161
|
+
cmd = option("pre_command")
|
|
162
|
+
return unless cmd
|
|
163
|
+
utils.log("Running pre-build command...")
|
|
164
|
+
result = utils.exec(cmd, out: [:child, :err])
|
|
165
|
+
unless result.success?
|
|
166
|
+
exit_step("Pre-build command failed: #{cmd.inspect}. Check the logs for details.")
|
|
167
|
+
end
|
|
168
|
+
utils.log("Completed pre-build command.")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
##
|
|
172
|
+
# Clean any files not part of the git repository, unless the `"clean"`
|
|
173
|
+
# option is explicitly set to false.
|
|
174
|
+
#
|
|
175
|
+
def pre_clean
|
|
176
|
+
return if option("clean") == false
|
|
177
|
+
count = clean_gitignored(".")
|
|
178
|
+
utils.log("Cleaned #{count} gitignored items")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
##
|
|
182
|
+
# Check whether gh_pages is enabled for this component. If not enabled
|
|
183
|
+
# and the step requires it, exit the step.
|
|
184
|
+
#
|
|
185
|
+
# @param required [boolean] Force this step to require gh_pages. If
|
|
186
|
+
# false, the `"require_gh_pages_enabled"` option can still specify
|
|
187
|
+
# that the step requires gh_pages.
|
|
188
|
+
#
|
|
189
|
+
def check_gh_pages_enabled(required:)
|
|
190
|
+
if (required || option("require_gh_pages_enabled")) && !component_settings.gh_pages_enabled
|
|
191
|
+
utils.log("Skipping step #{name.inspect} because gh_pages is not enabled.")
|
|
192
|
+
exit_step
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
##
|
|
197
|
+
# @return [boolean] Whether this step is being run in dry run mode
|
|
198
|
+
#
|
|
199
|
+
def dry_run?
|
|
200
|
+
@dry_run
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
##
|
|
204
|
+
# @return [Toys::Release::Repository]
|
|
205
|
+
#
|
|
206
|
+
attr_reader :repository
|
|
207
|
+
|
|
208
|
+
##
|
|
209
|
+
# @return [Toys::Release::Component]
|
|
210
|
+
#
|
|
211
|
+
attr_reader :component
|
|
212
|
+
|
|
213
|
+
##
|
|
214
|
+
# @return [Toys::Release::RepoSettings]
|
|
215
|
+
#
|
|
216
|
+
attr_reader :repo_settings
|
|
217
|
+
|
|
218
|
+
##
|
|
219
|
+
# @return [Toys::Release::ComponentSettings]
|
|
220
|
+
#
|
|
221
|
+
attr_reader :component_settings
|
|
222
|
+
|
|
223
|
+
##
|
|
224
|
+
# @return [Toys::Release::EnvironmentUtils]
|
|
225
|
+
#
|
|
226
|
+
attr_reader :utils
|
|
227
|
+
|
|
228
|
+
##
|
|
229
|
+
# @return [Gem::Version]
|
|
230
|
+
#
|
|
231
|
+
attr_reader :release_version
|
|
232
|
+
|
|
233
|
+
##
|
|
234
|
+
# @return [Toys::Release::Performer::Result]
|
|
235
|
+
#
|
|
236
|
+
attr_reader :performer_result
|
|
237
|
+
|
|
238
|
+
##
|
|
239
|
+
# @return [String]
|
|
240
|
+
#
|
|
241
|
+
attr_reader :name
|
|
242
|
+
|
|
243
|
+
##
|
|
244
|
+
# @return [String]
|
|
245
|
+
#
|
|
246
|
+
attr_reader :git_remote
|
|
247
|
+
|
|
248
|
+
##
|
|
249
|
+
# Run the step.
|
|
250
|
+
# This method must be overridden in a subclass.
|
|
251
|
+
#
|
|
252
|
+
def run
|
|
253
|
+
raise "Cannot run base step"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def clean_gitignored(dir)
|
|
259
|
+
count = 0
|
|
260
|
+
children = dir_children(dir)
|
|
261
|
+
result = utils.exec(["git", "check-ignore", "--stdin"], in: :controller, out: :capture) do |controller|
|
|
262
|
+
children.each { |child| controller.in.puts(child) }
|
|
263
|
+
end
|
|
264
|
+
result.captured_out.split("\n").each do |path|
|
|
265
|
+
::FileUtils.rm_rf(path)
|
|
266
|
+
utils.log("Cleaning: #{path}")
|
|
267
|
+
count += 1
|
|
268
|
+
end
|
|
269
|
+
dir_children(dir).each do |child|
|
|
270
|
+
count += clean_gitignored(child) if ::File.directory?(child)
|
|
271
|
+
end
|
|
272
|
+
count
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def dir_children(dir)
|
|
276
|
+
::Dir.entries(dir)
|
|
277
|
+
.grep_v(/^\.\.?$/)
|
|
278
|
+
.sort
|
|
279
|
+
.map { |entry| ::File.join(dir, entry) }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
##
|
|
284
|
+
# A step that runs a toys tool.
|
|
285
|
+
# The tool must be specified as a string array in the `"tool"` option.
|
|
286
|
+
#
|
|
287
|
+
class Tool < Base
|
|
288
|
+
##
|
|
289
|
+
# Run this step
|
|
290
|
+
#
|
|
291
|
+
def run
|
|
292
|
+
tool = Array(option("tool", required: true))
|
|
293
|
+
utils.log("Running tool #{tool.inspect}...")
|
|
294
|
+
result = utils.exec_separate_tool(tool, out: [:child, :err])
|
|
295
|
+
unless result.success?
|
|
296
|
+
exit_step("Tool failed: #{tool.inspect}. Check the logs for details.",
|
|
297
|
+
abort_pipeline: option("abort_pipeline_on_error"))
|
|
298
|
+
end
|
|
299
|
+
utils.log("Completed tool")
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
##
|
|
304
|
+
# A step that runs an arbitrary command.
|
|
305
|
+
# The command must be specified as a string array in the `"command"`
|
|
306
|
+
# option.
|
|
307
|
+
#
|
|
308
|
+
class Command < Base
|
|
309
|
+
##
|
|
310
|
+
# Run this step
|
|
311
|
+
#
|
|
312
|
+
def run
|
|
313
|
+
command = Array(option("command", required: true))
|
|
314
|
+
utils.log("Running command #{command.inspect}...")
|
|
315
|
+
result = utils.exec(command, out: [:child, :err])
|
|
316
|
+
unless result.success?
|
|
317
|
+
exit_step("Command failed: #{command.inspect}. Check the logs for details.",
|
|
318
|
+
abort_pipeline: option("abort_pipeline_on_error"))
|
|
319
|
+
end
|
|
320
|
+
utils.log("Completed command")
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
##
|
|
325
|
+
# A step that runs bundler
|
|
326
|
+
#
|
|
327
|
+
class Bundle < Base
|
|
328
|
+
##
|
|
329
|
+
# Run this step
|
|
330
|
+
#
|
|
331
|
+
def run
|
|
332
|
+
utils.log("Running bundler for #{component.name} ...")
|
|
333
|
+
component.bundle
|
|
334
|
+
utils.log("Completed bundler for #{component.name}")
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
##
|
|
339
|
+
# A step that builds the gem, and leaves the built gem file in the step's
|
|
340
|
+
# artifact directory. This step can also run a pre_command and/or a
|
|
341
|
+
# pre_tool.
|
|
342
|
+
#
|
|
343
|
+
class BuildGem < Base
|
|
344
|
+
##
|
|
345
|
+
# Run this step
|
|
346
|
+
#
|
|
347
|
+
def run
|
|
348
|
+
pre_clean
|
|
349
|
+
utils.log("Building gem: #{component.name} #{release_version}...")
|
|
350
|
+
pre_command
|
|
351
|
+
pre_tool
|
|
352
|
+
pkg_path = ::File.join(artifact_dir, "#{component.name}-#{release_version}.gem")
|
|
353
|
+
result = utils.exec(["gem", "build", "#{component.name}.gemspec", "-o", pkg_path], out: [:child, :err])
|
|
354
|
+
unless result.success?
|
|
355
|
+
exit_step("Gem build failed for #{component.name} #{release_version}. Check the logs for details.")
|
|
356
|
+
end
|
|
357
|
+
utils.log("Gem built to #{pkg_path}.")
|
|
358
|
+
utils.log("Completed gem build.")
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
##
|
|
363
|
+
# A step that builds yardocs, and leaves the built documentation file in
|
|
364
|
+
# the step's artifact directory. This step can also run a pre_command
|
|
365
|
+
# and/or a pre_tool.
|
|
366
|
+
#
|
|
367
|
+
class BuildYard < Base
|
|
368
|
+
##
|
|
369
|
+
# Run this step
|
|
370
|
+
#
|
|
371
|
+
def run
|
|
372
|
+
check_gh_pages_enabled(required: false)
|
|
373
|
+
pre_clean
|
|
374
|
+
utils.log("Building yard: #{component.name} #{release_version}...")
|
|
375
|
+
pre_command
|
|
376
|
+
pre_tool
|
|
377
|
+
::FileUtils.rm_rf(".yardoc")
|
|
378
|
+
::FileUtils.rm_rf("doc")
|
|
379
|
+
result = utils.exec(["bundle", "exec", "yard", "doc"], out: [:child, :err])
|
|
380
|
+
if !result.success? || !::File.directory?("doc")
|
|
381
|
+
exit_step("Yard build failed for #{component.name} #{release_version}. Check the logs for details.")
|
|
382
|
+
end
|
|
383
|
+
dest_path = ::File.join(artifact_dir, "doc")
|
|
384
|
+
::FileUtils.mv("doc", dest_path)
|
|
385
|
+
utils.log("Docs built to #{dest_path}.")
|
|
386
|
+
utils.log("Completed yard build.")
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
##
|
|
391
|
+
# A step that releases a gem built by a previous run of BuildGem. The
|
|
392
|
+
# `"input"` option provides the name of the artifact directory containing
|
|
393
|
+
# the built gem.
|
|
394
|
+
#
|
|
395
|
+
class ReleaseGem < Base
|
|
396
|
+
##
|
|
397
|
+
# Run this step
|
|
398
|
+
#
|
|
399
|
+
def run
|
|
400
|
+
check_existence
|
|
401
|
+
if dry_run?
|
|
402
|
+
push_dry_run
|
|
403
|
+
else
|
|
404
|
+
push_gem
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
private
|
|
409
|
+
|
|
410
|
+
def check_existence
|
|
411
|
+
utils.log("Checking whether #{component.name} #{release_version} already exists...")
|
|
412
|
+
if component.version_released?(release_version)
|
|
413
|
+
utils.warning("Gem already pushed for #{component.name} #{release_version}. Skipping.")
|
|
414
|
+
performer_result.successes << "Gem already pushed for #{component.name} #{release_version}"
|
|
415
|
+
exit_step
|
|
416
|
+
end
|
|
417
|
+
utils.log("Gem has not yet been released.")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def push_dry_run
|
|
421
|
+
unless ::File.file?(pkg_path)
|
|
422
|
+
exit_step("DRY RUN: Package not found at #{pkg_path}")
|
|
423
|
+
end
|
|
424
|
+
performer_result.successes << "DRY RUN Rubygems push for #{component.name} #{release_version}."
|
|
425
|
+
utils.log("DRY RUN: Gem not actually pushed to Rubygems.")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def push_gem
|
|
429
|
+
utils.log("Pushing gem: #{component.name} #{release_version}...")
|
|
430
|
+
result = utils.exec(["gem", "push", pkg_path], out: [:child, :err])
|
|
431
|
+
unless result.success?
|
|
432
|
+
exit_step("Rubygems push failed for #{component.name} #{release_version}. Check the logs for details.")
|
|
433
|
+
end
|
|
434
|
+
performer_result.successes << "Rubygems push for #{component.name} #{release_version}."
|
|
435
|
+
utils.log("Gem push successful.")
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def pkg_path
|
|
439
|
+
@pkg_path ||= ::File.join(artifact_dir(option("input")), "#{component.name}-#{release_version}.gem")
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
##
|
|
444
|
+
# A step that pushes to gh-pages documentation built by a previous run of
|
|
445
|
+
# BuildYard. The `"input"` option provides the name of the artifact
|
|
446
|
+
# directory containing the built documentation.
|
|
447
|
+
#
|
|
448
|
+
class PushGhPages < Base
|
|
449
|
+
##
|
|
450
|
+
# Run this step
|
|
451
|
+
#
|
|
452
|
+
def run
|
|
453
|
+
check_gh_pages_enabled(required: true)
|
|
454
|
+
setup_gh_pages_dir
|
|
455
|
+
check_existence
|
|
456
|
+
copy_docs_dir
|
|
457
|
+
update_docs_404_page
|
|
458
|
+
push_docs_to_git
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
private
|
|
462
|
+
|
|
463
|
+
def setup_gh_pages_dir
|
|
464
|
+
utils.log("Setting up gh-pages access ...")
|
|
465
|
+
gh_token = ::ENV["GITHUB_TOKEN"]
|
|
466
|
+
@gh_pages_dir = repository.checkout_separate_dir(
|
|
467
|
+
branch: "gh-pages", remote: git_remote, dir: artifact_dir("gh-pages"), gh_token: gh_token
|
|
468
|
+
)
|
|
469
|
+
exit_step("Unable to access the gh-pages branch.") unless @gh_pages_dir
|
|
470
|
+
utils.log("Checked out gh-pages")
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def check_existence
|
|
474
|
+
if ::File.directory?(dest_dir)
|
|
475
|
+
utils.warning("Docs already published for #{component.name} #{release_version}. Skipping.")
|
|
476
|
+
performer_result.successes << "Docs already published for #{component.name} #{release_version}"
|
|
477
|
+
exit_step
|
|
478
|
+
end
|
|
479
|
+
utils.log("Verified docs not yet published for #{component.name} #{release_version}")
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def copy_docs_dir
|
|
483
|
+
from_dir = ::File.join(artifact_dir(option("input")), "doc")
|
|
484
|
+
::FileUtils.mkdir_p(component_dir)
|
|
485
|
+
::FileUtils.cp_r(from_dir, dest_dir)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def update_docs_404_page
|
|
489
|
+
path = ::File.join(@gh_pages_dir, "404.html")
|
|
490
|
+
content = ::File.read(path)
|
|
491
|
+
content.sub!(/#{component.settings.gh_pages_version_var} = "[\w.]+";/,
|
|
492
|
+
"#{component.settings.gh_pages_version_var} = \"#{release_version}\";")
|
|
493
|
+
::File.write(path, content)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def push_docs_to_git # rubocop:disable Metrics/AbcSize
|
|
497
|
+
::Dir.chdir(@gh_pages_dir) do
|
|
498
|
+
repository.git_commit("Generated docs for #{component.name} #{release_version}",
|
|
499
|
+
signoff: repository.settings.signoff_commits?)
|
|
500
|
+
if dry_run?
|
|
501
|
+
performer_result.successes << "DRY RUN documentation published for #{component.name} #{release_version}."
|
|
502
|
+
utils.log("DRY RUN: Documentation not actually published to gh-pages.")
|
|
503
|
+
else
|
|
504
|
+
result = utils.exec(["git", "push", git_remote, "gh-pages"], out: [:child, :err])
|
|
505
|
+
unless result.success?
|
|
506
|
+
exit_step("Docs publication failed for #{component.name} #{release_version}." \
|
|
507
|
+
" Check the logs for details.")
|
|
508
|
+
end
|
|
509
|
+
performer_result.successes << "Published documentation for #{component.name} #{release_version}."
|
|
510
|
+
utils.log("Documentation publish successful.")
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def component_dir
|
|
516
|
+
@component_dir ||= ::File.expand_path(component.settings.gh_pages_directory, @gh_pages_dir)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def dest_dir
|
|
520
|
+
@dest_dir ||= ::File.join(component_dir, "v#{release_version}")
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
##
|
|
525
|
+
# A step that creates a GitHub tag and release.
|
|
526
|
+
#
|
|
527
|
+
class GitHubRelease < Base
|
|
528
|
+
##
|
|
529
|
+
# Run this step
|
|
530
|
+
#
|
|
531
|
+
def run
|
|
532
|
+
check_existence
|
|
533
|
+
push_tag
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
private
|
|
537
|
+
|
|
538
|
+
def check_existence
|
|
539
|
+
utils.log("Checking whether #{tag_name} already exists...")
|
|
540
|
+
cmd = ["gh", "api", "repos/#{repo_settings.repo_path}/releases/tags/#{tag_name}",
|
|
541
|
+
"-H", "Accept: application/vnd.github.v3+json"]
|
|
542
|
+
result = utils.exec(cmd, out: :null, err: :null)
|
|
543
|
+
if result.success?
|
|
544
|
+
utils.warning("GitHub tag #{tag_name} already exists. Skipping.")
|
|
545
|
+
performer_result.successes << "GitHub tag #{tag_name} already exists."
|
|
546
|
+
exit_step
|
|
547
|
+
end
|
|
548
|
+
utils.log("GitHub tag #{tag_name} has not yet been created.")
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def push_tag # rubocop:disable Metrics/AbcSize
|
|
552
|
+
utils.log("Creating GitHub release #{tag_name}...")
|
|
553
|
+
changelog_content = component.changelog_file.read_and_verify_latest_entry(release_version)
|
|
554
|
+
release_sha = repository.current_sha
|
|
555
|
+
body = ::JSON.dump(tag_name: tag_name,
|
|
556
|
+
target_commitish: release_sha,
|
|
557
|
+
name: "#{component.name} #{release_version}",
|
|
558
|
+
body: changelog_content)
|
|
559
|
+
if dry_run?
|
|
560
|
+
performer_result.successes << "DRY RUN GitHub tag #{tag_name}."
|
|
561
|
+
utils.log("DRY RUN: GitHub tag #{tag_name} not actually created.")
|
|
562
|
+
else
|
|
563
|
+
cmd = ["gh", "api", "repos/#{repo_settings.repo_path}/releases", "--input", "-",
|
|
564
|
+
"-H", "Accept: application/vnd.github.v3+json"]
|
|
565
|
+
result = utils.exec(cmd, in: [:string, body], out: :null)
|
|
566
|
+
unless result.success?
|
|
567
|
+
exit_step("Unable to create release #{tag_name}. Check the logs for details.")
|
|
568
|
+
end
|
|
569
|
+
performer_result.successes << "Created release with tag #{tag_name} on GitHub."
|
|
570
|
+
utils.log("GitHub release successful.")
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def tag_name
|
|
575
|
+
"#{component.name}/v#{release_version}"
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toys
|
|
4
|
+
module Release
|
|
5
|
+
##
|
|
6
|
+
# Represents a version.rb file
|
|
7
|
+
#
|
|
8
|
+
class VersionRbFile
|
|
9
|
+
##
|
|
10
|
+
# Create a version file object given a file path
|
|
11
|
+
#
|
|
12
|
+
# @param path [String] File path
|
|
13
|
+
# @param environment_utils [Toys::Release::EnvironmentUtils]
|
|
14
|
+
# @param constant_name [Array<String>] Fully qualified name of the version
|
|
15
|
+
# constant
|
|
16
|
+
#
|
|
17
|
+
def initialize(path, environment_utils, constant_name)
|
|
18
|
+
@path = path
|
|
19
|
+
@utils = environment_utils
|
|
20
|
+
@constant_name = constant_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @return [String] Path to the version file
|
|
25
|
+
#
|
|
26
|
+
attr_reader :path
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# @return [Array<String>] Fully qualified name of the version constant
|
|
30
|
+
#
|
|
31
|
+
attr_reader :constant_name
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @return [boolean] Whether the file exists
|
|
35
|
+
#
|
|
36
|
+
def exists?
|
|
37
|
+
::File.file?(path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# @return [String] Current contents of the file
|
|
42
|
+
#
|
|
43
|
+
def content
|
|
44
|
+
::File.read(@path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# @return [::Gem::Version,nil] Current latest version from the file
|
|
49
|
+
#
|
|
50
|
+
def current_version
|
|
51
|
+
VersionRbFile.current_version_from_content(content)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Attempt to evaluate the current version by evaluating Ruby.
|
|
56
|
+
#
|
|
57
|
+
# @return [::Gem::Version,nil] Current version, or nil if failed
|
|
58
|
+
#
|
|
59
|
+
def eval_version
|
|
60
|
+
joined_constant = constant_name.join("::")
|
|
61
|
+
script = "load #{path.inspect}; puts #{joined_constant}"
|
|
62
|
+
output = @utils.capture_ruby(script, err: :null).strip
|
|
63
|
+
output.empty? ? nil : ::Gem::Version.new(output)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Update the file to reflect a new version.
|
|
68
|
+
#
|
|
69
|
+
# @param version [String,::Gem::Version] The release version.
|
|
70
|
+
#
|
|
71
|
+
def update_version(version)
|
|
72
|
+
new_content = content.sub(/VERSION\s*=\s*"(\d+(?:\.[a-zA-Z0-9]+)+)"/,
|
|
73
|
+
"VERSION = \"#{version}\"")
|
|
74
|
+
::File.write(path, new_content)
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Returns the current version from the given file content
|
|
80
|
+
#
|
|
81
|
+
# @param content [String] File contents
|
|
82
|
+
# @return [::Gem::Version] Latest version in the changelog
|
|
83
|
+
# @return [nil] if no version was found
|
|
84
|
+
#
|
|
85
|
+
def self.current_version_from_content(content)
|
|
86
|
+
match = /VERSION\s*=\s*"(\d+(?:\.[a-zA-Z0-9]+)+)"/.match(content)
|
|
87
|
+
match ? ::Gem::Version.new(match[1]) : nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|