toys-release 0.1.1 → 0.3.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.
@@ -6,6 +6,6 @@ module Toys
6
6
  # Current version of the Toys release system.
7
7
  # @return [String]
8
8
  #
9
- VERSION = "0.1.1"
9
+ VERSION = "0.3.0"
10
10
  end
11
11
  end
@@ -24,7 +24,7 @@ jobs:
24
24
  with:
25
25
  ruby-version: ${{ env.ruby_version }}
26
26
  - name: Checkout repo
27
- uses: actions/checkout@v4
27
+ uses: actions/checkout@v5
28
28
  - name: Install Toys
29
29
  run: "gem install --no-document toys"
30
30
  - name: Open release pull request
@@ -15,11 +15,13 @@ module Toys
15
15
  # @param base_dir [String] Optional base directory, within which all the
16
16
  # artifact directories will be created. If not provided, a temporary
17
17
  # directory will be used.
18
+ # @param auto_cleanup [boolean] Whether to cleanup automatically at_exit.
18
19
  #
19
- def initialize(base_dir = nil)
20
+ def initialize(base_dir = nil, auto_cleanup: false)
20
21
  @base_dir = base_dir
21
22
  @needs_cleanup = false
22
23
  @initialized = {}
24
+ at_exit { cleanup } if auto_cleanup
23
25
  end
24
26
 
25
27
  ##
@@ -40,6 +42,24 @@ module Toys
40
42
  path
41
43
  end
42
44
 
45
+ ##
46
+ # Get the path to the output directory for the given step name.
47
+ #
48
+ # @param name [String] Step name
49
+ #
50
+ def output(name)
51
+ get("out-#{name}")
52
+ end
53
+
54
+ ##
55
+ # Get the path to the temp directory for the given step name.
56
+ #
57
+ # @param name [String] Step name
58
+ #
59
+ def temp(name)
60
+ get("temp-#{name}")
61
+ end
62
+
43
63
  ##
44
64
  # Perform cleanup, removing the directories if they were created under a
45
65
  # temporary directory.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "semver"
3
+ require "toys/release/semver"
4
4
 
5
5
  module Toys
6
6
  module Release
@@ -14,12 +14,12 @@ module Toys
14
14
  ##
15
15
  # Create a new ChangeSet
16
16
  #
17
- # @param settings [RepoSettings] the repo settings
17
+ # @param component_settings [ComponentSettings] the component settings
18
18
  #
19
- def initialize(settings)
20
- @release_commit_tags = settings.release_commit_tags
21
- @breaking_change_header = settings.breaking_change_header
22
- @no_significant_updates_notice = settings.no_significant_updates_notice
19
+ def initialize(component_settings)
20
+ @commit_tags = component_settings.commit_tags
21
+ @breaking_change_header = component_settings.breaking_change_header
22
+ @no_significant_updates_notice = component_settings.no_significant_updates_notice
23
23
  @semver = Semver::NONE
24
24
  @change_groups = nil
25
25
  @inputs = []
@@ -49,7 +49,7 @@ module Toys
49
49
  raise "ChangeSet locked" if finished?
50
50
  @semver = Semver::NONE
51
51
  change_groups = {breaking: Group.new(@breaking_change_header)}
52
- @release_commit_tags.each_value do |tag_info|
52
+ @commit_tags.each do |tag_info|
53
53
  tag_info.all_headers.each { |header| change_groups[header] = Group.new(header) }
54
54
  end
55
55
  @inputs.each do |input|
@@ -187,7 +187,7 @@ module Toys
187
187
  when /^revert-commit$/i
188
188
  @inputs.delete_if { |elem| elem.sha.start_with?(match[:content].split.first) }
189
189
  else
190
- tag_info = @release_commit_tags[match[:tag]]
190
+ tag_info = @commit_tags.find { |tag| tag.tag == match[:tag] }
191
191
  input.apply_commit(tag_info, match[:scope], match[:bang], match[:content])
192
192
  end
193
193
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "change_set"
4
- require_relative "changelog_file"
5
- require_relative "version_rb_file"
3
+ require "toys/release/change_set"
4
+ require "toys/release/changelog_file"
5
+ require "toys/release/version_rb_file"
6
6
 
7
7
  module Toys
8
8
  module Release
@@ -11,25 +11,15 @@ module Toys
11
11
  #
12
12
  class Component
13
13
  ##
14
- # Factory method
14
+ # Constructor
15
15
  #
16
16
  # @param repo_settings [Toys::Release::RepoSettings] the repo settings
17
17
  # @param name [String] The component name
18
18
  # @param environment_utils [Toys::Release::EnvironmentUtils] env utils
19
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)
20
+ def initialize(repo_settings, name, environment_utils)
31
21
  @repo_settings = repo_settings
32
- @settings = settings
22
+ @settings = repo_settings.component_settings(name)
33
23
  @utils = environment_utils
34
24
  @changelog_file = ChangelogFile.new(changelog_path(from: :absolute), @utils)
35
25
  @version_rb_file = VersionRbFile.new(version_rb_path(from: :absolute), @utils, @settings.version_constant)
@@ -83,17 +73,17 @@ module Toys
83
73
 
84
74
  ##
85
75
  # Returns the directory path. It can be returned either as a relative path
86
- # from the context directory or an absolute path.
76
+ # from the repo root directory or an absolute path.
87
77
  #
88
- # @param from [:context,:absolute] From where (defaults to `:context`)
78
+ # @param from [:repo_root,:absolute] From where (defaults to `:repo_root`)
89
79
  # @return [String] The directory path
90
80
  #
91
- def directory(from: :context)
81
+ def directory(from: :repo_root)
92
82
  case from
93
- when :context
83
+ when :repo_root
94
84
  settings.directory
95
85
  when :absolute
96
- ::File.expand_path(settings.directory, @utils.context_directory)
86
+ ::File.expand_path(settings.directory, @utils.repo_root_directory)
97
87
  else
98
88
  raise ArgumentError, "Unknown from value: #{from.inspect}"
99
89
  end
@@ -101,10 +91,10 @@ module Toys
101
91
 
102
92
  ##
103
93
  # 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
94
+ # from the component directory, a relative path from the repo root
105
95
  # directory, or an absolute path.
106
96
  #
107
- # @param from [:directory,:context,:absolute] From where (defaults to
97
+ # @param from [:directory,:repo_root,:absolute] From where (defaults to
108
98
  # `:directory`)
109
99
  # @return [String] The path to the file
110
100
  #
@@ -112,7 +102,7 @@ module Toys
112
102
  case from
113
103
  when :directory
114
104
  path
115
- when :context
105
+ when :repo_root
116
106
  ::File.join(directory, path)
117
107
  when :absolute
118
108
  ::File.expand_path(path, directory(from: :absolute))
@@ -123,10 +113,10 @@ module Toys
123
113
 
124
114
  ##
125
115
  # 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
116
+ # path from the component directory, a relative path from the repo root
127
117
  # directory, or an absolute path.
128
118
  #
129
- # @param from [:directory,:context,:absolute] From where (defaults to
119
+ # @param from [:directory,:repo_root,:absolute] From where (defaults to
130
120
  # `:directory`)
131
121
  # @return [String] The path to the changelog
132
122
  #
@@ -136,10 +126,10 @@ module Toys
136
126
 
137
127
  ##
138
128
  # 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
129
+ # path from the component directory, a relative path from the repo root
140
130
  # directory, or an absolute path.
141
131
  #
142
- # @param from [:directory,:context,:absolute] From where (defaults to
132
+ # @param from [:directory,:repo_root,:absolute] From where (defaults to
143
133
  # `:directory`)
144
134
  # @return [String] The path to the `version.rb` file
145
135
  #
@@ -213,7 +203,7 @@ module Toys
213
203
  #
214
204
  def current_changelog_version(at: nil)
215
205
  if at
216
- path = changelog_path(from: :context)
206
+ path = changelog_path(from: :repo_root)
217
207
  content = @utils.capture(["git", "show", "#{at}:#{path}"], e: true)
218
208
  return ChangelogFile.current_version_from_content(content)
219
209
  end
@@ -228,7 +218,7 @@ module Toys
228
218
  #
229
219
  def current_constant_version(at: nil)
230
220
  if at
231
- path = version_rb_path(from: :context)
221
+ path = version_rb_path(from: :repo_root)
232
222
  content = @utils.capture(["git", "show", "#{at}:#{path}"], e: true)
233
223
  return VersionRbFile.current_version_from_content(content)
234
224
  end
@@ -267,7 +257,7 @@ module Toys
267
257
  to ||= "HEAD"
268
258
  from = latest_tag(ref: to) if from == :default
269
259
  commits = from ? "#{from}..#{to}" : to
270
- changeset = ChangeSet.new(@repo_settings)
260
+ changeset = ChangeSet.new(@settings)
271
261
  shas = @utils.capture(["git", "log", commits, "--format=%H"], e: true).split("\n")
272
262
  shas.reverse_each do |sha|
273
263
  message = touched_message(sha)
@@ -279,12 +269,14 @@ module Toys
279
269
  ##
280
270
  # Run bundler
281
271
  #
272
+ # @return [Toys::Utils::Exec::Result]
273
+ #
282
274
  def bundle
283
275
  cd do
284
276
  exec_result = @utils.exec(["bundle", "install"])
285
277
  @utils.error("Bundle install failed for #{name}.") unless exec_result.success?
278
+ exec_result
286
279
  end
287
- self
288
280
  end
289
281
 
290
282
  ##
@@ -331,58 +323,5 @@ module Toys
331
323
  name.hash
332
324
  end
333
325
  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
326
  end
388
327
  end
@@ -74,6 +74,13 @@ module Toys
74
74
  tool_context.context_directory
75
75
  end
76
76
 
77
+ ##
78
+ # @return [String] Absolute path to the repo root directory
79
+ #
80
+ def repo_root_directory
81
+ @repo_root_directory ||= capture(["git", "rev-parse", "--show-toplevel"], chdir: context_directory).strip
82
+ end
83
+
77
84
  ##
78
85
  # Log a message at INFO level
79
86
  #
@@ -120,10 +127,13 @@ module Toys
120
127
  #
121
128
  def accumulate_errors(main_message = nil)
122
129
  previous_list = @error_list
123
- @error_list = []
124
- result = yield
125
- current_list = @error_list
126
- @error_list = previous_list
130
+ @error_list = current_list = []
131
+ result =
132
+ begin
133
+ yield
134
+ ensure
135
+ @error_list = previous_list
136
+ end
127
137
  unless current_list.empty?
128
138
  current_list.unshift(main_message) if main_message
129
139
  error(*current_list)
@@ -141,11 +151,11 @@ module Toys
141
151
  # @return [Object] The block's result if success
142
152
  # @return [nil] if errors happened
143
153
  #
144
- def capture_errors(errors = nil)
154
+ def capture_errors(errors = nil, &block)
145
155
  return yield unless errors
146
156
  previous_option = on_error_option
147
157
  @on_error_option = :raise
148
- yield
158
+ accumulate_errors(&block)
149
159
  rescue ReleaseError => e
150
160
  errors.concat(e.all_messages)
151
161
  nil
@@ -231,6 +241,19 @@ module Toys
231
241
  tool_context.exec_separate_tool(cmd, **opts, &block)
232
242
  end
233
243
 
244
+ ##
245
+ # Returns the URL for the current GitHub Actions workflow run.
246
+ #
247
+ # @return [String] Current workflow run URL
248
+ # @return [nil] If not called from within a workflow run
249
+ #
250
+ def current_workflow_run_url
251
+ return nil unless (server_url = ::ENV["GITHUB_SERVER_URL"])
252
+ return nil unless (repo = ::ENV["GITHUB_REPOSITORY"])
253
+ return nil unless (run_id = ::ENV["GITHUB_RUN_ID"])
254
+ "#{server_url}/#{repo}/actions/runs/#{run_id}"
255
+ end
256
+
234
257
  private
235
258
 
236
259
  def modify_exec_opts(opts, cmd)
@@ -3,8 +3,9 @@
3
3
  require "fileutils"
4
4
  require "json"
5
5
 
6
- require_relative "artifact_dir"
7
- require_relative "steps"
6
+ require "toys/release/artifact_dir"
7
+ require "toys/release/pipeline"
8
+ require "toys/release/steps"
8
9
 
9
10
  module Toys
10
11
  module Release
@@ -30,10 +31,10 @@ module Toys
30
31
  end
31
32
 
32
33
  ##
33
- # @return [boolean] Whether the result is capturing errors
34
+ # @return [boolean] Whether there were no errors
34
35
  #
35
- def capture_errors?
36
- !@errors.nil?
36
+ def succeeded?
37
+ @errors.empty?
37
38
  end
38
39
 
39
40
  ##
@@ -205,28 +206,13 @@ module Toys
205
206
  #
206
207
  def report_results
207
208
  report_text = build_report_text
208
- if @pull_request
209
- @utils.log("Updating release pull request #{@pull_request.url} ...")
210
- label = error? ? @settings.release_error_label : @settings.release_complete_label
211
- @pull_request.update(labels: label)
212
- @pull_request.add_comment(report_text)
213
- @utils.log("Updated release pull request #{@pull_request.url}")
214
- end
215
- if error?
216
- @utils.log("Opening a new issue to report the failure ...")
217
- body = <<~STR
218
- A release job failed.
219
-
220
- Release PR: #{@pull_request&.url || 'unknown'}
221
- Commit: https://github.com/#{@settings.repo_path}/commit/#{@release_sha}
222
-
223
- ----
224
-
225
- #{report_text}
226
- STR
227
- title = "Release PR ##{@pull_request&.number || 'unknown'} failed with errors"
228
- issue_number = @repository.open_issue(title, body)["number"]
229
- @utils.log("Issue ##{issue_number} opened")
209
+ puts report_text
210
+ if @dry_run
211
+ @utils.warning("DRY RUN: Skipped updating pull request #{@pull_request.url}") if @pull_request
212
+ @utils.warning("DRY RUN: Skipped opening release failure issue") if error?
213
+ else
214
+ update_pull_request(report_text) if @pull_request
215
+ open_error_issue(report_text) if error?
230
216
  end
231
217
  self
232
218
  end
@@ -274,13 +260,8 @@ module Toys
274
260
  else
275
261
  "* **All releases completed successfully.**"
276
262
  end
277
- if (server_url = ::ENV["GITHUB_SERVER_URL"])
278
- if (repo = ::ENV["GITHUB_REPOSITORY"])
279
- if (run_id = ::ENV["GITHUB_RUN_ID"])
280
- lines << "* Run logs: #{server_url}/#{repo}/actions/runs/#{run_id}"
281
- end
282
- end
283
- end
263
+ url = @utils.current_workflow_run_url
264
+ lines << "* Run logs: #{url}" if url
284
265
  lines
285
266
  end
286
267
 
@@ -317,17 +298,16 @@ module Toys
317
298
 
318
299
  def internal_perform_release(component, version, result)
319
300
  component_prechecks(component, version) if @enable_prechecks
320
- artifact_dir = Toys::Release::ArtifactDir.new(@work_dir)
301
+ artifact_dir = ArtifactDir.new(@work_dir)
321
302
  begin
322
303
  component.cd do
323
- component.settings.steps.each do |step_settings|
324
- result_code = Toys::Release::Steps.run(
325
- type: step_settings.type, name: step_settings.name, options: step_settings.options,
326
- repository: @repository, component: component, version: version, performer_result: result,
327
- artifact_dir: artifact_dir, dry_run: @dry_run, git_remote: @git_remote
328
- )
329
- break if result_code == :abort
330
- end
304
+ pipeline = Pipeline.new(
305
+ repository: @repository, component: component, version: version, performer_result: result,
306
+ artifact_dir: artifact_dir, dry_run: @dry_run, git_remote: @git_remote
307
+ )
308
+ component.settings.steps.each { |step_settings| pipeline.add_step(step_settings) }
309
+ pipeline.resolve_run
310
+ pipeline.run
331
311
  end
332
312
  ensure
333
313
  artifact_dir.cleanup
@@ -341,6 +321,31 @@ module Toys
341
321
  @utils.log("Completed prechecks for #{component.name.inspect}")
342
322
  self
343
323
  end
324
+
325
+ def update_pull_request(report_text)
326
+ @utils.log("Updating release pull request #{@pull_request.url} ...")
327
+ label = error? ? @settings.release_error_label : @settings.release_complete_label
328
+ @pull_request.update(labels: label)
329
+ @pull_request.add_comment(report_text)
330
+ @utils.log("Updated release pull request #{@pull_request.url}")
331
+ end
332
+
333
+ def open_error_issue(report_text)
334
+ @utils.log("Opening a new issue to report the failure ...")
335
+ body = <<~STR
336
+ A release job failed.
337
+
338
+ Release PR: #{@pull_request&.url || 'unknown'}
339
+ Commit: https://github.com/#{@settings.repo_path}/commit/#{@release_sha}
340
+
341
+ ----
342
+
343
+ #{report_text}
344
+ STR
345
+ title = "Release PR ##{@pull_request&.number || 'unknown'} failed with errors"
346
+ issue_number = @repository.open_issue(title, body)["number"]
347
+ @utils.log("Issue ##{issue_number} opened")
348
+ end
344
349
  end
345
350
  end
346
351
  end