reissue 0.5.0 → 0.5.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c5b7a3ee9dbd72238bf4b6212c953283caa5be24da67647358e74ac1fb1118f
4
- data.tar.gz: 07f9b5bc9c3263a010f2a942701c49c110dd6db566d154e501db59d13c2d6cac
3
+ metadata.gz: ec588273a5faaed4d2287b8bde8877deb2e64b4ff8ef84ff71ce5ce58dd85ffc
4
+ data.tar.gz: 4ae823291b5180145e82bdba30d87c89a69440f5ab41df9c6120813898afdc1c
5
5
  SHA512:
6
- metadata.gz: 9f3a8bcfcebd80438e8b0ac3f0702594f6a5f6b265fcc1f156b40a0c4481286dc9c007ed8c2949dffc9c7f6e640cb20e2495f1774387d0537adc3e5d485b6dd8
7
- data.tar.gz: ee70e94135baa48e89ecc033f071cc31814e02925b0df16c7c73b6891dc57bc671aa9e5af19a4257fdd213a8aacc859256a5f7cd9737504db8a5202df6482735
6
+ metadata.gz: 91d584ae8adb5bebee45f1ca5668df9e136399a56134350ea8cf76c25dff748406f909cfb3d4110d8c92907376c3105c6d1d94d89d7c9047b5d0955004f28e84
7
+ data.tar.gz: d27c47553f7ac4a2969a6ac15e91ad23e928f74c989526e7a20e6a92e8ef333363cd04817bed4ab89833bd2ae8f7bc234e02e6a1fa87e1110660a57551a39594
data/CHANGELOG.md CHANGED
@@ -5,14 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
- ## [0.4.23] - 2026-07-21
8
+ ## [0.5.2] - 2026-09-17
9
9
 
10
10
  ### Added
11
11
 
12
- - Optional runbook_file setting maintains a post-release runbook populated from Runbook: git trailers or direct edits (b41ba15)
12
+ - finalize_message option sets the finalize commit subject from the version and date (3716c08)
13
13
 
14
- ## [0.4.22] - 2026-04-09
14
+ ### Changed
15
+
16
+ - Finalize commits use the subject "Finalize changelog for VERSION" without the release date (3716c08)
17
+
18
+ ### Fixed
19
+
20
+ - Shared release workflow retries post-release PR creation up to three times before failing, since the gem is already published and the branch pushed by that point (84bf0da)
21
+
22
+ ## [0.5.1] - 2026-07-24
23
+
24
+ ### Added
25
+
26
+ - reissue:next_version task reports the version the next release would use without changing anything (9feb212)
27
+ - reissue:initialize[runbook] scaffolds a starter runbook file (dfa521a)
28
+
29
+ ### Changed
30
+
31
+ - Runbook finalize and new-version reset preserve a custom title and preamble (a0d874c)
15
32
 
16
33
  ### Fixed
17
34
 
18
- - changelog sections not following changelog_sections order on release (5f9c475)
35
+ - Releases no longer publish a version whose changelog entry and git tag name the previous version (acf4c1a)
data/README.md CHANGED
@@ -123,6 +123,10 @@ Reissue::Task.create :reissue do |task|
123
123
  # Optional: Whether to commit the results of the finalize task. Defaults to true
124
124
  task.commit_finalize = true
125
125
 
126
+ # Optional: A callable returning the finalize commit subject, given version and date.
127
+ # Defaults to: ->(version, date) { "Finalize changelog for #{version}" }
128
+ task.finalize_message = ->(version, date) { "Release #{version}" }
129
+
126
130
  # Optional: Whether to push the changes automatically. Defaults to false
127
131
  # Options: false, true (push working branch), :branch (create and push new branch)
128
132
  task.push_finalize = :branch
@@ -357,6 +361,8 @@ Reissue::Task.create :reissue do |task|
357
361
  end
358
362
  ```
359
363
 
364
+ You don't have to create the file yourself — the first `rake reissue` or `rake reissue:finalize` writes it when `runbook_file` is set. To start from a template you can customize, run `rake reissue:initialize[runbook]`, which creates `RUNBOOK.md` (pass a different name, e.g. `rake reissue:initialize[OPERATIONS.md]`, to use another path).
365
+
360
366
  The runbook holds only the latest release. Items are checkboxes so operators can tick them off:
361
367
 
362
368
  ```markdown
@@ -370,6 +376,8 @@ Steps to perform after releasing the version below.
370
376
  - [ ] Re-index search documents (def5678)
371
377
  ```
372
378
 
379
+ The title and the preamble beneath it are yours to edit. Give the file a custom heading and intro and reissue preserves them across finalize and new-version resets — only the version heading and the checklist are rewritten, the same way a custom preamble survives in `CHANGELOG.md`.
380
+
373
381
  ### Adding Runbook Items
374
382
 
375
383
  Add `Runbook:` trailers to commit messages (case-insensitive, independent of the `fragment` setting):
data/lib/reissue/gem.rb CHANGED
@@ -7,6 +7,30 @@ module Reissue
7
7
  super
8
8
  @updated_paths << "checksums"
9
9
  end
10
+
11
+ # Keep bundler's gemspec in step with the bump.
12
+ #
13
+ # Bundler loads the gemspec when bundler/gem_tasks is required, well before
14
+ # reissue:bump rewrites the version file. `gem build` shells out and re-reads
15
+ # the file, so the gem itself carries the bumped version, but the release tag
16
+ # and bundler's confirmation messages read the copy held in memory. Without
17
+ # this they name the version from before the bump, and the release publishes
18
+ # one version under another version's tag.
19
+ def apply_version_bump(updater, bump)
20
+ new_version = super
21
+ sync_bundler_gemspec_version(new_version)
22
+ new_version
23
+ end
24
+
25
+ # @param new_version [String] the version the bump landed on
26
+ def sync_bundler_gemspec_version(new_version)
27
+ return unless defined?(Bundler::GemHelper)
28
+
29
+ helper = Bundler::GemHelper.instance
30
+ return unless helper&.gemspec
31
+
32
+ helper.gemspec.version = ::Gem::Version.new(new_version)
33
+ end
10
34
  end
11
35
  end
12
36
  Reissue::Task.prepend Reissue::Gem
data/lib/reissue/rake.rb CHANGED
@@ -79,6 +79,11 @@ module Reissue
79
79
  # Whether to commit the finalize change to the changelog. Default: true.
80
80
  attr_accessor :commit_finalize
81
81
 
82
+ # Callable receiving the version and release date, returning the commit subject.
83
+ # The default omits the date to stay under the 50-character subject limit.
84
+ # Default: ->(version, date) { "Finalize changelog for #{version}" }
85
+ attr_accessor :finalize_message
86
+
82
87
  # Whether to commit the clear fragments change.
83
88
  # Returns false for :git fragments since there are no files to commit.
84
89
  attr_writer :commit_clear_fragments
@@ -137,6 +142,7 @@ module Reissue
137
142
  @clear_fragments = false
138
143
  @commit = true
139
144
  @commit_finalize = true
145
+ @finalize_message = ->(version, _date) { "Finalize changelog for #{version}" }
140
146
  @commit_clear_fragments = true
141
147
  @push_finalize = false
142
148
  @version_limit = 2
@@ -188,6 +194,78 @@ module Reissue
188
194
  end
189
195
  end
190
196
 
197
+ # The version the next release would use, derived exactly the way reissue:bump
198
+ # derives it, without writing anything.
199
+ #
200
+ # @return [Gem::Version] the version a release would publish
201
+ def next_release_version
202
+ require_relative "fragment_handler"
203
+ require_relative "version_updater"
204
+
205
+ # reissue:bump only consults git trailers for :git fragments.
206
+ handler = (Reissue::FragmentHandler.for(:git, tag_pattern: tag_pattern) if fragment == :git)
207
+ tag_version = handler&.last_tag_version
208
+ bump = handler&.read_version_bump
209
+ updater = Reissue::VersionUpdater.new(version_file, version_redo_proc: version_redo_proc)
210
+
211
+ if deferred_versioning
212
+ # The version file holds "Unreleased"; finalize resolves the real version
213
+ # from the last tag and the trailer.
214
+ unless tag_version && bump
215
+ raise "Cannot determine the next version. Deferred versioning resolves it from " \
216
+ "the last version tag and a Version: trailer, and one of those is missing."
217
+ end
218
+ return updater.redo(tag_version, bump)
219
+ end
220
+
221
+ current_version = ::Gem::Version.new(File.read(version_file).match(Reissue::VersionUpdater::VERSION_MATCH)[0])
222
+
223
+ if bump.nil?
224
+ current_version
225
+ elsif tag_version && tag_version != current_version
226
+ # A post-release bump already moved the file past the tag, so the trailer
227
+ # only counts if it would land beyond where the file already is.
228
+ [updater.redo(tag_version, bump), current_version].max
229
+ else
230
+ updater.redo(current_version, bump)
231
+ end
232
+ end
233
+
234
+ # Apply a version bump and leave the changelog agreeing with it.
235
+ #
236
+ # @param updater [Reissue::VersionUpdater] the updater to bump with
237
+ # @param bump [Symbol] the segment to bump
238
+ # @return [String] the version the bump landed on
239
+ def apply_version_bump(updater, bump)
240
+ new_version = updater.call(bump)
241
+ realign_unreleased_changelog(new_version)
242
+ bundle
243
+ puts "Version bumped (#{bump}) to #{new_version}"
244
+ new_version
245
+ end
246
+
247
+ # Point the changelog's unreleased entry at the version just bumped to.
248
+ #
249
+ # The bump moves the version file forward, but the unreleased entry still
250
+ # carries the number from the previous cycle. reissue:finalize dates whatever
251
+ # that entry says, so leaving it alone publishes a gem built at the new
252
+ # version while the only dated changelog entry names the old one.
253
+ #
254
+ # @param new_version [String] the version the bump landed on
255
+ def realign_unreleased_changelog(new_version)
256
+ return unless changelog_file && File.exist?(changelog_file)
257
+
258
+ changelog = Reissue::Parser.parse(File.read(changelog_file))
259
+ unreleased = changelog["versions"].find { |version| version["date"] == "Unreleased" }
260
+ return if unreleased.nil? || unreleased["version"] == new_version
261
+
262
+ unreleased["version"] = new_version
263
+
264
+ changelog_updater = Reissue::ChangelogUpdater.new(changelog_file)
265
+ changelog_updater.instance_variable_set(:@changelog, changelog)
266
+ changelog_updater.write(changelog_file)
267
+ end
268
+
191
269
  # Check if there are staged changes ready to commit.
192
270
  def changes_to_commit?
193
271
  _, _, status = Open3.capture3("git diff --cached --quiet")
@@ -312,7 +390,7 @@ module Reissue
312
390
  )
313
391
  end
314
392
 
315
- finalize_message = "Finalize the changelog for version #{version} on #{date}"
393
+ message = finalize_message.call(version, date)
316
394
  if commit_finalize
317
395
  if finalize_with_branch?
318
396
  tasker["#{name}:branch"].invoke("finalize/#{version}")
@@ -320,13 +398,13 @@ module Reissue
320
398
  run_command("git add -u", "Failed to stage finalized changelog")
321
399
  stage_updated_paths
322
400
  if changes_to_commit?
323
- run_command("git commit -m '#{finalize_message}'", "Failed to commit finalized changelog")
401
+ run_command("git commit -m '#{message}'", "Failed to commit finalized changelog")
324
402
  tasker["#{name}:push"].invoke if push_finalize?
325
403
  else
326
- puts finalize_message
404
+ puts message
327
405
  end
328
406
  else
329
- puts finalize_message
407
+ puts message
330
408
  end
331
409
  end
332
410
 
@@ -359,6 +437,15 @@ module Reissue
359
437
  end
360
438
  end
361
439
 
440
+ # Preview reports; it does not fail. next_release_version raises when
441
+ # deferred versioning has no tag or trailer to resolve from.
442
+ previewed_version = begin
443
+ next_release_version
444
+ rescue RuntimeError
445
+ nil
446
+ end
447
+ puts "Version to be released: #{previewed_version || "cannot be determined"}\n\n"
448
+
362
449
  entries = handler.read
363
450
 
364
451
  if entries.empty?
@@ -418,6 +505,11 @@ module Reissue
418
505
  end
419
506
  end
420
507
 
508
+ desc "Print the version the next release would use, without changing anything"
509
+ task "#{name}:next_version" do
510
+ puts next_release_version
511
+ end
512
+
421
513
  desc "Bump version based on git trailers"
422
514
  task "#{name}:bump" do
423
515
  next if deferred_versioning
@@ -441,9 +533,7 @@ module Reissue
441
533
  if tag_version && current_version == tag_version
442
534
  if bump
443
535
  updater = Reissue::VersionUpdater.new(version_file, version_redo_proc: version_redo_proc)
444
- updater.call(bump)
445
- bundle
446
- puts "Version bumped (#{bump}) to #{updater.instance_variable_get(:@new_version)}"
536
+ apply_version_bump(updater, bump)
447
537
  end
448
538
  elsif tag_version && current_version != tag_version
449
539
  if bump
@@ -451,9 +541,7 @@ module Reissue
451
541
  desired_version = updater.redo(tag_version, bump)
452
542
 
453
543
  if desired_version > current_version
454
- updater.call(bump)
455
- bundle
456
- puts "Version bumped (#{bump}) to #{updater.instance_variable_get(:@new_version)}"
544
+ apply_version_bump(updater, bump)
457
545
  else
458
546
  puts "Version already bumped (#{tag_version} → #{current_version}), skipping"
459
547
  end
@@ -462,9 +550,7 @@ module Reissue
462
550
  end
463
551
  elsif bump
464
552
  updater = Reissue::VersionUpdater.new(version_file, version_redo_proc: version_redo_proc)
465
- updater.call(bump)
466
- bundle
467
- puts "Version bumped (#{bump}) to #{updater.instance_variable_get(:@new_version)}"
553
+ apply_version_bump(updater, bump)
468
554
  end
469
555
  end
470
556
  end
@@ -474,8 +560,8 @@ end
474
560
  # Define the reissue:initialize task as a standalone task
475
561
  # This works without any configuration, allowing users to bootstrap their project
476
562
  namespace :reissue do
477
- desc "Initialize reissue by creating a CHANGELOG.md and showing Rakefile configuration"
478
- task :initialize do
563
+ desc "Initialize reissue by creating a CHANGELOG.md and showing Rakefile configuration. Pass [runbook] to also create a RUNBOOK.md."
564
+ task :initialize, [:runbook] do |task, args|
479
565
  changelog_file = "CHANGELOG.md"
480
566
 
481
567
  if File.exist?(changelog_file)
@@ -485,6 +571,16 @@ namespace :reissue do
485
571
  puts "✓ Created #{changelog_file}"
486
572
  end
487
573
 
574
+ if args[:runbook]
575
+ runbook_file = (args[:runbook] == "runbook") ? "RUNBOOK.md" : args[:runbook]
576
+ if File.exist?(runbook_file)
577
+ puts "✓ #{runbook_file} already exists"
578
+ else
579
+ Reissue::Runbook.new(runbook_file).generate
580
+ puts "✓ Created #{runbook_file}"
581
+ end
582
+ end
583
+
488
584
  puts
489
585
  puts "Add the following to your Rakefile to enable reissue tasks:"
490
586
  puts
@@ -514,6 +610,7 @@ namespace :reissue do
514
610
 
515
611
  # Post-release runbook (mainly for applications)
516
612
  # task.runbook_file = "RUNBOOK.md" # Maintain a runbook from Runbook: git trailers
613
+ # # Seed a starter file with: rake reissue:initialize[runbook]
517
614
 
518
615
  # Git workflow options
519
616
  task.commit = true # Auto-commit version bumps
@@ -6,15 +6,20 @@ module Reissue
6
6
  class Runbook
7
7
  ITEM_PATTERN = /^- (?:\[(?<checked>[ xX])\] )?(?<text>.+)$/
8
8
 
9
+ DEFAULT_TITLE = "Runbook"
10
+ DEFAULT_PREAMBLE = "Steps to perform after releasing the version below."
11
+
9
12
  def initialize(runbook_file)
10
13
  @runbook_file = runbook_file
11
14
  end
12
15
 
13
16
  attr_reader :runbook_file
14
17
 
15
- # Writes a header-only template for the given version.
18
+ # Writes a header-only template for the given version, preserving any
19
+ # custom title and preamble already in the file.
16
20
  def generate(version: "Unreleased")
17
- File.write(runbook_file, template(heading(version)))
21
+ title, preamble = parsed_header
22
+ File.write(runbook_file, template(heading(version), title:, preamble:))
18
23
  end
19
24
 
20
25
  alias_method :clear, :generate
@@ -25,18 +30,33 @@ module Reissue
25
30
  end
26
31
 
27
32
  # Stamps the header with the release version and date, merging directly
28
- # edited items with trailer items (deduplicated by text).
33
+ # edited items with trailer items (deduplicated by text). Preserves any
34
+ # custom title and preamble already in the file.
29
35
  def finalize(version:, date:, trailer_items: [])
36
+ title, preamble = parsed_header
30
37
  merged = parsed_items
31
38
  texts = merged.map { |item| item[:text] }
32
39
  trailer_items.each do |text|
33
40
  merged << {text: text, checked: false} unless texts.include?(text)
34
41
  end
35
- File.write(runbook_file, template(heading(version, date), merged))
42
+ File.write(runbook_file, template(heading(version, date), merged, title:, preamble:))
36
43
  end
37
44
 
38
45
  private
39
46
 
47
+ # Returns the [title, preamble] found before the first version heading,
48
+ # falling back to the defaults when the file is missing or header-only.
49
+ def parsed_header
50
+ return [DEFAULT_TITLE, DEFAULT_PREAMBLE] unless File.exist?(runbook_file)
51
+
52
+ header_chunk = File.read(runbook_file).split(/^## /, 2).first.to_s
53
+ title_match = header_chunk.match(/^#\s+(?<title>.+)$/)
54
+ title = title_match ? title_match[:title].strip : DEFAULT_TITLE
55
+ preamble = (title_match ? header_chunk.sub(title_match[0], "") : header_chunk).strip
56
+ preamble = DEFAULT_PREAMBLE if preamble.empty?
57
+ [title, preamble]
58
+ end
59
+
40
60
  def parsed_items
41
61
  return [] unless File.exist?(runbook_file)
42
62
 
@@ -51,11 +71,11 @@ module Reissue
51
71
  date ? "## [#{version}] - #{date}" : "## [#{version}]"
52
72
  end
53
73
 
54
- def template(heading, items = [])
74
+ def template(heading, items = [], title: DEFAULT_TITLE, preamble: DEFAULT_PREAMBLE)
55
75
  header = <<~MARKDOWN
56
- # Runbook
76
+ # #{title}
57
77
 
58
- Steps to perform after releasing the version below.
78
+ #{preamble}
59
79
 
60
80
  #{heading}
61
81
  MARKDOWN
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reissue
4
- VERSION = "0.5.0"
5
- RELEASE_DATE = "2026-07-21"
4
+ VERSION = "0.5.2"
5
+ RELEASE_DATE = "2026-09-17"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reissue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay