kettle-dev 2.5.27 → 3.0.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: 3ec35425bc4a7f6e3ee067f5e92c80f9b81559945d464bde2966e5667eb94979
4
- data.tar.gz: 1d6c6fc458b3667f2a2d6aff255da8f4cd6ff1503020d36cba21e8a3199a21e6
3
+ metadata.gz: 335bd9651ff7bbe5381c8e050a26f070c6accd8054ab581226ba3355c8a4aa6e
4
+ data.tar.gz: 7f59238412adde48ba5f2b524b08e4ce77a5348c81d0984e44013e6ce905b2dc
5
5
  SHA512:
6
- metadata.gz: 8a6b8bd1a2e8fb4c5a2c73f3791378f793d31ef9eb05643621c720175508147c791660453b1341f62d5fc6aedb4befb37f0caecb0615244a1f3e5ce5af70989e
7
- data.tar.gz: 0e1f22f3cd86b4544ff272dc78d3a24f7b6b1edc915be470d8760137cf91227482530899fc399c6398ed2ac44b30b675a1d3d2297fca03486f66f0bed76b5959
6
+ metadata.gz: 94e3d356b963078c8dd46d50d0f2b7ed92f656327bd3f766618617b56172ae0051dc32f6469cfd5c9faf539e00a4bbdf123947dd8febbd10c34172f51a2efc2d
7
+ data.tar.gz: d2eee5766a478c4846f05969ac4fcbf4614793740911c029219d85fb446fd308f9c376c5fa8d68edbdc71bd144dba09a1ad927c5915278c7933d0fc50ac74dca
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,49 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.0.1] - 2026-08-10
34
+
35
+ - TAG: [v3.0.1][3.0.1t]
36
+ - COVERAGE: 91.87% -- 5602/6098 lines in 47 files
37
+ - BRANCH COVERAGE: 76.29% -- 2117/2775 branches in 47 files
38
+ - 50.64% documented
39
+
40
+ ### Fixed
41
+
42
+ - Run 1Password authorization keepalives through the interactive terminal so release prompts remain visible.
43
+
44
+ - Re-normalize release lockfiles after release tasks before commit and CI validation.
45
+
46
+ ## [3.0.0] - 2026-08-09
47
+
48
+ - TAG: [v3.0.0][3.0.0t]
49
+ - COVERAGE: 91.48% -- 5552/6069 lines in 47 files
50
+ - BRANCH COVERAGE: 76.26% -- 2107/2763 branches in 47 files
51
+ - 50.77% documented
52
+
53
+ ### Changed
54
+
55
+ - Move the kettle-changelog executable to the standalone kettle-changelog gem while retaining kettle-dev support APIs for the transition.
56
+
57
+ - Resolve standalone kettle-changelog through the generated Ruby-4-only modular release-tool Gemfile, with local path wiring for development.
58
+
59
+ ### Fixed
60
+
61
+ - Use a dedicated standalone changelog bundle during bootstrap releases when the project bundle does not include kettle-changelog.
62
+
63
+ - Keep standalone changelog bootstrap paths out of target release tests and documentation.
64
+
65
+ ## [2.5.28] - 2026-08-09
66
+
67
+ - TAG: [v2.5.28][2.5.28t]
68
+ - COVERAGE: 91.71% -- 5544/6045 lines in 47 files
69
+ - BRANCH COVERAGE: 76.48% -- 2104/2751 branches in 47 files
70
+ - 50.77% documented
71
+
72
+ ### Fixed
73
+
74
+ - Retry an existing gem publication once with a fresh RubyGems MFA OTP when the release task reports an invalid OTP.
75
+
33
76
  ## [2.5.27] - 2026-08-09
34
77
 
35
78
  - TAG: [v2.5.27][2.5.27t]
@@ -3371,7 +3414,13 @@ Please file a bug if you notice a violation of semantic versioning.
3371
3414
  - Selecting will run the selected workflow via `act`
3372
3415
  - This may move to its own gem in the future.
3373
3416
 
3374
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.27...HEAD
3417
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.1...HEAD
3418
+ [3.0.1]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.0...v3.0.1
3419
+ [3.0.1t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.0.1
3420
+ [3.0.0]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.28...v3.0.0
3421
+ [3.0.0t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.0.0
3422
+ [2.5.28]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.27...v2.5.28
3423
+ [2.5.28t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.28
3375
3424
  [2.5.27]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.26...v2.5.27
3376
3425
  [2.5.27t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.27
3377
3426
  [2.5.26]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.25...v2.5.26
data/README.md CHANGED
@@ -1007,7 +1007,7 @@ Thanks for RTFM. ☺️
1007
1007
  [📌gitmoji]: https://gitmoji.dev
1008
1008
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1009
1009
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1010
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.026-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1010
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.098-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1011
1011
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
1012
1012
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1013
1013
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -1,39 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require "shellwords"
4
5
 
5
6
  module Kettle
6
7
  module Dev
7
8
  class InteractiveReleaseCommand
8
- def initialize(secrets_provider:, input: $stdin, output: $stdout, error: $stderr, secret_event_handler: nil)
9
+ def initialize(secrets_provider:, input: $stdin, output: $stdout, error: $stderr, secret_event_handler: nil, forward_stdin: false)
9
10
  @secrets_provider = secrets_provider
10
11
  @input = input
11
12
  @output = output
12
13
  @error = error
13
14
  @secret_event_handler = secret_event_handler
15
+ @forward_stdin = forward_stdin
14
16
  @gem_signing_passphrase = nil
15
17
  end
16
18
 
17
19
  def call(env, cmd)
18
- return call_with_pty(env, cmd) if pty_available?
20
+ call_argv(env, Shellwords.split(cmd.to_s))
21
+ end
22
+
23
+ def call_argv(env, argv)
24
+ return call_with_pty(env, argv) if pty_available?
19
25
 
20
- call_with_open3(env, cmd)
26
+ call_with_open3(env, argv)
21
27
  end
22
28
 
23
29
  private
24
30
 
25
31
  attr_reader :secrets_provider, :secret_event_handler
26
32
 
27
- def call_with_pty(env, cmd)
33
+ def call_with_pty(env, argv)
28
34
  stdout_str = +""
35
+ prompt_buffer = +""
29
36
  status = nil
30
- PTY.spawn(env, cmd) do |output, input, pid|
37
+ PTY.spawn(env, *argv) do |output, input, pid|
31
38
  begin
32
39
  loop do
33
- chunk = output.readpartial(1024)
34
- stdout_str << chunk
35
- @output.print(chunk)
36
- handle_prompt(input, chunk)
40
+ readers = [output]
41
+ readers << @input if @forward_stdin && @input.tty?
42
+ IO.select(readers).first.each do |reader|
43
+ if reader.equal?(@input)
44
+ input.write(@input.readpartial(1024))
45
+ else
46
+ chunk = output.readpartial(1024)
47
+ stdout_str << chunk
48
+ @output.print(chunk)
49
+ prompt_buffer = handle_prompt(input, chunk, prompt_buffer: prompt_buffer)
50
+ end
51
+ end
37
52
  end
38
53
  rescue Errno::EIO
39
54
  # PTY raises EIO when the child process exits after closing the slave.
@@ -47,27 +62,33 @@ module Kettle
47
62
  [stdout_str, "", status]
48
63
  end
49
64
 
50
- def call_with_open3(env, cmd)
65
+ def call_with_open3(env, argv)
51
66
  stdout_str = +""
52
67
  stderr_str = +""
68
+ prompt_buffer = +""
53
69
  status = nil
54
- Open3.popen3(env, cmd) do |input, output, error, wait_thread|
70
+ Open3.popen3(env, *argv) do |input, output, error, wait_thread|
55
71
  readers = [output, error]
72
+ readers << @input if @forward_stdin && @input.tty?
56
73
  until readers.empty?
57
74
  ready = IO.select(readers)
58
75
  ready.first.each do |reader|
59
- begin
60
- chunk = reader.readpartial(1024)
61
- if reader.equal?(output)
62
- stdout_str << chunk
63
- @output.print(chunk)
64
- else
65
- stderr_str << chunk
66
- @error.print(chunk)
76
+ if reader.equal?(@input)
77
+ input.write(@input.readpartial(1024))
78
+ else
79
+ begin
80
+ chunk = reader.readpartial(1024)
81
+ if reader.equal?(output)
82
+ stdout_str << chunk
83
+ @output.print(chunk)
84
+ else
85
+ stderr_str << chunk
86
+ @error.print(chunk)
87
+ end
88
+ prompt_buffer = handle_prompt(input, chunk, prompt_buffer: prompt_buffer)
89
+ rescue EOFError
90
+ readers.delete(reader)
67
91
  end
68
- handle_prompt(input, chunk)
69
- rescue EOFError
70
- readers.delete(reader)
71
92
  end
72
93
  end
73
94
  end
@@ -76,13 +97,21 @@ module Kettle
76
97
  [stdout_str, stderr_str, status]
77
98
  end
78
99
 
79
- def handle_prompt(input, chunk)
80
- if otp_prompt?(chunk)
81
- write_secret(input, label: "RubyGems MFA code", source: "rubygems_otp") { secrets_provider.rubygems_otp } if otp_response_prompt?(chunk)
82
- return
100
+ def handle_prompt(input, chunk, prompt_buffer: nil)
101
+ prompt_buffer = prompt_buffer.to_s.dup
102
+ prompt_buffer << chunk.to_s
103
+ prompt_buffer = prompt_buffer.byteslice([prompt_buffer.bytesize - 4096, 0].max, 4096).to_s
104
+
105
+ if otp_prompt?(prompt_buffer)
106
+ if otp_response_prompt?(prompt_buffer)
107
+ write_secret(input, label: "RubyGems MFA code", source: "rubygems_otp") { secrets_provider.rubygems_otp }
108
+ return +""
109
+ end
110
+ return prompt_buffer
83
111
  end
84
112
 
85
- write_secret(input, label: "gem signing passphrase", source: "gem_signing_passphrase") { gem_signing_passphrase } if signing_password_prompt?(chunk)
113
+ write_secret(input, label: "gem signing passphrase", source: "gem_signing_passphrase") { gem_signing_passphrase } if signing_password_prompt?(prompt_buffer)
114
+ prompt_buffer
86
115
  end
87
116
 
88
117
  def gem_signing_passphrase
@@ -25,6 +25,7 @@ require_relative "release_secrets"
25
25
  module Kettle
26
26
  module Dev
27
27
  class ReleaseCLI
28
+ RUBYGEMS_INVALID_OTP = /Your OTP code is incorrect\. Please check it and retry\./.freeze
28
29
  QUIET_ENV = {
29
30
  "KETTLE_JEM_QUIET" => "true",
30
31
  "KETTLE_JEM_DEBUG" => "false",
@@ -116,7 +117,8 @@ module Kettle
116
117
  words = effective_command_words(cmd)
117
118
  words == ["bin/setup"] ||
118
119
  words.first(1) == ["bin/rake"] ||
119
- words.first(3) == ["bundle", "exec", "rake"]
120
+ words.first(3) == ["bundle", "exec", "rake"] ||
121
+ words.first(3) == ["bundle", "exec", "kettle-changelog"]
120
122
  rescue ArgumentError
121
123
  false
122
124
  end
@@ -620,7 +622,10 @@ module Kettle
620
622
  end
621
623
 
622
624
  def prepare_release_lockfiles_for_commit!
623
- reset_release_lockfiles!(stage: "before release prep commit") unless @release_lockfiles_reset_for_release_tasks
625
+ # Release setup and documentation tasks can re-materialize local PATH
626
+ # sources in the parent development bundle after the preflight reset.
627
+ # Always normalize again at the commit boundary.
628
+ reset_release_lockfiles!(stage: "before release prep commit")
624
629
  validate_release_lockfiles!(stage: "before release prep commit")
625
630
  end
626
631
 
@@ -827,7 +832,7 @@ module Kettle
827
832
  end
828
833
 
829
834
  def run_changelog!
830
- cmd = "bundle exec kettle-changelog"
835
+ cmd = release_changelog_command
831
836
  cmd = "#{cmd} --version #{Shellwords.escape(@version_override)}" if @version_override
832
837
  cmd = "#{cmd} --yes" if @yes
833
838
  cmd = "#{cmd} --events=changelog" if @event_stream
@@ -835,6 +840,51 @@ module Kettle
835
840
  @changelog_generated_coverage = true
836
841
  end
837
842
 
843
+ # The changelog executable is owned by kettle-changelog, not kettle-dev.
844
+ # During the bootstrap release of kettle-dev it is therefore intentionally
845
+ # absent from kettle-dev's bundle. When a local kettle-dev workspace is
846
+ # supplied, run the standalone tool through that project's bundle instead
847
+ # of relying on an ambient installed executable.
848
+ def release_changelog_command
849
+ gemfile = release_changelog_gemfile
850
+ return "bundle exec kettle-changelog" unless gemfile
851
+
852
+ escaped_gemfile = Shellwords.escape(gemfile)
853
+ command = +"env -u BUNDLE_GEMFILE -u BUNDLE_LOCKFILE"
854
+ if (local_root = release_changelog_local_root)
855
+ command << " KETTLE_DEV_DEV=false KETTLE_CHANGELOG_DEV_ROOT=#{Shellwords.escape(local_root)}"
856
+ end
857
+ command << " BUNDLE_GEMFILE=#{escaped_gemfile} bundle exec kettle-changelog"
858
+ command
859
+ end
860
+
861
+ def release_changelog_gemfile
862
+ configured = ENV["K_RELEASE_CHANGELOG_GEMFILE"].to_s.strip
863
+ unless configured.empty?
864
+ path = File.expand_path(configured)
865
+ return path if File.file?(path)
866
+
867
+ abort("Configured K_RELEASE_CHANGELOG_GEMFILE does not exist: #{path}")
868
+ end
869
+
870
+ local_root = release_changelog_local_root
871
+ return nil unless local_root
872
+
873
+ changelog_root = File.join(File.expand_path(local_root), "kettle-changelog")
874
+ candidates = [
875
+ File.join(changelog_root, "gemfiles", "release.gemfile"),
876
+ File.join(changelog_root, "Gemfile")
877
+ ]
878
+ candidates.find { |path| File.file?(path) }
879
+ end
880
+
881
+ def release_changelog_local_root
882
+ local_root = ENV["KETTLE_DEV_DEV"].to_s.strip
883
+ return nil if local_root.empty? || %w[false 0 no off].include?(local_root.downcase)
884
+
885
+ File.expand_path(local_root)
886
+ end
887
+
838
888
  # Monorepo subprojects commonly disable their local hard coverage gate
839
889
  # because their narrow test suite cannot satisfy the aggregate root
840
890
  # thresholds. Preserve an explicit changelog policy, otherwise derive
@@ -1237,7 +1287,7 @@ module Kettle
1237
1287
 
1238
1288
  keep_release_secrets_alive!("prompt-bearing command")
1239
1289
  puts "$ #{cmd}"
1240
- _stdout_str, stderr_str, status = Kettle::Dev::InteractiveReleaseCommand.new(
1290
+ stdout_str, stderr_str, status = Kettle::Dev::InteractiveReleaseCommand.new(
1241
1291
  secrets_provider: @secrets_provider,
1242
1292
  secret_event_handler: ->(payload) { emit_secret_event(payload) }
1243
1293
  ).call(
@@ -1246,6 +1296,11 @@ module Kettle
1246
1296
  )
1247
1297
  return if status.success?
1248
1298
 
1299
+ if gem_release_command?(cmd) && rubygems_invalid_otp?(stdout_str, stderr_str)
1300
+ retry_gem_push_after_invalid_otp!
1301
+ return
1302
+ end
1303
+
1249
1304
  exit_code = status.respond_to?(:exitstatus) ? status.exitstatus : 1
1250
1305
  diag = stderr_str.to_s.empty? ? "" : "\n--- STDERR (last 20 lines) ---\n#{stderr_str.lines.last(20).join}".rstrip
1251
1306
  abort("Command failed: #{cmd} (exit #{exit_code})#{diag}")
@@ -1261,6 +1316,45 @@ module Kettle
1261
1316
  release_secrets_configured? ? @secrets_provider.class.name.to_s.split("::").last : "interactive"
1262
1317
  end
1263
1318
 
1319
+ def gem_release_command?(cmd)
1320
+ effective_command_words(cmd).first(4) == ["bundle", "exec", "rake", "release"]
1321
+ end
1322
+
1323
+ def rubygems_invalid_otp?(*output)
1324
+ output.any? { |chunk| chunk.to_s.match?(RUBYGEMS_INVALID_OTP) }
1325
+ end
1326
+
1327
+ def retry_gem_push_after_invalid_otp!
1328
+ version = @release_candidate&.version || detect_version
1329
+ gem_path = gem_file_for_version(version)
1330
+ unless gem_path && File.file?(gem_path)
1331
+ abort("RubyGems rejected the OTP, but the built gem for version #{version} could not be found in pkg/.")
1332
+ end
1333
+
1334
+ puts "RubyGems rejected the OTP after the release task completed; retrying the existing gem publication with a fresh OTP..."
1335
+ emit_secret_event(
1336
+ source: release_secrets_provider_label,
1337
+ action: "otp_retry",
1338
+ status: "started",
1339
+ reason: "RubyGems rejected the first OTP"
1340
+ )
1341
+ run_cmd!("gem push #{Shellwords.escape(gem_path)}")
1342
+ emit_secret_event(
1343
+ source: release_secrets_provider_label,
1344
+ action: "otp_retry",
1345
+ status: "ok",
1346
+ reason: "existing gem artifact published after OTP retry"
1347
+ )
1348
+ rescue SystemExit, Kettle::Dev::Error
1349
+ emit_secret_event(
1350
+ source: release_secrets_provider_label,
1351
+ action: "otp_retry",
1352
+ status: "failed",
1353
+ reason: "fresh OTP retry failed"
1354
+ )
1355
+ raise
1356
+ end
1357
+
1264
1358
  def signing_enabled?
1265
1359
  ENV.fetch("SKIP_GEM_SIGNING", "false").casecmp("false").zero?
1266
1360
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require "kettle/dev/interactive_release_command"
4
5
  require "kettle/dev/release_notifier"
5
6
 
6
7
  module Kettle
@@ -73,7 +74,7 @@ module Kettle
73
74
  argv = [op_cli, "account", "get"]
74
75
  account = string_config("account")
75
76
  argv.concat(["--account", account]) unless account.empty?
76
- run_op(argv, purpose: "authorization keepalive", elapsed: elapsed)
77
+ run_op(argv, purpose: "authorization keepalive", elapsed: elapsed, interactive: true)
77
78
  true
78
79
  end
79
80
 
@@ -112,9 +113,11 @@ module Kettle
112
113
  required_config("cli")
113
114
  end
114
115
 
115
- def run_op(argv, purpose:, elapsed: nil)
116
+ def run_op(argv, purpose:, elapsed: nil, interactive: false)
116
117
  elapsed_suffix = elapsed.to_s.empty? ? "" : " (elapsed #{elapsed})"
117
118
  Kettle::Dev::ReleaseNotifier.alert("1Password #{purpose} lookup starting#{elapsed_suffix}; watch for authorization prompt.")
119
+ return run_interactive_op(argv, purpose: purpose) if interactive
120
+
118
121
  stdout, stderr, status = Open3.capture3(*argv)
119
122
  return stdout.to_s.strip if status.success? && !stdout.to_s.strip.empty?
120
123
 
@@ -125,6 +128,23 @@ module Kettle
125
128
  raise Kettle::Dev::Error, "1Password CLI executable #{argv.first.inspect} was not found"
126
129
  end
127
130
 
131
+ def run_interactive_op(argv, purpose:)
132
+ _stdout, stderr, status = Kettle::Dev::InteractiveReleaseCommand.new(
133
+ secrets_provider: Provider.new,
134
+ input: $stdin,
135
+ output: $stdout,
136
+ error: $stderr,
137
+ forward_stdin: true
138
+ ).call_argv({}, argv)
139
+ return if status.success?
140
+
141
+ details = stderr.to_s.strip
142
+ details = "op exited #{status.exitstatus}" if details.empty?
143
+ raise Kettle::Dev::Error, "1Password #{purpose} lookup failed: #{details}"
144
+ rescue Errno::ENOENT
145
+ raise Kettle::Dev::Error, "1Password CLI executable #{argv.first.inspect} was not found"
146
+ end
147
+
128
148
  def required_config(key)
129
149
  value = string_config(key)
130
150
  raise Kettle::Dev::Error, "1Password release secrets require #{key}" if value.empty?
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "2.5.27"
8
+ VERSION = "3.0.1"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.27
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -236,7 +236,7 @@ dependencies:
236
236
  version: '3.2'
237
237
  - - ">="
238
238
  - !ruby/object:Gem::Version
239
- version: 3.2.0
239
+ version: 3.2.2
240
240
  type: :development
241
241
  prerelease: false
242
242
  version_requirements: !ruby/object:Gem::Requirement
@@ -246,7 +246,7 @@ dependencies:
246
246
  version: '3.2'
247
247
  - - ">="
248
248
  - !ruby/object:Gem::Version
249
- version: 3.2.0
249
+ version: 3.2.2
250
250
  - !ruby/object:Gem::Dependency
251
251
  name: turbo_tests2
252
252
  requirement: !ruby/object:Gem::Requirement
@@ -256,7 +256,7 @@ dependencies:
256
256
  version: '3.2'
257
257
  - - ">="
258
258
  - !ruby/object:Gem::Version
259
- version: 3.2.4
259
+ version: 3.2.5
260
260
  type: :development
261
261
  prerelease: false
262
262
  version_requirements: !ruby/object:Gem::Requirement
@@ -266,7 +266,7 @@ dependencies:
266
266
  version: '3.2'
267
267
  - - ">="
268
268
  - !ruby/object:Gem::Version
269
- version: 3.2.4
269
+ version: 3.2.5
270
270
  - !ruby/object:Gem::Dependency
271
271
  name: stone_checksums
272
272
  requirement: !ruby/object:Gem::Requirement
@@ -317,7 +317,6 @@ email:
317
317
  - floss@galtzo.com
318
318
  executables:
319
319
  - kettle-bump
320
- - kettle-changelog
321
320
  - kettle-check-eof
322
321
  - kettle-commit-msg
323
322
  - kettle-dev-setup
@@ -336,7 +335,6 @@ files:
336
335
  - LICENSE.md
337
336
  - README.md
338
337
  - exe/kettle-bump
339
- - exe/kettle-changelog
340
338
  - exe/kettle-check-eof
341
339
  - exe/kettle-check-eof.sh
342
340
  - exe/kettle-commit-msg
@@ -402,10 +400,10 @@ licenses:
402
400
  - AGPL-3.0-only
403
401
  metadata:
404
402
  homepage_uri: https://kettle-dev.galtzo.com
405
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.5.27
406
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.5.27/CHANGELOG.md
403
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.0.1
404
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.0.1/CHANGELOG.md
407
405
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
408
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.5.27
406
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.0.1
409
407
  funding_uri: https://github.com/sponsors/pboling
410
408
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
411
409
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file
data/exe/kettle-changelog DELETED
@@ -1,244 +0,0 @@
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/dev"
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::Dev::ChangelogEntryAdder.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::Dev::ChangelogCLI.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