create_github_release 0.2.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 +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