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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.envrc +1 -1
- data/.github/workflows/coverage.yml +2 -2
- data/.github/workflows/coverage.yml.example +127 -0
- data/.github/workflows/discord-notifier.yml +2 -1
- data/.github/workflows/truffle.yml +0 -8
- data/Appraisals +3 -1
- data/Appraisals.example +102 -0
- data/CHANGELOG.md +63 -29
- data/CHANGELOG.md.example +4 -4
- data/CONTRIBUTING.md +37 -1
- data/Gemfile +3 -0
- data/README.md +47 -9
- data/README.md.example +515 -0
- data/{Rakefile → Rakefile.example} +13 -27
- data/exe/kettle-changelog +401 -0
- data/exe/kettle-commit-msg +2 -0
- data/exe/kettle-readme-backers +2 -0
- data/exe/kettle-release +2 -7
- data/gemfiles/modular/optional.gemfile +5 -0
- data/lib/kettle/dev/git_adapter.rb +98 -33
- data/lib/kettle/dev/git_commit_footer.rb +1 -1
- data/lib/kettle/dev/input_adapter.rb +40 -0
- data/lib/kettle/dev/release_cli.rb +24 -22
- data/lib/kettle/dev/tasks/ci_task.rb +4 -1
- data/lib/kettle/dev/tasks/install_task.rb +313 -95
- data/lib/kettle/dev/tasks/template_task.rb +175 -73
- data/lib/kettle/dev/template_helpers.rb +61 -8
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev/versioning.rb +68 -0
- data/sig/kettle/dev/input_adapter.rbs +8 -0
- data/sig/kettle/dev/template_helpers.rbs +3 -1
- data.tar.gz.sig +0 -0
- metadata +21 -22
- metadata.gz.sig +0 -0
- data/.gitlab-ci.yml +0 -45
@@ -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
|
-
|
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|
|
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
|
-
|
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
|
-
|
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
|
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 =
|
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
|
-
|
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 =
|
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
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
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
|
-
|
234
|
-
indices
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
319
|
+
total_lines - 1
|
243
320
|
end
|
244
321
|
|
245
|
-
|
246
|
-
|
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].
|
253
|
-
|
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
|
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
|
-
|
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 =
|
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
|
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
|
-
|
293
|
-
if
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
#
|
298
|
-
|
299
|
-
|
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
|
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
|
-
|
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
|
-
|
419
|
-
|
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 =
|
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
|
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,
|
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",
|
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
|
-
|
283
|
+
forge_org = gh_match && gh_match[1]
|
252
284
|
gh_repo = gh_match && gh_match[2]&.sub(/\.git\z/, "")
|
253
|
-
if
|
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
|
-
|
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:
|
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,
|
data/lib/kettle/dev/version.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
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
|