reissue 0.4.22 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9e4d5dc8c0b04d5c41514285fa8e8f2e7e87cc028b156ab7de97473e1e58276
4
- data.tar.gz: e4af6b1a8061ec686f33e852b0abccd0d4e92335b87ad48c286e0645b7932ed7
3
+ metadata.gz: 9c5b7a3ee9dbd72238bf4b6212c953283caa5be24da67647358e74ac1fb1118f
4
+ data.tar.gz: 07f9b5bc9c3263a010f2a942701c49c110dd6db566d154e501db59d13c2d6cac
5
5
  SHA512:
6
- metadata.gz: 12fe862113b9d5a765f7efd562ce4555b873e8496068c480f3a0531b873fbbcedc10bf44c1d971e7404ab1236f43125f08c31bac988ba88fb800dd78d5021618
7
- data.tar.gz: 2235ef992fdfbda13a06e27bf1df4b582cb3ba9cfa034cdcc6bc1e7c61f8fa33167f553031dc7401eefb29edb59fc5e8ecbe699cdec3f2431796002356115b32
6
+ metadata.gz: 9f3a8bcfcebd80438e8b0ac3f0702594f6a5f6b265fcc1f156b40a0c4481286dc9c007ed8c2949dffc9c7f6e640cb20e2495f1774387d0537adc3e5d485b6dd8
7
+ data.tar.gz: ee70e94135baa48e89ecc033f071cc31814e02925b0df16c7c73b6891dc57bc671aa9e5af19a4257fdd213a8aacc859256a5f7cd9737504db8a5202df6482735
data/CHANGELOG.md CHANGED
@@ -5,10 +5,14 @@ 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
9
+
10
+ ### Added
11
+
12
+ - Optional runbook_file setting maintains a post-release runbook populated from Runbook: git trailers or direct edits (b41ba15)
13
+
8
14
  ## [0.4.22] - 2026-04-09
9
15
 
10
16
  ### Fixed
11
17
 
12
18
  - changelog sections not following changelog_sections order on release (5f9c475)
13
-
14
- ## [0.4.21] - 2026-03-11
data/README.md CHANGED
@@ -345,6 +345,54 @@ Version trailers are case-insensitive:
345
345
  - **New features:** Use `Version: minor` for backwards-compatible new functionality
346
346
  - **Bug fixes:** Use `Version: patch` for backwards-compatible bug fixes (or omit - patch is the default post-release bump)
347
347
 
348
+ ## Post-release Runbook
349
+
350
+ Reissue can maintain a runbook file listing manual steps to perform after a release — run a rake task, clean up data, re-index documents, and so on. This is aimed at applications; gems are unlikely to need it.
351
+
352
+ ```ruby
353
+ Reissue::Task.create :reissue do |task|
354
+ task.version_file = "config/version.rb"
355
+ task.fragment = :git
356
+ task.runbook_file = "RUNBOOK.md" # Default: nil (disabled)
357
+ end
358
+ ```
359
+
360
+ The runbook holds only the latest release. Items are checkboxes so operators can tick them off:
361
+
362
+ ```markdown
363
+ # Runbook
364
+
365
+ Steps to perform after releasing the version below.
366
+
367
+ ## [1.2.3] - 2026-07-21
368
+
369
+ - [ ] Run `rake data:cleanup` (abc1234)
370
+ - [ ] Re-index search documents (def5678)
371
+ ```
372
+
373
+ ### Adding Runbook Items
374
+
375
+ Add `Runbook:` trailers to commit messages (case-insensitive, independent of the `fragment` setting):
376
+
377
+ ```bash
378
+ git commit -m "Add data migration
379
+
380
+ Fixed: Duplicate user records
381
+ Runbook: Run \`rake data:cleanup\` after deploy"
382
+ ```
383
+
384
+ You can also edit the checklist under `## [Unreleased]` directly; direct edits and trailers are merged and deduplicated.
385
+
386
+ Note: `Runbook:` is reserved for the runbook — do not add "Runbook" to `changelog_sections`.
387
+
388
+ ### Lifecycle
389
+
390
+ 1. **During development** — items accumulate from `Runbook:` trailers and direct edits.
391
+ 2. **Finalize** (`rake reissue:finalize` / before `rake build`) — trailers since the last version tag are merged into the file and the header is stamped with the release version and date, matching the changelog. The file is committed with the finalize commit, so the release tag contains the finalized runbook.
392
+ 3. **New version setup** (`rake reissue` after release) — the runbook is cleared back to a header-only `## [Unreleased]` section. Prior runbooks remain reachable via release tags.
393
+
394
+ `rake reissue:preview` also shows pending runbook items when `runbook_file` is configured.
395
+
348
396
  ## Development
349
397
 
350
398
  After checking out the repo, run `bin/setup` to install dependencies. Then run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pathname"
4
-
5
3
  module Reissue
6
4
  # Handler for reading fragments from a directory
7
5
  class DirectoryFragmentHandler < FragmentHandler
@@ -44,6 +44,16 @@ module Reissue
44
44
  find_last_tag
45
45
  end
46
46
 
47
+ # Read runbook entries from git commit trailers
48
+ #
49
+ # @return [Array<String>] Runbook items with the short SHA appended
50
+ def read_runbook_entries
51
+ return [] unless git_available? && in_git_repo?
52
+
53
+ commits = commits_since_last_tag
54
+ parse_runbook_entries_from_commits(commits)
55
+ end
56
+
47
57
  # Read version bump from git commit trailers
48
58
  #
49
59
  # @return [Symbol, nil] One of :major, :minor, :patch, or nil if none found
@@ -185,6 +195,44 @@ module Reissue
185
195
  result
186
196
  end
187
197
 
198
+ RUNBOOK_TRAILER_REGEX = /^runbook:\s*(.+)$/i
199
+
200
+ def parse_runbook_entries_from_commits(commits)
201
+ entries = []
202
+
203
+ commits.each do |commit|
204
+ sha = commit[:sha]
205
+ lines = commit[:message].lines
206
+
207
+ i = 0
208
+ while i < lines.length
209
+ line = lines[i].rstrip
210
+ i += 1
211
+ next if line.strip.empty?
212
+
213
+ if (match = line.match(RUNBOOK_TRAILER_REGEX))
214
+ trailer_value = match[1].strip
215
+
216
+ # Collect continuation lines (non-empty lines that don't start a new trailer)
217
+ while i < lines.length
218
+ next_line = lines[i].rstrip
219
+ break if next_line.strip.empty?
220
+ break if next_line.match(RUNBOOK_TRAILER_REGEX)
221
+ break if next_line.match(trailer_regex)
222
+ break if next_line.match?(/^[A-Za-z][\w-]+:\s/)
223
+
224
+ trailer_value += " #{next_line.strip}"
225
+ i += 1
226
+ end
227
+
228
+ entries << "#{trailer_value} (#{sha})"
229
+ end
230
+ end
231
+ end
232
+
233
+ entries
234
+ end
235
+
188
236
  def normalize_section_name(name)
189
237
  # Normalize to proper case (e.g., "FIXED" -> "Fixed", "added" -> "Added")
190
238
  name.capitalize
data/lib/reissue/rake.rb CHANGED
@@ -37,6 +37,12 @@ module Reissue
37
37
  # Provide a callable to decide how to store the files.
38
38
  attr_accessor :retain_changelogs
39
39
 
40
+ # The path to a runbook file listing steps to perform after a release.
41
+ # Populated from "Runbook:" git trailers or direct edits, finalized with
42
+ # the release version, and cleared when a new version is prepared.
43
+ # Default: nil (disabled).
44
+ attr_accessor :runbook_file
45
+
40
46
  # The fragment configuration for changelog entries.
41
47
  # @return [String, nil] nil (disabled) or a directory path string for fragment files
42
48
  # @example Using directory-based fragments
@@ -126,6 +132,7 @@ module Reissue
126
132
  @updated_paths = []
127
133
  @changelog_file = "CHANGELOG.md"
128
134
  @retain_changelogs = false
135
+ @runbook_file = nil
129
136
  @fragment = nil
130
137
  @clear_fragments = false
131
138
  @commit = true
@@ -187,9 +194,9 @@ module Reissue
187
194
  !status.success?
188
195
  end
189
196
 
190
- # Stage updated_paths that exist on disk.
197
+ # Stage updated_paths (and the runbook file) that exist on disk.
191
198
  def stage_updated_paths
192
- existing = updated_paths.select { |p| File.exist?(p) }
199
+ existing = (updated_paths + [runbook_file].compact).select { |p| File.exist?(p) }
193
200
  if existing.any?
194
201
  run_command("git add #{existing.join(" ")}", "Failed to stage additional paths: #{existing.join(", ")}")
195
202
  end
@@ -206,7 +213,8 @@ module Reissue
206
213
  changelog_file:,
207
214
  version_limit:,
208
215
  fragment: fragment,
209
- tag_pattern:
216
+ tag_pattern:,
217
+ runbook_file:
210
218
  )
211
219
  else
212
220
  segment = args[:segment] || "patch"
@@ -217,7 +225,8 @@ module Reissue
217
225
  version_limit:,
218
226
  version_redo_proc:,
219
227
  fragment: fragment,
220
- tag_pattern:
228
+ tag_pattern:,
229
+ runbook_file:
221
230
  )
222
231
  end
223
232
  bundle
@@ -287,7 +296,8 @@ module Reissue
287
296
  fragment: fragment,
288
297
  tag_pattern:,
289
298
  version_file:,
290
- version_redo_proc:
299
+ version_redo_proc:,
300
+ runbook_file:
291
301
  )
292
302
  else
293
303
  date = args[:version_or_segment] || Time.now.strftime("%Y-%m-%d")
@@ -297,7 +307,8 @@ module Reissue
297
307
  retain_changelogs:,
298
308
  fragment: fragment,
299
309
  tag_pattern:,
300
- version_file:
310
+ version_file:,
311
+ runbook_file:
301
312
  )
302
313
  end
303
314
 
@@ -375,6 +386,20 @@ module Reissue
375
386
  puts "Fragment handling is not configured."
376
387
  puts "Set task.fragment to a directory path or :git to enable changelog fragments."
377
388
  end
389
+
390
+ if runbook_file
391
+ require_relative "fragment_handler"
392
+ runbook_entries = Reissue::FragmentHandler.for(:git, tag_pattern: tag_pattern).read_runbook_entries
393
+
394
+ puts
395
+ if runbook_entries.empty?
396
+ puts "No runbook items found."
397
+ puts " (No Runbook: trailers found since last version tag)"
398
+ else
399
+ puts "Runbook items that will be added to #{runbook_file}:\n\n"
400
+ runbook_entries.each { |item| puts "- [ ] #{item}" }
401
+ end
402
+ end
378
403
  end
379
404
 
380
405
  desc "Clear fragments"
@@ -487,6 +512,9 @@ namespace :reissue do
487
512
  # task.fragment = "changelog_fragments" # Or use a directory of fragment files
488
513
  task.clear_fragments = false # Clear fragment files after release. Not necessary when using git trailers.
489
514
 
515
+ # Post-release runbook (mainly for applications)
516
+ # task.runbook_file = "RUNBOOK.md" # Maintain a runbook from Runbook: git trailers
517
+
490
518
  # Git workflow options
491
519
  task.commit = true # Auto-commit version bumps
492
520
  task.commit_finalize = true # Auto-commit changelog finalization
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reissue
4
+ # Maintains a post-release runbook file listing steps to perform after
5
+ # releasing a version. The file holds only the latest release.
6
+ class Runbook
7
+ ITEM_PATTERN = /^- (?:\[(?<checked>[ xX])\] )?(?<text>.+)$/
8
+
9
+ def initialize(runbook_file)
10
+ @runbook_file = runbook_file
11
+ end
12
+
13
+ attr_reader :runbook_file
14
+
15
+ # Writes a header-only template for the given version.
16
+ def generate(version: "Unreleased")
17
+ File.write(runbook_file, template(heading(version)))
18
+ end
19
+
20
+ alias_method :clear, :generate
21
+
22
+ # Returns the text of each checklist item, without checkbox markers.
23
+ def items
24
+ parsed_items.map { |item| item[:text] }
25
+ end
26
+
27
+ # Stamps the header with the release version and date, merging directly
28
+ # edited items with trailer items (deduplicated by text).
29
+ def finalize(version:, date:, trailer_items: [])
30
+ merged = parsed_items
31
+ texts = merged.map { |item| item[:text] }
32
+ trailer_items.each do |text|
33
+ merged << {text: text, checked: false} unless texts.include?(text)
34
+ end
35
+ File.write(runbook_file, template(heading(version, date), merged))
36
+ end
37
+
38
+ private
39
+
40
+ def parsed_items
41
+ return [] unless File.exist?(runbook_file)
42
+
43
+ File.read(runbook_file).lines.filter_map do |line|
44
+ match = line.rstrip.match(ITEM_PATTERN)
45
+ next unless match
46
+ {text: match[:text], checked: match[:checked]&.downcase == "x"}
47
+ end
48
+ end
49
+
50
+ def heading(version, date = nil)
51
+ date ? "## [#{version}] - #{date}" : "## [#{version}]"
52
+ end
53
+
54
+ def template(heading, items = [])
55
+ header = <<~MARKDOWN
56
+ # Runbook
57
+
58
+ Steps to perform after releasing the version below.
59
+
60
+ #{heading}
61
+ MARKDOWN
62
+ return header if items.empty?
63
+
64
+ list = items.map { |item| "- [#{item[:checked] ? "x" : " "}] #{item[:text]}" }.join("\n")
65
+ "#{header}\n#{list}\n"
66
+ end
67
+ end
68
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reissue
4
- VERSION = "0.4.22"
5
- RELEASE_DATE = "2026-04-09"
4
+ VERSION = "0.5.0"
5
+ RELEASE_DATE = "2026-07-21"
6
6
  end
@@ -21,12 +21,12 @@ module Reissue
21
21
  when :patch
22
22
  segments.slice(0, 2) + segments.slice(2..-1).then { |array|
23
23
  array[-1] = array[-1].next
24
- [array.map(&:to_s).join]
24
+ [array.join]
25
25
  }
26
26
  when :pre
27
27
  segments.slice(0, 3) + segments.slice(3..-1).then { |array|
28
28
  array[-1] = array[-1].next
29
- [array.map(&:to_s).join]
29
+ [array.join]
30
30
  }
31
31
  else
32
32
  raise ArgumentError, "Invalid segment name: #{segment_name}"
data/lib/reissue.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "reissue/version"
4
4
  require_relative "reissue/version_updater"
5
5
  require_relative "reissue/changelog_updater"
6
6
  require_relative "reissue/fragment_handler"
7
+ require_relative "reissue/runbook"
7
8
 
8
9
  # Reissue is a module that provides functionality for updating version numbers and changelogs.
9
10
  module Reissue
@@ -40,7 +41,8 @@ module Reissue
40
41
  version_redo_proc: nil,
41
42
  fragment: nil,
42
43
  fragment_directory: nil,
43
- tag_pattern: nil
44
+ tag_pattern: nil,
45
+ runbook_file: nil
44
46
  )
45
47
  # Handle deprecation
46
48
  if fragment_directory && !fragment
@@ -55,10 +57,11 @@ module Reissue
55
57
  changelog_updater = ChangelogUpdater.new(changelog_file)
56
58
  changelog_updater.call(new_version, date:, changes:, changelog_file:, version_limit:, retain_changelogs:, fragment:, tag_pattern:)
57
59
  end
60
+ Runbook.new(runbook_file).clear if runbook_file
58
61
  new_version
59
62
  end
60
63
 
61
- def self.deferred_call(version_file:, changelog_file: "CHANGELOG.md", version_limit: 2, retain_changelogs: false, fragment: nil, tag_pattern: nil)
64
+ def self.deferred_call(version_file:, changelog_file: "CHANGELOG.md", version_limit: 2, retain_changelogs: false, fragment: nil, tag_pattern: nil, runbook_file: nil)
62
65
  version_updater = VersionUpdater.new(version_file)
63
66
  version_updater.set_version("Unreleased", version_file:)
64
67
  version_updater.update_release_date("Unreleased", version_file:)
@@ -68,10 +71,11 @@ module Reissue
68
71
  changelog_updater.call("Unreleased", date: nil, changes: {}, changelog_file:, version_limit:, retain_changelogs:, fragment:, tag_pattern:)
69
72
  end
70
73
 
74
+ Runbook.new(runbook_file).clear if runbook_file
71
75
  "Unreleased"
72
76
  end
73
77
 
74
- def self.deferred_finalize(date = Date.today, version: nil, segment: nil, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, tag_pattern: nil, version_file: nil, version_redo_proc: nil)
78
+ def self.deferred_finalize(date = Date.today, version: nil, segment: nil, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, tag_pattern: nil, version_file: nil, version_redo_proc: nil, runbook_file: nil)
75
79
  resolved_version = if version&.match?(/^\d/)
76
80
  version
77
81
  elsif segment || version
@@ -145,7 +149,14 @@ module Reissue
145
149
  end
146
150
 
147
151
  changelog = changelog_updater.finalize(date: date, changelog_file: changelog_file, retain_changelogs: retain_changelogs, resolved_version: resolved_version)
148
- changelog["versions"].first.slice("version", "date").values
152
+ changelog["versions"].first.slice("version", "date").values.tap do |finalized_version, finalized_date|
153
+ finalize_runbook(runbook_file, version: finalized_version, date: finalized_date, tag_pattern:) if runbook_file
154
+ end
155
+ end
156
+
157
+ private_class_method def self.finalize_runbook(runbook_file, version:, date:, tag_pattern: nil)
158
+ trailer_items = FragmentHandler.for(:git, tag_pattern:).read_runbook_entries
159
+ Runbook.new(runbook_file).finalize(version:, date:, trailer_items:)
149
160
  end
150
161
 
151
162
  private_class_method def self.resolve_version_from_tag(segment, tag_pattern: nil, version_redo_proc: nil)
@@ -165,7 +176,7 @@ module Reissue
165
176
  # @param fragment_directory [String] @deprecated Use fragment parameter instead
166
177
  #
167
178
  # @return [Array] The version number and release date.
168
- def self.finalize(date = Date.today, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, fragment_directory: nil, tag_pattern: nil, version_file: nil)
179
+ def self.finalize(date = Date.today, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, fragment_directory: nil, tag_pattern: nil, version_file: nil, runbook_file: nil)
169
180
  # Handle deprecation
170
181
  if fragment_directory && !fragment
171
182
  warn "[DEPRECATION] `fragment_directory` parameter is deprecated. Please use `fragment` instead."
@@ -224,7 +235,9 @@ module Reissue
224
235
  version_updater.update_release_date(date.to_s, version_file:)
225
236
  end
226
237
 
227
- changelog["versions"].first.slice("version", "date").values
238
+ changelog["versions"].first.slice("version", "date").values.tap do |finalized_version, finalized_date|
239
+ finalize_runbook(runbook_file, version: finalized_version, date: finalized_date, tag_pattern:) if runbook_file
240
+ end
228
241
  end
229
242
 
230
243
  # Reformats the changelog file to ensure it is correctly formatted.
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.4.22
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -46,6 +46,7 @@ files:
46
46
  - lib/reissue/parser.rb
47
47
  - lib/reissue/printer.rb
48
48
  - lib/reissue/rake.rb
49
+ - lib/reissue/runbook.rb
49
50
  - lib/reissue/version.rb
50
51
  - lib/reissue/version_updater.rb
51
52
  homepage: https://github.com/SOFware/reissue
@@ -68,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
69
  - !ruby/object:Gem::Version
69
70
  version: '0'
70
71
  requirements: []
71
- rubygems_version: 3.6.7
72
+ rubygems_version: 4.0.6
72
73
  specification_version: 4
73
74
  summary: Keep your versions and changelogs up to date and prepared for release.
74
75
  test_files: []