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,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
data/toys/.toys.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ toys_version!("~> 0.17")
4
+
5
+ desc "Release tools namespace"