create_github_release 0.2.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.markdownlint.yml +25 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +20 -0
  5. data/.yardopts +5 -0
  6. data/CHANGELOG.md +31 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +64 -0
  10. data/Rakefile +85 -0
  11. data/create_github_release.gemspec +49 -0
  12. data/exe/create-github-release +22 -0
  13. data/lib/create_github_release/assertion_base.rb +62 -0
  14. data/lib/create_github_release/assertions/bundle_is_up_to_date.rb +73 -0
  15. data/lib/create_github_release/assertions/changelog_docker_container_exists.rb +73 -0
  16. data/lib/create_github_release/assertions/docker_is_running.rb +42 -0
  17. data/lib/create_github_release/assertions/gh_command_exists.rb +42 -0
  18. data/lib/create_github_release/assertions/git_command_exists.rb +42 -0
  19. data/lib/create_github_release/assertions/in_git_repo.rb +44 -0
  20. data/lib/create_github_release/assertions/in_repo_root_directory.rb +47 -0
  21. data/lib/create_github_release/assertions/local_and_remote_on_same_commit.rb +45 -0
  22. data/lib/create_github_release/assertions/local_release_branch_does_not_exist.rb +43 -0
  23. data/lib/create_github_release/assertions/local_release_tag_does_not_exist.rb +45 -0
  24. data/lib/create_github_release/assertions/no_staged_changes.rb +46 -0
  25. data/lib/create_github_release/assertions/no_uncommitted_changes.rb +46 -0
  26. data/lib/create_github_release/assertions/on_default_branch.rb +44 -0
  27. data/lib/create_github_release/assertions/remote_release_branch_does_not_exist.rb +43 -0
  28. data/lib/create_github_release/assertions/remote_release_tag_does_not_exist.rb +43 -0
  29. data/lib/create_github_release/assertions.rb +25 -0
  30. data/lib/create_github_release/changelog.rb +372 -0
  31. data/lib/create_github_release/command_line_parser.rb +137 -0
  32. data/lib/create_github_release/options.rb +397 -0
  33. data/lib/create_github_release/release.rb +82 -0
  34. data/lib/create_github_release/release_assertions.rb +86 -0
  35. data/lib/create_github_release/release_tasks.rb +78 -0
  36. data/lib/create_github_release/task_base.rb +62 -0
  37. data/lib/create_github_release/tasks/commit_release.rb +42 -0
  38. data/lib/create_github_release/tasks/create_github_release.rb +106 -0
  39. data/lib/create_github_release/tasks/create_release_branch.rb +42 -0
  40. data/lib/create_github_release/tasks/create_release_pull_request.rb +107 -0
  41. data/lib/create_github_release/tasks/create_release_tag.rb +42 -0
  42. data/lib/create_github_release/tasks/push_release.rb +42 -0
  43. data/lib/create_github_release/tasks/update_changelog.rb +126 -0
  44. data/lib/create_github_release/tasks/update_version.rb +46 -0
  45. data/lib/create_github_release/tasks.rb +18 -0
  46. data/lib/create_github_release/version.rb +6 -0
  47. data/lib/create_github_release.rb +21 -0
  48. metadata +235 -0
@@ -0,0 +1,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CreateGithubRelease
4
+ # Generate a changelog for a new release
5
+ #
6
+ # Given an existing changelog and a description of a new release, generate a
7
+ # new changelog that includes the new release.
8
+ #
9
+ # @api public
10
+ #
11
+ class Changelog
12
+ # Create a new changelog object
13
+ #
14
+ # @example
15
+ # existing_changelog = <<~EXISTING_CHANGELOG.chomp
16
+ # # Change Log
17
+ #
18
+ # List of changes in each release of this project.
19
+ #
20
+ # ## v0.1.0 (2022-10-31)
21
+ #
22
+ # * 07a1167 Release v0.1.0 (#1)
23
+ # EXISTING_CHANGELOG
24
+ #
25
+ # new_release_tag = 'v1.0.0'
26
+ # new_release_date = Date.parse('2022-11-10')
27
+ # new_release_description = <<~RELEASE
28
+ # * f5e69d6 Release v1.0.0 (#4)
29
+ # * 8fe479b Update documentation for initial GA release (#3)
30
+ # RELEASE
31
+ #
32
+ # new_release = CreateGithubRelease::Release.new(new_release_tag, new_release_date, new_release_description)
33
+ #
34
+ # changelog = CreateGithubRelease::Changelog.new(existing_changelog, new_release)
35
+ #
36
+ # expected_new_changelog = <<~CHANGELOG
37
+ # # Change Log
38
+ #
39
+ # List of changes in each release of this project.
40
+ #
41
+ # ## v1.0.0 (2022-11-10)
42
+ #
43
+ # * f5e69d6 Release v1.0.0 (#4)
44
+ # * 8fe479b Update documentation for initial GA release (#3)
45
+ #
46
+ # ## v0.1.0 (2022-10-31)
47
+ #
48
+ # * 07a1167 Release v0.1.0 (#1)
49
+ # CHANGELOG
50
+ #
51
+ # changelog.front_matter # => "# Change Log\n\nList of changes in each release of this project."
52
+ # changelog.body # => "## v0.1.0 (2022-10-31)\n\n* 07a1167 Release v0.1.0 (#1)"
53
+ # changelog.new_release # => #<CreateGithubRelease::Release:0x000000010761aac8 ...>
54
+ # changelog.to_s == expected_new_changelog # => true
55
+ #
56
+ # @param existing_changelog [String] Contents of the changelog as a string
57
+ # @param new_release [CreateGihubRelease::Release] The new release to add to the changelog
58
+ #
59
+ def initialize(existing_changelog, new_release)
60
+ @existing_changelog = existing_changelog
61
+ @new_release = new_release
62
+
63
+ @lines = existing_changelog.lines.map(&:chomp)
64
+ end
65
+
66
+ # The front matter of the changelog
67
+ #
68
+ # This is the part of the changelog up until the body. The body contains the list
69
+ # of releases and is the first line starting with '## '.
70
+ #
71
+ # @example Changelog with front matter
72
+ # changelog_text = <<~CHANGELOG
73
+ # This is the front matter
74
+ # ## v0.1.0
75
+ # ...
76
+ # CHANGELOG
77
+ #
78
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
79
+ #
80
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
81
+ # changelog.front_matter # => "This is the front matter\n"
82
+ #
83
+ # @example Changelog without front matter
84
+ # changelog_text = <<~CHANGELOG
85
+ # ## v0.1.0
86
+ # ...
87
+ # CHANGELOG
88
+ #
89
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
90
+ #
91
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
92
+ # changelog.front_matter # => ""
93
+ #
94
+ # @example An empty changelog
95
+ # changelog_text = <<~CHANGELOG
96
+ # CHANGELOG
97
+ #
98
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
99
+ #
100
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
101
+ # changelog.front_matter # => ""
102
+ #
103
+ # @example An empty changelog
104
+ # changelog_text = ""
105
+ #
106
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
107
+ #
108
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
109
+ # changelog.front_matter # => ""
110
+ #
111
+ # @return [String] The front matter of the changelog
112
+ def front_matter
113
+ return '' if front_matter_start == front_matter_end
114
+
115
+ lines[front_matter_start..front_matter_end - 1].join("\n")
116
+ end
117
+
118
+ # The body of the existing changelog
119
+ #
120
+ # @example Changelog with front matter and a body
121
+ # changelog_text = <<~CHANGELOG
122
+ # This is the front matter
123
+ # ## v0.1.0
124
+ # ...
125
+ # CHANGELOG
126
+ #
127
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
128
+ #
129
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
130
+ # changelog.body # => "## v0.1.0\n..."
131
+ #
132
+ # @example Changelog without front matter
133
+ # changelog_text = <<~CHANGELOG
134
+ # ## v0.1.0
135
+ # ...
136
+ # CHANGELOG
137
+ #
138
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
139
+ #
140
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
141
+ # changelog.body # => "## v0.1.0\n..."
142
+ #
143
+ # @example Changelog without a body
144
+ # changelog_text = <<~CHANGELOG
145
+ # This is the front matter
146
+ # CHANGELOG
147
+ #
148
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
149
+ #
150
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
151
+ # changelog.body # => ""
152
+ #
153
+ # @example An empty changelog (new line only)
154
+ # changelog_text = <<~CHANGELOG
155
+ # CHANGELOG
156
+ #
157
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
158
+ #
159
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
160
+ # changelog.body # => ""
161
+ #
162
+ # @example An empty changelog (empty string)
163
+ # changelog_text = ""
164
+ #
165
+ # new_release = CreateGithubRelease::Release.new('v0.1.0', Date.today, '...')
166
+ #
167
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
168
+ # changelog.body # => ""
169
+ #
170
+ # @return [String] The body of the existing changelog
171
+ #
172
+ def body
173
+ return '' if body_start == body_end
174
+
175
+ lines[body_start..body_end - 1].join("\n")
176
+ end
177
+
178
+ # The changelog before the new release is added
179
+ #
180
+ # @example
181
+ # changelog.existing_changelog # => "# Change Log\n\n## v1.0.0...## v0.1.0...\n"
182
+ #
183
+ # @return [String] The changelog before the new release is added
184
+ #
185
+ attr_reader :existing_changelog
186
+
187
+ # The new release to add to the changelog
188
+ #
189
+ # @example
190
+ # changelog.new_release.tag # => 'v1.0.0'
191
+ # changelog.new_release.date # => #<Date: 2018-06-30>
192
+ # changelog.new_release.description # => "[Full Changelog](...)..."
193
+ #
194
+ # @return [CreateGithubRelease::Release] The new release to add to the changelog
195
+ #
196
+ attr_reader :new_release
197
+
198
+ # The changelog with the new release
199
+ #
200
+ # @example Changelog with front matter and a body
201
+ # changelog_text = <<~CHANGELOG
202
+ # This is the front matter
203
+ # ## v0.1.0
204
+ # ...
205
+ # CHANGELOG
206
+ #
207
+ # new_release = CreateGithubRelease::Release.new(
208
+ # 'v1.0.0', Date.parse('2022-11-08'), '...release description...'
209
+ # )
210
+ #
211
+ # expected_changelog = <<~CHANGELOG
212
+ # This is the front matter
213
+ #
214
+ # ## v1.0.0 (2022-11-08)
215
+ # ...release description...
216
+ #
217
+ # ## v0.1.0
218
+ # ...
219
+ # CHANGELOG
220
+ #
221
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
222
+ #
223
+ # changelog.to_s == expected_changelog # => true
224
+ #
225
+ # @example Changelog without front matter
226
+ # changelog_text = <<~CHANGELOG
227
+ # ## v0.1.0
228
+ # ...
229
+ # CHANGELOG
230
+ #
231
+ # new_release = CreateGithubRelease::Release.new(
232
+ # 'v1.0.0', Date.parse('2022-11-08'), '...release description...'
233
+ # )
234
+ #
235
+ # expected_changelog = <<~CHANGELOG
236
+ # ## v1.0.0 (2022-11-08)
237
+ # ...release description...
238
+ #
239
+ # ## v0.1.0
240
+ # ...
241
+ # CHANGELOG
242
+ #
243
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
244
+ #
245
+ # changelog.to_s == expected_changelog # => true
246
+ #
247
+ # @example Changelog without a body
248
+ # changelog_text = <<~CHANGELOG
249
+ # This is the front matter
250
+ # CHANGELOG
251
+ #
252
+ # new_release = CreateGithubRelease::Release.new(
253
+ # 'v1.0.0', Date.parse('2022-11-08'), '...release description...'
254
+ # )
255
+ #
256
+ # expected_changelog = <<~CHANGELOG
257
+ # This is the front matter
258
+ #
259
+ # ## v1.0.0 (2022-11-08)
260
+ # ...release description...
261
+ # CHANGELOG
262
+ #
263
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
264
+ #
265
+ # changelog.to_s == expected_changelog # => true
266
+ #
267
+ # @example A new release without a description
268
+ # changelog_text = <<~CHANGELOG
269
+ # This is the front matter
270
+ # ## v0.1.0
271
+ # ...
272
+ # CHANGELOG
273
+ #
274
+ # new_release = CreateGithubRelease::Release.new(
275
+ # 'v1.0.0', Date.parse('2022-11-08'), ''
276
+ # )
277
+ #
278
+ # expected_changelog = <<~CHANGELOG
279
+ # This is the front matter
280
+ #
281
+ # ## v1.0.0 (2022-11-08)
282
+ #
283
+ # ## v0.1.0
284
+ # ...
285
+ # CHANGELOG
286
+ #
287
+ # changelog = CreateGithubRelease::Changelog.new(changelog_text, new_release)
288
+ #
289
+ # changelog.to_s == expected_changelog # => true
290
+ #
291
+ # @return [String] The changelog with the new release details
292
+ #
293
+ def to_s
294
+ String.new.tap do |changelog|
295
+ changelog << "#{front_matter}\n\n" unless front_matter.empty?
296
+ changelog << release_header
297
+ changelog << release_description
298
+ changelog << "\n#{body}\n" unless body.empty?
299
+ end
300
+ end
301
+
302
+ private
303
+
304
+ # The index of the line in @lines where the front matter begins
305
+ # @return [Integer] The index of the line in @lines where the front matter begins
306
+ # @api private
307
+ def front_matter_start
308
+ @front_matter_start ||= begin
309
+ i = 0
310
+ i += 1 while i < body_start && lines[i] =~ /^\s*$/
311
+ i
312
+ end
313
+ end
314
+
315
+ # One past the index of the line in @lines where the front matter ends
316
+ # @return [Integer] One past the index of the line in @lines where the front matter ends
317
+ # @api private
318
+ def front_matter_end
319
+ @front_matter_end ||= begin
320
+ i = body_start
321
+ i -= 1 while i.positive? && lines[i - 1] =~ /^\s*$/
322
+ i
323
+ end
324
+ end
325
+
326
+ # The index of the line in @lines where the body begins
327
+ # @return [Integer] The index of the line in @lines where the body begins
328
+ # @api private
329
+ def body_start
330
+ @body_start ||=
331
+ lines.index { |l| l.start_with?('## ') } || lines.length
332
+ end
333
+
334
+ # One past the index of the line in @lines where the body ends
335
+ # @return [Integer] One past the index of the line in @lines where the body ends
336
+ # @api private
337
+ def body_end
338
+ @body_end ||=
339
+ if body_start == lines.length
340
+ body_start
341
+ else
342
+ i = lines.length
343
+ i -= 1 while i > body_start && lines[i - 1] =~ /^\s*$/
344
+ i
345
+ end
346
+ end
347
+
348
+ # The release header to output in the changelog
349
+ # @return [String] The release header to output in the changelog
350
+ # @api private
351
+ def release_header
352
+ "## #{new_release.tag} (#{new_release.date.strftime('%Y-%m-%d')})\n"
353
+ end
354
+
355
+ # The release description to output in the changelog
356
+ # @return [String] The release description to output in the changelog
357
+ # @api private
358
+ def release_description
359
+ new_release.description.empty? ? '' : "\n#{new_release.description.chomp}\n"
360
+ end
361
+
362
+ # Line number where the body of the changelog starts
363
+ # @return [Integer] Line number where the body of the changelog starts
364
+ # @api private
365
+ # attr_reader :body_start
366
+
367
+ # The existing changelog broken into an array of lines
368
+ # @return [Array<String>] The existing changelog broken into an array of lines
369
+ # @api private
370
+ attr_reader :lines
371
+ end
372
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'create_github_release/options'
5
+
6
+ module CreateGithubRelease
7
+ # Parses the options for this script
8
+ #
9
+ # @example Specifying the release type
10
+ # parser = CommandLineParser.new
11
+ # parser.parse(['major'])
12
+ # options = parser.options
13
+ # options.release_type # => "major"
14
+ # options.quiet # => false
15
+ #
16
+ # @example Specifying the release type and the quiet option
17
+ # parser = CommandLineParser.new
18
+ # parser.parse(['--quiet', 'minor'])
19
+ # options = parser.options
20
+ # options.release_type # => "minor"
21
+ # options.quiet # => true
22
+ #
23
+ # @example Showing the command line help
24
+ # parser = CommandLineParser.new
25
+ # parser.parse(['--help'])
26
+ #
27
+ # @api public
28
+ #
29
+ class CommandLineParser
30
+ # Create a new command line parser
31
+ #
32
+ # @example
33
+ # parser = CommandLineParser.new
34
+ #
35
+ def initialize
36
+ @option_parser = OptionParser.new
37
+ define_options
38
+ @options = CreateGithubRelease::Options.new
39
+ end
40
+
41
+ # Parse the command line arguements returning the options
42
+ #
43
+ # @example
44
+ # parser = CommandLineParser.new
45
+ # options = parser.parse(['major'])
46
+ #
47
+ # @param args [Array<String>] the command line arguments
48
+ #
49
+ # @return [CreateGithubRelease::Options] the options
50
+ #
51
+ def parse(args)
52
+ option_parser.parse!(remaining_args = args.dup)
53
+ parse_remaining_args(remaining_args)
54
+ # puts options unless options.quiet
55
+ options
56
+ end
57
+
58
+ private
59
+
60
+ # @!attribute [rw] options
61
+ #
62
+ # The options to used for the create-github-release script
63
+ #
64
+ # @example
65
+ # parser = CommandLineParser.new
66
+ # parser.parse(['major'])
67
+ # options = parser.options
68
+ # options.release_type # => 'major'
69
+ #
70
+ # @return [CreateGithubRelease::Options] the options
71
+ #
72
+ # @api private
73
+ #
74
+ attr_reader :options
75
+
76
+ # @!attribute [rw] option_parser
77
+ #
78
+ # The option parser
79
+ #
80
+ # @return [OptionParser] the option parser
81
+ #
82
+ # @api private
83
+ #
84
+ attr_reader :option_parser
85
+
86
+ # Parse non-option arguments (the release type)
87
+ # @return [void]
88
+ # @api private
89
+ def parse_remaining_args(remaining_args)
90
+ error_with_usage('No release type specified') if remaining_args.empty?
91
+ options.release_type = remaining_args.shift || nil
92
+ error_with_usage('Too many args') unless remaining_args.empty?
93
+ end
94
+
95
+ # Output an error message and useage to stderr and exit
96
+ # @return [void]
97
+ # @api private
98
+ def error_with_usage(message)
99
+ warn <<~MESSAGE
100
+ ERROR: #{message}
101
+ #{option_parser}
102
+ MESSAGE
103
+ exit 1
104
+ end
105
+
106
+ # Define the options for OptionParser
107
+ # @return [void]
108
+ # @api private
109
+ def define_options
110
+ option_parser.banner = 'Usage: create_release --help | release-type'
111
+ option_parser.separator ''
112
+ option_parser.separator 'Options:'
113
+
114
+ define_quiet_option
115
+ define_help_option
116
+ end
117
+
118
+ # Define the quiet option
119
+ # @return [void]
120
+ # @api private
121
+ def define_quiet_option
122
+ option_parser.on('-q', '--[no-]quiet', 'Do not show output') do |quiet|
123
+ options.quiet = quiet
124
+ end
125
+ end
126
+
127
+ # Define the help option
128
+ # @return [void]
129
+ # @api private
130
+ def define_help_option
131
+ option_parser.on_tail('-h', '--help', 'Show this message') do
132
+ puts option_parser
133
+ exit 0
134
+ end
135
+ end
136
+ end
137
+ end