kettle-dev 1.1.4 → 1.1.5

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.env.local.example +14 -1
  4. data/.envrc +2 -2
  5. data/.git-hooks/commit-msg +22 -16
  6. data/.git-hooks/prepare-commit-msg.example +19 -0
  7. data/.github/workflows/coverage.yml +2 -2
  8. data/.junie/guidelines.md +4 -0
  9. data/.opencollective.yml.example +3 -0
  10. data/CHANGELOG.md +31 -1
  11. data/CONTRIBUTING.md +29 -0
  12. data/FUNDING.md +2 -2
  13. data/README.md +148 -38
  14. data/README.md.example +7 -7
  15. data/Rakefile.example +1 -1
  16. data/exe/kettle-changelog +7 -2
  17. data/exe/kettle-commit-msg +20 -5
  18. data/exe/kettle-dev-setup +2 -1
  19. data/exe/kettle-dvcs +7 -2
  20. data/exe/kettle-pre-release +66 -0
  21. data/exe/kettle-readme-backers +7 -2
  22. data/exe/kettle-release +9 -2
  23. data/lib/kettle/dev/changelog_cli.rb +4 -5
  24. data/lib/kettle/dev/ci_helpers.rb +9 -5
  25. data/lib/kettle/dev/ci_monitor.rb +229 -8
  26. data/lib/kettle/dev/gem_spec_reader.rb +105 -39
  27. data/lib/kettle/dev/git_adapter.rb +6 -3
  28. data/lib/kettle/dev/git_commit_footer.rb +5 -2
  29. data/lib/kettle/dev/pre_release_cli.rb +248 -0
  30. data/lib/kettle/dev/readme_backers.rb +4 -2
  31. data/lib/kettle/dev/release_cli.rb +27 -17
  32. data/lib/kettle/dev/tasks/ci_task.rb +112 -22
  33. data/lib/kettle/dev/tasks/install_task.rb +23 -17
  34. data/lib/kettle/dev/tasks/template_task.rb +64 -23
  35. data/lib/kettle/dev/template_helpers.rb +44 -31
  36. data/lib/kettle/dev/version.rb +1 -1
  37. data/lib/kettle/dev.rb +5 -0
  38. data/sig/kettle/dev/ci_monitor.rbs +6 -0
  39. data/sig/kettle/dev/gem_spec_reader.rbs +8 -5
  40. data/sig/kettle/dev/pre_release_cli.rbs +20 -0
  41. data/sig/kettle/dev/template_helpers.rbs +2 -0
  42. data/sig/kettle/dev.rbs +1 -0
  43. data.tar.gz.sig +0 -0
  44. metadata +30 -4
  45. metadata.gz.sig +0 -0
@@ -8,23 +8,54 @@ module Kettle
8
8
  # Returns a Hash with all data used by this project from gemspecs.
9
9
  # Cache within the process to avoid repeated loads.
10
10
  class GemSpecReader
11
+ # Default minimum Ruby version to assume when a gemspec doesn't specify one.
12
+ # @return [Gem::Version]
11
13
  DEFAULT_MINIMUM_RUBY = Gem::Version.new("1.8").freeze
12
14
  class << self
13
- # Load gemspec data for the project at root.
14
- # @param root [String]
15
- # @return [Hash]
15
+ # Load gemspec data for the project at root using RubyGems.
16
+ # The reader is lenient: failures to load or missing fields are handled with defaults and warnings.
17
+ #
18
+ # @param root [String] project root containing a *.gemspec file
19
+ # @return [Hash{Symbol=>Object}] a Hash of gem metadata used by templating and tasks
20
+ # @option return [String, nil] :gemspec_path absolute path to gemspec or nil when not found
21
+ # @option return [String] :gem_name gem name ("" when not derivable)
22
+ # @option return [Gem::Version] :min_ruby minimum Ruby version derived or DEFAULT_MINIMUM_RUBY
23
+ # @option return [String] :homepage homepage string (may be "")
24
+ # @option return [String] :gh_org GitHub org (falls back to "kettle-rb")
25
+ # @option return [String] :forge_org primary forge org (currently same as gh_org)
26
+ # @option return [String, nil] :funding_org OpenCollective/org handle or nil when not discovered
27
+ # @option return [String, nil] :gh_repo GitHub repo name, if discoverable
28
+ # @option return [String] :namespace Ruby namespace derived from gem name (e.g., "Kettle::Dev")
29
+ # @option return [String] :namespace_shield URL-escaped namespace for shields
30
+ # @option return [String] :entrypoint_require require path for gem (e.g., "kettle/dev")
31
+ # @option return [String] :gem_shield shield-safe gem name
32
+ # @option return [Array<String>] :authors
33
+ # @option return [Array<String>] :email
34
+ # @option return [String] :summary
35
+ # @option return [String] :description
36
+ # @option return [Array<String>] :licenses includes both license and licenses values
37
+ # @option return [Gem::Requirement, nil] :required_ruby_version
38
+ # @option return [Array<String>] :require_paths
39
+ # @option return [String] :bindir
40
+ # @option return [Array<String>] :executables
16
41
  def load(root)
17
42
  gemspec_path = Dir.glob(File.join(root.to_s, "*.gemspec")).first
18
43
  spec = nil
19
44
  if gemspec_path && File.file?(gemspec_path)
20
45
  begin
21
46
  spec = Gem::Specification.load(gemspec_path)
22
- rescue StandardError
47
+ rescue StandardError => e
48
+ Kettle::Dev.debug_error(e, __method__)
23
49
  spec = nil
24
50
  end
25
51
  end
26
52
 
27
53
  gem_name = spec&.name.to_s
54
+ if gem_name.nil? || gem_name.strip.empty?
55
+ # Be lenient here for tasks that can proceed without gem_name (e.g., choosing destination filenames).
56
+ Kernel.warn("kettle-dev: Could not derive gem name. Ensure a valid <name> is set in the gemspec.\n - Tip: set the gem name in your .gemspec file (spec.name).\n - Path searched: #{gemspec_path || "(none found)"}")
57
+ gem_name = ""
58
+ end
28
59
  # minimum ruby version: derived from spec.required_ruby_version
29
60
  # Always an instance of Gem::Version
30
61
  min_ruby =
@@ -41,34 +72,21 @@ module Kettle
41
72
  DEFAULT_MINIMUM_RUBY
42
73
  end
43
74
  rescue StandardError => e
75
+ puts "WARNING: Minimum Ruby detection failed:"
76
+ Kettle::Dev.debug_error(e, __method__)
44
77
  # Default to a minimum of Ruby 1.8
45
- puts "WARNING: Minimum Ruby detection failed: #{e.class}: #{e.message}"
46
78
  DEFAULT_MINIMUM_RUBY
47
79
  end
48
80
 
49
81
  homepage_val = spec&.homepage.to_s
50
82
 
51
83
  # Derive org/repo from homepage or git remote
52
- forge_org = nil
53
- gh_repo = nil
54
- if homepage_val && !homepage_val.empty?
55
- if (m = homepage_val.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
56
- forge_org = m[1]
57
- gh_repo = m[2].to_s.sub(/\.git\z/, "")
58
- end
59
- end
60
- if forge_org.nil?
61
- begin
62
- origin_out = IO.popen(["git", "-C", root.to_s, "remote", "get-url", "origin"], &:read)
63
- origin_out = origin_out.read if origin_out.respond_to?(:read)
64
- origin_url = origin_out.to_s.strip
65
- if (m = origin_url.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
66
- forge_org = m[1]
67
- gh_repo = m[2].to_s.sub(/\.git\z/, "")
68
- end
69
- rescue StandardError
70
- # ignore
71
- end
84
+ forge_info = derive_forge_and_origin_repo(homepage_val)
85
+ forge_org = forge_info[:forge_org]
86
+ gh_repo = forge_info[:origin_repo]
87
+ if forge_org.to_s.empty?
88
+ Kernel.warn("kettle-dev: Could not determine forge org from spec.homepage or git remote.\n - Ensure gemspec.homepage is set to a GitHub URL or that the git remote 'origin' points to GitHub.\n - Example homepage: https://github.com/<org>/<repo>\n - Proceeding with default org: kettle-rb.")
89
+ forge_org = "kettle-rb"
72
90
  end
73
91
 
74
92
  camel = lambda do |s|
@@ -79,24 +97,35 @@ module Kettle
79
97
  entrypoint_require = gem_name.to_s.tr("-", "/")
80
98
  gem_shield = gem_name.to_s.gsub("-", "--").gsub("_", "__")
81
99
 
82
- # Funding org detection (ENV, .opencollective.yml, fallback to forge_org)
83
- funding_org = ENV["FUNDING_ORG"].to_s.strip
84
- funding_org = ENV["OPENCOLLECTIVE_ORG"].to_s.strip if funding_org.empty?
85
- funding_org = ENV["OPENCOLLECTIVE_HANDLE"].to_s.strip if funding_org.empty?
86
- if funding_org.empty?
87
- begin
88
- oc_path = File.join(root.to_s, ".opencollective.yml")
89
- if File.file?(oc_path)
90
- txt = File.read(oc_path)
91
- if (m = txt.match(/\borg:\s*([\w\-]+)/i))
92
- funding_org = m[1].to_s
100
+ # Funding org detection with bypass support.
101
+ # By default a funding org must be discoverable, unless explicitly disabled by ENV['FUNDING_ORG'] == 'false'.
102
+ funding_org_env = ENV["FUNDING_ORG"]
103
+ funding_org = funding_org_env.to_s.strip
104
+ begin
105
+ # Handle bypass: allow explicit string 'false' (any case) to disable funding org requirement.
106
+ if funding_org_env && funding_org_env.to_s.strip.casecmp("false").zero?
107
+ funding_org = nil
108
+ else
109
+ funding_org = ENV["OPENCOLLECTIVE_HANDLE"].to_s.strip if funding_org.empty?
110
+ if funding_org.to_s.empty?
111
+ oc_path = File.join(root.to_s, ".opencollective.yml")
112
+ if File.file?(oc_path)
113
+ txt = File.read(oc_path)
114
+ if (m = txt.match(/\borg:\s*([\w\-]+)/i))
115
+ funding_org = m[1].to_s
116
+ end
93
117
  end
94
118
  end
95
- rescue StandardError
96
- # ignore
119
+ # Be lenient: if funding_org cannot be determined, do not raise — leave it nil and warn.
120
+ if funding_org.to_s.empty?
121
+ Kernel.warn("kettle-dev: Could not determine funding org.\n - Options:\n * Set ENV['FUNDING_ORG'] to your funding handle (e.g., 'opencollective-handle').\n * Or set ENV['OPENCOLLECTIVE_HANDLE'].\n * Or add .opencollective.yml with: org: <handle>\n * Or bypass by setting ENV['FUNDING_ORG']=false for gems without funding.")
122
+ funding_org = nil
123
+ end
97
124
  end
125
+ rescue StandardError => error
126
+ Kettle::Dev.debug_error(error, __method__)
127
+ raise Error, "Unable to determine funding org from env or .opencollective.yml.\n\tError was: #{error.class}: #{error.message}"
98
128
  end
99
- funding_org = forge_org.to_s if funding_org.to_s.empty?
100
129
 
101
130
  {
102
131
  gemspec_path: gemspec_path,
@@ -123,6 +152,43 @@ module Kettle
123
152
  executables: Array(spec&.executables),
124
153
  }
125
154
  end
155
+
156
+ private
157
+
158
+ # Derive the forge organization and origin repository name using homepage or git remotes.
159
+ # Prefers GitHub-style URLs.
160
+ #
161
+ # @param homepage_val [String] the homepage string from the gemspec (may be empty)
162
+ # @return [Hash{Symbol=>String,nil}] keys: :forge_org, :origin_repo (both may be nil when not discoverable)
163
+ def derive_forge_and_origin_repo(homepage_val)
164
+ forge_info = {}
165
+
166
+ if homepage_val && !homepage_val.empty?
167
+ m = homepage_val.match(%r{github\.com[/:]([^/]+)/([^/]+)}i)
168
+
169
+ if m
170
+ forge_info[:forge_org] = m[1]
171
+ forge_info[:origin_repo] = m[2].to_s.sub(/\.git\z/, "")
172
+ end
173
+ end
174
+
175
+ if forge_info[:forge_org].nil? || forge_info[:forge_org].to_s.empty?
176
+ begin
177
+ ga = Kettle::Dev::GitAdapter.new
178
+ origin_url = ga.remote_url("origin") || ga.remotes_with_urls["origin"]
179
+ origin_url = origin_url.to_s.strip
180
+ if (m = origin_url.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
181
+ forge_info[:forge_org] = m[1]
182
+ forge_info[:origin_repo] = m[2].to_s.sub(/\.git\z/, "")
183
+ end
184
+ rescue StandardError => error
185
+ Kettle::Dev.debug_error(error, __method__)
186
+ # be lenient here; actual error raising will occur in caller if required
187
+ end
188
+ end
189
+
190
+ forge_info
191
+ end
126
192
  end
127
193
  end
128
194
  end
@@ -183,7 +183,8 @@ module Kettle
183
183
  else
184
184
  system("git", "checkout", branch.to_s)
185
185
  end
186
- rescue StandardError
186
+ rescue StandardError => e
187
+ Kettle::Dev.debug_error(e, __method__)
187
188
  false
188
189
  end
189
190
 
@@ -198,7 +199,8 @@ module Kettle
198
199
  else
199
200
  system("git", "pull", remote.to_s, branch.to_s)
200
201
  end
201
- rescue StandardError
202
+ rescue StandardError => e
203
+ Kettle::Dev.debug_error(e, __method__)
202
204
  false
203
205
  end
204
206
 
@@ -219,7 +221,8 @@ module Kettle
219
221
  else
220
222
  system("git", "fetch", remote.to_s)
221
223
  end
222
- rescue StandardError
224
+ rescue StandardError => e
225
+ Kettle::Dev.debug_error(e, __method__)
223
226
  false
224
227
  end
225
228
  end
@@ -22,7 +22,9 @@ module Kettle
22
22
  begin
23
23
  out = %x(git rev-parse --show-toplevel 2>/dev/null)
24
24
  toplevel = out.strip unless out.nil? || out.empty?
25
- rescue StandardError
25
+ rescue StandardError => e
26
+ Kettle::Dev.debug_error(e, __method__)
27
+ nil
26
28
  end
27
29
  toplevel
28
30
  end
@@ -111,7 +113,8 @@ module Kettle
111
113
  if @name_index
112
114
  return $2
113
115
  end
114
- rescue StandardError
116
+ rescue StandardError => e
117
+ Kettle::Dev.debug_error(e, __method__)
115
118
  end
116
119
  nil
117
120
  end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "uri"
5
+ require "net/http"
6
+ require "openssl"
7
+ begin
8
+ require "addressable/uri"
9
+ rescue LoadError
10
+ # addressable is optional; code will fallback to URI
11
+ end
12
+
13
+ module Kettle
14
+ module Dev
15
+ # PreReleaseCLI: run pre-release checks before invoking full release workflow.
16
+ # Checks:
17
+ # 1) Normalize Markdown image URLs using Addressable normalization.
18
+ # 2) Validate Markdown image links resolve via HTTP(S) HEAD.
19
+ #
20
+ # Usage: Kettle::Dev::PreReleaseCLI.new(check_num: 1).run
21
+ class PreReleaseCLI
22
+ # Simple HTTP helpers for link validation
23
+ module HTTP
24
+ module_function
25
+
26
+ # Unicode-friendly HTTP URI parser with Addressable fallback.
27
+ # @param url_str [String]
28
+ # @return [URI]
29
+ def parse_http_uri(url_str)
30
+ if defined?(Addressable::URI)
31
+ addr = Addressable::URI.parse(url_str)
32
+ # Build a standard URI with properly encoded host/path/query for Net::HTTP
33
+ # Addressable handles unicode and punycode automatically via normalization
34
+ addr = addr.normalize
35
+ # Net::HTTP expects a ::URI; convert via to_s then URI.parse
36
+ URI.parse(addr.to_s)
37
+ else
38
+ # Fallback: try URI.parse directly; users can add addressable to unlock unicode support
39
+ URI.parse(url_str)
40
+ end
41
+ end
42
+
43
+ # Perform HTTP HEAD against the given url.
44
+ # Falls back to GET when HEAD is not allowed.
45
+ # @param url_str [String]
46
+ # @param limit [Integer] max redirects
47
+ # @param timeout [Integer] per-request timeout seconds
48
+ # @return [Boolean] true when successful (2xx) after following redirects
49
+ def head_ok?(url_str, limit: 5, timeout: 10)
50
+ uri = parse_http_uri(url_str)
51
+ raise ArgumentError, "unsupported URI scheme: #{uri.scheme.inspect}" unless %w[http https].include?(uri.scheme)
52
+
53
+ request = Net::HTTP::Head.new(uri)
54
+ perform(uri, request, limit: limit, timeout: timeout)
55
+ end
56
+
57
+ # @api private
58
+ def perform(uri, request, limit:, timeout:)
59
+ raise ArgumentError, "too many redirects" if limit <= 0
60
+
61
+ http = Net::HTTP.new(uri.host, uri.port)
62
+ http.use_ssl = uri.scheme == "https"
63
+ http.read_timeout = timeout
64
+ http.open_timeout = timeout
65
+ http.ssl_timeout = timeout if http.respond_to?(:ssl_timeout=)
66
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
67
+
68
+ response = http.start { |h| h.request(request) }
69
+
70
+ case response
71
+ when Net::HTTPRedirection
72
+ location = response["location"]
73
+ return false unless location
74
+ new_uri = parse_http_uri(location)
75
+ new_uri = uri + location if new_uri.relative?
76
+ head_ok?(new_uri.to_s, limit: limit - 1, timeout: timeout)
77
+ when Net::HTTPSuccess
78
+ true
79
+ else
80
+ if response.is_a?(Net::HTTPMethodNotAllowed)
81
+ get_req = Net::HTTP::Get.new(uri)
82
+ get_resp = http.start { |h| h.request(get_req) }
83
+ return get_resp.is_a?(Net::HTTPSuccess)
84
+ end
85
+ false
86
+ end
87
+ rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError, OpenSSL::SSL::SSLError => e
88
+ warn("[kettle-pre-release] HTTP error for #{uri}: #{e.class}: #{e.message}")
89
+ false
90
+ end
91
+ end
92
+
93
+ # Markdown parsing helpers
94
+ module Markdown
95
+ module_function
96
+
97
+ # Extract unique remote HTTP(S) image URLs from markdown or HTML images.
98
+ # @param text [String]
99
+ # @return [Array<String>]
100
+ def extract_image_urls_from_text(text)
101
+ urls = []
102
+
103
+ # Inline image syntax
104
+ text.scan(/!\[[^\]]*\]\(([^\s)]+)(?:\s+\"[^\"]*\")?\)/) { |m| urls << m[0] }
105
+
106
+ # Reference definitions
107
+ ref_defs = {}
108
+ text.scan(/^\s*\[([^\]]+)\]:\s*(\S+)/) { |m| ref_defs[m[0]] = m[1] }
109
+
110
+ # Reference image usage
111
+ text.scan(/!\[[^\]]*\]\[([^\]]+)\]/) do |m|
112
+ id = m[0]
113
+ url = ref_defs[id]
114
+ urls << url if url
115
+ end
116
+
117
+ # HTML <img src="...">
118
+ text.scan(/<img\b[^>]*\bsrc\s*=\s*\"([^\"]+)\"[^>]*>/i) { |m| urls << m[0] }
119
+ text.scan(/<img\b[^>]*\bsrc\s*=\s*\'([^\']+)\'[^>]*>/i) { |m| urls << m[0] }
120
+
121
+ urls.reject! { |u| u.nil? || u.strip.empty? }
122
+ urls.select! { |u| u =~ %r{^https?://}i }
123
+ urls.uniq
124
+ end
125
+
126
+ # Extract from files matching glob.
127
+ # @param glob_pattern [String]
128
+ # @return [Array<String>]
129
+ def extract_image_urls_from_files(glob_pattern = "*.md")
130
+ files = Dir.glob(glob_pattern)
131
+ urls = files.flat_map do |f|
132
+ begin
133
+ extract_image_urls_from_text(File.read(f))
134
+ rescue StandardError => e
135
+ warn("[kettle-pre-release] Could not read #{f}: #{e.class}: #{e.message}")
136
+ []
137
+ end
138
+ end
139
+ urls.uniq
140
+ end
141
+ end
142
+
143
+ # @param check_num [Integer] 1-based index to resume from
144
+ def initialize(check_num: 1)
145
+ @check_num = (check_num || 1).to_i
146
+ @check_num = 1 if @check_num < 1
147
+ end
148
+
149
+ # Execute configured checks starting from @check_num.
150
+ # @return [void]
151
+ def run
152
+ checks = []
153
+ checks << method(:check_markdown_uri_normalization!)
154
+ checks << method(:check_markdown_images_http!)
155
+
156
+ start = @check_num
157
+ raise ArgumentError, "check_num must be >= 1" if start < 1
158
+ begin_idx = start - 1
159
+ checks[begin_idx..-1].each_with_index do |check, i|
160
+ idx = begin_idx + i + 1
161
+ puts "[kettle-pre-release] Running check ##{idx} of #{checks.size}"
162
+ check.call
163
+ end
164
+ nil
165
+ end
166
+
167
+ # Check 1: Normalize Markdown image URLs
168
+ # Compares URLs to Addressable-normalized form and rewrites Markdown when needed.
169
+ # @return [void]
170
+ def check_markdown_uri_normalization!
171
+ puts "[kettle-pre-release] Check 1: Normalize Markdown image URLs"
172
+ files = Dir.glob(["**/*.md", "**/*.md.example"])
173
+ changed = []
174
+ total_candidates = 0
175
+
176
+ files.each do |file|
177
+ begin
178
+ original = File.read(file)
179
+ rescue StandardError => e
180
+ warn("[kettle-pre-release] Could not read #{file}: #{e.class}: #{e.message}")
181
+ next
182
+ end
183
+
184
+ text = original.dup
185
+ urls = Markdown.extract_image_urls_from_text(text)
186
+ next if urls.empty?
187
+
188
+ total_candidates += urls.size
189
+ updated = text.dup
190
+ modified = false
191
+
192
+ urls.each do |url_str|
193
+ addr = Addressable::URI.parse(url_str)
194
+ normalized = addr.normalize.to_s
195
+ next if normalized == url_str
196
+
197
+ # Replace exact occurrences of the URL in the markdown content
198
+ updated.gsub!(url_str, normalized)
199
+ modified = true
200
+ puts " -> #{file}: normalized #{url_str} -> #{normalized}"
201
+ end
202
+
203
+ if modified && updated != original
204
+ begin
205
+ File.write(file, updated)
206
+ changed << file
207
+ rescue StandardError => e
208
+ warn("[kettle-pre-release] Could not write #{file}: #{e.class}: #{e.message}")
209
+ end
210
+ end
211
+ end
212
+
213
+ puts "[kettle-pre-release] Normalization candidates: #{total_candidates}. Files changed: #{changed.uniq.size}."
214
+ nil
215
+ end
216
+
217
+ # Check 2: Validate Markdown image links by HTTP HEAD (no rescue for parse failures)
218
+ # @return [void]
219
+ def check_markdown_images_http!
220
+ puts "[kettle-pre-release] Check 2: Validate Markdown image links (HTTP HEAD)"
221
+ urls = [
222
+ Markdown.extract_image_urls_from_files("**/*.md"),
223
+ Markdown.extract_image_urls_from_files("**/*.md.example"),
224
+ ].flatten.uniq
225
+ puts "[kettle-pre-release] Found #{urls.size} unique image URL(s)."
226
+ failures = []
227
+ urls.each do |url|
228
+ print(" -> #{url} … ")
229
+ ok = HTTP.head_ok?(url)
230
+ if ok
231
+ puts "OK"
232
+ else
233
+ puts "FAIL"
234
+ failures << url
235
+ end
236
+ end
237
+ if failures.any?
238
+ warn("[kettle-pre-release] #{failures.size} image URL(s) failed validation:")
239
+ failures.each { |u| warn(" - #{u}") }
240
+ Kettle::Dev::ExitAdapter.abort("Image link validation failed")
241
+ else
242
+ puts "[kettle-pre-release] All image links validated."
243
+ end
244
+ nil
245
+ end
246
+ end
247
+ end
248
+ end
@@ -127,7 +127,8 @@ module Kettle
127
127
  from_yml = from_yml.to_s if from_yml
128
128
  return from_yml unless from_yml.nil? || from_yml.strip.empty?
129
129
  end
130
- rescue StandardError
130
+ rescue StandardError => e
131
+ Kettle::Dev.debug_error(e, __method__)
131
132
  end
132
133
  end
133
134
  README_OSC_TAG_DEFAULT
@@ -321,7 +322,8 @@ module Kettle
321
322
  from_yml = from_yml.to_s if from_yml
322
323
  return from_yml unless from_yml.nil? || from_yml.strip.empty?
323
324
  end
324
- rescue StandardError
325
+ rescue StandardError => e
326
+ Kettle::Dev.debug_error(e, __method__)
325
327
  end
326
328
  end
327
329
  COMMIT_SUBJECT_DEFAULT
@@ -16,6 +16,21 @@ require "ruby-progressbar"
16
16
  module Kettle
17
17
  module Dev
18
18
  class ReleaseCLI
19
+ class << self
20
+ def run_cmd!(cmd)
21
+ # For Bundler-invoked build/release, explicitly prefix SKIP_GEM_SIGNING so
22
+ # the signing step is skipped even when Bundler scrubs ENV.
23
+ if ENV["SKIP_GEM_SIGNING"] && cmd =~ /\Abundle(\s+exec)?\s+rake\s+(build|release)\b/
24
+ cmd = "SKIP_GEM_SIGNING=true #{cmd}"
25
+ end
26
+ puts "$ #{cmd}"
27
+ # Pass a plain Hash for the environment to satisfy tests and avoid ENV object oddities
28
+ env_hash = ENV.respond_to?(:to_hash) ? ENV.to_hash : ENV.to_h
29
+ success = system(env_hash, cmd)
30
+ abort("Command failed: #{cmd}") unless success
31
+ end
32
+ end
33
+
19
34
  private
20
35
 
21
36
  def abort(msg)
@@ -243,7 +258,8 @@ module Kettle
243
258
  version ||= detect_version
244
259
  gem_name = detect_gem_name
245
260
  puts "\nšŸš€ Release #{gem_name} v#{version} Complete šŸš€"
246
- rescue StandardError
261
+ rescue StandardError => e
262
+ Kettle::Dev.debug_error(e, __method__)
247
263
  # Fallback if detection fails for any reason
248
264
  puts "\nšŸš€ Release v#{version || "unknown"} Complete šŸš€"
249
265
  end
@@ -455,21 +471,12 @@ module Kettle
455
471
  end
456
472
 
457
473
  def monitor_workflows_after_push!
458
- # Delegate to shared CI monitor to keep logic DRY across release flow and rake tasks
474
+ # Use abort-on-failure CI monitor to match historical behavior and specs
459
475
  Kettle::Dev::CIMonitor.monitor_all!(restart_hint: "bundle exec kettle-release start_step=10")
460
476
  end
461
477
 
462
478
  def run_cmd!(cmd)
463
- # For Bundler-invoked build/release, explicitly prefix SKIP_GEM_SIGNING so
464
- # the signing step is skipped even when Bundler scrubs ENV.
465
- if ENV["SKIP_GEM_SIGNING"] && cmd =~ /\Abundle(\s+exec)?\s+rake\s+(build|release)\b/
466
- cmd = "SKIP_GEM_SIGNING=true #{cmd}"
467
- end
468
- puts "$ #{cmd}"
469
- # Pass a plain Hash for the environment to satisfy tests and avoid ENV object oddities
470
- env_hash = ENV.respond_to?(:to_hash) ? ENV.to_hash : ENV.to_h
471
- success = system(env_hash, cmd)
472
- abort("Command failed: #{cmd}") unless success
479
+ self.class.run_cmd!(cmd)
473
480
  end
474
481
 
475
482
  def git_output(args)
@@ -479,15 +486,16 @@ module Kettle
479
486
  end
480
487
 
481
488
  def ensure_git_user!
482
- name, ok1 = git_output(["config", "user.name"])
483
- email, ok2 = git_output(["config", "user.email"])
489
+ name, ok1 = git_output(%w[config user.name])
490
+ email, ok2 = git_output(%w[config user.email])
484
491
  abort("Git user.name or user.email not configured.") unless ok1 && ok2 && !name.empty? && !email.empty?
485
492
  end
486
493
 
487
494
  def ensure_bundler_2_7_plus!
488
495
  begin
489
496
  require "bundler"
490
- rescue LoadError
497
+ rescue LoadError => e
498
+ Kettle::Dev.debug_error(e, __method__)
491
499
  abort("Bundler is required. Please install bundler >= 2.7.0 and try again.")
492
500
  end
493
501
  ver = Gem::Version.new(Bundler::VERSION)
@@ -512,7 +520,8 @@ module Kettle
512
520
 
513
521
  act_ok = begin
514
522
  system("act", "--version", out: File::NULL, err: File::NULL)
515
- rescue StandardError
523
+ rescue StandardError => e
524
+ Kettle::Dev.debug_error(e, __method__)
516
525
  false
517
526
  end
518
527
  unless act_ok
@@ -592,7 +601,8 @@ module Kettle
592
601
  series_versions = gversions.select { |gv| gv.segments[0, 2] == series }
593
602
  latest_series = series_versions.last&.to_s
594
603
  [latest_overall, latest_series]
595
- rescue StandardError
604
+ rescue StandardError => e
605
+ Kettle::Dev.debug_error(e, __method__)
596
606
  [nil, nil]
597
607
  end
598
608