Skip to content
This repository was archived by the owner on Jul 16, 2026. It is now read-only.

Commit a6a937b

Browse files
committed
🔧 Config defaults
1 parent c51dea0 commit a6a937b

5 files changed

Lines changed: 232 additions & 96 deletions

File tree

lib/kettle/jem/setup_cli.rb

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -407,12 +407,7 @@ def ensure_template_config_bootstrap!
407407
end
408408

409409
def seed_bootstrap_template_config(content)
410-
values = bootstrap_template_config_values
411-
values.reduce(content.to_s) do |memo, (token, value)|
412-
next memo if value.to_s.strip.empty?
413-
414-
memo.gsub("{#{token}}", value)
415-
end
410+
Kettle::Jem::TemplateHelpers.seed_kettle_config_content(content, bootstrap_template_config_values)
416411
end
417412

418413
def bootstrap_template_config_values
@@ -422,28 +417,36 @@ def bootstrap_template_config_values
422417
given_names, family_names = split_author_name(author_name)
423418

424419
{
425-
"KJ|AUTHOR:NAME" => author_name,
426-
"KJ|AUTHOR:GIVEN_NAMES" => preferred_bootstrap_env("KJ_AUTHOR_GIVEN_NAMES") || given_names,
427-
"KJ|AUTHOR:FAMILY_NAMES" => preferred_bootstrap_env("KJ_AUTHOR_FAMILY_NAMES") || family_names,
428-
"KJ|AUTHOR:EMAIL" => author_email,
429-
"KJ|AUTHOR:DOMAIN" => author_domain,
430-
"KJ|AUTHOR:ORCID" => preferred_bootstrap_env("KJ_AUTHOR_ORCID"),
431-
"KJ|GH:USER" => preferred_bootstrap_env("KJ_GH_USER"),
432-
"KJ|GL:USER" => preferred_bootstrap_env("KJ_GL_USER"),
433-
"KJ|CB:USER" => preferred_bootstrap_env("KJ_CB_USER"),
434-
"KJ|SH:USER" => preferred_bootstrap_env("KJ_SH_USER"),
435-
"KJ|FUNDING:PATREON" => preferred_bootstrap_env("KJ_FUNDING_PATREON"),
436-
"KJ|FUNDING:KOFI" => preferred_bootstrap_env("KJ_FUNDING_KOFI"),
437-
"KJ|FUNDING:PAYPAL" => preferred_bootstrap_env("KJ_FUNDING_PAYPAL"),
438-
"KJ|FUNDING:BUYMEACOFFEE" => preferred_bootstrap_env("KJ_FUNDING_BUYMEACOFFEE"),
439-
"KJ|FUNDING:POLAR" => preferred_bootstrap_env("KJ_FUNDING_POLAR"),
440-
"KJ|FUNDING:LIBERAPAY" => preferred_bootstrap_env("KJ_FUNDING_LIBERAPAY"),
441-
"KJ|FUNDING:ISSUEHUNT" => preferred_bootstrap_env("KJ_FUNDING_ISSUEHUNT"),
442-
"KJ|SOCIAL:MASTODON" => preferred_bootstrap_env("KJ_SOCIAL_MASTODON"),
443-
"KJ|SOCIAL:BLUESKY" => preferred_bootstrap_env("KJ_SOCIAL_BLUESKY"),
444-
"KJ|SOCIAL:LINKTREE" => preferred_bootstrap_env("KJ_SOCIAL_LINKTREE"),
445-
"KJ|SOCIAL:DEVTO" => preferred_bootstrap_env("KJ_SOCIAL_DEVTO"),
446-
}
420+
"forge" => {
421+
"gh_user" => preferred_bootstrap_env("KJ_GH_USER"),
422+
"gl_user" => preferred_bootstrap_env("KJ_GL_USER"),
423+
"cb_user" => preferred_bootstrap_env("KJ_CB_USER"),
424+
"sh_user" => preferred_bootstrap_env("KJ_SH_USER"),
425+
}.reject { |_, value| value.to_s.strip.empty? },
426+
"author" => {
427+
"name" => author_name,
428+
"given_names" => preferred_bootstrap_env("KJ_AUTHOR_GIVEN_NAMES") || given_names,
429+
"family_names" => preferred_bootstrap_env("KJ_AUTHOR_FAMILY_NAMES") || family_names,
430+
"email" => author_email,
431+
"domain" => author_domain,
432+
"orcid" => preferred_bootstrap_env("KJ_AUTHOR_ORCID"),
433+
}.reject { |_, value| value.to_s.strip.empty? },
434+
"funding" => {
435+
"patreon" => preferred_bootstrap_env("KJ_FUNDING_PATREON"),
436+
"kofi" => preferred_bootstrap_env("KJ_FUNDING_KOFI"),
437+
"paypal" => preferred_bootstrap_env("KJ_FUNDING_PAYPAL"),
438+
"buymeacoffee" => preferred_bootstrap_env("KJ_FUNDING_BUYMEACOFFEE"),
439+
"polar" => preferred_bootstrap_env("KJ_FUNDING_POLAR"),
440+
"liberapay" => preferred_bootstrap_env("KJ_FUNDING_LIBERAPAY"),
441+
"issuehunt" => preferred_bootstrap_env("KJ_FUNDING_ISSUEHUNT"),
442+
}.reject { |_, value| value.to_s.strip.empty? },
443+
"social" => {
444+
"mastodon" => preferred_bootstrap_env("KJ_SOCIAL_MASTODON"),
445+
"bluesky" => preferred_bootstrap_env("KJ_SOCIAL_BLUESKY"),
446+
"linktree" => preferred_bootstrap_env("KJ_SOCIAL_LINKTREE"),
447+
"devto" => preferred_bootstrap_env("KJ_SOCIAL_DEVTO"),
448+
}.reject { |_, value| value.to_s.strip.empty? },
449+
}.reject { |_, values| values.empty? }
447450
end
448451

449452
def preferred_bootstrap_env(key)

lib/kettle/jem/tasks/template_task.rb

Lines changed: 6 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -379,88 +379,26 @@ def prerequisite_validation_available?(options)
379379

380380
def seeded_kettle_config_content(helpers, config_src, token_options)
381381
helpers.configure_tokens!(**token_options, include_config_tokens: false)
382-
helpers.read_template(config_src)
382+
seeded_content = helpers.read_template(config_src)
383+
helpers.seed_kettle_config_content(seeded_content, helpers.derived_token_config_values)
383384
ensure
384385
helpers.clear_tokens!
385386
end
386387

387388
def placeholder_or_blank_scalar?(raw_value)
388-
stripped = raw_value.to_s.strip
389-
return true if stripped.empty?
390-
391-
parsed = begin
392-
YAML.safe_load(stripped, permitted_classes: [], aliases: false)
393-
rescue StandardError
394-
stripped.delete_prefix('"').delete_suffix('"').delete_prefix("'").delete_suffix("'")
395-
end
396-
397-
value = parsed.is_a?(String) ? parsed : parsed.to_s
398-
value.to_s.strip.empty? || Kettle::Jem::TemplateHelpers.token_placeholder?(value)
389+
Kettle::Jem::TemplateHelpers.placeholder_or_blank_kettle_config_scalar?(raw_value)
399390
end
400391

401392
def yaml_scalar_for_backfill(value, current_raw)
402-
stripped = current_raw.to_s.strip
403-
if stripped.start_with?("'") && stripped.end_with?("'")
404-
"'#{value.to_s.gsub("'", "''")}'"
405-
else
406-
value.to_s.dump
407-
end
393+
Kettle::Jem::TemplateHelpers.yaml_scalar_for_kettle_config_backfill(value, current_raw)
408394
end
409395

410396
def backfill_kettle_config_token_lines(content, token_values, helpers:)
411-
in_tokens = false
412-
current_section = nil
413-
changed = false
414-
415-
updated = content.lines.map do |line|
416-
stripped = line.lstrip
417-
indent = line[/\A\s*/].to_s.length
418-
419-
if indent.zero? && stripped.match?(/\Atokens:\s*(?:#.*)?\z/)
420-
in_tokens = true
421-
current_section = nil
422-
next line
423-
elsif indent.zero? && stripped.match?(/\A[\w-]+:\s*(?:#.*)?\z/)
424-
in_tokens = false
425-
current_section = nil
426-
end
427-
428-
next line unless in_tokens
429-
430-
if indent == 2 && (match = stripped.match(/\A([a-z_]+):\s*(?:#.*)?\z/))
431-
current_section = match[1]
432-
next line
433-
end
434-
435-
next line unless indent == 4 && current_section
436-
437-
match = line.match(/\A(\s*)([a-z_]+):(\s*)([^#\n]*?)(\s*(?:#.*)?)?(\n?)\z/)
438-
next line unless match
439-
440-
key = match[2]
441-
desired_value = token_values.dig(current_section, key)
442-
next line unless helpers.present_string?(desired_value)
443-
next line unless placeholder_or_blank_scalar?(match[4])
444-
445-
changed = true
446-
"#{match[1]}#{key}:#{match[3]}#{yaml_scalar_for_backfill(desired_value, match[4])}#{match[5]}#{match[6]}"
447-
end.join
448-
449-
[updated, changed]
397+
helpers.backfill_kettle_config_token_lines(content, token_values)
450398
end
451399

452400
def merge_missing_backfilled_token_values(destination_content, token_values)
453-
source_hash = {"tokens" => token_values}
454-
source_content = YAML.dump(source_hash)
455-
Psych::Merge::SmartMerger.new(
456-
source_content,
457-
destination_content,
458-
preference: :destination,
459-
add_template_only_nodes: true,
460-
).merge
461-
rescue StandardError => e
462-
Kettle::Dev.debug_error(e, __method__)
463-
destination_content
401+
Kettle::Jem::TemplateHelpers.merge_missing_kettle_config_token_values(destination_content, token_values)
464402
end
465403

466404
def backfill_project_kettle_config_tokens!(helpers:, project_root:)

lib/kettle/jem/template_helpers.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,92 @@ def derived_token_config_values
368368
}.reject { |_, values| values.empty? }
369369
end
370370

371+
def seed_kettle_config_content(content, token_values)
372+
return content.to_s if token_values.nil? || token_values.empty?
373+
374+
updated_content, = backfill_kettle_config_token_lines(content.to_s, token_values)
375+
merge_missing_kettle_config_token_values(updated_content, token_values)
376+
end
377+
378+
def placeholder_or_blank_kettle_config_scalar?(raw_value)
379+
stripped = raw_value.to_s.strip
380+
return true if stripped.empty?
381+
382+
parsed = begin
383+
YAML.safe_load(stripped, permitted_classes: [], aliases: false)
384+
rescue StandardError
385+
stripped.delete_prefix('"').delete_suffix('"').delete_prefix("'").delete_suffix("'")
386+
end
387+
388+
value = parsed.is_a?(String) ? parsed : parsed.to_s
389+
value.to_s.strip.empty? || token_placeholder?(value)
390+
end
391+
392+
def yaml_scalar_for_kettle_config_backfill(value, current_raw)
393+
stripped = current_raw.to_s.strip
394+
if stripped.start_with?("'") && stripped.end_with?("'")
395+
"'#{value.to_s.gsub("'", "''")}'"
396+
else
397+
value.to_s.dump
398+
end
399+
end
400+
401+
def backfill_kettle_config_token_lines(content, token_values)
402+
in_tokens = false
403+
current_section = nil
404+
changed = false
405+
406+
updated = content.lines.map do |line|
407+
stripped = line.lstrip
408+
indent = line[/\A\s*/].to_s.length
409+
410+
if indent.zero? && stripped.match?(/\Atokens:\s*(?:#.*)?\z/)
411+
in_tokens = true
412+
current_section = nil
413+
next line
414+
elsif indent.zero? && stripped.match?(/\A[\w-]+:\s*(?:#.*)?\z/)
415+
in_tokens = false
416+
current_section = nil
417+
end
418+
419+
next line unless in_tokens
420+
421+
if indent == 2 && (match = stripped.match(/\A([a-z_]+):\s*(?:#.*)?\z/))
422+
current_section = match[1]
423+
next line
424+
end
425+
426+
next line unless indent == 4 && current_section
427+
428+
match = line.match(/\A(\s*)([a-z_]+):(\s*)([^#\n]*?)(\s*(?:#.*)?)?(\n?)\z/)
429+
next line unless match
430+
431+
key = match[2]
432+
desired_value = token_values.dig(current_section, key)
433+
next line unless present_string?(desired_value)
434+
next line unless placeholder_or_blank_kettle_config_scalar?(match[4])
435+
436+
changed = true
437+
"#{match[1]}#{key}:#{match[3]}#{yaml_scalar_for_kettle_config_backfill(desired_value, match[4])}#{match[5]}#{match[6]}"
438+
end.join
439+
440+
[updated, changed]
441+
end
442+
443+
def merge_missing_kettle_config_token_values(destination_content, token_values)
444+
source_hash = {"tokens" => token_values}
445+
source_content = YAML.dump(source_hash)
446+
Psych::Merge::SmartMerger.new(
447+
source_content,
448+
destination_content,
449+
preference: :destination,
450+
add_template_only_nodes: true,
451+
).merge
452+
rescue StandardError => e
453+
Kettle::Dev.debug_error(e, __method__)
454+
destination_content
455+
end
456+
371457
def safe_gemspec_metadata
372458
gemspec_metadata
373459
rescue StandardError => e

spec/kettle/jem/setup_cli_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,38 @@ def read(file)
195195
end
196196
end
197197

198+
describe "template config bootstrap seeding" do
199+
it "fills env-backed token values into blank .kettle-jem.yml slots" do
200+
cli = described_class.allocate
201+
example_path = File.expand_path("../../../template/.kettle-jem.yml.example", __dir__)
202+
203+
stub_env(
204+
"KJ_GH_USER" => "pboling",
205+
"KJ_AUTHOR_NAME" => "Peter H. Boling",
206+
"KJ_AUTHOR_GIVEN_NAMES" => "Peter H.",
207+
"KJ_AUTHOR_FAMILY_NAMES" => "Boling",
208+
"KJ_AUTHOR_EMAIL" => "floss@glatzo.com",
209+
"KJ_AUTHOR_DOMAIN" => "galtzo.com",
210+
"KJ_AUTHOR_ORCID" => "0009-0008-8519-441X",
211+
"KJ_FUNDING_KOFI" => "pboling",
212+
"KJ_SOCIAL_MASTODON" => "galtzo",
213+
)
214+
215+
seeded = cli.send(:seed_bootstrap_template_config, File.read(example_path))
216+
parsed = YAML.safe_load(seeded, permitted_classes: [], aliases: false)
217+
218+
expect(parsed.dig("tokens", "forge", "gh_user")).to eq("pboling")
219+
expect(parsed.dig("tokens", "author", "name")).to eq("Peter H. Boling")
220+
expect(parsed.dig("tokens", "author", "given_names")).to eq("Peter H.")
221+
expect(parsed.dig("tokens", "author", "family_names")).to eq("Boling")
222+
expect(parsed.dig("tokens", "author", "email")).to eq("floss@glatzo.com")
223+
expect(parsed.dig("tokens", "author", "domain")).to eq("galtzo.com")
224+
expect(parsed.dig("tokens", "author", "orcid")).to eq("0009-0008-8519-441X")
225+
expect(parsed.dig("tokens", "funding", "kofi")).to eq("pboling")
226+
expect(parsed.dig("tokens", "social", "mastodon")).to eq("galtzo")
227+
end
228+
end
229+
198230
describe "#ensure_modular_gemfiles!" do
199231
it "calls ModularGemfiles.sync! and rescues metadata errors (min_ruby=nil)" do
200232
cli = described_class.allocate

spec/kettle/jem/tasks/template_task_spec.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,83 @@
270270

271271
end
272272

273+
describe ".kettle-jem.yml bootstrap seeding" do
274+
it "fills env-backed token values into blank config slots on first write" do
275+
Dir.mktmpdir do |gem_root|
276+
Dir.mktmpdir do |project_root|
277+
template_root = File.join(gem_root, "template")
278+
FileUtils.mkdir_p(template_root)
279+
280+
File.write(File.join(template_root, ".kettle-jem.yml.example"), <<~YAML)
281+
defaults:
282+
preference: template
283+
add_template_only_nodes: true
284+
freeze_token: kettle-jem
285+
286+
tokens:
287+
forge:
288+
gh_user: ""
289+
author:
290+
name: "{KJ|AUTHOR:NAME}"
291+
email: "{KJ|AUTHOR:EMAIL}"
292+
orcid: ""
293+
funding:
294+
kofi: ""
295+
social:
296+
mastodon: ""
297+
files: {}
298+
YAML
299+
300+
stub_env(
301+
"KJ_GH_USER" => "pboling",
302+
"KJ_AUTHOR_NAME" => "Peter H. Boling",
303+
"KJ_AUTHOR_EMAIL" => "floss@glatzo.com",
304+
"KJ_AUTHOR_ORCID" => "0009-0008-8519-441X",
305+
"KJ_FUNDING_KOFI" => "pboling",
306+
"KJ_SOCIAL_MASTODON" => "galtzo",
307+
)
308+
309+
allow(helpers).to receive_messages(
310+
project_root: project_root,
311+
template_root: template_root,
312+
ask: true,
313+
)
314+
315+
result = described_class.send(
316+
:ensure_kettle_config_bootstrap!,
317+
helpers: helpers,
318+
project_root: project_root,
319+
template_root: template_root,
320+
token_options: {
321+
org: "acme",
322+
gem_name: "demo",
323+
namespace: "Demo",
324+
namespace_shield: "Demo",
325+
gem_shield: "demo",
326+
funding_org: "acme",
327+
min_ruby: "3.1",
328+
},
329+
)
330+
331+
expect(result).to eq(:bootstrap_only)
332+
333+
parsed = YAML.safe_load(
334+
File.read(File.join(project_root, ".kettle-jem.yml")),
335+
permitted_classes: [],
336+
aliases: false,
337+
)
338+
339+
expect(parsed.dig("tokens", "forge", "gh_user")).to eq("pboling")
340+
expect(parsed.dig("tokens", "author", "name")).to eq("Peter H. Boling")
341+
expect(parsed.dig("tokens", "author", "email")).to eq("floss@glatzo.com")
342+
expect(parsed.dig("tokens", "author", "orcid")).to eq("0009-0008-8519-441X")
343+
expect(parsed.dig("tokens", "funding", "kofi")).to eq("pboling")
344+
expect(parsed.dig("tokens", "social", "mastodon")).to eq("galtzo")
345+
end
346+
end
347+
end
348+
end
349+
273350
describe ".kettle-jem.yml syncing" do
274351
it "restores template-only trailing instructional comments" do
275352
Dir.mktmpdir do |gem_root|

0 commit comments

Comments
 (0)