kettle-dev 1.0.10 → 1.0.11

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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "kettle/dev/exit_adapter"
4
+ require "kettle/dev/input_adapter"
4
5
 
5
6
  module Kettle
6
7
  module Dev
@@ -34,7 +35,8 @@ module Kettle
34
35
  meta = helpers.gemspec_metadata(project_root)
35
36
  gem_name = meta[:gem_name]
36
37
  min_ruby = meta[:min_ruby]
37
- gh_org = meta[:gh_org]
38
+ forge_org = meta[:forge_org] || meta[:gh_org]
39
+ funding_org = meta[:funding_org] || forge_org
38
40
  entrypoint_require = meta[:entrypoint_require]
39
41
  namespace = meta[:namespace]
40
42
  namespace_shield = meta[:namespace_shield]
@@ -56,14 +58,14 @@ module Kettle
56
58
  if File.basename(rel) == "FUNDING.yml"
57
59
  helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
58
60
  c = content.dup
59
- c = c.gsub(/^open_collective:\s+.*$/i) { |line| gh_org ? "open_collective: #{gh_org}" : line }
61
+ c = c.gsub(/^open_collective:\s+.*$/i) { |line| funding_org ? "open_collective: #{funding_org}" : line }
60
62
  if gem_name && !gem_name.empty?
61
63
  c = c.gsub(/^tidelift:\s+.*$/i, "tidelift: rubygems/#{gem_name}")
62
64
  end
63
65
  # Also apply common replacements for org/gem/namespace/shields
64
66
  helpers.apply_common_replacements(
65
67
  c,
66
- gh_org: gh_org,
68
+ org: forge_org,
67
69
  gem_name: gem_name,
68
70
  namespace: namespace,
69
71
  namespace_shield: namespace_shield,
@@ -74,7 +76,7 @@ module Kettle
74
76
  helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
75
77
  helpers.apply_common_replacements(
76
78
  content,
77
- gh_org: gh_org,
79
+ org: forge_org,
78
80
  gem_name: gem_name,
79
81
  namespace: namespace,
80
82
  namespace_shield: namespace_shield,
@@ -163,9 +165,9 @@ module Kettle
163
165
  end
164
166
  end
165
167
 
166
- # 6) .env.local special case: never overwrite project .env.local; copy template as .env.local.example
168
+ # 6) .env.local special case: never read or touch .env.local from source; only copy .env.local.example to .env.local.example
167
169
  begin
168
- envlocal_src = helpers.prefer_example(File.join(gem_checkout_root, ".env.local"))
170
+ envlocal_src = File.join(gem_checkout_root, ".env.local.example")
169
171
  envlocal_dest = File.join(project_root, ".env.local.example")
170
172
  if File.exist?(envlocal_src)
171
173
  helpers.copy_file_with_prompt(envlocal_src, envlocal_dest, allow_create: true, allow_replace: true)
@@ -201,16 +203,58 @@ module Kettle
201
203
  .junie/guidelines-rbs.md
202
204
  ]
203
205
 
206
+ # Snapshot existing README content once (for H1 prefix preservation after write)
207
+ existing_readme_before = begin
208
+ path = File.join(project_root, "README.md")
209
+ File.file?(path) ? File.read(path) : nil
210
+ rescue StandardError
211
+ nil
212
+ end
213
+
204
214
  files_to_copy.each do |rel|
205
215
  src = helpers.prefer_example(File.join(gem_checkout_root, rel))
206
216
  dest = File.join(project_root, rel)
207
217
  next unless File.exist?(src)
208
218
  if File.basename(rel) == "README.md"
219
+ # Precompute destination README H1 prefix (emoji(s) or first grapheme) before any overwrite occurs
220
+ prev_readme = File.exist?(dest) ? File.read(dest) : nil
221
+ begin
222
+ if prev_readme
223
+ first_h1_prev = prev_readme.lines.find { |ln| ln =~ /^#\s+/ }
224
+ if first_h1_prev
225
+ require "kettle/emoji_regex"
226
+ emoji_re = Kettle::EmojiRegex::REGEX
227
+ tail = first_h1_prev.sub(/^#\s+/, "")
228
+ # Extract consecutive leading emoji graphemes
229
+ out = +""
230
+ s = tail.dup
231
+ loop do
232
+ cluster = s[/\A\X/u]
233
+ break if cluster.nil? || cluster.empty?
234
+ if emoji_re.match?(cluster)
235
+ out << cluster
236
+ s = s[cluster.length..-1].to_s
237
+ else
238
+ break
239
+ end
240
+ end
241
+ if !out.empty?
242
+ out
243
+ else
244
+ # Fallback to first grapheme
245
+ tail[/\A\X/u]
246
+ end
247
+ end
248
+ end
249
+ rescue StandardError
250
+ # ignore, leave dest_preserve_prefix as nil
251
+ end
252
+
209
253
  helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
210
254
  # 1) Do token replacements on the template content (org/gem/namespace/shields)
211
255
  c = helpers.apply_common_replacements(
212
256
  content,
213
- gh_org: gh_org,
257
+ org: forge_org,
214
258
  gem_name: gem_name,
215
259
  namespace: namespace,
216
260
  namespace_shield: namespace_shield,
@@ -219,38 +263,75 @@ module Kettle
219
263
 
220
264
  # 2) Merge specific sections from destination README, if present
221
265
  begin
222
- dest_existing = File.exist?(dest) ? File.read(dest) : nil
266
+ dest_existing = prev_readme
267
+
268
+ # Parse Markdown headings while ignoring fenced code blocks (``` ... ```)
269
+ build_sections = lambda do |md|
270
+ return {lines: [], sections: [], line_count: 0} unless md
271
+ lines = md.split("\n", -1)
272
+ line_count = lines.length
223
273
 
224
- # Helper to parse markdown sections at any heading level (#, ##, ###, ...)
225
- parse_sections = lambda do |md|
226
274
  sections = []
227
- return sections unless md
228
- lines = md.split("\n", -1) # keep trailing empty lines
229
- indices = []
275
+ in_code = false
276
+ fence_re = /^\s*```/ # start or end of fenced block
277
+
230
278
  lines.each_with_index do |ln, i|
231
- indices << i if ln =~ /^#+\s+.+/
279
+ if ln =~ fence_re
280
+ in_code = !in_code
281
+ next
282
+ end
283
+ next if in_code
284
+ if (m = ln.match(/^(#+)\s+.+/))
285
+ level = m[1].length
286
+ title = ln.sub(/^#+\s+/, "")
287
+ base = title.sub(/\A[^\p{Alnum}]+/u, "").strip.downcase
288
+ sections << {start: i, level: level, heading: ln, base: base}
289
+ end
232
290
  end
233
- indices << lines.length
234
- indices.each_cons(2) do |start_i, nxt|
235
- heading = lines[start_i]
236
- body_lines = lines[(start_i + 1)...nxt] || []
237
- title = heading.sub(/^#+\s+/, "")
238
- # Normalize by removing leading emoji/non-alnum and extra spaces
239
- base = title.sub(/\A[^\p{Alnum}]+/u, "").strip.downcase
240
- sections << {start: start_i, stop: nxt - 1, heading: heading, body: body_lines.join("\n"), base: base}
291
+
292
+ # Compute stop indices based on next heading of same or higher level
293
+ sections.each_with_index do |sec, i|
294
+ j = i + 1
295
+ stop = line_count - 1
296
+ while j < sections.length
297
+ if sections[j][:level] <= sec[:level]
298
+ stop = sections[j][:start] - 1
299
+ break
300
+ end
301
+ j += 1
302
+ end
303
+ sec[:stop_to_next_any] = stop
304
+ body_lines_any = lines[(sec[:start] + 1)..stop] || []
305
+ sec[:body_to_next_any] = body_lines_any.join("\n")
306
+ end
307
+
308
+ {lines: lines, sections: sections, line_count: line_count}
309
+ end
310
+
311
+ # Helper: Compute the branch end (inclusive) for a section at index i
312
+ branch_end_index = lambda do |sections_arr, i, total_lines|
313
+ current = sections_arr[i]
314
+ j = i + 1
315
+ while j < sections_arr.length
316
+ return sections_arr[j][:start] - 1 if sections_arr[j][:level] <= current[:level]
317
+ j += 1
241
318
  end
242
- {lines: lines, sections: sections}
319
+ total_lines - 1
243
320
  end
244
321
 
245
- # Parse src (c) and dest
246
- src_parsed = parse_sections.call(c)
247
- dest_parsed = parse_sections.call(dest_existing)
322
+ src_parsed = build_sections.call(c)
323
+ dest_parsed = build_sections.call(dest_existing)
248
324
 
249
- # Build lookup for destination sections by base title
325
+ # Build lookup for destination sections by base title, using full branch body (to next heading of same or higher level)
250
326
  dest_lookup = {}
251
327
  if dest_parsed && dest_parsed[:sections]
252
- dest_parsed[:sections].each do |s|
253
- dest_lookup[s[:base]] = s[:body]
328
+ dest_parsed[:sections].each_with_index do |s, idx|
329
+ base = s[:base]
330
+ # Only set once (first occurrence wins)
331
+ next if dest_lookup.key?(base)
332
+ be = branch_end_index.call(dest_parsed[:sections], idx, dest_parsed[:line_count])
333
+ body_lines = dest_parsed[:lines][(s[:start] + 1)..be] || []
334
+ dest_lookup[base] = {body_branch: body_lines.join("\n"), level: s[:level]}
254
335
  end
255
336
  end
256
337
 
@@ -263,17 +344,21 @@ module Kettle
263
344
  end
264
345
  targets = ["synopsis", "configuration", "basic usage"] + note_bases
265
346
 
266
- # Replace matching sections in src
347
+ # Replace matching sections in src using full branch ranges
267
348
  if src_parsed && src_parsed[:sections] && !src_parsed[:sections].empty?
268
349
  lines = src_parsed[:lines].dup
269
- # Iterate over src sections; when base is in targets, rewrite its body
270
- src_parsed[:sections].reverse_each do |sec|
350
+ # Iterate in reverse to keep indices valid
351
+ src_parsed[:sections].reverse_each.with_index do |sec, rev_i|
271
352
  next unless targets.include?(sec[:base])
272
- new_body = dest_lookup.fetch(sec[:base], "\n\n")
353
+ # Determine branch range in src for this section
354
+ # rev_i is reverse index; compute forward index
355
+ i = src_parsed[:sections].length - 1 - rev_i
356
+ src_end = branch_end_index.call(src_parsed[:sections], i, src_parsed[:line_count])
357
+ dest_entry = dest_lookup[sec[:base]]
358
+ new_body = dest_entry ? dest_entry[:body_branch] : "\n\n"
273
359
  new_block = [sec[:heading], new_body].join("\n")
274
- # Replace the range from start+0 to stop with new_block lines
275
360
  range_start = sec[:start]
276
- range_end = sec[:stop]
361
+ range_end = src_end
277
362
  # Remove old range
278
363
  lines.slice!(range_start..range_end)
279
364
  # Insert new block (split preserves potential empty tail)
@@ -283,46 +368,22 @@ module Kettle
283
368
  c = lines.join("\n")
284
369
  end
285
370
 
286
- # 3) Preserve first H1 emojis from destination README, if any
371
+ # 3) Preserve entire H1 line from destination README, if any
287
372
  begin
288
- emoji_re = Kettle::EmojiRegex::REGEX
289
-
290
- dest_emojis = nil
291
373
  if dest_existing
292
- first_h1_dest = dest_existing.lines.find { |ln| ln =~ /^#\s+/ }
293
- if first_h1_dest
294
- after = first_h1_dest.sub(/^#\s+/, "")
295
- emojis = +""
296
- while after =~ /\A#{emoji_re.source}/u
297
- # Capture the entire grapheme cluster for the emoji (handles VS16/ZWJ sequences)
298
- cluster = after[/\A\X/u]
299
- emojis << cluster
300
- after = after[cluster.length..-1].to_s
374
+ dest_h1 = dest_existing.lines.find { |ln| ln =~ /^#\s+/ }
375
+ if dest_h1
376
+ lines_new = c.split("\n", -1)
377
+ src_h1_idx = lines_new.index { |ln| ln =~ /^#\s+/ }
378
+ if src_h1_idx
379
+ # Replace the entire H1 line with the destination's H1 exactly
380
+ lines_new[src_h1_idx] = dest_h1.chomp
381
+ c = lines_new.join("\n")
301
382
  end
302
- dest_emojis = emojis unless emojis.empty?
303
- end
304
- end
305
-
306
- if dest_emojis && !dest_emojis.empty?
307
- lines_new = c.split("\n", -1)
308
- idx = lines_new.index { |ln| ln =~ /^#\s+/ }
309
- if idx
310
- rest = lines_new[idx].sub(/^#\s+/, "")
311
- # Remove any leading emojis from the H1 by peeling full grapheme clusters
312
- rest_wo_emoji = begin
313
- tmp = rest.dup
314
- while tmp =~ /\A#{emoji_re.source}/u
315
- cluster = tmp[/\A\X/u]
316
- tmp = tmp[cluster.length..-1].to_s
317
- end
318
- tmp.sub(/\A\s+/, "")
319
- end
320
- lines_new[idx] = ["#", dest_emojis, rest_wo_emoji].join(" ").gsub(/\s+/, " ").sub(/^#\s+/, "# ")
321
- c = lines_new.join("\n")
322
383
  end
323
384
  end
324
385
  rescue StandardError
325
- # ignore emoji preservation errors
386
+ # ignore H1 preservation errors
326
387
  end
327
388
  rescue StandardError
328
389
  # Best effort; if anything fails, keep c as-is
@@ -332,20 +393,54 @@ module Kettle
332
393
  end
333
394
  elsif ["CHANGELOG.md", "CITATION.cff", "CONTRIBUTING.md", ".opencollective.yml", ".junie/guidelines.md"].include?(rel)
334
395
  helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
335
- helpers.apply_common_replacements(
396
+ c = helpers.apply_common_replacements(
336
397
  content,
337
- gh_org: gh_org,
398
+ org: ((File.basename(rel) == ".opencollective.yml") ? funding_org : forge_org),
338
399
  gem_name: gem_name,
339
400
  namespace: namespace,
340
401
  namespace_shield: namespace_shield,
341
402
  gem_shield: gem_shield,
342
403
  )
404
+ # Retain whitespace everywhere, except collapse repeated whitespace in CHANGELOG release headers only
405
+ if File.basename(rel) == "CHANGELOG.md"
406
+ lines = c.split("\n", -1)
407
+ lines.map! do |ln|
408
+ if ln =~ /^##\s+\[.*\]/
409
+ ln.gsub(/[ \t]+/, " ")
410
+ else
411
+ ln
412
+ end
413
+ end
414
+ c = lines.join("\n")
415
+ end
416
+ c
343
417
  end
344
418
  else
345
419
  helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true)
346
420
  end
347
421
  end
348
422
 
423
+ # Post-process README H1 preservation using snapshot (replace entire H1 line)
424
+ begin
425
+ if existing_readme_before
426
+ readme_path = File.join(project_root, "README.md")
427
+ if File.file?(readme_path)
428
+ prev = existing_readme_before
429
+ newc = File.read(readme_path)
430
+ prev_h1 = prev.lines.find { |ln| ln =~ /^#\s+/ }
431
+ lines = newc.split("\n", -1)
432
+ cur_h1_idx = lines.index { |ln| ln =~ /^#\s+/ }
433
+ if prev_h1 && cur_h1_idx
434
+ # Replace the entire H1 line with the previous README's H1 exactly
435
+ lines[cur_h1_idx] = prev_h1.chomp
436
+ File.open(readme_path, "w") { |f| f.write(lines.join("\n")) }
437
+ end
438
+ end
439
+ end
440
+ rescue StandardError
441
+ # ignore post-processing errors
442
+ end
443
+
349
444
  # 7b) certs/pboling.pem
350
445
  begin
351
446
  cert_src = File.join(gem_checkout_root, "certs", "pboling.pem")
@@ -415,8 +510,15 @@ module Kettle
415
510
  puts " [l] Local to this project (#{File.join(project_root, ".git-hooks")})"
416
511
  puts " [g] Global for this user (#{File.join(ENV["HOME"], ".git-hooks")})"
417
512
  puts " [s] Skip copying"
418
- print("Choose (l/g/s) [l]: ")
419
- choice = $stdin.gets&.strip
513
+ # Allow non-interactive selection via environment
514
+ # Precedence: CLI switch (hook_templates) > KETTLE_DEV_HOOK_TEMPLATES > prompt
515
+ env_choice = ENV["hook_templates"]
516
+ env_choice = ENV["KETTLE_DEV_HOOK_TEMPLATES"] if env_choice.nil? || env_choice.strip.empty?
517
+ choice = env_choice&.strip
518
+ unless choice && !choice.empty?
519
+ print("Choose (l/g/s) [l]: ")
520
+ choice = Kettle::Dev::InputAdapter.gets&.strip
521
+ end
420
522
  choice = "l" if choice.nil? || choice.empty?
421
523
  dest_dir = case choice.downcase
422
524
  when "g", "global" then File.join(ENV["HOME"], ".git-hooks")
@@ -2,6 +2,8 @@
2
2
 
3
3
  # External stdlibs
4
4
  require "find"
5
+ # Internal
6
+ require "kettle/dev/input_adapter"
5
7
 
6
8
  module Kettle
7
9
  module Dev
@@ -38,7 +40,7 @@ module Kettle
38
40
  return true
39
41
  end
40
42
  print("#{prompt} #{default ? "[Y/n]" : "[y/N]"}: ")
41
- ans = $stdin.gets&.strip
43
+ ans = Kettle::Dev::InputAdapter.gets&.strip
42
44
  ans = "" if ans.nil?
43
45
  if default
44
46
  ans.empty? || ans =~ /\Ay(es)?\z/i
@@ -175,6 +177,21 @@ module Kettle
175
177
  FileUtils.mkdir_p(target)
176
178
  else
177
179
  FileUtils.mkdir_p(File.dirname(target))
180
+ if File.exist?(target)
181
+ # Skip only if contents are identical. If source and target paths are the same,
182
+ # avoid FileUtils.cp (which raises) and do an in-place rewrite to satisfy "copy".
183
+ begin
184
+ if FileUtils.compare_file(path, target)
185
+ next
186
+ elsif path == target
187
+ data = File.binread(path)
188
+ File.open(target, "wb") { |f| f.write(data) }
189
+ next
190
+ end
191
+ rescue StandardError
192
+ # ignore compare errors; fall through to copy
193
+ end
194
+ end
178
195
  FileUtils.cp(path, target)
179
196
  end
180
197
  end
@@ -194,6 +211,21 @@ module Kettle
194
211
  FileUtils.mkdir_p(target)
195
212
  else
196
213
  FileUtils.mkdir_p(File.dirname(target))
214
+ if File.exist?(target)
215
+ # Skip only if contents are identical. If source and target paths are the same,
216
+ # avoid FileUtils.cp (which raises) and do an in-place rewrite to satisfy "copy".
217
+ begin
218
+ if FileUtils.compare_file(path, target)
219
+ next
220
+ elsif path == target
221
+ data = File.binread(path)
222
+ File.open(target, "wb") { |f| f.write(data) }
223
+ next
224
+ end
225
+ rescue StandardError
226
+ # ignore compare errors; fall through to copy
227
+ end
228
+ end
197
229
  FileUtils.cp(path, target)
198
230
  end
199
231
  end
@@ -204,15 +236,15 @@ module Kettle
204
236
 
205
237
  # Apply common token replacements used when templating text files
206
238
  # @param content [String]
207
- # @param gh_org [String, nil]
239
+ # @param org [String, nil]
208
240
  # @param gem_name [String]
209
241
  # @param namespace [String]
210
242
  # @param namespace_shield [String]
211
243
  # @param gem_shield [String]
212
244
  # @return [String]
213
- def apply_common_replacements(content, gh_org:, gem_name:, namespace:, namespace_shield:, gem_shield:)
245
+ def apply_common_replacements(content, org:, gem_name:, namespace:, namespace_shield:, gem_shield:)
214
246
  c = content.dup
215
- c = c.gsub("kettle-rb", gh_org.to_s) if gh_org && !gh_org.empty?
247
+ c = c.gsub("kettle-rb", org.to_s) if org && !org.empty?
216
248
  if gem_name && !gem_name.empty?
217
249
  # Replace occurrences of the template gem name in text, including inside
218
250
  # markdown reference labels like [🖼️kettle-dev] and identifiers like kettle-dev-i
@@ -248,15 +280,15 @@ module Kettle
248
280
  end
249
281
  end
250
282
  gh_match = homepage_val&.match(%r{github\.com/([^/]+)/([^/]+)}i)
251
- gh_org = gh_match && gh_match[1]
283
+ forge_org = gh_match && gh_match[1]
252
284
  gh_repo = gh_match && gh_match[2]&.sub(/\.git\z/, "")
253
- if gh_org.nil?
285
+ if forge_org.nil?
254
286
  begin
255
287
  origin_out = IO.popen(["git", "-C", root.to_s, "remote", "get-url", "origin"], &:read)
256
288
  origin_out = origin_out.read if origin_out.respond_to?(:read)
257
289
  origin_url = origin_out.to_s.strip
258
290
  if (m = origin_url.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
259
- gh_org = m[1]
291
+ forge_org = m[1]
260
292
  gh_repo = m[2]&.sub(/\.git\z/, "")
261
293
  end
262
294
  rescue StandardError
@@ -272,12 +304,33 @@ module Kettle
272
304
  entrypoint_require = gem_name.to_s.tr("-", "/")
273
305
  gem_shield = gem_name.to_s.gsub("-", "--").gsub("_", "__")
274
306
 
307
+ # Determine funding_org independently of forge_org (GitHub org)
308
+ funding_org = ENV["FUNDING_ORG"].to_s.strip
309
+ funding_org = ENV["OPENCOLLECTIVE_ORG"].to_s.strip if funding_org.empty?
310
+ funding_org = ENV["OPENCOLLECTIVE_HANDLE"].to_s.strip if funding_org.empty?
311
+ if funding_org.empty?
312
+ begin
313
+ oc_path = File.join(root.to_s, ".opencollective.yml")
314
+ if File.file?(oc_path)
315
+ txt = File.read(oc_path)
316
+ if (m = txt.match(/\borg:\s*([\w\-]+)/i))
317
+ funding_org = m[1].to_s
318
+ end
319
+ end
320
+ rescue StandardError
321
+ # ignore
322
+ end
323
+ end
324
+ funding_org = forge_org.to_s if funding_org.to_s.empty?
325
+
275
326
  {
276
327
  gemspec_path: gemspec_path,
277
328
  gem_name: gem_name,
278
329
  min_ruby: min_ruby,
279
330
  homepage: homepage_val,
280
- gh_org: gh_org,
331
+ gh_org: forge_org, # Backward compat: keep old key synonymous with forge_org
332
+ forge_org: forge_org,
333
+ funding_org: funding_org,
281
334
  gh_repo: gh_repo,
282
335
  namespace: namespace,
283
336
  namespace_shield: namespace_shield,
@@ -6,7 +6,7 @@ module Kettle
6
6
  module Version
7
7
  # The gem version.
8
8
  # @return [String]
9
- VERSION = "1.0.10"
9
+ VERSION = "1.0.11"
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Dev
5
+ # Shared helpers for version detection and bump classification.
6
+ module Versioning
7
+ module_function
8
+
9
+ # Detects a unique VERSION constant declared under lib/**/version.rb
10
+ # @param root [String] project root
11
+ # @return [String] version string
12
+ def detect_version(root)
13
+ candidates = Dir[File.join(root, "lib", "**", "version.rb")]
14
+ abort!("Could not find version.rb under lib/**.") if candidates.empty?
15
+ versions = candidates.map do |path|
16
+ content = File.read(path)
17
+ m = content.match(/VERSION\s*=\s*(["'])([^"']+)\1/)
18
+ next unless m
19
+ m[2]
20
+ end.compact
21
+ abort!("VERSION constant not found in #{root}/lib/**/version.rb") if versions.none?
22
+ abort!("Multiple VERSION constants found to be out of sync (#{versions.inspect}) in #{root}/lib/**/version.rb") unless versions.uniq.length == 1
23
+ versions.first
24
+ end
25
+
26
+ # Classify the bump type from prev -> cur.
27
+ # EPIC is a MAJOR > 1000.
28
+ # @param prev [String] previous released version
29
+ # @param cur [String] current version (from version.rb)
30
+ # @return [Symbol] one of :epic, :major, :minor, :patch, :same, :downgrade
31
+ def classify_bump(prev, cur)
32
+ pv = Gem::Version.new(prev)
33
+ cv = Gem::Version.new(cur)
34
+ return :same if cv == pv
35
+ return :downgrade if cv < pv
36
+
37
+ pmaj, pmin, ppatch = (pv.segments + [0, 0, 0])[0, 3]
38
+ cmaj, cmin, cpatch = (cv.segments + [0, 0, 0])[0, 3]
39
+
40
+ if cmaj > pmaj
41
+ return :epic if cmaj && cmaj > 1000
42
+ :major
43
+ elsif cmin > pmin
44
+ :minor
45
+ elsif cpatch > ppatch
46
+ :patch
47
+ else
48
+ # Fallback; should be covered by :same above, but in case of weird segment shapes
49
+ :same
50
+ end
51
+ end
52
+
53
+ # Whether MAJOR is an EPIC version (strictly > 1000)
54
+ # @param major [Integer]
55
+ # @return [Boolean]
56
+ def epic_major?(major)
57
+ major && major > 1000
58
+ end
59
+
60
+ # Abort via ExitAdapter if available; otherwise Kernel.abort
61
+ # @param msg [String]
62
+ # @return [void]
63
+ def abort!(msg)
64
+ Kettle::Dev::ExitAdapter.abort(msg)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ module Kettle
2
+ module Dev
3
+ module InputAdapter
4
+ def self.gets: (*untyped args) -> String?
5
+ def self.readline: (*untyped args) -> String
6
+ end
7
+ end
8
+ end
@@ -44,7 +44,7 @@ module Kettle
44
44
  # Apply common token replacements used when templating text files
45
45
  def self.apply_common_replacements: (
46
46
  String content,
47
- gh_org: String?,
47
+ org: String?,
48
48
  gem_name: String,
49
49
  namespace: String,
50
50
  namespace_shield: String,
@@ -59,6 +59,8 @@ module Kettle
59
59
  min_ruby: String,
60
60
  homepage: String,
61
61
  gh_org: String?,
62
+ forge_org: String?,
63
+ funding_org: String?,
62
64
  gh_repo: String?,
63
65
  namespace: String,
64
66
  namespace_shield: String,
data.tar.gz.sig CHANGED
Binary file