reissue 0.5.0 → 0.5.1

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: 6c5e8636f0e88ac91af9c6dec7033c12a62e55d6c47a0eb2212dc03d51877058
4
+ data.tar.gz: a83714b57eb820b00a4b3cbfdab77640d5062f7d3e00aba0b52aca83d89cbad0
5
5
  SHA512:
6
- metadata.gz: 9f3a8bcfcebd80438e8b0ac3f0702594f6a5f6b265fcc1f156b40a0c4481286dc9c007ed8c2949dffc9c7f6e640cb20e2495f1774387d0537adc3e5d485b6dd8
7
- data.tar.gz: ee70e94135baa48e89ecc033f071cc31814e02925b0df16c7c73b6891dc57bc671aa9e5af19a4257fdd213a8aacc859256a5f7cd9737504db8a5202df6482735
6
+ metadata.gz: 63a416ec48050137cd7ee8ecd1d295395aa2e32b243fbb0835aecd185a48ddf36abebfe1c979450ef6be9cec46a67c2163896fba5b265874dc7b13eb1bc76ad5
7
+ data.tar.gz: d6b7f8c2e94997cd952c8e84aa74f29659d06cae56d338e20c10d37405ee1eb8dbf5615688268d366855942d1745bd36d7cd2f8881a1c29104c6045ad29a1947
data/CHANGELOG.md CHANGED
@@ -5,14 +5,23 @@ 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.1] - 2026-07-24
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
+ - reissue:next_version task reports the version the next release would use without changing anything (9feb212)
13
+ - reissue:initialize[runbook] scaffolds a starter runbook file (dfa521a)
14
+
15
+ ### Changed
13
16
 
14
- ## [0.4.22] - 2026-04-09
17
+ - Runbook finalize and new-version reset preserve a custom title and preamble (a0d874c)
15
18
 
16
19
  ### Fixed
17
20
 
18
- - changelog sections not following changelog_sections order on release (5f9c475)
21
+ - Releases no longer publish a version whose changelog entry and git tag name the previous version (acf4c1a)
22
+
23
+ ## [0.4.23] - 2026-07-21
24
+
25
+ ### Added
26
+
27
+ - Optional runbook_file setting maintains a post-release runbook populated from Runbook: git trailers or direct edits (b41ba15)
data/README.md CHANGED
@@ -357,6 +357,8 @@ Reissue::Task.create :reissue do |task|
357
357
  end
358
358
  ```
359
359
 
360
+ 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).
361
+
360
362
  The runbook holds only the latest release. Items are checkboxes so operators can tick them off:
361
363
 
362
364
  ```markdown
@@ -370,6 +372,8 @@ Steps to perform after releasing the version below.
370
372
  - [ ] Re-index search documents (def5678)
371
373
  ```
372
374
 
375
+ 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`.
376
+
373
377
  ### Adding Runbook Items
374
378
 
375
379
  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
@@ -188,6 +188,78 @@ module Reissue
188
188
  end
189
189
  end
190
190
 
191
+ # The version the next release would use, derived exactly the way reissue:bump
192
+ # derives it, without writing anything.
193
+ #
194
+ # @return [Gem::Version] the version a release would publish
195
+ def next_release_version
196
+ require_relative "fragment_handler"
197
+ require_relative "version_updater"
198
+
199
+ # reissue:bump only consults git trailers for :git fragments.
200
+ handler = (Reissue::FragmentHandler.for(:git, tag_pattern: tag_pattern) if fragment == :git)
201
+ tag_version = handler&.last_tag_version
202
+ bump = handler&.read_version_bump
203
+ updater = Reissue::VersionUpdater.new(version_file, version_redo_proc: version_redo_proc)
204
+
205
+ if deferred_versioning
206
+ # The version file holds "Unreleased"; finalize resolves the real version
207
+ # from the last tag and the trailer.
208
+ unless tag_version && bump
209
+ raise "Cannot determine the next version. Deferred versioning resolves it from " \
210
+ "the last version tag and a Version: trailer, and one of those is missing."
211
+ end
212
+ return updater.redo(tag_version, bump)
213
+ end
214
+
215
+ current_version = ::Gem::Version.new(File.read(version_file).match(Reissue::VersionUpdater::VERSION_MATCH)[0])
216
+
217
+ if bump.nil?
218
+ current_version
219
+ elsif tag_version && tag_version != current_version
220
+ # A post-release bump already moved the file past the tag, so the trailer
221
+ # only counts if it would land beyond where the file already is.
222
+ [updater.redo(tag_version, bump), current_version].max
223
+ else
224
+ updater.redo(current_version, bump)
225
+ end
226
+ end
227
+
228
+ # Apply a version bump and leave the changelog agreeing with it.
229
+ #
230
+ # @param updater [Reissue::VersionUpdater] the updater to bump with
231
+ # @param bump [Symbol] the segment to bump
232
+ # @return [String] the version the bump landed on
233
+ def apply_version_bump(updater, bump)
234
+ new_version = updater.call(bump)
235
+ realign_unreleased_changelog(new_version)
236
+ bundle
237
+ puts "Version bumped (#{bump}) to #{new_version}"
238
+ new_version
239
+ end
240
+
241
+ # Point the changelog's unreleased entry at the version just bumped to.
242
+ #
243
+ # The bump moves the version file forward, but the unreleased entry still
244
+ # carries the number from the previous cycle. reissue:finalize dates whatever
245
+ # that entry says, so leaving it alone publishes a gem built at the new
246
+ # version while the only dated changelog entry names the old one.
247
+ #
248
+ # @param new_version [String] the version the bump landed on
249
+ def realign_unreleased_changelog(new_version)
250
+ return unless changelog_file && File.exist?(changelog_file)
251
+
252
+ changelog = Reissue::Parser.parse(File.read(changelog_file))
253
+ unreleased = changelog["versions"].find { |version| version["date"] == "Unreleased" }
254
+ return if unreleased.nil? || unreleased["version"] == new_version
255
+
256
+ unreleased["version"] = new_version
257
+
258
+ changelog_updater = Reissue::ChangelogUpdater.new(changelog_file)
259
+ changelog_updater.instance_variable_set(:@changelog, changelog)
260
+ changelog_updater.write(changelog_file)
261
+ end
262
+
191
263
  # Check if there are staged changes ready to commit.
192
264
  def changes_to_commit?
193
265
  _, _, status = Open3.capture3("git diff --cached --quiet")
@@ -359,6 +431,15 @@ module Reissue
359
431
  end
360
432
  end
361
433
 
434
+ # Preview reports; it does not fail. next_release_version raises when
435
+ # deferred versioning has no tag or trailer to resolve from.
436
+ previewed_version = begin
437
+ next_release_version
438
+ rescue RuntimeError
439
+ nil
440
+ end
441
+ puts "Version to be released: #{previewed_version || "cannot be determined"}\n\n"
442
+
362
443
  entries = handler.read
363
444
 
364
445
  if entries.empty?
@@ -418,6 +499,11 @@ module Reissue
418
499
  end
419
500
  end
420
501
 
502
+ desc "Print the version the next release would use, without changing anything"
503
+ task "#{name}:next_version" do
504
+ puts next_release_version
505
+ end
506
+
421
507
  desc "Bump version based on git trailers"
422
508
  task "#{name}:bump" do
423
509
  next if deferred_versioning
@@ -441,9 +527,7 @@ module Reissue
441
527
  if tag_version && current_version == tag_version
442
528
  if bump
443
529
  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)}"
530
+ apply_version_bump(updater, bump)
447
531
  end
448
532
  elsif tag_version && current_version != tag_version
449
533
  if bump
@@ -451,9 +535,7 @@ module Reissue
451
535
  desired_version = updater.redo(tag_version, bump)
452
536
 
453
537
  if desired_version > current_version
454
- updater.call(bump)
455
- bundle
456
- puts "Version bumped (#{bump}) to #{updater.instance_variable_get(:@new_version)}"
538
+ apply_version_bump(updater, bump)
457
539
  else
458
540
  puts "Version already bumped (#{tag_version} → #{current_version}), skipping"
459
541
  end
@@ -462,9 +544,7 @@ module Reissue
462
544
  end
463
545
  elsif bump
464
546
  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)}"
547
+ apply_version_bump(updater, bump)
468
548
  end
469
549
  end
470
550
  end
@@ -474,8 +554,8 @@ end
474
554
  # Define the reissue:initialize task as a standalone task
475
555
  # This works without any configuration, allowing users to bootstrap their project
476
556
  namespace :reissue do
477
- desc "Initialize reissue by creating a CHANGELOG.md and showing Rakefile configuration"
478
- task :initialize do
557
+ desc "Initialize reissue by creating a CHANGELOG.md and showing Rakefile configuration. Pass [runbook] to also create a RUNBOOK.md."
558
+ task :initialize, [:runbook] do |task, args|
479
559
  changelog_file = "CHANGELOG.md"
480
560
 
481
561
  if File.exist?(changelog_file)
@@ -485,6 +565,16 @@ namespace :reissue do
485
565
  puts "✓ Created #{changelog_file}"
486
566
  end
487
567
 
568
+ if args[:runbook]
569
+ runbook_file = (args[:runbook] == "runbook") ? "RUNBOOK.md" : args[:runbook]
570
+ if File.exist?(runbook_file)
571
+ puts "✓ #{runbook_file} already exists"
572
+ else
573
+ Reissue::Runbook.new(runbook_file).generate
574
+ puts "✓ Created #{runbook_file}"
575
+ end
576
+ end
577
+
488
578
  puts
489
579
  puts "Add the following to your Rakefile to enable reissue tasks:"
490
580
  puts
@@ -514,6 +604,7 @@ namespace :reissue do
514
604
 
515
605
  # Post-release runbook (mainly for applications)
516
606
  # task.runbook_file = "RUNBOOK.md" # Maintain a runbook from Runbook: git trailers
607
+ # # Seed a starter file with: rake reissue:initialize[runbook]
517
608
 
518
609
  # Git workflow options
519
610
  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.1"
5
+ RELEASE_DATE = "2026-07-24"
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay