reissue 0.4.22 → 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 +4 -4
- data/CHANGELOG.md +16 -3
- data/README.md +52 -0
- data/lib/reissue/fragment_handler/directory_fragment_handler.rb +0 -2
- data/lib/reissue/fragment_handler/git_fragment_handler.rb +48 -0
- data/lib/reissue/gem.rb +24 -0
- data/lib/reissue/rake.rb +136 -17
- data/lib/reissue/runbook.rb +88 -0
- data/lib/reissue/version.rb +2 -2
- data/lib/reissue/version_updater.rb +2 -2
- data/lib/reissue.rb +19 -6
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c5e8636f0e88ac91af9c6dec7033c12a62e55d6c47a0eb2212dc03d51877058
|
|
4
|
+
data.tar.gz: a83714b57eb820b00a4b3cbfdab77640d5062f7d3e00aba0b52aca83d89cbad0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63a416ec48050137cd7ee8ecd1d295395aa2e32b243fbb0835aecd185a48ddf36abebfe1c979450ef6be9cec46a67c2163896fba5b265874dc7b13eb1bc76ad5
|
|
7
|
+
data.tar.gz: d6b7f8c2e94997cd952c8e84aa74f29659d06cae56d338e20c10d37405ee1eb8dbf5615688268d366855942d1745bd36d7cd2f8881a1c29104c6045ad29a1947
|
data/CHANGELOG.md
CHANGED
|
@@ -5,10 +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.
|
|
8
|
+
## [0.5.1] - 2026-07-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
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
|
|
16
|
+
|
|
17
|
+
- Runbook finalize and new-version reset preserve a custom title and preamble (a0d874c)
|
|
9
18
|
|
|
10
19
|
### Fixed
|
|
11
20
|
|
|
12
|
-
- changelog
|
|
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
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
- Optional runbook_file setting maintains a post-release runbook populated from Runbook: git trailers or direct edits (b41ba15)
|
data/README.md
CHANGED
|
@@ -345,6 +345,58 @@ 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
|
+
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
|
+
|
|
362
|
+
The runbook holds only the latest release. Items are checkboxes so operators can tick them off:
|
|
363
|
+
|
|
364
|
+
```markdown
|
|
365
|
+
# Runbook
|
|
366
|
+
|
|
367
|
+
Steps to perform after releasing the version below.
|
|
368
|
+
|
|
369
|
+
## [1.2.3] - 2026-07-21
|
|
370
|
+
|
|
371
|
+
- [ ] Run `rake data:cleanup` (abc1234)
|
|
372
|
+
- [ ] Re-index search documents (def5678)
|
|
373
|
+
```
|
|
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
|
+
|
|
377
|
+
### Adding Runbook Items
|
|
378
|
+
|
|
379
|
+
Add `Runbook:` trailers to commit messages (case-insensitive, independent of the `fragment` setting):
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
git commit -m "Add data migration
|
|
383
|
+
|
|
384
|
+
Fixed: Duplicate user records
|
|
385
|
+
Runbook: Run \`rake data:cleanup\` after deploy"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
You can also edit the checklist under `## [Unreleased]` directly; direct edits and trailers are merged and deduplicated.
|
|
389
|
+
|
|
390
|
+
Note: `Runbook:` is reserved for the runbook — do not add "Runbook" to `changelog_sections`.
|
|
391
|
+
|
|
392
|
+
### Lifecycle
|
|
393
|
+
|
|
394
|
+
1. **During development** — items accumulate from `Runbook:` trailers and direct edits.
|
|
395
|
+
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.
|
|
396
|
+
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.
|
|
397
|
+
|
|
398
|
+
`rake reissue:preview` also shows pending runbook items when `runbook_file` is configured.
|
|
399
|
+
|
|
348
400
|
## Development
|
|
349
401
|
|
|
350
402
|
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.
|
|
@@ -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/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
|
@@ -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
|
|
@@ -181,15 +188,87 @@ module Reissue
|
|
|
181
188
|
end
|
|
182
189
|
end
|
|
183
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
|
+
|
|
184
263
|
# Check if there are staged changes ready to commit.
|
|
185
264
|
def changes_to_commit?
|
|
186
265
|
_, _, status = Open3.capture3("git diff --cached --quiet")
|
|
187
266
|
!status.success?
|
|
188
267
|
end
|
|
189
268
|
|
|
190
|
-
# Stage updated_paths that exist on disk.
|
|
269
|
+
# Stage updated_paths (and the runbook file) that exist on disk.
|
|
191
270
|
def stage_updated_paths
|
|
192
|
-
existing = updated_paths.select { |p| File.exist?(p) }
|
|
271
|
+
existing = (updated_paths + [runbook_file].compact).select { |p| File.exist?(p) }
|
|
193
272
|
if existing.any?
|
|
194
273
|
run_command("git add #{existing.join(" ")}", "Failed to stage additional paths: #{existing.join(", ")}")
|
|
195
274
|
end
|
|
@@ -206,7 +285,8 @@ module Reissue
|
|
|
206
285
|
changelog_file:,
|
|
207
286
|
version_limit:,
|
|
208
287
|
fragment: fragment,
|
|
209
|
-
tag_pattern
|
|
288
|
+
tag_pattern:,
|
|
289
|
+
runbook_file:
|
|
210
290
|
)
|
|
211
291
|
else
|
|
212
292
|
segment = args[:segment] || "patch"
|
|
@@ -217,7 +297,8 @@ module Reissue
|
|
|
217
297
|
version_limit:,
|
|
218
298
|
version_redo_proc:,
|
|
219
299
|
fragment: fragment,
|
|
220
|
-
tag_pattern
|
|
300
|
+
tag_pattern:,
|
|
301
|
+
runbook_file:
|
|
221
302
|
)
|
|
222
303
|
end
|
|
223
304
|
bundle
|
|
@@ -287,7 +368,8 @@ module Reissue
|
|
|
287
368
|
fragment: fragment,
|
|
288
369
|
tag_pattern:,
|
|
289
370
|
version_file:,
|
|
290
|
-
version_redo_proc
|
|
371
|
+
version_redo_proc:,
|
|
372
|
+
runbook_file:
|
|
291
373
|
)
|
|
292
374
|
else
|
|
293
375
|
date = args[:version_or_segment] || Time.now.strftime("%Y-%m-%d")
|
|
@@ -297,7 +379,8 @@ module Reissue
|
|
|
297
379
|
retain_changelogs:,
|
|
298
380
|
fragment: fragment,
|
|
299
381
|
tag_pattern:,
|
|
300
|
-
version_file
|
|
382
|
+
version_file:,
|
|
383
|
+
runbook_file:
|
|
301
384
|
)
|
|
302
385
|
end
|
|
303
386
|
|
|
@@ -348,6 +431,15 @@ module Reissue
|
|
|
348
431
|
end
|
|
349
432
|
end
|
|
350
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
|
+
|
|
351
443
|
entries = handler.read
|
|
352
444
|
|
|
353
445
|
if entries.empty?
|
|
@@ -375,6 +467,20 @@ module Reissue
|
|
|
375
467
|
puts "Fragment handling is not configured."
|
|
376
468
|
puts "Set task.fragment to a directory path or :git to enable changelog fragments."
|
|
377
469
|
end
|
|
470
|
+
|
|
471
|
+
if runbook_file
|
|
472
|
+
require_relative "fragment_handler"
|
|
473
|
+
runbook_entries = Reissue::FragmentHandler.for(:git, tag_pattern: tag_pattern).read_runbook_entries
|
|
474
|
+
|
|
475
|
+
puts
|
|
476
|
+
if runbook_entries.empty?
|
|
477
|
+
puts "No runbook items found."
|
|
478
|
+
puts " (No Runbook: trailers found since last version tag)"
|
|
479
|
+
else
|
|
480
|
+
puts "Runbook items that will be added to #{runbook_file}:\n\n"
|
|
481
|
+
runbook_entries.each { |item| puts "- [ ] #{item}" }
|
|
482
|
+
end
|
|
483
|
+
end
|
|
378
484
|
end
|
|
379
485
|
|
|
380
486
|
desc "Clear fragments"
|
|
@@ -393,6 +499,11 @@ module Reissue
|
|
|
393
499
|
end
|
|
394
500
|
end
|
|
395
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
|
+
|
|
396
507
|
desc "Bump version based on git trailers"
|
|
397
508
|
task "#{name}:bump" do
|
|
398
509
|
next if deferred_versioning
|
|
@@ -416,9 +527,7 @@ module Reissue
|
|
|
416
527
|
if tag_version && current_version == tag_version
|
|
417
528
|
if bump
|
|
418
529
|
updater = Reissue::VersionUpdater.new(version_file, version_redo_proc: version_redo_proc)
|
|
419
|
-
updater
|
|
420
|
-
bundle
|
|
421
|
-
puts "Version bumped (#{bump}) to #{updater.instance_variable_get(:@new_version)}"
|
|
530
|
+
apply_version_bump(updater, bump)
|
|
422
531
|
end
|
|
423
532
|
elsif tag_version && current_version != tag_version
|
|
424
533
|
if bump
|
|
@@ -426,9 +535,7 @@ module Reissue
|
|
|
426
535
|
desired_version = updater.redo(tag_version, bump)
|
|
427
536
|
|
|
428
537
|
if desired_version > current_version
|
|
429
|
-
updater
|
|
430
|
-
bundle
|
|
431
|
-
puts "Version bumped (#{bump}) to #{updater.instance_variable_get(:@new_version)}"
|
|
538
|
+
apply_version_bump(updater, bump)
|
|
432
539
|
else
|
|
433
540
|
puts "Version already bumped (#{tag_version} → #{current_version}), skipping"
|
|
434
541
|
end
|
|
@@ -437,9 +544,7 @@ module Reissue
|
|
|
437
544
|
end
|
|
438
545
|
elsif bump
|
|
439
546
|
updater = Reissue::VersionUpdater.new(version_file, version_redo_proc: version_redo_proc)
|
|
440
|
-
updater
|
|
441
|
-
bundle
|
|
442
|
-
puts "Version bumped (#{bump}) to #{updater.instance_variable_get(:@new_version)}"
|
|
547
|
+
apply_version_bump(updater, bump)
|
|
443
548
|
end
|
|
444
549
|
end
|
|
445
550
|
end
|
|
@@ -449,8 +554,8 @@ end
|
|
|
449
554
|
# Define the reissue:initialize task as a standalone task
|
|
450
555
|
# This works without any configuration, allowing users to bootstrap their project
|
|
451
556
|
namespace :reissue do
|
|
452
|
-
desc "Initialize reissue by creating a CHANGELOG.md and showing Rakefile configuration"
|
|
453
|
-
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|
|
|
454
559
|
changelog_file = "CHANGELOG.md"
|
|
455
560
|
|
|
456
561
|
if File.exist?(changelog_file)
|
|
@@ -460,6 +565,16 @@ namespace :reissue do
|
|
|
460
565
|
puts "✓ Created #{changelog_file}"
|
|
461
566
|
end
|
|
462
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
|
+
|
|
463
578
|
puts
|
|
464
579
|
puts "Add the following to your Rakefile to enable reissue tasks:"
|
|
465
580
|
puts
|
|
@@ -487,6 +602,10 @@ namespace :reissue do
|
|
|
487
602
|
# task.fragment = "changelog_fragments" # Or use a directory of fragment files
|
|
488
603
|
task.clear_fragments = false # Clear fragment files after release. Not necessary when using git trailers.
|
|
489
604
|
|
|
605
|
+
# Post-release runbook (mainly for applications)
|
|
606
|
+
# task.runbook_file = "RUNBOOK.md" # Maintain a runbook from Runbook: git trailers
|
|
607
|
+
# # Seed a starter file with: rake reissue:initialize[runbook]
|
|
608
|
+
|
|
490
609
|
# Git workflow options
|
|
491
610
|
task.commit = true # Auto-commit version bumps
|
|
492
611
|
task.commit_finalize = true # Auto-commit changelog finalization
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
DEFAULT_TITLE = "Runbook"
|
|
10
|
+
DEFAULT_PREAMBLE = "Steps to perform after releasing the version below."
|
|
11
|
+
|
|
12
|
+
def initialize(runbook_file)
|
|
13
|
+
@runbook_file = runbook_file
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :runbook_file
|
|
17
|
+
|
|
18
|
+
# Writes a header-only template for the given version, preserving any
|
|
19
|
+
# custom title and preamble already in the file.
|
|
20
|
+
def generate(version: "Unreleased")
|
|
21
|
+
title, preamble = parsed_header
|
|
22
|
+
File.write(runbook_file, template(heading(version), title:, preamble:))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias_method :clear, :generate
|
|
26
|
+
|
|
27
|
+
# Returns the text of each checklist item, without checkbox markers.
|
|
28
|
+
def items
|
|
29
|
+
parsed_items.map { |item| item[:text] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Stamps the header with the release version and date, merging directly
|
|
33
|
+
# edited items with trailer items (deduplicated by text). Preserves any
|
|
34
|
+
# custom title and preamble already in the file.
|
|
35
|
+
def finalize(version:, date:, trailer_items: [])
|
|
36
|
+
title, preamble = parsed_header
|
|
37
|
+
merged = parsed_items
|
|
38
|
+
texts = merged.map { |item| item[:text] }
|
|
39
|
+
trailer_items.each do |text|
|
|
40
|
+
merged << {text: text, checked: false} unless texts.include?(text)
|
|
41
|
+
end
|
|
42
|
+
File.write(runbook_file, template(heading(version, date), merged, title:, preamble:))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
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
|
+
|
|
60
|
+
def parsed_items
|
|
61
|
+
return [] unless File.exist?(runbook_file)
|
|
62
|
+
|
|
63
|
+
File.read(runbook_file).lines.filter_map do |line|
|
|
64
|
+
match = line.rstrip.match(ITEM_PATTERN)
|
|
65
|
+
next unless match
|
|
66
|
+
{text: match[:text], checked: match[:checked]&.downcase == "x"}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def heading(version, date = nil)
|
|
71
|
+
date ? "## [#{version}] - #{date}" : "## [#{version}]"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def template(heading, items = [], title: DEFAULT_TITLE, preamble: DEFAULT_PREAMBLE)
|
|
75
|
+
header = <<~MARKDOWN
|
|
76
|
+
# #{title}
|
|
77
|
+
|
|
78
|
+
#{preamble}
|
|
79
|
+
|
|
80
|
+
#{heading}
|
|
81
|
+
MARKDOWN
|
|
82
|
+
return header if items.empty?
|
|
83
|
+
|
|
84
|
+
list = items.map { |item| "- [#{item[:checked] ? "x" : " "}] #{item[:text]}" }.join("\n")
|
|
85
|
+
"#{header}\n#{list}\n"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/reissue/version.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
+
version: 0.5.1
|
|
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:
|
|
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: []
|