create_github_release 0.2.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +36 -21
  5. data/create_github_release.gemspec +1 -0
  6. data/exe/create-github-release +12 -8
  7. data/lib/create_github_release/assertion_base.rb +25 -11
  8. data/lib/create_github_release/assertions/bundle_is_up_to_date.rb +4 -3
  9. data/lib/create_github_release/assertions/{docker_is_running.rb → gh_authenticated.rb} +9 -8
  10. data/lib/create_github_release/assertions/gh_command_exists.rb +3 -2
  11. data/lib/create_github_release/assertions/git_command_exists.rb +3 -2
  12. data/lib/create_github_release/assertions/in_git_repo.rb +3 -2
  13. data/lib/create_github_release/assertions/in_repo_root_directory.rb +3 -2
  14. data/lib/create_github_release/assertions/last_release_tag_exists.rb +45 -0
  15. data/lib/create_github_release/assertions/local_and_remote_on_same_commit.rb +4 -3
  16. data/lib/create_github_release/assertions/local_release_branch_does_not_exist.rb +6 -5
  17. data/lib/create_github_release/assertions/local_release_tag_does_not_exist.rb +3 -3
  18. data/lib/create_github_release/assertions/no_staged_changes.rb +3 -2
  19. data/lib/create_github_release/assertions/no_uncommitted_changes.rb +3 -2
  20. data/lib/create_github_release/assertions/on_default_branch.rb +5 -4
  21. data/lib/create_github_release/assertions/remote_release_branch_does_not_exist.rb +6 -5
  22. data/lib/create_github_release/assertions/remote_release_tag_does_not_exist.rb +6 -5
  23. data/lib/create_github_release/assertions.rb +2 -2
  24. data/lib/create_github_release/backtick_debug.rb +69 -0
  25. data/lib/create_github_release/change.rb +73 -0
  26. data/lib/create_github_release/changelog.rb +40 -68
  27. data/lib/create_github_release/command_line_options.rb +367 -0
  28. data/lib/create_github_release/command_line_parser.rb +113 -25
  29. data/lib/create_github_release/project.rb +787 -0
  30. data/lib/create_github_release/release_assertions.rb +3 -3
  31. data/lib/create_github_release/release_tasks.rb +1 -1
  32. data/lib/create_github_release/task_base.rb +25 -11
  33. data/lib/create_github_release/tasks/commit_release.rb +4 -3
  34. data/lib/create_github_release/tasks/create_github_release.rb +16 -35
  35. data/lib/create_github_release/tasks/create_release_branch.rb +6 -5
  36. data/lib/create_github_release/tasks/create_release_pull_request.rb +10 -42
  37. data/lib/create_github_release/tasks/create_release_tag.rb +6 -5
  38. data/lib/create_github_release/tasks/push_release.rb +5 -4
  39. data/lib/create_github_release/tasks/update_changelog.rb +16 -67
  40. data/lib/create_github_release/tasks/update_version.rb +4 -3
  41. data/lib/create_github_release/version.rb +1 -1
  42. data/lib/create_github_release.rb +4 -2
  43. metadata +23 -8
  44. data/.vscode/settings.json +0 -7
  45. data/lib/create_github_release/assertions/changelog_docker_container_exists.rb +0 -73
  46. data/lib/create_github_release/options.rb +0 -397
  47. data/lib/create_github_release/release.rb +0 -82
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ # rubocop:disable Metrics/ModuleLength
6
+
7
+ module CreateGithubRelease
8
+ # An array of the valid release types
9
+ # @return [Array<String>]
10
+ # @api private
11
+ VALID_RELEASE_TYPES = %w[major minor patch].freeze
12
+
13
+ # Regex pattern for a [valid git reference](https://git-scm.com/docs/git-check-ref-format)
14
+ # @return [Regexp]
15
+ # @api private
16
+ VALID_REF_PATTERN = /^(?:(?:[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)|(?:[a-zA-Z0-9-]+))$/.freeze
17
+
18
+ # rubocop:disable Metrics/ClassLength
19
+
20
+ # Stores and validates the command line options
21
+ #
22
+ # @example
23
+ # options = CreateGithubRelease::CommandLineOptions.new
24
+ # options.release_type = 'major'
25
+ # options.valid? #=> true
26
+ # options.errors #=> []
27
+ #
28
+ # @api public
29
+ #
30
+ CommandLineOptions = Struct.new(
31
+ :release_type, :default_branch, :release_branch, :remote, :last_release_version,
32
+ :next_release_version, :changelog_path, :quiet, :verbose,
33
+ keyword_init: true
34
+ ) do
35
+ # @attribute release_type [rw] the type of release to create
36
+ #
37
+ # Must be one of 'major', 'minor', or 'patch'
38
+ #
39
+ # @example
40
+ # options = CreateGithubRelease::CommandLineOptions.new(release_type: 'major')
41
+ # options.release_type #=> 'major'
42
+ # @return [String]
43
+ # @api public
44
+
45
+ # @attribute default_branch [rw] the default branch of the repository
46
+ # @example
47
+ # options = CreateGithubRelease::CommandLineOptions.new(default_branch: 'main')
48
+ # options.default_branch #=> 'main'
49
+ # @return [String]
50
+ # @api public
51
+
52
+ # @attribute release_branch [rw] the branch use to create the release
53
+ # @example
54
+ # options = CreateGithubRelease::CommandLineOptions.new(release_branch: 'release-v1.0.0')
55
+ # options.release_branch #=> 'release-v1.0.0'
56
+ # @return [String]
57
+ # @api public
58
+
59
+ # @attribute remote [rw] the name of the remote to use to access Github
60
+ # @example
61
+ # options = CreateGithubRelease::CommandLineOptions.new(remote: 'origin')
62
+ # options.remote #=> 'origin'
63
+ # @return [String]
64
+ # @api public
65
+
66
+ # @attribute last_release_version [rw] the version of the last release
67
+ # @example
68
+ # options = CreateGithubRelease::CommandLineOptions.new(last_release_version: '0.1.1')
69
+ # options.last_release_version #=> '0.1.1'
70
+ # @return [String]
71
+ # @api public
72
+
73
+ # @attribute next_release_version [rw] the version of the next release
74
+ # @example
75
+ # options = CreateGithubRelease::CommandLineOptions.new(next_release_version: '1.0.0')
76
+ # options.next_release_version #=> '1.0.0'
77
+ # @return [String]
78
+ # @api public
79
+
80
+ # @attribute changelog_path [rw] the path to the changelog file
81
+ # @example
82
+ # options = CreateGithubRelease::CommandLineOptions.new(changelog_path: 'CHANGELOG.md')
83
+ # options.changelog_path #=> 'CHANGELOG.md'
84
+ # @return [String]
85
+ # @api public
86
+
87
+ # @attribute quiet [rw] if `true`, suppresses all output
88
+ # @example
89
+ # options = CreateGithubRelease::CommandLineOptions.new(quiet: true)
90
+ # options.quiet #=> true
91
+ # @return [Boolean]
92
+ # @api public
93
+
94
+ # @attribute verbose [rw] if `true`, enables verbose output
95
+ # @example
96
+ # options = CreateGithubRelease::CommandLineOptions.new(verbose: true)
97
+ # options.verbose #=> true
98
+ # @return [Boolean]
99
+ # @api public
100
+
101
+ # Create a new instance of this class
102
+ #
103
+ # @example No arguments or block given
104
+ # options = CreateGithubRelease::CommandLineOptions.new
105
+ # options.release_type #=> nil
106
+ # options.valid? #=> false
107
+ # options.errors #=> ["--release-type must be given and be one of 'major', 'minor', 'patch'"]
108
+ #
109
+ # @example With keyword arguments
110
+ # config = { release_type: 'major', default_branch: 'main', quiet: true }
111
+ # options = CreateGithubRelease::CommandLineOptions.new(**config)
112
+ # options.release_type #=> 'major'
113
+ # options.default_branch #=> 'main'
114
+ # options.quiet #=> true
115
+ # options.valid? #=> true
116
+ #
117
+ # @example with a configuration block
118
+ # options = CreateGithubRelease::CommandLineOptions.new do |o|
119
+ # o.release_type = 'major'
120
+ # o.default_branch = 'main'
121
+ # o.quiet = true
122
+ # end
123
+ # options.release_type #=> 'major'
124
+ # options.default_branch #=> 'main'
125
+ # options.quiet #=> true
126
+ # options.valid? #=> true
127
+ #
128
+ # @yield [self] an initialization block
129
+ # @yieldparam self [CreateGithubRelease::CommandLineOptions] the instance being initialized
130
+ # @yieldreturn [void] the return value is ignored
131
+ #
132
+ def initialize(*)
133
+ super
134
+ self.quiet ||= false
135
+ self.verbose ||= false
136
+ @errors = []
137
+ yield(self) if block_given?
138
+ end
139
+
140
+ # Returns `true` if all options are valid and `false` otherwise
141
+ #
142
+ # * If the options are valid, returns `true` clears the `#errors` array
143
+ # * If the options are not valid, returns `false` and populates the `#errors` array
144
+ #
145
+ # @example when all options are valid
146
+ # options = CreateGithubRelease::CommandLineOptions.new
147
+ # options.release_type = 'major'
148
+ # options.valid? #=> true
149
+ # options.errors #=> []
150
+ #
151
+ # @example when one or more options are not valid
152
+ # options = CreateGithubRelease::CommandLineOptions.new
153
+ # options.release_type #=> nil
154
+ # options.valid? #=> false
155
+ # options.errors #=> ["--release-type must be given and be one of 'major', 'minor', 'patch'"]
156
+ #
157
+ # @return [Boolean]
158
+ #
159
+ def valid?
160
+ @errors = []
161
+ private_methods(false).select { |m| m.to_s.start_with?('validate_') }.each { |m| send(m) }
162
+ @errors.empty?
163
+ end
164
+
165
+ # Returns an array of error messages
166
+ #
167
+ # * If the options are valid, returns an empty array
168
+ # * If the options are not valid, returns an array of error messages
169
+ #
170
+ # @example when all options are valid
171
+ # options = CreateGithubRelease::CommandLineOptions.new
172
+ # options.release_type = 'major'
173
+ # options.valid? #=> true
174
+ # options.errors #=> []
175
+ #
176
+ # @example when one or more options are not valid
177
+ # options = CreateGithubRelease::CommandLineOptions.new
178
+ # options.release_type #=> nil
179
+ # options.quiet = options.verbose = true
180
+ # options.valid? #=> false
181
+ # options.errors #=> [
182
+ # "Both --quiet and --verbose cannot be given",
183
+ # "--release-type must be given and be one of 'major', 'minor', 'patch'"
184
+ # ]
185
+ #
186
+ # @return [Array<String>] an array of error messages
187
+ #
188
+ def errors
189
+ valid?
190
+ @errors
191
+ end
192
+
193
+ private
194
+
195
+ # Returns `true` if the given name is a valid git reference
196
+ # @return [Boolean]
197
+ # @api private
198
+ def valid_reference?(name)
199
+ VALID_REF_PATTERN.match?(name)
200
+ end
201
+
202
+ # Returns `true` if the `#quiet` is `true` or `false` and `false` otherwise
203
+ # @return [Boolean]
204
+ # @api private
205
+ def validate_quiet
206
+ return true if quiet == true || quiet == false
207
+
208
+ @errors << 'quiet must be either true or false'
209
+ false
210
+ end
211
+
212
+ # Returns `true` if the `#verbose` is `true` or `false` and `false` otherwise
213
+ # @return [Boolean]
214
+ # @api private
215
+ def validate_verbose
216
+ return true if verbose == true || verbose == false
217
+
218
+ @errors << 'verbose must be either true or false'
219
+ false
220
+ end
221
+
222
+ # Returns `true` if only one of `#quiet` or `#verbose` is `true`
223
+ # @return [Boolean]
224
+ # @api private
225
+ def validate_only_quiet_or_verbose_given
226
+ return true unless quiet && verbose
227
+
228
+ @errors << 'Both --quiet and --verbose cannot both be used'
229
+ false
230
+ end
231
+
232
+ # Returns a string representation of the valid release types
233
+ # @return [String]
234
+ # @api private
235
+ def valid_release_types
236
+ "'#{VALID_RELEASE_TYPES.join("', '")}'"
237
+ end
238
+
239
+ # Returns `true` if the `#release_type` is not nil
240
+ # @return [Boolean]
241
+ # @api private
242
+ def validate_release_type_given
243
+ return true unless release_type.nil?
244
+
245
+ @errors << "RELEASE_TYPE must be given and be one of #{valid_release_types}"
246
+ false
247
+ end
248
+
249
+ # Returns `true` if the `#release_type` is nil or a valid release type
250
+ # @return [Boolean]
251
+ # @api private
252
+ def validate_release_type
253
+ return true if release_type.nil? || VALID_RELEASE_TYPES.include?(release_type)
254
+
255
+ @errors << "RELEASE_TYPE '#{release_type}' is not valid. Must be one of #{valid_release_types}"
256
+ false
257
+ end
258
+
259
+ # Returns `true` if the `#default_branch` is nil or is a valid git reference
260
+ # @return [Boolean]
261
+ # @api private
262
+ def validate_default_branch
263
+ return true if default_branch.nil? || valid_reference?(default_branch)
264
+
265
+ @errors << "--default-branch='#{default_branch}' is not valid"
266
+ false
267
+ end
268
+
269
+ # Returns `true` if the `#release_branch` is nil or is a valid git reference
270
+ # @return [Boolean]
271
+ # @api private
272
+ def validate_release_branch
273
+ return true if release_branch.nil? || valid_reference?(release_branch)
274
+
275
+ @errors << "--release-branch='#{release_branch}' is not valid"
276
+ false
277
+ end
278
+
279
+ # Returns `true` if the `#remote` is nil or is a valid git reference
280
+ # @return [Boolean]
281
+ # @api private
282
+ def validate_remote
283
+ return true if remote.nil? || valid_reference?(remote)
284
+
285
+ @errors << "--remote='#{remote}' is not valid"
286
+ false
287
+ end
288
+
289
+ # Returns `true` if the given version is a valid gem version
290
+ # @return [Boolean]
291
+ # @api private
292
+ def valid_gem_version?(version)
293
+ Gem::Version.new(version)
294
+ true
295
+ rescue ArgumentError
296
+ false
297
+ end
298
+
299
+ # Returns `true` if the `#last_release_version` is nil or is a valid gem version
300
+ # @return [Boolean]
301
+ # @api private
302
+ def validate_last_release_version
303
+ return true if last_release_version.nil?
304
+
305
+ if valid_gem_version?(last_release_version)
306
+ true
307
+ else
308
+ @errors << "--last-release-version='#{last_release_version}' is not valid"
309
+ false
310
+ end
311
+ end
312
+
313
+ # Returns `true` if the `#next_release_version` is nil or is a valid gem version
314
+ # @return [Boolean]
315
+ # @api private
316
+ def validate_next_release_version
317
+ return true if next_release_version.nil?
318
+
319
+ if valid_gem_version?(next_release_version)
320
+ true
321
+ else
322
+ @errors << "--next-release-version='#{next_release_version}' is not valid"
323
+ false
324
+ end
325
+ end
326
+
327
+ # Returns `true` if the given path is valid
328
+ # @param path [String] the path to validate
329
+ # @return [Boolean]
330
+ # @api private
331
+ def valid_path?(path)
332
+ File.expand_path(path)
333
+ true
334
+ rescue ArgumentError
335
+ false
336
+ end
337
+
338
+ # Returns `true` if `#changelog_path` is nil or is a valid regular file path
339
+ # @return [Boolean]
340
+ # @api private
341
+ def validate_changelog_path
342
+ changelog_path.nil? || (changelog_path_valid? && changelog_regular_file?)
343
+ end
344
+
345
+ # `true` if `#changelog_path` is a valid path
346
+ # @return [Boolean]
347
+ # @api private
348
+ def changelog_path_valid?
349
+ return true if valid_path?(changelog_path)
350
+
351
+ @errors << "--changelog-path='#{changelog_path}' is not valid"
352
+ false
353
+ end
354
+
355
+ # `true` if `#changelog_path` does not exist OR if it exists and is a regular file
356
+ # @return [Boolean]
357
+ # @api private
358
+ def changelog_regular_file?
359
+ return true unless File.exist?(changelog_path) && !File.file?(changelog_path)
360
+
361
+ @errors << "--changelog-path='#{changelog_path}' must be a regular file"
362
+ false
363
+ end
364
+ end
365
+ # rubocop:enable Metrics/ClassLength
366
+ end
367
+ # rubocop:enable Metrics/ModuleLength
@@ -1,28 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  require 'optparse'
4
- require 'create_github_release/options'
5
+ require 'create_github_release/command_line_options'
5
6
 
6
7
  module CreateGithubRelease
7
8
  # Parses the options for this script
8
9
  #
9
- # @example Specifying the release type
10
- # parser = CommandLineParser.new
11
- # parser.parse(['major'])
12
- # options = parser.options
10
+ # @example Specify the release type
11
+ # options = CommandLineParser.new.parse('major')
12
+ # options.valid? # => true
13
13
  # options.release_type # => "major"
14
14
  # options.quiet # => false
15
15
  #
16
- # @example Specifying the release type and the quiet option
16
+ # @example Specify the release type and the quiet option
17
17
  # parser = CommandLineParser.new
18
- # parser.parse(['--quiet', 'minor'])
19
- # options = parser.options
18
+ # args = %w[minor --quiet]
19
+ # options = parser.parse(*args)
20
20
  # options.release_type # => "minor"
21
21
  # options.quiet # => true
22
22
  #
23
- # @example Showing the command line help
24
- # parser = CommandLineParser.new
25
- # parser.parse(['--help'])
23
+ # @example Show the command line help
24
+ # CommandLineParser.new.parse('--help')
25
+ # parser.parse('--help')
26
26
  #
27
27
  # @api public
28
28
  #
@@ -35,7 +35,7 @@ module CreateGithubRelease
35
35
  def initialize
36
36
  @option_parser = OptionParser.new
37
37
  define_options
38
- @options = CreateGithubRelease::Options.new
38
+ @options = CreateGithubRelease::CommandLineOptions.new
39
39
  end
40
40
 
41
41
  # Parse the command line arguements returning the options
@@ -48,10 +48,15 @@ module CreateGithubRelease
48
48
  #
49
49
  # @return [CreateGithubRelease::Options] the options
50
50
  #
51
- def parse(args)
52
- option_parser.parse!(remaining_args = args.dup)
51
+ def parse(*args)
52
+ begin
53
+ option_parser.parse!(remaining_args = args.dup)
54
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
55
+ report_errors(e.message)
56
+ end
53
57
  parse_remaining_args(remaining_args)
54
58
  # puts options unless options.quiet
59
+ report_errors(*options.errors) unless options.valid?
55
60
  options
56
61
  end
57
62
 
@@ -87,32 +92,52 @@ module CreateGithubRelease
87
92
  # @return [void]
88
93
  # @api private
89
94
  def parse_remaining_args(remaining_args)
90
- error_with_usage('No release type specified') if remaining_args.empty?
91
95
  options.release_type = remaining_args.shift || nil
92
- error_with_usage('Too many args') unless remaining_args.empty?
96
+ report_errors('Too many args') unless remaining_args.empty?
97
+ end
98
+
99
+ # An error message constructed from the given errors array
100
+ # @return [String]
101
+ # @api private
102
+ def error_message(errors)
103
+ <<~MESSAGE
104
+ #{errors.map { |e| "ERROR: #{e}" }.join("\n")}
105
+
106
+ Use --help for usage
107
+ MESSAGE
93
108
  end
94
109
 
95
110
  # Output an error message and useage to stderr and exit
96
111
  # @return [void]
97
112
  # @api private
98
- def error_with_usage(message)
99
- warn <<~MESSAGE
100
- ERROR: #{message}
101
- #{option_parser}
102
- MESSAGE
113
+ def report_errors(*errors)
114
+ warn error_message(errors)
103
115
  exit 1
104
116
  end
105
117
 
118
+ # The command line template as a string
119
+ # @return [String]
120
+ # @api private
121
+ def command_template
122
+ <<~COMMAND
123
+ #{File.basename($PROGRAM_NAME)} --help | RELEASE_TYPE [options]
124
+ COMMAND
125
+ end
126
+
106
127
  # Define the options for OptionParser
107
128
  # @return [void]
108
129
  # @api private
109
130
  def define_options
110
- option_parser.banner = 'Usage: create_release --help | release-type'
131
+ option_parser.banner = "Usage:\n#{command_template}"
132
+ option_parser.separator ''
133
+ option_parser.separator "RELEASE_TYPE must be 'major', 'minor', or 'patch'"
111
134
  option_parser.separator ''
112
135
  option_parser.separator 'Options:'
113
-
114
- define_quiet_option
115
- define_help_option
136
+ %i[
137
+ define_help_option define_default_branch_option define_release_branch_option
138
+ define_remote_option define_last_release_version_option define_next_release_version_option
139
+ define_changelog_path_option define_quiet_option define_verbose_option
140
+ ].each { |m| send(m) }
116
141
  end
117
142
 
118
143
  # Define the quiet option
@@ -124,6 +149,15 @@ module CreateGithubRelease
124
149
  end
125
150
  end
126
151
 
152
+ # Define the verbose option
153
+ # @return [void]
154
+ # @api private
155
+ def define_verbose_option
156
+ option_parser.on('-v', '--[no-]verbose', 'Show extra output') do |verbose|
157
+ options.verbose = verbose
158
+ end
159
+ end
160
+
127
161
  # Define the help option
128
162
  # @return [void]
129
163
  # @api private
@@ -133,5 +167,59 @@ module CreateGithubRelease
133
167
  exit 0
134
168
  end
135
169
  end
170
+
171
+ # Define the default_branch option which requires a value
172
+ # @return [void]
173
+ # @api private
174
+ def define_default_branch_option
175
+ option_parser.on('--default-branch=BRANCH_NAME', 'Override the default branch') do |name|
176
+ options.default_branch = name
177
+ end
178
+ end
179
+
180
+ # Define the release_branch option which requires a value
181
+ # @return [void]
182
+ # @api private
183
+ def define_release_branch_option
184
+ option_parser.on('--release-branch=BRANCH_NAME', 'Override the release branch to create') do |name|
185
+ options.release_branch = name
186
+ end
187
+ end
188
+
189
+ # Define the remote option which requires a value
190
+ # @return [void]
191
+ # @api private
192
+ def define_remote_option
193
+ option_parser.on('--remote=REMOTE_NAME', "Use this remote name instead of 'origin'") do |name|
194
+ options.remote = name
195
+ end
196
+ end
197
+
198
+ # Define the last_release_version option which requires a value
199
+ # @return [void]
200
+ # @api private
201
+ def define_last_release_version_option
202
+ option_parser.on('--last-release-version=VERSION', 'Use this version instead `bump current`') do |version|
203
+ options.last_release_version = version
204
+ end
205
+ end
206
+
207
+ # Define the next_release_version option which requires a value
208
+ # @return [void]
209
+ # @api private
210
+ def define_next_release_version_option
211
+ option_parser.on('--next-release-version=VERSION', 'Use this version instead `bump RELEASE_TYPE`') do |version|
212
+ options.next_release_version = version
213
+ end
214
+ end
215
+
216
+ # Define the changelog_path option which requires a value
217
+ # @return [void]
218
+ # @api private
219
+ def define_changelog_path_option
220
+ option_parser.on('--changelog-path=PATH', 'Use this file instead of CHANGELOG.md') do |name|
221
+ options.changelog_path = name
222
+ end
223
+ end
136
224
  end
137
225
  end