kettle-dev 2.2.19 → 2.2.21

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: 309dfdff5a98f7d5d2f189a3b51a4d0f231d037b61ea37023918a70ec436d997
4
- data.tar.gz: 9dff167d60de601c34738beb35c81df5e3c1a8874630ccaebf753507fa205a41
3
+ metadata.gz: 5c84739dfdebb3ae9c5c6fd438c7cc1db6e5707d9cecfe7a3e2e754e41390301
4
+ data.tar.gz: 9e1f2b9e2f3c842c218977027c6cf69c1a753072f2d281e39239dc3e7097e9ee
5
5
  SHA512:
6
- metadata.gz: baf9a43aeac7238588295facc22c7a151c68abeed3b754872d539bc25571ea43e87eaac9ffb81ea4f5133206d1e9f1d0ef7d2fc609b666fa0bfad54df27069d0
7
- data.tar.gz: 6f607059c8cd545bad791de6277f40f0b75d956fbc7654677cdcfb53345c2b658e98881e80f694992041e872e96b4e643d69f61a578ca305821fe449fe6c21ae
6
+ metadata.gz: 287be3949983f25366e89ec2de3058bc63cf14dee7ff5b45a5da6a49a09d524493e91318788a370bb6ff92d4bed313771596368638f5026802850523be74f4df
7
+ data.tar.gz: 20bf852b21d4fa605b151e9ee4fe91b50921fa7320ded4ec937951e3cfcc4177ae653246448139ede0188d4741d70f0b5d1b7afce04ed042442d31cbbbb4f98b
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,47 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [2.2.21] - 2026-06-28
34
+
35
+ - TAG: [v2.2.21][2.2.21t]
36
+ - COVERAGE: 92.34% -- 4182/4529 lines in 33 files
37
+ - BRANCH COVERAGE: 73.34% -- 1659/2262 branches in 33 files
38
+ - 61.04% documented
39
+
40
+ ### Fixed
41
+
42
+ - `kettle-release` now detects local `.kettle-family.yml` branch-stack release
43
+ targets and skips trunk sync, trunk merge, and trunk checkout for those release
44
+ branches.
45
+
46
+ ## [2.2.20] - 2026-06-27
47
+
48
+ - TAG: [v2.2.20][2.2.20t]
49
+ - COVERAGE: 91.82% -- 4131/4499 lines in 33 files
50
+ - BRANCH COVERAGE: 72.73% -- 1632/2244 branches in 33 files
51
+ - 61.04% documented
52
+
53
+ ### Added
54
+
55
+ - `kettle-release` now accepts `skip_steps=...` / `--skip-steps ...` to skip
56
+ selected numbered release steps while running the rest of the release flow.
57
+
58
+ ### Fixed
59
+
60
+ - `kettle-release` now suppresses inherited Bundler/debug verbosity when running
61
+ release child commands and Appraisal bundle installs.
62
+
63
+ - `kettle-release` now switches a local `RUBOCOP_LTS_LOCAL` checkout to the
64
+ branch matching the project's selected `rubocop-rubyN_N` style gem before
65
+ running setup and default rake tasks.
66
+
67
+ - Restored Ruby 2.4 parser compatibility for recent release and CI helper code.
68
+
69
+ ### Changed
70
+
71
+ - `kettle-release` now reads RuboCop LTS branch selection from
72
+ `Kettle::Rb::CompatMatrix`.
73
+
33
74
  ## [2.2.19] - 2026-06-26
34
75
 
35
76
  - TAG: [v2.2.19][2.2.19t]
@@ -2293,7 +2334,11 @@ Please file a bug if you notice a violation of semantic versioning.
2293
2334
  - Selecting will run the selected workflow via `act`
2294
2335
  - This may move to its own gem in the future.
2295
2336
 
2296
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.19...HEAD
2337
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.21...HEAD
2338
+ [2.2.21]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.20...v2.2.21
2339
+ [2.2.21t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.2.21
2340
+ [2.2.20]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.19...v2.2.20
2341
+ [2.2.20t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.2.20
2297
2342
  [2.2.19]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.18...v2.2.19
2298
2343
  [2.2.19t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.2.19
2299
2344
  [2.2.18]: https://github.com/kettle-dev/kettle-dev/compare/v2.2.17...v2.2.18
data/README.md CHANGED
@@ -874,7 +874,7 @@ Thanks for RTFM. ☺️
874
874
  [📌gitmoji]: https://gitmoji.dev
875
875
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
876
876
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
877
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.449-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
877
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.529-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
878
878
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
879
879
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
880
880
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/exe/kettle-release CHANGED
@@ -43,7 +43,7 @@ end
43
43
  # Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
44
44
  if ARGV.include?("-h") || ARGV.include?("--help")
45
45
  puts <<~USAGE
46
- Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [start_step=<0-19>]
46
+ Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-steps STEPS] [start_step=<0-19>]
47
47
 
48
48
  Automates the release flow for a Ruby gem in the host project.
49
49
 
@@ -76,6 +76,8 @@ if ARGV.include?("-h") || ARGV.include?("--help")
76
76
 
77
77
  Options:
78
78
  start_step=<number> # Begin at the numbered step above (e.g., 10 to resume at CI monitoring)
79
+ skip_steps=<numbers> # Comma-separated numbered steps to skip while running the rest
80
+ --skip-steps NUMBERS # Alias for skip_steps=NUMBERS (e.g., --skip-steps 10 or --skip-steps 10,18)
79
81
  --version VERSION # Use this version instead of detecting VERSION from lib/**/version.rb
80
82
  --appraisal-update # Use slower appraisal:update instead of default appraisal:generate
81
83
  --local-ci # Sensitive release mode: run act locally, publish before any git push,
@@ -97,12 +99,13 @@ if ARGV.include?("-h") || ARGV.include?("--help")
97
99
  kettle-release --local-ci # run local act CI, publish gem, then push commits/tags
98
100
  kettle-release start_step=1 # skip pre-release checks and start numbered release steps
99
101
  kettle-release start_step=10 # resume by monitoring CI for the current branch
102
+ kettle-release skip_steps=10 # run the release but skip remote CI monitoring
100
103
  SKIP_GEM_SIGNING=true kettle-release start_step=14 # build+release without signing
101
104
  USAGE
102
105
  exit 0
103
106
  end
104
107
 
105
- # Parse start_step=<n> from ARGV
108
+ # Parse release options from ARGV
106
109
  def extract_version_arg!(argv)
107
110
  version = nil
108
111
  if (idx = argv.index("--version"))
@@ -121,14 +124,33 @@ def extract_version_arg!(argv)
121
124
  Kettle::Dev::Versioning.normalize_explicit_version(version)
122
125
  end
123
126
 
127
+ def extract_skip_steps_arg!(argv)
128
+ skip_steps = nil
129
+ if (idx = argv.index("--skip-steps"))
130
+ skip_steps = argv[idx + 1]
131
+ Kettle::Dev::ExitAdapter.abort("--skip-steps requires a comma-separated step list") if skip_steps.to_s.empty?
132
+ argv.slice!(idx, 2)
133
+ end
134
+ argv.delete_if do |arg|
135
+ if arg.start_with?("--skip-steps=", "skip_steps=")
136
+ skip_steps = arg.split("=", 2)[1]
137
+ true
138
+ else
139
+ false
140
+ end
141
+ end
142
+ skip_steps
143
+ end
144
+
124
145
  local_ci = ARGV.include?("--local-ci")
125
146
  appraisal_task = ARGV.delete("--appraisal-update") ? "appraisal:update" : nil
126
147
  start_step_arg = ARGV.find { |a| a.start_with?("start_step=") }
127
148
  start_step = start_step_arg ? start_step_arg.split("=", 2)[1].to_i : 0
128
149
  version_override = extract_version_arg!(ARGV)
150
+ skip_steps = extract_skip_steps_arg!(ARGV)
129
151
 
130
152
  begin
131
- Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task).run
153
+ Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps).run
132
154
  rescue LoadError => e
133
155
  warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
134
156
  warn(Array(e.backtrace).join("\n")) if ENV["DEBUG"]
@@ -196,7 +196,7 @@ module Kettle
196
196
  end
197
197
 
198
198
  def declared_version_file_path(root)
199
- gemspec_path = Dir.glob(File.join(root.to_s, "*.gemspec")).sort.first
199
+ gemspec_path = Dir.glob(File.join(root.to_s, "*.gemspec")).min
200
200
  return unless gemspec_path && File.file?(gemspec_path)
201
201
 
202
202
  gemspec_source = File.read(gemspec_path)
@@ -264,9 +264,11 @@ module Kettle
264
264
  @options[:progress] = bool
265
265
  end
266
266
  opt.on("--skip-pattern PATTERN", "Skip workflow paths matching pattern (repeatable)") do |pattern|
267
- @options[:reject_patterns] << Regexp.new(pattern)
268
- rescue RegexpError => e
269
- Kettle::Dev::ExitAdapter.abort("Invalid --skip-pattern #{pattern.inspect}: #{e.message}")
267
+ begin
268
+ @options[:reject_patterns] << Regexp.new(pattern)
269
+ rescue RegexpError => e
270
+ Kettle::Dev::ExitAdapter.abort("Invalid --skip-pattern #{pattern.inspect}: #{e.message}")
271
+ end
270
272
  end
271
273
  opt.on("--[no-]validate", "Validate YAML after editing") do |bool|
272
274
  @options[:validate] = bool
@@ -282,25 +284,27 @@ module Kettle
282
284
  def load_workflows(paths, state)
283
285
  file_progress = progress_bar(title: "Files", total: paths.length)
284
286
  paths.each_with_object([]) do |path, workflows|
285
- state[:files_scanned] += 1
286
- text = begin
287
- File.read(path)
288
- rescue Errno::EACCES => e
289
- record_failure(state, path: path, error: "read_error: #{e.message}")
290
- next
291
- end
287
+ begin
288
+ state[:files_scanned] += 1
289
+ text = begin
290
+ File.read(path)
291
+ rescue Errno::EACCES => e
292
+ record_failure(state, path: path, error: "read_error: #{e.message}")
293
+ next
294
+ end
292
295
 
293
- parsed = begin
294
- Psych.parse_stream(text)
295
- rescue Psych::Exception => e
296
- record_failure(state, path: path, error: "yaml_parse_error: #{e.message}")
297
- next
298
- end
296
+ parsed = begin
297
+ Psych.parse_stream(text)
298
+ rescue Psych::Exception => e
299
+ record_failure(state, path: path, error: "yaml_parse_error: #{e.message}")
300
+ next
301
+ end
299
302
 
300
- uses_nodes = extract_uses_nodes(parsed, text)
301
- workflows << {path: path, text: text, uses_nodes: uses_nodes} unless uses_nodes.empty?
302
- ensure
303
- file_progress&.increment
303
+ uses_nodes = extract_uses_nodes(parsed, text)
304
+ workflows << {path: path, text: text, uses_nodes: uses_nodes} unless uses_nodes.empty?
305
+ ensure
306
+ file_progress&.increment
307
+ end
304
308
  end
305
309
  end
306
310
 
@@ -198,10 +198,12 @@ module Kettle
198
198
  def remotes_with_urls
199
199
  if @backend == :gem
200
200
  @git.remotes.each_with_object({}) do |r, h|
201
- h[r.name] = r.url
202
- rescue => e
203
- Kettle::Dev.debug_error(e, __method__)
204
- # ignore
201
+ begin
202
+ h[r.name] = r.url
203
+ rescue => e
204
+ Kettle::Dev.debug_error(e, __method__)
205
+ # ignore
206
+ end
205
207
  end
206
208
  else
207
209
  out, status = Open3.capture2("git", "remote", "-v")
@@ -253,10 +253,12 @@ module Kettle
253
253
  Array(glob_pattern)
254
254
  end
255
255
  urls = files.flat_map do |f|
256
- extract_image_urls_from_text(File.read(f))
257
- rescue => e
258
- warn("[kettle-pre-release] Could not read #{Kettle::Dev.display_path(f)}: #{e.class}: #{e.message}")
259
- []
256
+ begin
257
+ extract_image_urls_from_text(File.read(f))
258
+ rescue => e
259
+ warn("[kettle-pre-release] Could not read #{Kettle::Dev.display_path(f)}: #{e.class}: #{e.message}")
260
+ []
261
+ end
260
262
  end
261
263
  urls.uniq
262
264
  end
@@ -5,7 +5,27 @@ begin
5
5
  require "appraisal/task"
6
6
 
7
7
  bundle = "bundle"
8
- appraisal_env = {"BUNDLE_GEMFILE" => "Appraisal.root.gemfile"}
8
+ quiet_env = {
9
+ "KETTLE_JEM_QUIET" => "true",
10
+ "KETTLE_JEM_DEBUG" => "false",
11
+ "KETTLE_DEV_DEBUG" => "false",
12
+ "SMORG_RB_DEBUG" => "false",
13
+ "DEBUG" => nil,
14
+ "BUNDLE_QUIET" => "true",
15
+ "BUNDLE_DEBUG" => "false",
16
+ "BUNDLER_DEBUG" => "false",
17
+ "BUNDLE_VERBOSE" => "false",
18
+ "DEBUG_RESOLVER" => nil,
19
+ "DEBUG_RESOLVER_TREE" => nil,
20
+ "BUNDLER_DEBUG_RESOLVER" => nil,
21
+ "BUNDLER_DEBUG_RESOLVER_TREE" => nil,
22
+ "DEBUG_COMPACT_INDEX" => nil,
23
+ "MOLINILLO_DEBUG" => nil,
24
+ "BUNDLE_SILENCE_DEPRECATIONS" => "true",
25
+ "BUNDLE_SILENCE_ROOT_WARNING" => "true",
26
+ "BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES" => "true"
27
+ }
28
+ appraisal_env = quiet_env.merge("BUNDLE_GEMFILE" => "Appraisal.root.gemfile")
9
29
 
10
30
  run_command = lambda do |failure_message, *args|
11
31
  ok = system(*args)
@@ -18,7 +38,8 @@ begin
18
38
  "appraisal:generate failed: BUNDLE_GEMFILE=Appraisal.root.gemfile bundle install",
19
39
  appraisal_env,
20
40
  bundle,
21
- "install"
41
+ "install",
42
+ "--quiet"
22
43
  )
23
44
 
24
45
  # 2) BUNDLE_GEMFILE=Appraisal.root.gemfile bundle exec appraisal generate
@@ -33,18 +54,20 @@ begin
33
54
  end
34
55
 
35
56
  run_appraisal_task = lambda do |task_name, primary_steps = nil|
36
- if primary_steps
37
- begin
38
- primary_steps.call
39
- rescue RuntimeError => e
40
- warn("[kettle-dev][#{task_name}] #{e.message}; falling back to appraisal:generate")
57
+ begin
58
+ if primary_steps
59
+ begin
60
+ primary_steps.call
61
+ rescue RuntimeError => e
62
+ warn("[kettle-dev][#{task_name}] #{e.message}; falling back to appraisal:generate")
63
+ run_generate_steps.call
64
+ end
65
+ else
41
66
  run_generate_steps.call
42
67
  end
43
- else
44
- run_generate_steps.call
68
+ rescue RuntimeError => e
69
+ abort(e.message)
45
70
  end
46
- rescue RuntimeError => e
47
- abort(e.message)
48
71
  end
49
72
 
50
73
  desc("Install Appraisal gemfiles (initial setup for projects that didn't previously use Appraisal)")
@@ -58,7 +81,8 @@ begin
58
81
  "appraisal:install failed: BUNDLE_GEMFILE=Appraisal.root.gemfile bundle install",
59
82
  appraisal_env,
60
83
  bundle,
61
- "install"
84
+ "install",
85
+ "--quiet"
62
86
  )
63
87
 
64
88
  # 2) BUNDLE_GEMFILE=Appraisal.root.gemfile bundle exec appraisal generate-install
@@ -105,7 +129,8 @@ begin
105
129
  "appraisal:update failed: BUNDLE_GEMFILE=Appraisal.root.gemfile bundle install",
106
130
  appraisal_env,
107
131
  bundle,
108
- "install"
132
+ "install",
133
+ "--quiet"
109
134
  )
110
135
 
111
136
  # 2) BUNDLE_GEMFILE=Appraisal.root.gemfile bundle update --bundler
@@ -122,7 +147,8 @@ begin
122
147
  "appraisal:update failed: BUNDLE_GEMFILE=Appraisal.root.gemfile bundle install",
123
148
  appraisal_env,
124
149
  bundle,
125
- "install"
150
+ "install",
151
+ "--quiet"
126
152
  )
127
153
 
128
154
  # 4) BUNDLE_GEMFILE=Appraisal.root.gemfile bundle exec appraisal generate-update
@@ -157,11 +183,13 @@ begin
157
183
  else
158
184
  failures = []
159
185
  locks.each do |f|
160
- File.delete(f)
161
- rescue Errno::ENOENT
162
- # Ignore if already gone
163
- rescue => e
164
- failures << [f, e]
186
+ begin
187
+ File.delete(f)
188
+ rescue Errno::ENOENT
189
+ # Ignore if already gone
190
+ rescue => e
191
+ failures << [f, e]
192
+ end
165
193
  end
166
194
 
167
195
  unless failures.empty?
@@ -9,13 +9,36 @@ require "fileutils"
9
9
  require "net/http"
10
10
  require "json"
11
11
  require "uri"
12
+ require "yaml"
12
13
 
13
14
  # External gems
15
+ require "kettle/rb/compat_matrix"
14
16
  require "ruby-progressbar"
15
17
 
16
18
  module Kettle
17
19
  module Dev
18
20
  class ReleaseCLI
21
+ QUIET_ENV = {
22
+ "KETTLE_JEM_QUIET" => "true",
23
+ "KETTLE_JEM_DEBUG" => "false",
24
+ "KETTLE_DEV_DEBUG" => "false",
25
+ "SMORG_RB_DEBUG" => "false",
26
+ "DEBUG" => nil,
27
+ "BUNDLE_QUIET" => "true",
28
+ "BUNDLE_DEBUG" => "false",
29
+ "BUNDLER_DEBUG" => "false",
30
+ "BUNDLE_VERBOSE" => "false",
31
+ "DEBUG_RESOLVER" => nil,
32
+ "DEBUG_RESOLVER_TREE" => nil,
33
+ "BUNDLER_DEBUG_RESOLVER" => nil,
34
+ "BUNDLER_DEBUG_RESOLVER_TREE" => nil,
35
+ "DEBUG_COMPACT_INDEX" => nil,
36
+ "MOLINILLO_DEBUG" => nil,
37
+ "BUNDLE_SILENCE_DEPRECATIONS" => "true",
38
+ "BUNDLE_SILENCE_ROOT_WARNING" => "true",
39
+ "BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES" => "true"
40
+ }.freeze
41
+ DEBUG_TRUE_VALUES = %w[1 true yes on].freeze
19
42
  class << self
20
43
  def run_cmd!(cmd)
21
44
  # For Bundler-invoked build/release, explicitly prefix SKIP_GEM_SIGNING so
@@ -26,7 +49,7 @@ module Kettle
26
49
  end
27
50
  puts "$ #{cmd}"
28
51
  # Pass a plain Hash for the environment to satisfy tests and avoid ENV object oddities
29
- env_hash = ENV.respond_to?(:to_hash) ? ENV.to_hash : ENV.to_h
52
+ env_hash = command_env
30
53
 
31
54
  # Some commands are interactive (e.g., `bundle exec rake release` prompting for RubyGems MFA).
32
55
  # Using capture3 detaches STDIN, preventing prompts from working. For such commands, use system
@@ -62,6 +85,19 @@ module Kettle
62
85
  abort("Command failed: #{cmd} (exit #{exit_code})#{diag}")
63
86
  end
64
87
  end
88
+
89
+ private
90
+
91
+ def command_env
92
+ env_hash = ENV.respond_to?(:to_hash) ? ENV.to_hash : ENV.to_h
93
+ return env_hash if debug_env_enabled?
94
+
95
+ env_hash.merge(QUIET_ENV)
96
+ end
97
+
98
+ def debug_env_enabled?
99
+ DEBUG_TRUE_VALUES.include?(ENV.fetch("KETTLE_DEV_DEBUG", "").downcase)
100
+ end
65
101
  end
66
102
 
67
103
  private
@@ -72,29 +108,31 @@ module Kettle
72
108
 
73
109
  public
74
110
 
75
- def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil)
111
+ def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil)
76
112
  @root = Kettle::Dev::CIHelpers.project_root
77
113
  @git = Kettle::Dev::GitAdapter.new
78
114
  @start_step = (start_step || 0).to_i
79
115
  @start_step = 0 if @start_step < 0
116
+ @skip_steps = normalize_skip_steps(skip_steps)
80
117
  @local_ci = !!local_ci
81
118
  @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
82
119
  @appraisal_task = normalize_appraisal_task(appraisal_task || ENV["KETTLE_RELEASE_APPRAISAL_TASK"])
83
120
  end
84
121
 
85
122
  def run
86
- run_pre_release_checks! if @start_step <= 0
123
+ run_pre_release_checks! if run_step?(0)
87
124
 
88
125
  # 1. Ensure Bundler version ✓
89
- ensure_bundler_2_7_plus!
126
+ ensure_bundler_2_7_plus! if run_step?(1)
90
127
 
91
128
  version = nil
92
129
  committed = nil
93
130
  trunk = nil
94
131
  feature = nil
132
+ branch_stack_release = false
95
133
 
96
134
  # 2. Version detection and sanity checks + prompt
97
- if @start_step <= 2
135
+ if run_step?(2)
98
136
  version = detect_version
99
137
  puts "Detected version: #{version.inspect}"
100
138
 
@@ -186,13 +224,15 @@ module Kettle
186
224
  end
187
225
  end
188
226
 
227
+ prepare_rubocop_lts_local_branch! if rubocop_lts_release_preflight_needed?
228
+
189
229
  # 3. bin/setup
190
- run_cmd!("bin/setup") if @start_step <= 3
230
+ run_cmd!("bin/setup") if run_step?(3)
191
231
  # 4. bin/rake
192
- run_cmd!("bin/rake") if @start_step <= 4
232
+ run_cmd!("bin/rake") if run_step?(4)
193
233
 
194
234
  # 5. appraisal:generate (optional) + canonical docs build
195
- if @start_step <= 5
235
+ if run_step?(5)
196
236
  appraisals_path = File.join(@root, "Appraisals")
197
237
  if File.file?(appraisals_path)
198
238
  puts "Appraisals detected at #{Kettle::Dev.display_path(appraisals_path)}. Running: bin/rake #{@appraisal_task}"
@@ -206,47 +246,62 @@ module Kettle
206
246
  end
207
247
 
208
248
  # 6. git user + commit release prep
209
- if @start_step <= 6
249
+ if run_step?(6)
210
250
  ensure_git_user!
211
251
  version ||= detect_version
212
252
  committed = commit_release_prep!(version)
213
253
  end
214
254
 
215
255
  # 7. optional local CI via act
216
- maybe_run_local_ci_before_push!(committed, force: local_ci?) if @start_step <= 7
256
+ maybe_run_local_ci_before_push!(committed, force: local_ci?) if run_step?(7)
217
257
 
218
258
  # 8. ensure trunk synced
219
- if @start_step <= 8 && !local_ci?
259
+ if run_step?(8) && !local_ci?
220
260
  trunk = detect_trunk_branch
221
261
  feature = current_branch
262
+ branch_stack_release = branch_stack_release_branch?(feature, trunk)
263
+ if branch_stack_release
264
+ puts "Kettle-family branch stack release branch detected: #{feature}; skipping trunk sync/rebase."
265
+ end
222
266
  puts "Trunk branch detected: #{trunk}"
223
- ensure_trunk_synced_before_push!(trunk, feature)
224
- elsif @start_step <= 8
267
+ ensure_trunk_synced_before_push!(trunk, feature) unless branch_stack_release
268
+ elsif run_step?(8)
225
269
  puts "Local CI release mode: skipping remote trunk sync before publishing."
226
270
  end
227
271
 
228
272
  # 9. push branches
229
- push! if @start_step <= 9 && !local_ci?
273
+ push! if run_step?(9) && !local_ci?
230
274
 
231
275
  # 10. monitor CI after push
232
- monitor_workflows_after_push! if @start_step <= 10 && !local_ci?
276
+ monitor_workflows_after_push! if run_step?(10) && !local_ci?
233
277
 
234
278
  # 11. merge feature into trunk and push
235
- if @start_step <= 11 && !local_ci?
279
+ if run_step?(11) && !local_ci?
236
280
  trunk ||= detect_trunk_branch
237
281
  feature ||= current_branch
238
- merge_feature_into_trunk_and_push!(trunk, feature)
282
+ branch_stack_release ||= branch_stack_release_branch?(feature, trunk)
283
+ if branch_stack_release
284
+ puts "Kettle-family branch stack release branch detected: #{feature}; skipping merge into #{trunk}."
285
+ else
286
+ merge_feature_into_trunk_and_push!(trunk, feature)
287
+ end
239
288
  end
240
289
 
241
290
  # 12. checkout trunk and pull
242
- if @start_step <= 12 && !local_ci?
291
+ if run_step?(12) && !local_ci?
243
292
  trunk ||= detect_trunk_branch
244
- checkout!(trunk)
245
- pull!(trunk)
293
+ feature ||= current_branch
294
+ branch_stack_release ||= branch_stack_release_branch?(feature, trunk)
295
+ if branch_stack_release
296
+ puts "Kettle-family branch stack release branch detected: #{feature}; staying on release branch."
297
+ else
298
+ checkout!(trunk)
299
+ pull!(trunk)
300
+ end
246
301
  end
247
302
 
248
303
  # 13. signing guidance and checks
249
- if @start_step <= 13
304
+ if run_step?(13)
250
305
  if ENV.fetch("SKIP_GEM_SIGNING", "false").casecmp("false").zero?
251
306
  puts "TIP: For local dry-runs or testing the release workflow, set SKIP_GEM_SIGNING=true to avoid PEM password prompts."
252
307
  if Kettle::Dev::InputAdapter.tty?
@@ -267,13 +322,13 @@ module Kettle
267
322
  end
268
323
 
269
324
  # 14. build
270
- if @start_step <= 14
325
+ if run_step?(14)
271
326
  puts "Running build (you may be prompted for the signing key password)..."
272
327
  run_cmd!("bundle exec rake build")
273
328
  end
274
329
 
275
330
  # 15. release and tag
276
- if @start_step <= 15
331
+ if run_step?(15)
277
332
  if local_ci?
278
333
  version ||= detect_version
279
334
  release_gem_and_tag_locally!(version)
@@ -287,7 +342,7 @@ module Kettle
287
342
  # Checksums are generated after release to avoid including checksums/ in gem package
288
343
  # Rationale: Running gem_checksums before release may commit checksums/ and cause Bundler's
289
344
  # release build to include them in the gem, thus altering the artifact, and invalidating the checksums.
290
- if @start_step <= 16
345
+ if run_step?(16)
291
346
  # Generate checksums for the just-built artifact, commit them, then validate
292
347
  run_cmd!("bin/gem_checksums")
293
348
  version ||= detect_version
@@ -295,19 +350,19 @@ module Kettle
295
350
  end
296
351
 
297
352
  # 17. push checksum commit (gem_checksums already commits)
298
- if @start_step <= 17
353
+ if run_step?(17)
299
354
  push!
300
355
  push_tags! if local_ci?
301
356
  end
302
357
 
303
358
  # 18. create GitHub release (optional)
304
- if @start_step <= 18
359
+ if run_step?(18)
305
360
  version ||= detect_version
306
361
  maybe_create_github_release!(version)
307
362
  end
308
363
 
309
364
  # 19. push tags to remotes (final step)
310
- push_tags! if @start_step <= 19 && !local_ci?
365
+ push_tags! if run_step?(19) && !local_ci?
311
366
 
312
367
  # Final success message
313
368
  begin
@@ -330,12 +385,72 @@ module Kettle
330
385
  abort("Unsupported appraisal task #{value.inspect}; use appraisal:generate or appraisal:update.")
331
386
  end
332
387
 
388
+ def normalize_skip_steps(value)
389
+ raw_steps = Array(value).flat_map { |part| part.to_s.split(",") }.map(&:strip).reject(&:empty?)
390
+ raw_steps.map do |raw|
391
+ abort("Invalid skip_steps value #{raw.inspect}; use comma-separated release step numbers from 0 to 19.") unless raw.match?(/\A\d+\z/)
392
+
393
+ step = raw.to_i
394
+ abort("Invalid skip_steps value #{raw.inspect}; release steps are numbered 0 to 19.") unless step.between?(0, 19)
395
+
396
+ step
397
+ end.uniq
398
+ end
399
+
333
400
  private
334
401
 
335
402
  def local_ci?
336
403
  @local_ci
337
404
  end
338
405
 
406
+ def run_step?(step)
407
+ @start_step <= step && !@skip_steps.include?(step)
408
+ end
409
+
410
+ def rubocop_lts_release_preflight_needed?
411
+ (3..5).any? { |step| run_step?(step) }
412
+ end
413
+
414
+ def prepare_rubocop_lts_local_branch!
415
+ local_root = rubocop_lts_local_root
416
+ return unless local_root
417
+
418
+ ruby_gem = selected_rubocop_lts_ruby_gem
419
+ return unless ruby_gem
420
+
421
+ branch = Kettle::Rb::CompatMatrix.rubocop_lts_branch_for_gem(ruby_gem)
422
+ abort("Cannot select RUBOCOP_LTS_LOCAL branch for #{ruby_gem.inspect}.") unless branch
423
+
424
+ checkout = File.join(local_root, "rubocop-lts")
425
+ current, ok = git_output(["-C", checkout, "branch", "--show-current"])
426
+ abort("Cannot inspect RUBOCOP_LTS_LOCAL checkout at #{checkout}.") unless ok
427
+ return if current == branch
428
+
429
+ puts "Switching RUBOCOP_LTS_LOCAL checkout #{checkout} to #{branch} for #{ruby_gem}."
430
+ _stdout, switched = git_output(["-C", checkout, "switch", branch])
431
+ abort("Cannot switch RUBOCOP_LTS_LOCAL checkout at #{checkout} to #{branch}. Commit or stash local changes, then retry.") unless switched
432
+ end
433
+
434
+ def rubocop_lts_local_root
435
+ value = ENV["RUBOCOP_LTS_LOCAL"].to_s.strip
436
+ return nil if value.empty? || %w[false 0 no off].include?(value.downcase)
437
+ return File.join(Dir.home, "src", "rubocop-lts") if %w[true 1 yes on].include?(value.downcase)
438
+ return value if value.start_with?("/")
439
+
440
+ File.join(Dir.home, value)
441
+ end
442
+
443
+ def selected_rubocop_lts_ruby_gem
444
+ path = File.join(@root, "gemfiles", "modular", "style_local.gemfile")
445
+ return nil unless File.file?(path)
446
+
447
+ content = File.read(path)
448
+ # This reads the generated kettle-jem style_local.gemfile declaration
449
+ # without evaluating the Gemfile during release preflight.
450
+ local_gems = content[/\blocal_gems\s*=\s*%w\[(.*?)\]/m, 1].to_s.split
451
+ local_gems.find { |gem_name| Kettle::Rb::CompatMatrix.rubocop_ruby_gem?(gem_name) }
452
+ end
453
+
339
454
  def run_pre_release_checks!
340
455
  puts "Running pre-release checks via kettle-pre-release..."
341
456
  Kettle::Dev::PreReleaseCLI.new(check_num: 1).run
@@ -988,6 +1103,44 @@ module Kettle
988
1103
  puts "Merged #{feature} into #{trunk} and pushed. The PR (if any) should auto-close."
989
1104
  end
990
1105
 
1106
+ def branch_stack_release_branch?(branch, trunk = nil)
1107
+ return false if branch.to_s.empty?
1108
+ return false if trunk && branch == trunk
1109
+
1110
+ local_kettle_family_release_target_branches.include?(branch)
1111
+ end
1112
+
1113
+ def local_kettle_family_release_target_branches
1114
+ local_kettle_family_config_paths.each do |path|
1115
+ next unless File.file?(path)
1116
+
1117
+ begin
1118
+ data = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false) || {}
1119
+ branches = Array(dig_string_keys(data, "release", "target_branches")) +
1120
+ Array(dig_string_keys(data, "branches", "release_targets"))
1121
+ return branches.map(&:to_s).reject(&:empty?) unless branches.empty?
1122
+ rescue Psych::Exception => e
1123
+ warn("Ignoring invalid kettle-family config #{Kettle::Dev.display_path(path)}: #{e.message}")
1124
+ end
1125
+ end
1126
+ []
1127
+ end
1128
+
1129
+ def local_kettle_family_config_paths
1130
+ [
1131
+ File.join(@root, ".kettle-family.yml"),
1132
+ File.join(@root, ".structuredmerge", "kettle-family.yml")
1133
+ ]
1134
+ end
1135
+
1136
+ def dig_string_keys(data, *keys)
1137
+ keys.reduce(data) do |memo, key|
1138
+ break nil unless memo.is_a?(Hash)
1139
+
1140
+ memo[key] || memo[key.to_sym]
1141
+ end
1142
+ end
1143
+
991
1144
  def ensure_signing_setup_or_skip!
992
1145
  # Treat any non-/true/i value as an explicit skip signal
993
1146
  return if ENV.fetch("SKIP_GEM_SIGNING", "").casecmp("true").zero?
@@ -91,46 +91,48 @@ module Kettle
91
91
 
92
92
  # Print GitLab pipeline status (if configured) for the current branch.
93
93
  print_gitlab_status = proc do
94
- branch = Kettle::Dev::CIHelpers.current_branch
95
- # Detect any GitLab remote (not just origin), mirroring CIMonitor behavior
96
- gl_remotes = Kettle::Dev::CIMonitor.gitlab_remote_candidates
97
- if gl_remotes.nil? || gl_remotes.empty? || branch.nil?
98
- puts "Latest GL (#{branch || "n/a"}) pipeline: n/a"
99
- next
100
- end
94
+ begin
95
+ branch = Kettle::Dev::CIHelpers.current_branch
96
+ # Detect any GitLab remote (not just origin), mirroring CIMonitor behavior
97
+ gl_remotes = Kettle::Dev::CIMonitor.gitlab_remote_candidates
98
+ if gl_remotes.nil? || gl_remotes.empty? || branch.nil?
99
+ puts "Latest GL (#{branch || "n/a"}) pipeline: n/a"
100
+ next
101
+ end
101
102
 
102
- # Parse owner/repo from the first GitLab remote URL
103
- gl_url = Kettle::Dev::CIMonitor.remote_url(gl_remotes.first)
104
- owner = repo = nil
105
- if gl_url =~ %r{git@gitlab.com:(.+?)/(.+?)(\.git)?$}
106
- owner = Regexp.last_match(1)
107
- repo = Regexp.last_match(2).sub(/\.git\z/, "")
108
- elsif gl_url =~ %r{https://gitlab.com/(.+?)/(.+?)(\.git)?$}
109
- owner = Regexp.last_match(1)
110
- repo = Regexp.last_match(2).sub(/\.git\z/, "")
111
- end
103
+ # Parse owner/repo from the first GitLab remote URL
104
+ gl_url = Kettle::Dev::CIMonitor.remote_url(gl_remotes.first)
105
+ owner = repo = nil
106
+ if gl_url =~ %r{git@gitlab.com:(.+?)/(.+?)(\.git)?$}
107
+ owner = Regexp.last_match(1)
108
+ repo = Regexp.last_match(2).sub(/\.git\z/, "")
109
+ elsif gl_url =~ %r{https://gitlab.com/(.+?)/(.+?)(\.git)?$}
110
+ owner = Regexp.last_match(1)
111
+ repo = Regexp.last_match(2).sub(/\.git\z/, "")
112
+ end
112
113
 
113
- unless owner && repo
114
- puts "Latest GL (#{branch}) pipeline: n/a"
115
- next
116
- end
114
+ unless owner && repo
115
+ puts "Latest GL (#{branch}) pipeline: n/a"
116
+ next
117
+ end
117
118
 
118
- pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
119
- if pipe
120
- st = pipe["status"].to_s
121
- status = if st == "success"
122
- "success"
119
+ pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
120
+ if pipe
121
+ st = pipe["status"].to_s
122
+ status = if st == "success"
123
+ "success"
124
+ else
125
+ ((st == "failed") ? "failure" : nil)
126
+ end
127
+ emoji = Kettle::Dev::CIMonitor.status_emoji(st, status)
128
+ details = [st, pipe["failure_reason"]].compact.join("/")
129
+ puts "Latest GL (#{branch}) pipeline: #{emoji} (#{details})"
123
130
  else
124
- ((st == "failed") ? "failure" : nil)
131
+ puts "Latest GL (#{branch}) pipeline: none"
125
132
  end
126
- emoji = Kettle::Dev::CIMonitor.status_emoji(st, status)
127
- details = [st, pipe["failure_reason"]].compact.join("/")
128
- puts "Latest GL (#{branch}) pipeline: #{emoji} (#{details})"
129
- else
130
- puts "Latest GL (#{branch}) pipeline: none"
133
+ rescue => e
134
+ puts "GL status: error #{e.class}: #{e.message}"
131
135
  end
132
- rescue => e
133
- puts "GL status: error #{e.class}: #{e.message}"
134
136
  end
135
137
 
136
138
  run_act_for = proc do |file_path|
@@ -262,10 +264,12 @@ module Kettle
262
264
  selected = nil
263
265
  input_thread = nil
264
266
  read_input = proc do
265
- selected = Kettle::Dev::InputAdapter.gets&.strip
266
- rescue StandardError, SystemExit, Interrupt => error
267
- puts "Error reading input: #{error.class}: #{error.message}" if Kettle::Dev::DEBUGGING
268
- selected = :input_error
267
+ begin
268
+ selected = Kettle::Dev::InputAdapter.gets&.strip
269
+ rescue StandardError, SystemExit, Interrupt => error
270
+ puts "Error reading input: #{error.class}: #{error.message}" if Kettle::Dev::DEBUGGING
271
+ selected = :input_error
272
+ end
269
273
  end
270
274
  if tty
271
275
  input_thread = Thread.new(&read_input) # rubocop:disable ThreadSafety/NewThread
@@ -279,52 +283,54 @@ module Kettle
279
283
 
280
284
  options.each do |code, file|
281
285
  workers << Thread.new(code, file, owner, repo, branch, token, start_at) do |c, f, ow, rp, br, tk, st_at| # rubocop:disable ThreadSafety/NewThread
282
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
283
- delay = 0.12 - (now - st_at)
284
- sleep(delay) if delay && delay > 0
285
-
286
- if ow.nil? || rp.nil? || br.nil?
287
- status_q << [c, f, "n/a"]
288
- Thread.exit
289
- end
290
- uri = URI("https://api.github.com/repos/#{ow}/#{rp}/actions/workflows/#{f}/runs?branch=#{URI.encode_www_form_component(br)}&per_page=1")
291
- poll_interval = Integer(ENV["CI_ACT_POLL_INTERVAL"] || 5)
292
- loop do
293
- begin
294
- req = Net::HTTP::Get.new(uri)
295
- req["User-Agent"] = "ci:act rake task"
296
- req["Authorization"] = "token #{tk}" if tk && !tk.empty?
297
- res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
298
- if res.is_a?(Net::HTTPSuccess)
299
- data = JSON.parse(res.body)
300
- run = data["workflow_runs"]&.first
301
- if run
302
- st = run["status"]
303
- con = run["conclusion"]
304
- emoji = Kettle::Dev::CIMonitor.status_emoji(st, con)
305
- details = [st, con].compact.join("/")
306
- status_q << [c, f, "#{emoji} (#{details})"]
307
- break if st == "completed"
286
+ begin
287
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
288
+ delay = 0.12 - (now - st_at)
289
+ sleep(delay) if delay && delay > 0
290
+
291
+ if ow.nil? || rp.nil? || br.nil?
292
+ status_q << [c, f, "n/a"]
293
+ Thread.exit
294
+ end
295
+ uri = URI("https://api.github.com/repos/#{ow}/#{rp}/actions/workflows/#{f}/runs?branch=#{URI.encode_www_form_component(br)}&per_page=1")
296
+ poll_interval = Integer(ENV["CI_ACT_POLL_INTERVAL"] || 5)
297
+ loop do
298
+ begin
299
+ req = Net::HTTP::Get.new(uri)
300
+ req["User-Agent"] = "ci:act rake task"
301
+ req["Authorization"] = "token #{tk}" if tk && !tk.empty?
302
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
303
+ if res.is_a?(Net::HTTPSuccess)
304
+ data = JSON.parse(res.body)
305
+ run = data["workflow_runs"]&.first
306
+ if run
307
+ st = run["status"]
308
+ con = run["conclusion"]
309
+ emoji = Kettle::Dev::CIMonitor.status_emoji(st, con)
310
+ details = [st, con].compact.join("/")
311
+ status_q << [c, f, "#{emoji} (#{details})"]
312
+ break if st == "completed"
313
+ else
314
+ status_q << [c, f, "none"]
315
+ break
316
+ end
308
317
  else
309
- status_q << [c, f, "none"]
310
- break
318
+ status_q << [c, f, "fail #{res.code}"]
311
319
  end
312
- else
313
- status_q << [c, f, "fail #{res.code}"]
320
+ rescue Exception => e # rubocop:disable Lint/RescueException
321
+ Kettle::Dev.debug_error(e, __method__)
322
+ # Catch all exceptions to prevent crashing the process from a worker thread
323
+ status_q << [c, f, "err"]
314
324
  end
315
- rescue Exception => e # rubocop:disable Lint/RescueException
316
- Kettle::Dev.debug_error(e, __method__)
317
- # Catch all exceptions to prevent crashing the process from a worker thread
318
- status_q << [c, f, "err"]
325
+ sleep(poll_interval)
319
326
  end
320
- sleep(poll_interval)
327
+ rescue Exception => e # rubocop:disable Lint/RescueException
328
+ Kettle::Dev.debug_error(e, __method__)
329
+ # simplecov:disable
330
+ # Catch all exceptions in the worker thread boundary, including SystemExit
331
+ status_q << [c, f, "err"]
332
+ # simplecov:enable
321
333
  end
322
- rescue Exception => e # rubocop:disable Lint/RescueException
323
- Kettle::Dev.debug_error(e, __method__)
324
- # simplecov:disable
325
- # Catch all exceptions in the worker thread boundary, including SystemExit
326
- status_q << [c, f, "err"]
327
- # simplecov:enable
328
334
  end
329
335
  end
330
336
 
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Dev
5
5
  module Version
6
- VERSION = "2.2.19"
6
+ VERSION = "2.2.21"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
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.2.19
4
+ version: 2.2.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -37,6 +37,26 @@ cert_chain:
37
37
  -----END CERTIFICATE-----
38
38
  date: 1980-01-02 00:00:00.000000000 Z
39
39
  dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: kettle-rb
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 0.1.0
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '0.1'
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 0.1.0
40
60
  - !ruby/object:Gem::Dependency
41
61
  name: kettle-test
42
62
  requirement: !ruby/object:Gem::Requirement
@@ -339,10 +359,10 @@ licenses:
339
359
  - AGPL-3.0-only
340
360
  metadata:
341
361
  homepage_uri: https://kettle-dev.galtzo.com
342
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.2.19
343
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.2.19/CHANGELOG.md
362
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.2.21
363
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.2.21/CHANGELOG.md
344
364
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
345
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.2.19
365
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.2.21
346
366
  funding_uri: https://github.com/sponsors/pboling
347
367
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
348
368
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file