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.
@@ -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