kettle-dev 1.1.12 → 1.1.13

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: 28d4d222b4d85b17a9392755d884f8f0bf0e2193140bc4a5264f54fbf55448e0
4
- data.tar.gz: 1495e5a4b252b2b6537727725165dfdb67211c70f078d5b293a3ce912038983c
3
+ metadata.gz: 6b2236329716c5ee01b5cf04dd99ee6b0595a7fa95a76c8ab959c04130aaada7
4
+ data.tar.gz: 9ea676b1fd79e9247c56bf3bb5c584870e572ed6ed462ba399227c3e03047ea5
5
5
  SHA512:
6
- metadata.gz: 5861e1d242db51b88b9625e4dfcd09bb598b57ae25b505f3e3674d0a250b325133b2cae2644e111d9db649f690d5f94a31cebfc098a19bfd0d4aeba5f9efebbe
7
- data.tar.gz: 78c1003a70069e48e768dbec76298b50a8a2207419a27d398b0ab8fef89e31cfb64d9564ed9fa3c52ca4840c982298885ce1ead3426db4e50e82e251865010c8
6
+ metadata.gz: f2d83150965bedec8642543e4c818447203c0c4d5de60d41a41b2bf0672995a1e80c8501c463d448746e9d76622553df389a6fecf1a1313358c1c203c2ea29a0
7
+ data.tar.gz: ba80dea5b32c6ae569e63ef2f5134683d6867c0857ed988c2bfe243076b02728c9ca12a5ae874cd94459794af63163ef215753243256c7d6157d91251f19e8ca
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,30 @@
1
+ RSpec/MultipleExpectations:
2
+ Enabled: false
3
+
4
+ RSpec/NamedSubject:
5
+ Enabled: false
6
+
7
+ RSpec/ExampleLength:
8
+ Enabled: false
9
+
10
+ RSpec/VerifiedDoubles:
11
+ Enabled: false
12
+
13
+ RSpec/MessageSpies:
14
+ Enabled: false
15
+
16
+ RSpec/InstanceVariable:
17
+ Enabled: false
18
+
19
+ RSpec/NestedGroups:
20
+ Enabled: false
21
+
22
+ RSpec/ExpectInHook:
23
+ Enabled: false
24
+
25
+ RSpec/DescribeClass:
26
+ Exclude:
27
+ - 'spec/examples/*'
28
+
29
+ RSpec/MultipleMemoizedHelpers:
30
+ Enabled: false
data/Appraisals CHANGED
@@ -143,7 +143,7 @@ end
143
143
  # Only run linter on the latest version of Ruby (but, in support of oldest supported Ruby version)
144
144
  appraise "style" do
145
145
  eval_gemfile "modular/style.gemfile"
146
- eval_gemfile "modular/x_std_libs/r3/libs.gemfile"
146
+ eval_gemfile "modular/x_std_libs.gemfile"
147
147
  # Dependencies injected by the kettle-dev-setup script & kettle:dev:install rake task
148
148
  # eval_gemfile "modular/injected.gemfile"
149
149
  end
data/CHANGELOG.md CHANGED
@@ -24,6 +24,15 @@ Please file a bug if you notice a violation of semantic versioning.
24
24
  ### Fixed
25
25
  ### Security
26
26
 
27
+ ## [1.1.13] - 2025-09-09
28
+ - TAG: [v1.1.13][1.1.13t]
29
+ - COVERAGE: 96.29% -- 3479/3613 lines in 25 files
30
+ - BRANCH COVERAGE: 80.96% -- 1424/1759 branches in 25 files
31
+ - 76.88% documented
32
+ ### Fixed
33
+ - include .rubocop_rspec.yml during install / template task's file copy
34
+ - kettle-dev-setup now honors `--force` option
35
+
27
36
  ## [1.1.12] - 2025-09-09
28
37
  - TAG: [v1.1.12][1.1.12t]
29
38
  - COVERAGE: 94.84% -- 3422/3608 lines in 25 files
@@ -590,7 +599,9 @@ Please file a bug if you notice a violation of semantic versioning.
590
599
  - Selecting will run the selected workflow via `act`
591
600
  - This may move to its own gem in the future.
592
601
 
593
- [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.12...HEAD
602
+ [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.13...HEAD
603
+ [1.1.13]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.12...v1.1.13
604
+ [1.1.13t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.13
594
605
  [1.1.12]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.11...v1.1.12
595
606
  [1.1.12t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.12
596
607
  [1.1.11]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.10...v1.1.11
data/README.md CHANGED
@@ -907,7 +907,7 @@ Thanks for RTFM. ☺️
907
907
  [📌gitmoji]:https://gitmoji.dev
908
908
  [📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
909
909
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
910
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-3.608-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
910
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-3.613-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
911
911
  [🔐security]: SECURITY.md
912
912
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
913
913
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/README.md.example CHANGED
@@ -507,7 +507,7 @@ Thanks for RTFM. ☺️
507
507
  [📌gitmoji]:https://gitmoji.dev
508
508
  [📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
509
509
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
510
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-3.608-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
510
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-3.613-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
511
511
  [🔐security]: SECURITY.md
512
512
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
513
513
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/Rakefile.example CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # kettle-dev Rakefile v1.1.12 - 2025-09-09
3
+ # kettle-dev Rakefile v1.1.13 - 2025-09-09
4
4
  # Ruby 2.3 (Safe Navigation) or higher required
5
5
  #
6
6
  # MIT License (see License.txt)
@@ -106,6 +106,7 @@ module Kettle
106
106
  content = File.read(path)
107
107
  m = content.match(/VERSION\s*=\s*(["'])([^"']+)\1/)
108
108
  next unless m
109
+
109
110
  m[2]
110
111
  end.compact
111
112
  abort("VERSION constant not found in #{@root}/lib/**/version.rb") if versions.none?
@@ -117,6 +118,7 @@ module Kettle
117
118
  lines = content.lines
118
119
  start_i = lines.index { |l| l.start_with?("## [Unreleased]") }
119
120
  return [nil, nil, nil] unless start_i
121
+
120
122
  # Find the next version heading after Unreleased
121
123
  next_i = (start_i + 1)
122
124
  while next_i < lines.length && !lines[next_i].start_with?("## [")
@@ -133,6 +135,7 @@ module Kettle
133
135
  # after_text begins with the first released section following Unreleased
134
136
  m = after_text.match(/^## \[(\d+\.\d+\.\d+)\]/)
135
137
  return m[1] if m
138
+
136
139
  nil
137
140
  end
138
141
 
@@ -199,8 +202,10 @@ module Kettle
199
202
  branches = h["branches"] || []
200
203
  branches.each do |b|
201
204
  next unless b.is_a?(Hash)
205
+
202
206
  cov = b["coverage"]
203
207
  next unless cov.is_a?(Numeric)
208
+
204
209
  total_branches += 1
205
210
  covered_branches += 1 if cov > 0
206
211
  end
@@ -35,6 +35,7 @@ module Kettle
35
35
  def repo_info
36
36
  out, status = Open3.capture2("git", "config", "--get", "remote.origin.url")
37
37
  return unless status.success?
38
+
38
39
  url = out.strip
39
40
  if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
40
41
  [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
@@ -88,8 +89,10 @@ module Kettle
88
89
  # @return [Hash{String=>String,Integer}, nil] minimal run info or nil on error/none
89
90
  def latest_run(owner:, repo:, workflow_file:, branch: nil, token: default_token)
90
91
  return unless owner && repo
92
+
91
93
  b = branch || current_branch
92
94
  return unless b
95
+
93
96
  # Scope to the exact commit SHA when available to avoid picking up a previous run on the same branch.
94
97
  sha_out, status = Open3.capture2("git", "rev-parse", "HEAD")
95
98
  sha = status.success? ? sha_out.strip : nil
@@ -100,6 +103,7 @@ module Kettle
100
103
  req["Authorization"] = "token #{token}" if token && !token.empty?
101
104
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
102
105
  return unless res.is_a?(Net::HTTPSuccess)
106
+
103
107
  data = JSON.parse(res.body)
104
108
  runs = Array(data["workflow_runs"]) || []
105
109
  # Try to match by head_sha first; fall back to first run (branch-scoped) if none matches yet.
@@ -109,6 +113,7 @@ module Kettle
109
113
  runs.first
110
114
  end
111
115
  return unless run
116
+
112
117
  {
113
118
  "status" => run["status"],
114
119
  "conclusion" => run["conclusion"],
@@ -154,6 +159,7 @@ module Kettle
154
159
  def repo_info_gitlab
155
160
  url = origin_url
156
161
  return unless url
162
+
157
163
  if url =~ %r{git@gitlab.com:(.+?)/(.+?)(\.git)?$}
158
164
  [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
159
165
  elsif url =~ %r{https://gitlab.com/(.+?)/(.+?)(\.git)?$}
@@ -176,8 +182,10 @@ module Kettle
176
182
  # @return [Hash{String=>String,Integer}, nil]
177
183
  def gitlab_latest_pipeline(owner:, repo:, branch: nil, host: "gitlab.com", token: default_gitlab_token)
178
184
  return unless owner && repo
185
+
179
186
  b = branch || current_branch
180
187
  return unless b
188
+
181
189
  project = URI.encode_www_form_component("#{owner}/#{repo}")
182
190
  uri = URI("https://#{host}/api/v4/projects/#{project}/pipelines?ref=#{URI.encode_www_form_component(b)}&per_page=1")
183
191
  req = Net::HTTP::Get.new(uri)
@@ -185,10 +193,13 @@ module Kettle
185
193
  req["PRIVATE-TOKEN"] = token if token && !token.empty?
186
194
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
187
195
  return unless res.is_a?(Net::HTTPSuccess)
196
+
188
197
  data = JSON.parse(res.body)
189
198
  return unless data.is_a?(Array)
199
+
190
200
  pipe = data.first
191
201
  return unless pipe.is_a?(Hash)
202
+
192
203
  # Attempt to enrich with failure_reason by querying the single pipeline endpoint
193
204
  begin
194
205
  if pipe["id"]
@@ -36,6 +36,7 @@ module Kettle
36
36
  # Some APIs report only a final state string like "success"/"failed"
37
37
  return "✅" if conclusion.to_s == "success" || status.to_s == "success"
38
38
  return "🍅" if conclusion.to_s == "failure" || status.to_s == "failed"
39
+
39
40
  "⏳️"
40
41
  end
41
42
  end
@@ -100,7 +101,7 @@ module Kettle
100
101
  emoji = status_emoji(it[:status], it[:conclusion])
101
102
  details = [it[:status], it[:conclusion]].compact.join("/")
102
103
  wf = it[:workflow]
103
- puts " - #{wf}: #{emoji} (#{details}) #{it[:url] ? "-> #{it[:url]}" : ""}"
104
+ puts " - #{wf}: #{emoji} (#{details}) #{"-> #{it[:url]}" if it[:url]}"
104
105
  all_ok &&= (it[:conclusion] == "success")
105
106
  end
106
107
  end
@@ -113,7 +114,7 @@ module Kettle
113
114
  end
114
115
  emoji = status_emoji(gl[:status], status)
115
116
  details = gl[:status].to_s
116
- puts "GitLab Pipeline: #{emoji} (#{details}) #{gl[:url] ? "-> #{gl[:url]}" : ""}"
117
+ puts "GitLab Pipeline: #{emoji} (#{details}) #{"-> #{gl[:url]}" if gl[:url]}"
117
118
  all_ok &&= (gl[:status] != "failed")
118
119
  end
119
120
  all_ok
@@ -209,6 +210,7 @@ module Kettle
209
210
  end
210
211
  end
211
212
  break if results.size == total
213
+
212
214
  idx = (idx + 1) % total
213
215
  sleep(1)
214
216
  end
@@ -313,6 +315,7 @@ module Kettle
313
315
  end
314
316
  end
315
317
  break if passed.size == total
318
+
316
319
  idx = (idx + 1) % total
317
320
  sleep(1)
318
321
  end
@@ -395,15 +398,18 @@ module Kettle
395
398
  def preferred_github_remote
396
399
  cands = github_remote_candidates
397
400
  return if cands.empty?
401
+
398
402
  explicit = cands.find { |n| n == "github" } || cands.find { |n| n == "gh" }
399
403
  return explicit if explicit
400
404
  return "origin" if cands.include?("origin")
405
+
401
406
  cands.first
402
407
  end
403
408
  module_function :preferred_github_remote
404
409
 
405
410
  def parse_github_owner_repo(url)
406
411
  return [nil, nil] unless url
412
+
407
413
  if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
408
414
  [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
409
415
  elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
@@ -18,6 +18,7 @@ module Kettle
18
18
  validate = ENV.fetch("GIT_HOOK_BRANCH_VALIDATE", "false")
19
19
  branch_rule_type = (!validate.casecmp("false").zero? && validate) || nil
20
20
  return unless branch_rule_type
21
+
21
22
  branch_rule = BRANCH_RULES[branch_rule_type]
22
23
  return unless branch_rule
23
24
 
@@ -69,6 +69,7 @@ module Kettle
69
69
  %i[github gitlab codeberg].each do |forge|
70
70
  r = names[forge]
71
71
  next unless r && r != names[:origin]
72
+
72
73
  git.fetch(r)
73
74
  end
74
75
  show_status!(git, names, branch)
@@ -106,8 +107,10 @@ module Kettle
106
107
  def detect_default_branch!(git)
107
108
  _out, ok = git.capture(["rev-parse", "--verify", "origin/main"])
108
109
  return "main" if ok
110
+
109
111
  _out2, ok2 = git.capture(["rev-parse", "--verify", "origin/master"])
110
112
  return "master" if ok2
113
+
111
114
  # Default to main if neither verifies
112
115
  "main"
113
116
  end
@@ -125,6 +128,7 @@ module Kettle
125
128
  next unless remote
126
129
  next if remote == names[:origin]
127
130
  next unless existing.include?(remote)
131
+
128
132
  ref = "#{remote}/#{branch}"
129
133
  out, ok = git.capture(["rev-list", "--left-right", "--count", "#{base}...#{ref}"])
130
134
  if ok && !out.to_s.strip.empty?
@@ -232,6 +236,7 @@ module Kettle
232
236
  if org && repo
233
237
  return [org, repo]
234
238
  end
239
+
235
240
  # Try to infer from any existing remote url
236
241
  urls = git.remotes_with_urls
237
242
  sample = urls["origin"] || urls.values.first
@@ -252,7 +257,8 @@ module Kettle
252
257
 
253
258
  def prompt(label, default: nil)
254
259
  return default if @opts[:force]
255
- print("#{label}#{default ? " [#{default}]" : ""}: ")
260
+
261
+ print("#{label}#{" [#{default}]" if default}: ")
256
262
  ans = $stdin.gets&.strip
257
263
  ans = nil if ans == ""
258
264
  ans || default || abort!("#{label} is required")
@@ -341,6 +347,7 @@ module Kettle
341
347
  codeberg: names[:codeberg],
342
348
  }.each do |forge, remote_name|
343
349
  next unless remote_name
350
+
344
351
  ok = git.fetch(remote_name)
345
352
  results[forge] = !!ok
346
353
  say("Fetched from #{forge} (remote: #{remote_name}) => #{ok ? "OK" : "FAILED"}")
@@ -352,6 +359,7 @@ module Kettle
352
359
  def update_readme_federation_status!(org, repo, results)
353
360
  readme_path = File.join(Dir.pwd, "README.md")
354
361
  return unless File.exist?(readme_path)
362
+
355
363
  content = File.read(readme_path)
356
364
  # Determine if all succeeded
357
365
  forges = [:github, :gitlab, :codeberg]
@@ -376,6 +384,7 @@ module Kettle
376
384
  say("\nSome forges are not yet available. Use these import links to create mirrors:")
377
385
  [:github, :gitlab, :codeberg].each do |forge|
378
386
  next if results[forge]
387
+
379
388
  say(" - #{forge.capitalize} import: #{FORGE_MIGRATION_TOOLS[forge]}")
380
389
  end
381
390
  end
@@ -144,6 +144,7 @@ module Kettle
144
144
  else
145
145
  out, status = Open3.capture2("git", "remote", "-v")
146
146
  return {} unless status.success?
147
+
147
148
  urls = {}
148
149
  out.each_line do |line|
149
150
  # Example: origin https://github.com/me/repo.git (fetch)
@@ -32,6 +32,7 @@ module Kettle
32
32
  def local_hooks_dir
33
33
  top = git_toplevel
34
34
  return unless top && !top.empty?
35
+
35
36
  File.join(top, ".git-hooks")
36
37
  end
37
38
 
@@ -37,6 +37,7 @@ module Kettle
37
37
  def readline(*args)
38
38
  line = gets(*args)
39
39
  raise EOFError, "end of file reached" if line.nil?
40
+
40
41
  line
41
42
  end
42
43
  end
@@ -71,6 +71,7 @@ module Kettle
71
71
  when Net::HTTPRedirection
72
72
  location = response["location"]
73
73
  return false unless location
74
+
74
75
  new_uri = parse_http_uri(location)
75
76
  new_uri = uri + location if new_uri.relative?
76
77
  head_ok?(new_uri.to_s, limit: limit - 1, timeout: timeout)
@@ -155,6 +156,7 @@ module Kettle
155
156
 
156
157
  start = @check_num
157
158
  raise ArgumentError, "check_num must be >= 1" if start < 1
159
+
158
160
  begin_idx = start - 1
159
161
  checks[begin_idx..-1].each_with_index do |check, i|
160
162
  idx = begin_idx + i + 1
@@ -119,6 +119,7 @@ module Kettle
119
119
  def readme_osc_tag
120
120
  env = ENV["KETTLE_DEV_BACKER_README_OSC_TAG"].to_s
121
121
  return env unless env.strip.empty?
122
+
122
123
  if File.file?(OC_YML_PATH)
123
124
  begin
124
125
  yml = YAML.safe_load(File.read(OC_YML_PATH))
@@ -149,6 +150,7 @@ module Kettle
149
150
  def resolve_handle
150
151
  env = ENV["OPENCOLLECTIVE_HANDLE"]
151
152
  return env unless env.nil? || env.strip.empty?
153
+
152
154
  if File.file?(OC_YML_PATH)
153
155
  yml = YAML.safe_load(File.read(OC_YML_PATH))
154
156
  handle = yml.is_a?(Hash) ? yml["collective"] || yml[:collective] : nil
@@ -167,6 +169,7 @@ module Kettle
167
169
  conn.request(req)
168
170
  end
169
171
  return [] unless response.is_a?(Net::HTTPSuccess)
172
+
170
173
  parsed = JSON.parse(response.body)
171
174
  Array(parsed).map do |h|
172
175
  Backer.new(
@@ -186,6 +189,7 @@ module Kettle
186
189
 
187
190
  def generate_markdown(members, empty_message:, default_name:)
188
191
  return empty_message if members.nil? || members.empty?
192
+
189
193
  members.map do |m|
190
194
  image_url = m.image || DEFAULT_AVATAR
191
195
  link = m.website || m.profile || "#"
@@ -196,14 +200,17 @@ module Kettle
196
200
 
197
201
  def replace_between_tags(content, start_tag, end_tag, new_content)
198
202
  return :not_found if start_tag == :not_found || end_tag == :not_found
203
+
199
204
  start_index = content.index(start_tag)
200
205
  end_index = content.index(end_tag)
201
206
  return :not_found if start_index.nil? || end_index.nil? || end_index < start_index
207
+
202
208
  before = content[0..start_index + start_tag.length - 1]
203
209
  after = content[end_index..-1]
204
210
  replacement = "#{start_tag}\n#{new_content}\n#{end_tag}"
205
211
  current_block = content[start_index..end_index + end_tag.length - 1]
206
212
  return :no_change if current_block == replacement
213
+
207
214
  trailing = after[end_tag.length..-1] || ""
208
215
  "#{before}\n#{new_content}\n#{end_tag}#{trailing}"
209
216
  end
@@ -230,9 +237,11 @@ module Kettle
230
237
 
231
238
  def extract_section_identities(content, start_tag, end_tag)
232
239
  return Set.new unless start_tag && end_tag && start_tag != :not_found && end_tag != :not_found
240
+
233
241
  start_index = content.index(start_tag)
234
242
  end_index = content.index(end_tag)
235
243
  return Set.new if start_index.nil? || end_index.nil? || end_index < start_index
244
+
236
245
  block = content[(start_index + start_tag.length)...end_index]
237
246
  identities = Set.new
238
247
  block.to_s.scan(/\[!\[[^\]]*\]\([^\)]*\)\]\(([^\)]+)\)/) do |m|
@@ -269,6 +278,7 @@ module Kettle
269
278
  def mention_for_member(m, default_name: "Member")
270
279
  handle = github_handle_from_urls(m.profile, m.website)
271
280
  return "@#{handle}" if handle
281
+
272
282
  name = (m.name && !m.name.strip.empty?) ? m.name.strip : default_name
273
283
  name
274
284
  end
@@ -281,8 +291,10 @@ module Kettle
281
291
  next
282
292
  end
283
293
  next unless uri&.host&.downcase&.end_with?("github.com")
294
+
284
295
  path = (uri.path || "").sub(%r{^/}, "").sub(%r{/$}, "")
285
296
  next if path.empty?
297
+
286
298
  parts = path.split("/")
287
299
  candidate = if parts[0].downcase == "sponsors" && parts[1]
288
300
  parts[1]
@@ -308,12 +320,14 @@ module Kettle
308
320
  if system("git", "diff", "--cached", "--quiet")
309
321
  return
310
322
  end
323
+
311
324
  system("git", "commit", "-m", message)
312
325
  end
313
326
 
314
327
  def commit_subject
315
328
  env = ENV["KETTLE_README_BACKERS_COMMIT_SUBJECT"].to_s
316
329
  return env unless env.strip.empty?
330
+
317
331
  if File.file?(OC_YML_PATH)
318
332
  begin
319
333
  yml = YAML.safe_load(File.read(OC_YML_PATH))
@@ -283,6 +283,7 @@ module Kettle
283
283
  # Example match: "- COVERAGE: 97.70% -- 2125/2175 lines in 20 files"
284
284
  m = section.lines.find { |l| l =~ /-\s*COVERAGE:\s*.+--\s*\d+\/(\d+)\s+lines/i }
285
285
  return unless m
286
+
286
287
  denom = m.match(/-\s*COVERAGE:\s*.+--\s*\d+\/(\d+)\s+lines/i)[1].to_i
287
288
  kloc = denom.to_f / 1000.0
288
289
  kloc_str = format("%.3f", kloc)
@@ -296,6 +297,7 @@ module Kettle
296
297
  # Replaces only the numeric portion after "KLOC-" keeping other URL parts intact.
297
298
  def update_badge_number_in_file(path, kloc_str)
298
299
  return unless File.file?(path)
300
+
299
301
  content = File.read(path)
300
302
  # Match the specific reference line, capture groups around the number
301
303
  # Example: [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.175-FFDD67.svg?style=...
@@ -310,6 +312,7 @@ module Kettle
310
312
  def update_rakefile_example_header!(version)
311
313
  path = File.join(@root, "Rakefile.example")
312
314
  return unless File.file?(path)
315
+
313
316
  content = File.read(path)
314
317
  today = Time.now.strftime("%Y-%m-%d")
315
318
  new_line = "# kettle-dev Rakefile v#{version} - #{today}"
@@ -394,6 +397,7 @@ module Kettle
394
397
  def collapse_years(enum)
395
398
  arr = enum.to_a.map(&:to_i).uniq.sort
396
399
  return "" if arr.empty?
400
+
397
401
  segments = []
398
402
  start = arr.first
399
403
  prev = start
@@ -422,10 +426,12 @@ module Kettle
422
426
  unless line =~ /copyright/i
423
427
  next line
424
428
  end
429
+
425
430
  m = line.match(/\A(?<pre>.*?copyright[^0-9]*)(?<years>(?:\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)(?:\s*,\s*\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)*)(?<post>.*)\z/i)
426
431
  unless m
427
432
  next line
428
433
  end
434
+
429
435
  new_line = "#{m[:pre]}#{canonical_all}#{m[:post]}"
430
436
  changed ||= (new_line != line)
431
437
  new_line
@@ -444,12 +450,14 @@ module Kettle
444
450
  unless line =~ /copyright/i
445
451
  next line
446
452
  end
453
+
447
454
  # Capture three parts: prefix up to first year, the year blob, and the rest
448
455
  m = line.match(/\A(?<pre>.*?copyright[^0-9]*)(?<years>(?:\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)(?:\s*,\s*\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)*)(?<post>.*)\z/i)
449
456
  unless m
450
457
  # No parsable year sequence on this line; leave as-is
451
458
  next line
452
459
  end
460
+
453
461
  years_blob = m[:years]
454
462
  # Reuse extraction logic on just the years blob
455
463
  years = []
@@ -731,15 +739,18 @@ module Kettle
731
739
  def preferred_github_remote
732
740
  cands = github_remote_candidates
733
741
  return if cands.empty?
742
+
734
743
  # Prefer explicitly named GitHub remotes first, then origin (only if it points to GitHub), else the first candidate
735
744
  explicit = cands.find { |n| n == "github" } || cands.find { |n| n == "gh" }
736
745
  return explicit if explicit
737
746
  return "origin" if cands.include?("origin")
747
+
738
748
  cands.first
739
749
  end
740
750
 
741
751
  def parse_github_owner_repo(url)
742
752
  return [nil, nil] unless url
753
+
743
754
  if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
744
755
  [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
745
756
  elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
@@ -761,6 +772,7 @@ module Kettle
761
772
  def ahead_behind_counts(local_ref, remote_ref)
762
773
  out, ok = git_output(["rev-list", "--left-right", "--count", "#{local_ref}...#{remote_ref}"])
763
774
  return [0, 0] unless ok && !out.empty?
775
+
764
776
  parts = out.split
765
777
  left = parts[0].to_i
766
778
  right = parts[1].to_i
@@ -769,6 +781,7 @@ module Kettle
769
781
 
770
782
  def trunk_behind_remote?(trunk, remote)
771
783
  return false unless remote_branch_exists?(remote, trunk)
784
+
772
785
  _ahead, behind = ahead_behind_counts(trunk, "#{remote}/#{trunk}")
773
786
  behind.positive?
774
787
  end
@@ -781,6 +794,7 @@ module Kettle
781
794
  missing_from = []
782
795
  remotes.each do |r|
783
796
  next if r == "all"
797
+
784
798
  if remote_branch_exists?(r, trunk)
785
799
  _ahead, behind = ahead_behind_counts(trunk, "#{r}/#{trunk}")
786
800
  missing_from << r if behind.positive?
@@ -853,6 +867,7 @@ module Kettle
853
867
 
854
868
  def merge_feature_into_trunk_and_push!(trunk, feature)
855
869
  return if feature.nil? || feature == trunk
870
+
856
871
  puts "Merging #{feature} into #{trunk} (after CI success)..."
857
872
  checkout!(trunk)
858
873
  run_cmd!("git pull --rebase origin #{Shellwords.escape(trunk)}")
@@ -971,12 +986,14 @@ module Kettle
971
986
  def extract_changelog_for_version(version)
972
987
  path = File.join(@root, "CHANGELOG.md")
973
988
  return [nil, nil, nil] unless File.file?(path)
989
+
974
990
  content = File.read(path)
975
991
  lines = content.lines
976
992
 
977
993
  # Find section start
978
994
  start_idx = lines.index { |l| l.start_with?("## [#{version}]") }
979
995
  return [nil, nil, nil] unless start_idx
996
+
980
997
  i = start_idx + 1
981
998
  # Find next section heading or EOF
982
999
  while i < lines.length && !lines[i].start_with?("## [")
@@ -999,12 +1016,14 @@ module Kettle
999
1016
  def extract_release_notes_footer
1000
1017
  path = File.join(@root, "FUNDING.md")
1001
1018
  return unless File.file?(path)
1019
+
1002
1020
  content = File.read(path)
1003
1021
  start_tag = "<!-- RELEASE-NOTES-FOOTER-START -->"
1004
1022
  end_tag = "<!-- RELEASE-NOTES-FOOTER-END -->"
1005
1023
  s = content.index(start_tag)
1006
1024
  e = content.index(end_tag)
1007
1025
  return unless s && e && e > s
1026
+
1008
1027
  # Extract between tags, excluding the tags themselves
1009
1028
  block = content[(s + start_tag.length)...e]
1010
1029
  # Normalize: trim trailing whitespace but keep internal formatting
@@ -46,6 +46,7 @@ module Kettle
46
46
 
47
47
  def debug(msg)
48
48
  return if ENV.fetch("DEBUG", "false").casecmp("true").nonzero?
49
+
49
50
  $stderr.puts("[kettle-dev-setup] DEBUG: #{msg}")
50
51
  end
51
52
 
@@ -106,7 +107,11 @@ module Kettle
106
107
  parser = OptionParser.new do |opts|
107
108
  opts.banner = "Usage: kettle-dev-setup [options]"
108
109
  opts.on("--allowed=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "allowed=#{v}" }
109
- opts.on("--force", "Pass through to kettle:dev:install") { @passthrough << "force=true" }
110
+ opts.on("--force", "Pass through to kettle:dev:install") do
111
+ # Ensure in-process helpers (TemplateHelpers.ask) also see force mode
112
+ ENV["force"] = "true"
113
+ @passthrough << "force=true"
114
+ end
110
115
  opts.on("--hook_templates=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "hook_templates=#{v}" }
111
116
  opts.on("--only=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "only=#{v}" }
112
117
  opts.on("-h", "--help", "Show help") do
@@ -260,6 +265,7 @@ module Kettle
260
265
  example.each_line do |ln|
261
266
  s = ln.strip
262
267
  next if s.empty?
268
+
263
269
  if s.start_with?("source ")
264
270
  ex_sources << ln.rstrip
265
271
  elsif (m = s.match(/^git_source\(\s*:(\w+)\s*\)/))
@@ -302,6 +308,7 @@ module Kettle
302
308
  # Add missing eval_gemfile paths (recreate the exact example line when possible)
303
309
  ex_eval_paths.each do |path|
304
310
  next if tg_eval_paths.include?(path)
311
+
305
312
  additions << "eval_gemfile \"#{path}\""
306
313
  end
307
314
 
@@ -398,6 +405,7 @@ module Kettle
398
405
  here = File.expand_path(File.join(__dir__, "..", "..", "..")) # lib/kettle/dev/ -> project root
399
406
  path = File.join(here, rel)
400
407
  return path if File.exist?(path)
408
+
401
409
  nil
402
410
  end
403
411
  end
@@ -174,14 +174,19 @@ module Kettle
174
174
  end
175
175
  end
176
176
 
177
- # If no grapheme found in README H1, ask the user which to use
177
+ # If no grapheme found in README H1, either use a default in force mode, or ask the user.
178
178
  if chosen_grapheme.nil? || chosen_grapheme.empty?
179
- puts "No grapheme found after README H1. Enter a grapheme (emoji/symbol) to use for README, summary, and description:"
180
- print("Grapheme: ")
181
- ans = Kettle::Dev::InputAdapter.gets&.strip.to_s
182
- chosen_grapheme = ans[/\A\X/u].to_s
183
- # If still empty, skip synchronization silently
184
- chosen_grapheme = nil if chosen_grapheme.empty?
179
+ if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
180
+ # Non-interactive install: default to pizza slice to match template style.
181
+ chosen_grapheme = "🍕"
182
+ else
183
+ puts "No grapheme found after README H1. Enter a grapheme (emoji/symbol) to use for README, summary, and description:"
184
+ print("Grapheme: ")
185
+ ans = Kettle::Dev::InputAdapter.gets&.strip.to_s
186
+ chosen_grapheme = ans[/\A\X/u].to_s
187
+ # If still empty, skip synchronization silently
188
+ chosen_grapheme = nil if chosen_grapheme.empty?
189
+ end
185
190
  end
186
191
 
187
192
  if chosen_grapheme
@@ -311,15 +316,18 @@ module Kettle
311
316
 
312
317
  github_repo_from_url = lambda do |url|
313
318
  return unless url
319
+
314
320
  url = url.strip
315
321
  m = url.match(%r{github\.com[/:]([^/\s:]+)/([^/\s]+?)(?:\.git)?/?\z}i)
316
322
  return unless m
323
+
317
324
  [m[1], m[2]]
318
325
  end
319
326
 
320
327
  github_homepage_literal = lambda do |val|
321
328
  return false unless val
322
329
  return false if val.include?('#{')
330
+
323
331
  v = val.to_s.strip
324
332
  if (v.start_with?("\"") && v.end_with?("\"")) || (v.start_with?("'") && v.end_with?("'"))
325
333
  v = begin
@@ -329,6 +337,7 @@ module Kettle
329
337
  end
330
338
  end
331
339
  return false unless v =~ %r{\Ahttps?://github\.com/}i
340
+
332
341
  !!github_repo_from_url.call(v)
333
342
  end
334
343
 
@@ -364,12 +373,13 @@ module Kettle
364
373
  puts "Current spec.homepage appears #{interpolated ? "interpolated" : "invalid"}: #{assigned}"
365
374
  puts "Suggested literal homepage: \"#{suggested}\""
366
375
  print("Update #{File.basename(gemspec_path)} to use this homepage? [Y/n]: ")
367
- ans = Kettle::Dev::InputAdapter.gets&.strip
368
- do_update = if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
369
- true
370
- else
371
- ans.nil? || ans.empty? || ans =~ /\Ay(es)?\z/i
372
- end
376
+ do_update =
377
+ if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
378
+ true
379
+ else
380
+ ans = Kettle::Dev::InputAdapter.gets&.strip
381
+ ans.nil? || ans.empty? || ans =~ /\Ay(es)?\z/i
382
+ end
373
383
 
374
384
  if do_update
375
385
  new_line = homepage_line.sub(/=.*/, "= \"#{suggested}\"\n")
@@ -385,6 +395,7 @@ module Kettle
385
395
  rescue StandardError => e
386
396
  # Do not swallow intentional task aborts signaled via Kettle::Dev::Error
387
397
  raise if e.is_a?(Kettle::Dev::Error)
398
+
388
399
  puts "WARNING: An error occurred while checking gemspec homepage: #{e.class}: #{e.message}"
389
400
  end
390
401
 
@@ -406,6 +417,7 @@ module Kettle
406
417
  [:create, :replace, :dir_create, :dir_replace].each do |sym|
407
418
  items = meaningful.select { |_, rec| rec[:action] == sym }.map { |path, _| path }
408
419
  next if items.empty?
420
+
409
421
  puts " #{action_labels[sym]}:"
410
422
  items.sort.each do |abs|
411
423
  rel = begin
@@ -472,10 +484,8 @@ module Kettle
472
484
 
473
485
  if defined?(updated_envrc_by_install) && updated_envrc_by_install
474
486
  allowed_truthy = ENV.fetch("allowed", "").to_s =~ ENV_TRUE_RE
475
- force_truthy = ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
476
- if allowed_truthy || force_truthy
477
- reason = allowed_truthy ? "allowed=true" : "force=true"
478
- puts "Proceeding after .envrc update because #{reason}."
487
+ if allowed_truthy
488
+ puts "Proceeding after .envrc update because allowed=true."
479
489
  else
480
490
  puts
481
491
  puts "IMPORTANT: .envrc was updated during kettle:dev:install."
@@ -509,7 +519,10 @@ module Kettle
509
519
  puts "Would you like to add '.env.local' to #{gitignore_path}?"
510
520
  print("Add to .gitignore now [Y/n]: ")
511
521
  answer = Kettle::Dev::InputAdapter.gets&.strip
512
- add_it = if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
522
+ # Respect an explicit negative answer even when force=true
523
+ add_it = if answer && answer =~ /\An(o)?\z/i
524
+ false
525
+ elsif ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
513
526
  true
514
527
  else
515
528
  answer.nil? || answer.empty? || answer =~ /\Ay(es)?\z/i
@@ -349,6 +349,7 @@ module Kettle
349
349
  src = helpers.prefer_example(File.join(gem_checkout_root, rel))
350
350
  dest = File.join(project_root, rel)
351
351
  next unless File.exist?(src)
352
+
352
353
  if File.basename(rel) == "README.md"
353
354
  # Precompute destination README H1 prefix (emoji(s) or first grapheme) before any overwrite occurs
354
355
  prev_readme = File.exist?(dest) ? File.read(dest) : nil
@@ -364,6 +365,7 @@ module Kettle
364
365
  loop do
365
366
  cluster = s[/\A\X/u]
366
367
  break if cluster.nil? || cluster.empty?
368
+
367
369
  if emoji_re =~ cluster
368
370
  out << cluster
369
371
  s = s[cluster.length..-1].to_s
@@ -403,6 +405,7 @@ module Kettle
403
405
  # Parse Markdown headings while ignoring fenced code blocks (``` ... ```)
404
406
  build_sections = lambda do |md|
405
407
  return {lines: [], sections: [], line_count: 0} unless md
408
+
406
409
  lines = md.split("\n", -1)
407
410
  line_count = lines.length
408
411
 
@@ -416,6 +419,7 @@ module Kettle
416
419
  next
417
420
  end
418
421
  next if in_code
422
+
419
423
  if (m = ln.match(/^(#+)\s+.+/))
420
424
  level = m[1].length
421
425
  title = ln.sub(/^#+\s+/, "")
@@ -449,6 +453,7 @@ module Kettle
449
453
  j = i + 1
450
454
  while j < sections_arr.length
451
455
  return sections_arr[j][:start] - 1 if sections_arr[j][:level] <= current[:level]
456
+
452
457
  j += 1
453
458
  end
454
459
  total_lines - 1
@@ -464,6 +469,7 @@ module Kettle
464
469
  base = s[:base]
465
470
  # Only set once (first occurrence wins)
466
471
  next if dest_lookup.key?(base)
472
+
467
473
  be = branch_end_index.call(dest_parsed[:sections], idx, dest_parsed[:line_count])
468
474
  body_lines = dest_parsed[:lines][(s[:start] + 1)..be] || []
469
475
  dest_lookup[base] = {body_branch: body_lines.join("\n"), level: s[:level]}
@@ -485,6 +491,7 @@ module Kettle
485
491
  # Iterate in reverse to keep indices valid
486
492
  src_parsed[:sections].reverse_each.with_index do |sec, rev_i|
487
493
  next unless targets.include?(sec[:base])
494
+
488
495
  # Determine branch range in src for this section
489
496
  # rev_i is reverse index; compute forward index
490
497
  i = src_parsed[:sections].length - 1 - rev_i
@@ -652,6 +659,7 @@ module Kettle
652
659
  rescue StandardError => e
653
660
  # Do not swallow intentional task aborts
654
661
  raise if e.is_a?(Kettle::Dev::Error)
662
+
655
663
  puts "WARNING: Could not determine env file changes: #{e.class}: #{e.message}"
656
664
  end
657
665
 
@@ -753,6 +761,7 @@ module Kettle
753
761
  hook_pairs = [[hook_ruby_src, "commit-msg", 0o755], [hook_sh_src, "prepare-commit-msg", 0o755]]
754
762
  hook_pairs.each do |src, base, mode|
755
763
  next unless File.file?(src)
764
+
756
765
  hook_dests.each do |dstdir|
757
766
  begin
758
767
  FileUtils.mkdir_p(dstdir)
@@ -42,9 +42,13 @@ module Kettle
42
42
  print("#{prompt} #{default ? "[Y/n]" : "[y/N]"}: ")
43
43
  ans = Kettle::Dev::InputAdapter.gets&.strip
44
44
  ans = "" if ans.nil?
45
+ # Normalize explicit no first
46
+ return false if ans =~ /\An(o)?\z/i
45
47
  if default
48
+ # Empty -> default true; explicit yes -> true; anything else -> false
46
49
  ans.empty? || ans =~ /\Ay(es)?\z/i
47
50
  else
51
+ # Empty -> default false; explicit yes -> true; others (including garbage) -> false
48
52
  ans =~ /\Ay(es)?\z/i
49
53
  end
50
54
  end
@@ -6,7 +6,7 @@ module Kettle
6
6
  module Version
7
7
  # The gem version.
8
8
  # @return [String]
9
- VERSION = "1.1.12"
9
+ VERSION = "1.1.13"
10
10
 
11
11
  module_function
12
12
 
@@ -16,6 +16,7 @@ module Kettle
16
16
  content = File.read(path)
17
17
  m = content.match(/VERSION\s*=\s*(["'])([^"']+)\1/)
18
18
  next unless m
19
+
19
20
  m[2]
20
21
  end.compact
21
22
  abort!("VERSION constant not found in #{root}/lib/**/version.rb") if versions.none?
@@ -39,6 +40,7 @@ module Kettle
39
40
 
40
41
  if cmaj > pmaj
41
42
  return :epic if cmaj && cmaj > 1000
43
+
42
44
  :major
43
45
  elsif cmin > pmin
44
46
  :minor
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: 1.1.12
4
+ version: 1.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -257,6 +257,7 @@ files:
257
257
  - ".qlty/qlty.toml"
258
258
  - ".rspec"
259
259
  - ".rubocop.yml"
260
+ - ".rubocop_rspec.yml"
260
261
  - ".simplecov"
261
262
  - ".simplecov.example"
262
263
  - ".tool-versions"
@@ -385,10 +386,10 @@ licenses:
385
386
  - MIT
386
387
  metadata:
387
388
  homepage_uri: https://kettle-dev.galtzo.com/
388
- source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.1.12
389
- changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.12/CHANGELOG.md
389
+ source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.1.13
390
+ changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.1.13/CHANGELOG.md
390
391
  bug_tracker_uri: https://github.com/kettle-rb/kettle-dev/issues
391
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.1.12
392
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.1.13
392
393
  funding_uri: https://github.com/sponsors/pboling
393
394
  wiki_uri: https://github.com/kettle-rb/kettle-dev/wiki
394
395
  news_uri: https://www.railsbling.com/tags/kettle-dev
@@ -417,7 +418,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
417
418
  - !ruby/object:Gem::Version
418
419
  version: '0'
419
420
  requirements: []
420
- rubygems_version: 3.7.1
421
+ rubygems_version: 3.7.2
421
422
  specification_version: 4
422
423
  summary: "\U0001F372 A kettle-rb meta tool to streamline development and testing"
423
424
  test_files: []
metadata.gz.sig CHANGED
Binary file