create_github_release 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.markdownlint.yml +1 -1
  3. data/CHANGELOG.md +21 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +254 -32
  6. data/create_github_release.gemspec +6 -2
  7. data/exe/create-github-release +12 -8
  8. data/lib/create_github_release/assertion_base.rb +25 -11
  9. data/lib/create_github_release/assertions/bundle_is_up_to_date.rb +4 -3
  10. data/lib/create_github_release/assertions/{docker_is_running.rb → gh_authenticated.rb} +9 -8
  11. data/lib/create_github_release/assertions/gh_command_exists.rb +3 -2
  12. data/lib/create_github_release/assertions/git_command_exists.rb +3 -2
  13. data/lib/create_github_release/assertions/in_git_repo.rb +3 -2
  14. data/lib/create_github_release/assertions/in_repo_root_directory.rb +3 -2
  15. data/lib/create_github_release/assertions/last_release_tag_exists.rb +47 -0
  16. data/lib/create_github_release/assertions/local_and_remote_on_same_commit.rb +4 -3
  17. data/lib/create_github_release/assertions/local_release_branch_does_not_exist.rb +6 -5
  18. data/lib/create_github_release/assertions/local_release_tag_does_not_exist.rb +3 -3
  19. data/lib/create_github_release/assertions/no_staged_changes.rb +3 -2
  20. data/lib/create_github_release/assertions/no_uncommitted_changes.rb +3 -2
  21. data/lib/create_github_release/assertions/on_default_branch.rb +5 -4
  22. data/lib/create_github_release/assertions/remote_release_branch_does_not_exist.rb +6 -5
  23. data/lib/create_github_release/assertions/remote_release_tag_does_not_exist.rb +6 -5
  24. data/lib/create_github_release/assertions.rb +2 -2
  25. data/lib/create_github_release/backtick_debug.rb +69 -0
  26. data/lib/create_github_release/change.rb +73 -0
  27. data/lib/create_github_release/changelog.rb +40 -68
  28. data/lib/create_github_release/command_line_options.rb +367 -0
  29. data/lib/create_github_release/command_line_parser.rb +113 -25
  30. data/lib/create_github_release/project.rb +868 -0
  31. data/lib/create_github_release/release_assertions.rb +3 -3
  32. data/lib/create_github_release/release_tasks.rb +1 -1
  33. data/lib/create_github_release/task_base.rb +25 -11
  34. data/lib/create_github_release/tasks/commit_release.rb +4 -3
  35. data/lib/create_github_release/tasks/create_github_release.rb +16 -35
  36. data/lib/create_github_release/tasks/create_release_branch.rb +6 -5
  37. data/lib/create_github_release/tasks/create_release_pull_request.rb +10 -42
  38. data/lib/create_github_release/tasks/create_release_tag.rb +6 -5
  39. data/lib/create_github_release/tasks/push_release.rb +5 -4
  40. data/lib/create_github_release/tasks/update_changelog.rb +16 -67
  41. data/lib/create_github_release/tasks/update_version.rb +33 -5
  42. data/lib/create_github_release/version.rb +1 -1
  43. data/lib/create_github_release.rb +4 -2
  44. metadata +27 -10
  45. data/.vscode/settings.json +0 -7
  46. data/lib/create_github_release/assertions/changelog_docker_container_exists.rb +0 -73
  47. data/lib/create_github_release/options.rb +0 -397
  48. 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 first].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 the VALID_RELEASE_TYPES
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