kettle-changelog 1.0.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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +119 -0
- data/LICENSE.md +12 -0
- data/README.md +429 -0
- data/exe/kettle-changelog +244 -0
- data/lib/kettle/changelog/cli.rb +1384 -0
- data/lib/kettle/changelog/entry_adder.rb +115 -0
- data/lib/kettle/changelog/version.rb +13 -0
- data/lib/kettle/changelog.rb +18 -0
- data/sig/kettle/changelog.rbs +9 -0
- data.tar.gz.sig +0 -0
- metadata +341 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# vim: set syntax=ruby
|
|
5
|
+
|
|
6
|
+
# kettle-changelog: Generate a CHANGELOG.md entry for the current VERSION.
|
|
7
|
+
# - Reads VERSION from lib/**/version.rb (must be unique across files)
|
|
8
|
+
# - Set K_CHANGELOG_PATH to read/write a changelog outside the current gem root
|
|
9
|
+
# - Set K_CHANGELOG_COVERAGE_ROOT to collect coverage from another bundle root
|
|
10
|
+
# - Set K_CHANGELOG_VERSION_FILE to use a specific version.rb for monorepo roots
|
|
11
|
+
# - Set K_CHANGELOG_GEM_NAME to use a specific gem name for root changelogs
|
|
12
|
+
# - Moves entries from the "Unreleased" section into a new versioned section
|
|
13
|
+
# - Prepends 4 heading lines:
|
|
14
|
+
# - TAG
|
|
15
|
+
# - COVERAGE (line coverage)
|
|
16
|
+
# - BRANCH COVERAGE (branch coverage)
|
|
17
|
+
# - percent documented (parsed from `bin/rake yard` output)
|
|
18
|
+
# - Updates bottom link references to GitHub style, converts any existing
|
|
19
|
+
# GitLab links to GitHub links, and appends the new [X.Y.Z] and [X.Y.Zt] links.
|
|
20
|
+
#
|
|
21
|
+
# Notes:
|
|
22
|
+
# - Strict mode (default) removes stale coverage output, runs `bundle exec
|
|
23
|
+
# kettle-test` with JSON coverage enabled, and reads the freshly generated
|
|
24
|
+
# coverage/coverage.json.
|
|
25
|
+
# - Non-strict mode reads existing coverage JSON when present and leaves
|
|
26
|
+
# unavailable coverage or documentation stats blank with a warning.
|
|
27
|
+
# - Expects `bin/rake yard` or `bin/yard` to be available for documentation stats
|
|
28
|
+
# when strict mode is enabled.
|
|
29
|
+
|
|
30
|
+
$stdout.sync = true
|
|
31
|
+
|
|
32
|
+
# Do not rely on Bundler; allow running in repos that do not depend on kettle-dev
|
|
33
|
+
# Ensure RubyGems is available for 'require' lookups
|
|
34
|
+
begin
|
|
35
|
+
require "rubygems"
|
|
36
|
+
rescue LoadError
|
|
37
|
+
# Older Rubies always have rubygems; continue anyway
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
script_basename = File.basename(__FILE__)
|
|
41
|
+
repo_lib = File.expand_path("../lib", __dir__)
|
|
42
|
+
$LOAD_PATH.unshift(repo_lib) if File.directory?(repo_lib) && !$LOAD_PATH.include?(repo_lib)
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
# Standard library
|
|
46
|
+
require "json"
|
|
47
|
+
require "time"
|
|
48
|
+
require "open3"
|
|
49
|
+
require "shellwords"
|
|
50
|
+
|
|
51
|
+
# This library
|
|
52
|
+
require "kettle/changelog"
|
|
53
|
+
require "kettle/ndjson"
|
|
54
|
+
require "kettle/dev/executable_version"
|
|
55
|
+
Kettle::Dev::ExecutableVersion.print_and_exit!(script_basename, ARGV, value_option: true)
|
|
56
|
+
machine_output = ARGV.any? { |arg| arg == "--json" || arg.start_with?("--events") }
|
|
57
|
+
Kettle::Dev::ExecutableVersion.print_header(script_basename) unless machine_output
|
|
58
|
+
rescue LoadError => e
|
|
59
|
+
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
60
|
+
warn("Hint: Ensure the host project has kettle-dev as a dependency and run bundle install.")
|
|
61
|
+
exit(1)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if ARGV.include?("-h") || ARGV.include?("--help")
|
|
65
|
+
puts <<~USAGE
|
|
66
|
+
Usage: kettle-changelog [--version VERSION] [--yes] [--update-prep|--reformat|--backfill-released-version VERSION] [--pending-release] [--release-state] [--refresh-cache] [--add-unreleased-entry --section SECTION --entry TEXT] [--json|--events[=TYPES]] [--no-strict] [--no-coverage-threshold]
|
|
67
|
+
|
|
68
|
+
Detects the current version from lib/**/version.rb, the latest live release, and
|
|
69
|
+
the most recent CHANGELOG.md release section, then prompts to confirm the selected plan:
|
|
70
|
+
create a new release section, update the prepared release section in place, or reformat only.
|
|
71
|
+
|
|
72
|
+
Release plans add coverage and documentation stats, move entries from [Unreleased],
|
|
73
|
+
and update bottom link references to GitHub style.
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--version VERSION Use this version instead of detecting VERSION from lib/**/version.rb
|
|
77
|
+
--yes Auto-approve the selected release plan
|
|
78
|
+
--update-prep Force updating the most recent prepared release section in place
|
|
79
|
+
--reformat Normalize CHANGELOG.md structure without release-state planning
|
|
80
|
+
--backfill-released-version VERSION
|
|
81
|
+
Add a tagged historical section without moving Unreleased entries
|
|
82
|
+
--pending-release Query whether the changelog has release work pending; exits 0 for yes, 1 for no
|
|
83
|
+
--release-state,
|
|
84
|
+
--release-status Print changelog release state, including latest published release and pending sources
|
|
85
|
+
--refresh-cache Bypass cached gem.coop version data for this invocation
|
|
86
|
+
--add-unreleased-entry Add one entry to an existing section under ## [Unreleased]
|
|
87
|
+
--section SECTION Unreleased section to receive the entry (Added, Changed, Deprecated, Removed, Fixed, Security)
|
|
88
|
+
--entry TEXT Changelog entry text; "- " is added when omitted
|
|
89
|
+
--json Print query output as JSON
|
|
90
|
+
--events[=TYPES] Print newline-delimited JSON changelog events
|
|
91
|
+
--no-strict Allow missing coverage and yard data (warnings only, no errors)
|
|
92
|
+
--no-coverage-threshold Generate coverage without hard-failing below configured thresholds
|
|
93
|
+
|
|
94
|
+
Environment:
|
|
95
|
+
K_CHANGELOG_STRICT=false Disable strict mode (equivalent to --no-strict flag)
|
|
96
|
+
K_CHANGELOG_COVERAGE_HARD=false Disable coverage threshold hard-failure
|
|
97
|
+
K_CHANGELOG_PATH=path Read and write this changelog instead of ./CHANGELOG.md
|
|
98
|
+
K_CHANGELOG_COVERAGE_ROOT=path Run/read coverage from this root instead of the gem root
|
|
99
|
+
K_CHANGELOG_VERSION_FILE=path Read VERSION from this file instead of lib/**/version.rb
|
|
100
|
+
K_CHANGELOG_GEM_NAME=name Read live release data as this gem instead of root .gemspec
|
|
101
|
+
KETTLE_GEM_COOP_REFRESH=true Bypass cached gem.coop version data
|
|
102
|
+
|
|
103
|
+
Data generation:
|
|
104
|
+
Strict mode is the default. Release plans remove stale coverage output,
|
|
105
|
+
run bundle exec kettle-test with JSON coverage enabled, read the resulting
|
|
106
|
+
coverage/coverage.json, and collect documentation stats from bin/rake yard
|
|
107
|
+
or bin/yard.
|
|
108
|
+
|
|
109
|
+
Strict mode fails if specs fail, coverage JSON is not produced, coverage
|
|
110
|
+
is below the configured project thresholds, or documentation stats cannot
|
|
111
|
+
be collected.
|
|
112
|
+
|
|
113
|
+
Non-strict mode does not generate missing coverage or documentation data;
|
|
114
|
+
it warns and leaves unavailable stats blank.
|
|
115
|
+
|
|
116
|
+
Runtime requirements:
|
|
117
|
+
- bundle exec kettle-test available for strict coverage generation
|
|
118
|
+
- bin/rake yard or bin/yard available for strict documentation stats
|
|
119
|
+
|
|
120
|
+
Use --no-strict or K_CHANGELOG_STRICT=false to allow missing data.
|
|
121
|
+
Use --no-coverage-threshold or K_CHANGELOG_COVERAGE_HARD=false to run fresh
|
|
122
|
+
coverage without hard-failing below configured thresholds.
|
|
123
|
+
USAGE
|
|
124
|
+
exit(0)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
def extract_version_arg!(argv)
|
|
129
|
+
version = nil
|
|
130
|
+
if (idx = argv.index("--version"))
|
|
131
|
+
version = argv[idx + 1]
|
|
132
|
+
Kettle::Dev::ExitAdapter.abort("--version requires a VERSION") if version.to_s.empty?
|
|
133
|
+
argv.slice!(idx, 2)
|
|
134
|
+
end
|
|
135
|
+
argv.delete_if do |arg|
|
|
136
|
+
if arg.start_with?("--version=", "version=")
|
|
137
|
+
version = arg.split("=", 2)[1]
|
|
138
|
+
true
|
|
139
|
+
else
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
Kettle::Dev::Versioning.normalize_explicit_version(version)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def extract_events_arg!(argv)
|
|
147
|
+
events = false
|
|
148
|
+
event_types = nil
|
|
149
|
+
argv.delete_if do |arg|
|
|
150
|
+
if arg == "--events"
|
|
151
|
+
events = true
|
|
152
|
+
elsif arg.start_with?("--events=")
|
|
153
|
+
events = true
|
|
154
|
+
event_types = arg.split("=", 2)[1]
|
|
155
|
+
else
|
|
156
|
+
false
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
[events, event_types]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Determine if strict mode is enabled (default: true)
|
|
163
|
+
strict_mode = !ARGV.include?("--no-strict") && ENV.fetch("K_CHANGELOG_STRICT", "true").downcase != "false"
|
|
164
|
+
update_prep = ARGV.delete("--update-prep")
|
|
165
|
+
reformat_only = ARGV.delete("--reformat")
|
|
166
|
+
yes = !!ARGV.delete("--yes")
|
|
167
|
+
pending_release_query = ARGV.delete("--pending-release")
|
|
168
|
+
release_state_query = ARGV.delete("--release-state") || ARGV.delete("--release-status")
|
|
169
|
+
refresh_cache = ARGV.delete("--refresh-cache")
|
|
170
|
+
add_unreleased_entry = ARGV.delete("--add-unreleased-entry") || ARGV.delete("--add-changelog-entry")
|
|
171
|
+
json_output = ARGV.delete("--json")
|
|
172
|
+
events_output, event_types = extract_events_arg!(ARGV)
|
|
173
|
+
Kettle::Dev::ExitAdapter.abort("--events cannot be combined with --json") if events_output && json_output
|
|
174
|
+
coverage_threshold_disabled = ARGV.include?("--no-coverage-threshold") || ARGV.include?("--no-coverage-thresholds")
|
|
175
|
+
coverage_hard = !coverage_threshold_disabled && ENV.fetch("K_CHANGELOG_COVERAGE_HARD", "true").downcase != "false"
|
|
176
|
+
version_override = extract_version_arg!(ARGV)
|
|
177
|
+
|
|
178
|
+
def extract_option_arg!(argv, name)
|
|
179
|
+
value = nil
|
|
180
|
+
if (idx = argv.index(name))
|
|
181
|
+
value = argv[idx + 1]
|
|
182
|
+
Kettle::Dev::ExitAdapter.abort("#{name} requires a value") if value.to_s.empty?
|
|
183
|
+
argv.slice!(idx, 2)
|
|
184
|
+
end
|
|
185
|
+
argv.delete_if do |arg|
|
|
186
|
+
if arg.start_with?("#{name}=")
|
|
187
|
+
value = arg.split("=", 2)[1]
|
|
188
|
+
true
|
|
189
|
+
else
|
|
190
|
+
false
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
value
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
historical_backfill_version = extract_option_arg!(ARGV, "--backfill-released-version")
|
|
197
|
+
|
|
198
|
+
if add_unreleased_entry
|
|
199
|
+
section = extract_option_arg!(ARGV, "--section")
|
|
200
|
+
entry = extract_option_arg!(ARGV, "--entry")
|
|
201
|
+
Kettle::Dev::ExitAdapter.abort("--section is required with --add-unreleased-entry") if section.to_s.empty?
|
|
202
|
+
Kettle::Dev::ExitAdapter.abort("--entry is required with --add-unreleased-entry") if entry.to_s.empty?
|
|
203
|
+
|
|
204
|
+
result = Kettle::Changelog::EntryAdder.new(section: section, entry: entry).run
|
|
205
|
+
message = (result == :changed) ? "CHANGELOG.md updated under Unreleased #{section}." : "CHANGELOG.md already contains that Unreleased #{section} entry."
|
|
206
|
+
puts(json_output ? JSON.pretty_generate({changed: result == :changed, section: section, entry: entry}) : message)
|
|
207
|
+
exit(0)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
event_stream = events_output ? Kettle::Ndjson.event_stream($stdout, types: event_types) : nil
|
|
211
|
+
cli = Kettle::Changelog::CLI.new(strict: strict_mode, enforce_coverage_thresholds: coverage_hard, update_prep: update_prep, reformat_only: reformat_only, historical_backfill_version: historical_backfill_version, version: version_override, refresh_cache: refresh_cache, yes: yes, event_stream: event_stream)
|
|
212
|
+
if release_state_query
|
|
213
|
+
state = cli.release_state
|
|
214
|
+
puts(json_output ? JSON.pretty_generate(state) : cli.release_state_table(state))
|
|
215
|
+
exit(0)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if pending_release_query
|
|
219
|
+
state = cli.pending_release_status
|
|
220
|
+
output = if json_output
|
|
221
|
+
JSON.pretty_generate(state)
|
|
222
|
+
elsif state.fetch(:pending_release)
|
|
223
|
+
"true"
|
|
224
|
+
else
|
|
225
|
+
"false"
|
|
226
|
+
end
|
|
227
|
+
puts(output)
|
|
228
|
+
exit(state.fetch(:pending_release) ? 0 : 1)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
cli.run
|
|
232
|
+
rescue LoadError => e
|
|
233
|
+
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
234
|
+
warn(e.backtrace.join("\n")) if ENV["DEBUG"]
|
|
235
|
+
exit(1)
|
|
236
|
+
rescue SystemExit => e
|
|
237
|
+
# Preserve exit status, but ensure at least a newline so shells don't show an empty line only.
|
|
238
|
+
warn("#{script_basename}: exited (status=#{e.status}, msg=#{e.message})") if e.status != 0
|
|
239
|
+
raise
|
|
240
|
+
rescue => e
|
|
241
|
+
warn("#{script_basename}: unexpected error: #{e.class}: #{e.message}")
|
|
242
|
+
warn(e.backtrace.join("\n"))
|
|
243
|
+
exit(1)
|
|
244
|
+
end
|