kettle-jem 7.1.39 → 7.1.41
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/CHANGELOG.md +117 -1
- data/README.md +43 -44
- data/lib/kettle/jem/templates/.duel.rb.example +25 -0
- data/lib/kettle/jem/templates/.github/FUNDING.yml.example +0 -1
- data/lib/kettle/jem/templates/.github/FUNDING.yml.no-osc.example +0 -1
- data/lib/kettle/jem/templates/.github/workflows/auto-assign.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/coverage.yml.example +1 -0
- data/lib/kettle/jem/templates/.github/workflows/perf.yml.example +186 -0
- data/lib/kettle/jem/templates/.github/workflows/truffleruby-22.3.yml.example +3 -19
- data/lib/kettle/jem/templates/.structuredmerge/kettle-jem.yml.example +33 -2
- data/lib/kettle/jem/templates/AGENTS.md.example +1 -0
- data/lib/kettle/jem/templates/CHANGELOG.transfer.md +6 -1
- data/lib/kettle/jem/templates/CONTRIBUTING.md.example +1 -0
- data/lib/kettle/jem/templates/README.md.example +17 -40
- data/lib/kettle/jem/templates/Rakefile.example +1 -0
- data/lib/kettle/jem/templates/gem.gemspec.example +10 -6
- data/lib/kettle/jem/templates/gemfiles/modular/cgi/vHEAD.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/coverage.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/debug.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/documentation.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/duel.gemfile.example +18 -0
- data/lib/kettle/jem/templates/gemfiles/modular/optional.gemfile.example +2 -2
- data/lib/kettle/jem/templates/gemfiles/modular/recording/r3/recording.gemfile.example +2 -2
- data/lib/kettle/jem/templates/gemfiles/modular/recording/r4/recording.gemfile.example +2 -2
- data/lib/kettle/jem/templates/gemfiles/modular/stringio/r2/v3.0.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/stringio/r3/v3.0.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/stringio/r4/v3.0.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/style.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/templating.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/webrick/vHEAD.gemfile.example +1 -1
- data/lib/kettle/jem/templates/mise.toml.example +1 -0
- data/lib/kettle/jem/templates/mise.toml.no-osc.example +1 -0
- data/lib/kettle/jem/templates/shim/Gemfile.example +1 -1
- data/lib/kettle/jem/templates/shim/gem.gemspec.example +2 -2
- data/lib/kettle/jem/version.rb +1 -1
- data/lib/kettle/jem.rb +527 -140
- data.tar.gz.sig +0 -0
- metadata +40 -16
- metadata.gz.sig +0 -0
data/lib/kettle/jem.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "find"
|
|
5
5
|
require "English"
|
|
6
6
|
require "digest"
|
|
7
|
+
require "date"
|
|
7
8
|
require "json"
|
|
8
9
|
require "net/http"
|
|
9
10
|
require "open3"
|
|
@@ -25,6 +26,7 @@ require "kettle/dev"
|
|
|
25
26
|
require "kettle/changelog"
|
|
26
27
|
require "kettle/rb/compat_matrix"
|
|
27
28
|
require "kettle/rb/gem_floors"
|
|
29
|
+
require "kettle/rb/ruby_series_buckets"
|
|
28
30
|
require "kettle/rb/platform_support"
|
|
29
31
|
require_relative "jem/version"
|
|
30
32
|
require_relative "jem/license_txt_migrator"
|
|
@@ -44,7 +46,7 @@ module Kettle
|
|
|
44
46
|
PACKAGE_NAME = "kettle-jem"
|
|
45
47
|
CONTENT_RECIPE_TRANSPORT_VERSION = Ast::Merge::STRUCTURED_EDIT_TRANSPORT_VERSION
|
|
46
48
|
TEMPLATE_SOURCE_APPLICATION_FINGERPRINT_VERSION = 1
|
|
47
|
-
APPRAISALS_TEMPLATE_POLICY_FINGERPRINT_VERSION =
|
|
49
|
+
APPRAISALS_TEMPLATE_POLICY_FINGERPRINT_VERSION = 3
|
|
48
50
|
GEMSPEC_TEMPLATE_POLICY_FINGERPRINT_VERSION = 2
|
|
49
51
|
SPEC_HELPER_TEMPLATE_POLICY_FINGERPRINT_VERSION = 1
|
|
50
52
|
VERSION_NAMESPACE_TEMPLATE_POLICY_FINGERPRINT_VERSION = 2
|
|
@@ -62,11 +64,11 @@ module Kettle
|
|
|
62
64
|
# set in place before Bundler is allowed to evaluate that destination.
|
|
63
65
|
TEMPLATE_MANAGED_DEPENDENCIES = [
|
|
64
66
|
{name: PACKAGE_NAME, requirements: ["~> 7.1", ">= 7.1.28"], bootstrap: false},
|
|
65
|
-
{name: "nomono", requirements: ["~> 1.1", ">= 1.1.
|
|
66
|
-
{name: "kettle-dev", requirements: ["~> 3.1", ">= 3.1.
|
|
67
|
+
{name: "nomono", requirements: ["~> 1.1", ">= 1.1.17"], bootstrap: true},
|
|
68
|
+
{name: "kettle-dev", requirements: ["~> 3.1", ">= 3.1.26"], bootstrap: true},
|
|
67
69
|
{
|
|
68
70
|
name: "kettle-changelog",
|
|
69
|
-
requirements: ["~> 1.0", ">= 1.0.
|
|
71
|
+
requirements: ["~> 1.0", ">= 1.0.15"],
|
|
70
72
|
bootstrap: true,
|
|
71
73
|
ruby_requirement: ">= 4.0.0"
|
|
72
74
|
}
|
|
@@ -88,6 +90,12 @@ module Kettle
|
|
|
88
90
|
].freeze
|
|
89
91
|
OPENCOLLECTIVE_DISABLED_FILES = %w[.opencollective.yml .github/workflows/opencollective.yml].freeze
|
|
90
92
|
OPT_IN_GITHUB_WORKFLOWS = %w[.github/workflows/discord-notifier.yml].freeze
|
|
93
|
+
# Templates that exist only for projects that set `workflows.duel: true`.
|
|
94
|
+
# While Duel is off, kettle-jem neither writes nor removes these paths, so a
|
|
95
|
+
# project's own files at them stay exactly as they are.
|
|
96
|
+
DUEL_TEMPLATE_PATHS = %w[.duel.rb .github/workflows/perf.yml gemfiles/modular/duel.gemfile].freeze
|
|
97
|
+
DUEL_MAIN_GEMFILE_EVAL_PATH = "gemfiles/modular/duel.gemfile"
|
|
98
|
+
DUEL_APPRAISAL_NAME = "perf"
|
|
91
99
|
OBSOLETE_APPRAISAL_SPEC_EXEC_CMDS = [
|
|
92
100
|
"env KETTLE_TEST_RUNNER=rspec kettle-test -I ../spec --options ../.rspec ../spec",
|
|
93
101
|
"KETTLE_TEST_RUNNER=rspec kettle-test -I ../spec --options ../.rspec ../spec",
|
|
@@ -228,9 +236,10 @@ module Kettle
|
|
|
228
236
|
"feature.dedicated_version_gem" => :boolean,
|
|
229
237
|
"workflow.dep_heads" => :boolean,
|
|
230
238
|
"workflow.jruby_94" => :boolean,
|
|
239
|
+
"workflow.duel" => :boolean,
|
|
231
240
|
"version_gem.mode" => :enum
|
|
232
241
|
}.freeze
|
|
233
|
-
COPY_ONLY_WHEN_MISSING_TEMPLATE_PATHS = %w[REEK bin/setup].freeze
|
|
242
|
+
COPY_ONLY_WHEN_MISSING_TEMPLATE_PATHS = %w[REEK bin/setup .duel.rb].freeze
|
|
234
243
|
MONOREPO_ROOT_TEMPLATE_PROFILE = "monorepo-root"
|
|
235
244
|
MONOREPO_SUBGEM_PACKAGE_TEMPLATE_PROFILE = "monorepo-subgem-package"
|
|
236
245
|
MONOREPO_SUBGEM_RELEASE_TEMPLATE_PROFILE = "monorepo-subgem-release"
|
|
@@ -574,14 +583,18 @@ module Kettle
|
|
|
574
583
|
KJ|MIN_RUBY
|
|
575
584
|
KJ|KETTLE_CHANGELOG_GEMFILE_DEPENDENCY
|
|
576
585
|
KJ|OPENCOLLECTIVE_ORG
|
|
586
|
+
KJ|RAKE:DUEL_TASK
|
|
577
587
|
KJ|RAKE:TOP_LEVEL_DOCS_TASK
|
|
578
588
|
KJ|README:COPYRIGHT_NOTICE
|
|
579
589
|
KJ|README:CORPORATE_SPONSORS
|
|
590
|
+
KJ|README:CORPORATE_SPONSORS_DETAIL
|
|
580
591
|
KJ|README:FAMILY_INTRO_BACKEND_MATRIX
|
|
581
592
|
KJ|README:LICENSE_BADGE
|
|
582
593
|
KJ|README:LICENSE_COMPAT_BADGE
|
|
583
594
|
KJ|README:LICENSE_EYE_WORKFLOW_BADGE
|
|
584
595
|
KJ|README:DEV_TEST_STACK_TABLE
|
|
596
|
+
KJ|README:DUEL_BADGE
|
|
597
|
+
KJ|README:DUEL_REFS
|
|
585
598
|
KJ|README:FOSSA_BADGE
|
|
586
599
|
KJ|README:FOSSA_REFS
|
|
587
600
|
KJ|README:H2_SYNOPSIS_LOGO_ROW
|
|
@@ -1493,6 +1506,7 @@ module Kettle
|
|
|
1493
1506
|
README_LICENSE_EYE_WORKFLOW_LINK_LABELS = ["🚎15-🪪-wf", "🚎15-🪪-wfi"].freeze
|
|
1494
1507
|
README_FOSSA_BADGE = "[![FOSSA Status][🧪fossa-img]][🧪fossa]"
|
|
1495
1508
|
README_FOSSA_LINK_LABELS = ["🧪fossa", "🧪fossa-img"].freeze
|
|
1509
|
+
README_DUEL_BADGE = "[![Duel][⚔️duel-img]][⚔️duel]"
|
|
1496
1510
|
LICENSE_EYE_COMPATIBILITY_LICENSES = %w[MIT].freeze
|
|
1497
1511
|
SKYWALKING_EYES_INTEGRATION = "skywalking-eyes"
|
|
1498
1512
|
README_OPEN_COLLECTIVE_FUNDING_BADGES = "[![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors]"
|
|
@@ -1519,9 +1533,9 @@ module Kettle
|
|
|
1519
1533
|
patreon
|
|
1520
1534
|
polar
|
|
1521
1535
|
thanks_dev
|
|
1522
|
-
tidelift
|
|
1523
1536
|
].freeze
|
|
1524
|
-
|
|
1537
|
+
# Tidelift remains supported for legacy funding configuration and cleanup.
|
|
1538
|
+
FUNDING_README_PLATFORMS = (FUNDING_YML_PLATFORMS + %w[paypal tidelift]).freeze
|
|
1525
1539
|
FUNDING_DEFAULT_DISABLED_PLATFORMS = %w[issuehunt patreon polar].freeze
|
|
1526
1540
|
README_FUNDING_BADGE_POLICIES = {
|
|
1527
1541
|
"github" => {
|
|
@@ -1559,9 +1573,7 @@ module Kettle
|
|
|
1559
1573
|
labels: ["🖇kofi", "🖇kofi-img"]
|
|
1560
1574
|
},
|
|
1561
1575
|
"tidelift" => {
|
|
1562
|
-
badges: [
|
|
1563
|
-
"[![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift]"
|
|
1564
|
-
],
|
|
1576
|
+
badges: ["[![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift]"],
|
|
1565
1577
|
labels: ["🏙️entsup-tidelift", "🏙️entsup-tidelift-img"]
|
|
1566
1578
|
}
|
|
1567
1579
|
}.freeze
|
|
@@ -3201,6 +3213,7 @@ module Kettle
|
|
|
3201
3213
|
custom_workflows: github_actions_custom_workflows(project_root, template_config, opencollective_disabled: opencollective_disabled)
|
|
3202
3214
|
}
|
|
3203
3215
|
standard_appraisal_gemfiles = github_actions_standard_appraisal_gemfiles(kettle_config)
|
|
3216
|
+
facts[:ci][:duel] = true if template_config["resolved_duel"]
|
|
3204
3217
|
facts[:ci][:standard_appraisal_gemfiles] = standard_appraisal_gemfiles unless standard_appraisal_gemfiles.empty?
|
|
3205
3218
|
facts[:ci][:opt_in_workflow_cleanups] = opt_in_workflows unless opt_in_workflows.empty?
|
|
3206
3219
|
facts[:ci][:inactive_packaged_workflow_cleanups] = inactive_workflows unless inactive_workflows.empty?
|
|
@@ -3289,7 +3302,13 @@ module Kettle
|
|
|
3289
3302
|
"yaml",
|
|
3290
3303
|
"supplied_github_funding_yaml_synchronization",
|
|
3291
3304
|
facts: %w[package funding]
|
|
3292
|
-
)
|
|
3305
|
+
).tap do |recipe|
|
|
3306
|
+
# Cold-start only: on a bootstrap run (no config yet, hence no
|
|
3307
|
+
# template source preferences) this recipe materializes a first
|
|
3308
|
+
# FUNDING.yml. Configured runs get FUNDING.yml from
|
|
3309
|
+
# template_source_application instead.
|
|
3310
|
+
recipe[:seed_if_empty] = true if facts[:kettle_config_bootstrap]
|
|
3311
|
+
end
|
|
3293
3312
|
]
|
|
3294
3313
|
end
|
|
3295
3314
|
if facts[:kettle_config_bootstrap]
|
|
@@ -3627,6 +3646,13 @@ module Kettle
|
|
|
3627
3646
|
changed_files = changed_files_from_recipe_reports(recipe_reports)
|
|
3628
3647
|
file_outcomes = template_file_outcomes(recipe_reports)
|
|
3629
3648
|
diagnostics = recipe_reports.flat_map { |report| report[:diagnostics] }
|
|
3649
|
+
legacy_config_migration = legacy_kettle_config_migration(project_root)
|
|
3650
|
+
if legacy_config_migration
|
|
3651
|
+
diagnostics << {
|
|
3652
|
+
severity: "advisory",
|
|
3653
|
+
message: "kettle:jem legacy config #{LEGACY_KETTLE_CONFIG_PATH} will be moved to #{KETTLE_CONFIG_PATH} on apply"
|
|
3654
|
+
}
|
|
3655
|
+
end
|
|
3630
3656
|
phase_reports = phase_reports_for(recipe_reports)
|
|
3631
3657
|
decision_evaluations = recipe_reports.map { |report| report.fetch(:decision_evaluation) }
|
|
3632
3658
|
prompt_requests = decision_evaluations.filter_map { |decision| decision[:prompt] if decision[:prompt_required] }
|
|
@@ -3670,6 +3696,7 @@ module Kettle
|
|
|
3670
3696
|
}
|
|
3671
3697
|
end
|
|
3672
3698
|
emit_summary_event(events, report)
|
|
3699
|
+
report[:legacy_config_migration] = legacy_config_migration.merge(applied: false) if legacy_config_migration
|
|
3673
3700
|
report
|
|
3674
3701
|
end
|
|
3675
3702
|
|
|
@@ -4300,8 +4327,10 @@ module Kettle
|
|
|
4300
4327
|
end
|
|
4301
4328
|
|
|
4302
4329
|
def apply_project(project_root, env: ENV, run_options: {})
|
|
4330
|
+
legacy_config_migration = apply_legacy_kettle_config_migration!(project_root)
|
|
4303
4331
|
events = event_stream_from_options(run_options)
|
|
4304
4332
|
report = plan_project(project_root, env: env, run_options: run_options).merge(mode: "apply")
|
|
4333
|
+
report[:legacy_config_migration] = legacy_config_migration if legacy_config_migration
|
|
4305
4334
|
file_work_workers = with_event_phase(events, "file_work_workers") { file_work_workers_for(env, run_options) }
|
|
4306
4335
|
file_work_thread_workers = with_event_phase(events, "file_work_thread_workers") { file_work_thread_workers_for(env, run_options) }
|
|
4307
4336
|
report[:file_work_workers] = file_work_workers
|
|
@@ -4333,6 +4362,9 @@ module Kettle
|
|
|
4333
4362
|
report[:changed_files] = (report.fetch(:changed_files, []) + report.fetch(:post_apply_steps).flat_map do |step|
|
|
4334
4363
|
reported_post_apply_changed_files(step)
|
|
4335
4364
|
end).uniq.sort
|
|
4365
|
+
if legacy_config_migration
|
|
4366
|
+
report[:changed_files] = (report.fetch(:changed_files, []) + [LEGACY_KETTLE_CONFIG_PATH, KETTLE_CONFIG_PATH]).uniq.sort
|
|
4367
|
+
end
|
|
4336
4368
|
report[:duplicate_drift] = if DecisionPolicy.value_to_boolean((run_options || {})[:defer_drift_check])
|
|
4337
4369
|
{available: false, skipped: true, reason: "deferred_until_postprocessing"}
|
|
4338
4370
|
else
|
|
@@ -5120,11 +5152,11 @@ module Kettle
|
|
|
5120
5152
|
[
|
|
5121
5153
|
{name: "appraisal2", source: %(gem "appraisal2", "~> 3.2", ">= 3.2.6"\n)},
|
|
5122
5154
|
{name: "bundler-audit", source: %(gem "bundler-audit", "~> 0.9.3"\n)},
|
|
5123
|
-
{name: "kettle-dev", source: %(gem "kettle-dev", "~> 3.1", ">= 3.1.
|
|
5124
|
-
{name: "kettle-drift", source: %(gem "kettle-drift", "~> 1.0", ">= 1.0.
|
|
5125
|
-
{name: "kettle-family", source: %(gem "kettle-family", "~> 1.3", ">= 1.3.
|
|
5155
|
+
{name: "kettle-dev", source: %(gem "kettle-dev", "~> 3.1", ">= 3.1.26"\n)},
|
|
5156
|
+
{name: "kettle-drift", source: %(gem "kettle-drift", "~> 1.0", ">= 1.0.19"\n)},
|
|
5157
|
+
{name: "kettle-family", source: %(gem "kettle-family", "~> 1.3", ">= 1.3.36"\n)},
|
|
5126
5158
|
{name: PACKAGE_NAME, source: kettle_jem_dependency_source},
|
|
5127
|
-
{name: "kettle-test", source: %(gem "kettle-test", "~> 2.0", ">= 2.0.
|
|
5159
|
+
{name: "kettle-test", source: %(gem "kettle-test", "~> 2.0", ">= 2.0.29"\n)},
|
|
5128
5160
|
{name: "rake", source: %(gem "rake", "~> 13.0"\n)},
|
|
5129
5161
|
{name: "rspec", source: %(gem "rspec", "~> 3.0"\n)},
|
|
5130
5162
|
{name: "stone_checksums", source: %(gem "stone_checksums", "~> 1.0", ">= 1.0.9"\n)},
|
|
@@ -5638,6 +5670,7 @@ module Kettle
|
|
|
5638
5670
|
"KJ|PROJECT_EMOJI" => "💎",
|
|
5639
5671
|
"KJ|README:COPYRIGHT_NOTICE" => "",
|
|
5640
5672
|
"KJ|README:CORPORATE_SPONSORS" => "",
|
|
5673
|
+
"KJ|README:CORPORATE_SPONSORS_DETAIL" => "",
|
|
5641
5674
|
"KJ|README:LICENSE_BADGE" => "",
|
|
5642
5675
|
"KJ|README:LICENSE_COMPAT_BADGE" => "",
|
|
5643
5676
|
"KJ|README:DEV_TEST_STACK_TABLE" => "",
|
|
@@ -6025,6 +6058,7 @@ module Kettle
|
|
|
6025
6058
|
"feature.dedicated_version_gem" => version_gem_mode == "dedicated" || File.file?(File.join(project_root, "lib", entrypoint, "version_gem.rb")),
|
|
6026
6059
|
"workflow.dep_heads" => File.file?(File.join(project_root, ".github", "workflows", "dep-heads.yml")),
|
|
6027
6060
|
"workflow.jruby_94" => File.file?(File.join(project_root, ".github", "workflows", "jruby-9.4.yml")),
|
|
6061
|
+
"workflow.duel" => duel_configured?(config) && duel_template_profile?(profile),
|
|
6028
6062
|
"version_gem.mode" => version_gem_mode
|
|
6029
6063
|
}
|
|
6030
6064
|
end
|
|
@@ -6419,7 +6453,7 @@ module Kettle
|
|
|
6419
6453
|
when "generated_block_sync"
|
|
6420
6454
|
synchronize_managed_block(original, facts)
|
|
6421
6455
|
when "github_funding_yml"
|
|
6422
|
-
synchronize_github_funding_yml(original, facts)
|
|
6456
|
+
synchronize_github_funding_yml(original, facts, seed_if_empty: recipe[:seed_if_empty])
|
|
6423
6457
|
when "github_actions_ci"
|
|
6424
6458
|
synchronize_github_actions_ci(original, facts)
|
|
6425
6459
|
when "github_actions_framework_ci"
|
|
@@ -6827,7 +6861,7 @@ module Kettle
|
|
|
6827
6861
|
|
|
6828
6862
|
content = recipe_template_content(project_root, recipe, template_contents: template_contents)
|
|
6829
6863
|
return finalize_github_workflow_template(content, facts) if strategy == "raw_copy" && github_workflow_template_recipe?(recipe)
|
|
6830
|
-
return finalize_template_source_content(recipe, content) if strategy == "raw_copy"
|
|
6864
|
+
return finalize_template_source_content(recipe, content, facts: facts) if strategy == "raw_copy"
|
|
6831
6865
|
|
|
6832
6866
|
resolved = recipe.fetch(:resolved_template_content) do
|
|
6833
6867
|
resolve_template_tokens(
|
|
@@ -6882,7 +6916,7 @@ module Kettle
|
|
|
6882
6916
|
return preserve_newer_github_workflow_action_pins(merged, original)
|
|
6883
6917
|
end
|
|
6884
6918
|
|
|
6885
|
-
return finalize_template_source_content(recipe, merged)
|
|
6919
|
+
return finalize_template_source_content(recipe, merged, facts: facts)
|
|
6886
6920
|
end
|
|
6887
6921
|
if strategy == "accept_template"
|
|
6888
6922
|
accepted = finalize_accepted_template_source(recipe, resolved, original, facts: facts, project_root: project_root)
|
|
@@ -6902,7 +6936,7 @@ module Kettle
|
|
|
6902
6936
|
)
|
|
6903
6937
|
end
|
|
6904
6938
|
|
|
6905
|
-
return finalize_template_source_content(recipe, accepted)
|
|
6939
|
+
return finalize_template_source_content(recipe, accepted, facts: facts)
|
|
6906
6940
|
end
|
|
6907
6941
|
|
|
6908
6942
|
resolved = finalize_github_workflow_template(resolved, facts) if github_workflow_template_recipe?(recipe)
|
|
@@ -6916,11 +6950,11 @@ module Kettle
|
|
|
6916
6950
|
)
|
|
6917
6951
|
end
|
|
6918
6952
|
|
|
6919
|
-
finalize_template_source_content(recipe, resolved)
|
|
6953
|
+
finalize_template_source_content(recipe, resolved, facts: facts)
|
|
6920
6954
|
end
|
|
6921
6955
|
|
|
6922
|
-
def finalize_template_source_content(recipe, content)
|
|
6923
|
-
return finalize_rubocop_config(content) if recipe.fetch(:target_path).to_s == ".rubocop.yml"
|
|
6956
|
+
def finalize_template_source_content(recipe, content, facts: nil)
|
|
6957
|
+
return finalize_rubocop_config(content, facts) if recipe.fetch(:target_path).to_s == ".rubocop.yml"
|
|
6924
6958
|
return finalize_kettle_config_template_source(recipe, content) if recipe.fetch(:target_path).to_s == KETTLE_CONFIG_PATH
|
|
6925
6959
|
return normalize_simplecov_template_source(content) if recipe.fetch(:target_path).to_s == ".simplecov"
|
|
6926
6960
|
return normalize_spec_helper_simplecov_template_source(content) if recipe.fetch(:target_path).to_s == "spec/spec_helper.rb"
|
|
@@ -6934,9 +6968,77 @@ module Kettle
|
|
|
6934
6968
|
assert_no_unresolved_template_tokens_in_yaml_values(resolved, KETTLE_CONFIG_PATH)
|
|
6935
6969
|
end
|
|
6936
6970
|
|
|
6937
|
-
def finalize_rubocop_config(content)
|
|
6971
|
+
def finalize_rubocop_config(content, facts = nil)
|
|
6938
6972
|
finalized = remove_yaml_scalar_path(content, %w[AllCops TargetRubyVersion])
|
|
6939
|
-
ensure_yaml_top_level_sequence_items(finalized, "plugins", ["rubocop-rspec"])
|
|
6973
|
+
finalized = ensure_yaml_top_level_sequence_items(finalized, "plugins", ["rubocop-rspec"])
|
|
6974
|
+
return finalized unless facts.to_h.dig(:ci, :duel)
|
|
6975
|
+
|
|
6976
|
+
ensure_rubocop_duel_config(finalized)
|
|
6977
|
+
end
|
|
6978
|
+
|
|
6979
|
+
# Duel's configuration is a dotfile, which RuboCop skips unless it is
|
|
6980
|
+
# included. Merging Include, as Exclude is, keeps RuboCop's own patterns.
|
|
6981
|
+
def ensure_rubocop_duel_config(content)
|
|
6982
|
+
merged = ensure_yaml_child_sequence_items(
|
|
6983
|
+
content,
|
|
6984
|
+
"inherit_mode",
|
|
6985
|
+
"merge",
|
|
6986
|
+
["Include"],
|
|
6987
|
+
comment: "# Merge Include too, so .duel.rb (a dotfile) is added to the defaults"
|
|
6988
|
+
)
|
|
6989
|
+
ensure_yaml_child_sequence_items(
|
|
6990
|
+
merged,
|
|
6991
|
+
"AllCops",
|
|
6992
|
+
"Include",
|
|
6993
|
+
[".duel.rb"],
|
|
6994
|
+
comment: "# Duel's configuration is a dotfile, which RuboCop skips unless included"
|
|
6995
|
+
)
|
|
6996
|
+
end
|
|
6997
|
+
|
|
6998
|
+
# Ensures that the block sequence at parent_key.key holds items, adding
|
|
6999
|
+
# the parent mapping or the key when missing. The comment goes above the
|
|
7000
|
+
# added key, or above the added items when the key exists. A flow
|
|
7001
|
+
# sequence or mapping is left alone.
|
|
7002
|
+
def ensure_yaml_child_sequence_items(content, parent_key, key, items, comment:)
|
|
7003
|
+
document = Psych.parse_stream(content.to_s).children.first
|
|
7004
|
+
root = document&.root
|
|
7005
|
+
return content unless root.is_a?(Psych::Nodes::Mapping)
|
|
7006
|
+
|
|
7007
|
+
lines = content.to_s.lines
|
|
7008
|
+
parent_key_node, parent_node = root.children.each_slice(2).find do |candidate_key, _candidate_value|
|
|
7009
|
+
candidate_key.is_a?(Psych::Nodes::Scalar) && candidate_key.value.to_s == parent_key.to_s
|
|
7010
|
+
end
|
|
7011
|
+
unless parent_key_node
|
|
7012
|
+
lines << "\n" unless lines.empty? || lines.last.strip.empty?
|
|
7013
|
+
lines << "#{parent_key}:\n" << " #{comment}\n" << " #{key}:\n"
|
|
7014
|
+
items.each { |item| lines << " - #{item}\n" }
|
|
7015
|
+
return lines.join
|
|
7016
|
+
end
|
|
7017
|
+
return content unless parent_node.is_a?(Psych::Nodes::Mapping) && parent_node.style == Psych::Nodes::Mapping::BLOCK
|
|
7018
|
+
return content if parent_node.children.empty?
|
|
7019
|
+
|
|
7020
|
+
key_node, value_node = parent_node.children.each_slice(2).find do |candidate_key, _candidate_value|
|
|
7021
|
+
candidate_key.is_a?(Psych::Nodes::Scalar) && candidate_key.value.to_s == key.to_s
|
|
7022
|
+
end
|
|
7023
|
+
unless key_node
|
|
7024
|
+
indent = lines.fetch(parent_node.children.first.start_line)[/\A\s*/]
|
|
7025
|
+
added = ["#{indent}#{comment}\n", "#{indent}#{key}:\n", *items.map { |item| "#{indent} - #{item}\n" }]
|
|
7026
|
+
lines.insert(parent_key_node.start_line + 1, *added)
|
|
7027
|
+
return lines.join
|
|
7028
|
+
end
|
|
7029
|
+
return content unless value_node.is_a?(Psych::Nodes::Sequence) && value_node.style == Psych::Nodes::Sequence::BLOCK
|
|
7030
|
+
return content if value_node.children.empty?
|
|
7031
|
+
|
|
7032
|
+
existing = value_node.children.filter_map { |child| child.value.to_s if child.is_a?(Psych::Nodes::Scalar) }
|
|
7033
|
+
missing = items.map(&:to_s) - existing
|
|
7034
|
+
return content if missing.empty?
|
|
7035
|
+
|
|
7036
|
+
last = value_node.children.last
|
|
7037
|
+
indent = lines.fetch(last.start_line)[/\A\s*/]
|
|
7038
|
+
lines.insert(last.end_line + 1, "#{indent}#{comment}\n", *missing.map { |item| "#{indent}- #{item}\n" })
|
|
7039
|
+
lines.join
|
|
7040
|
+
rescue Psych::Exception
|
|
7041
|
+
content
|
|
6940
7042
|
end
|
|
6941
7043
|
|
|
6942
7044
|
def finalize_accepted_template_source(recipe, content, destination_content, facts:, project_root: nil)
|
|
@@ -6944,7 +7046,7 @@ module Kettle
|
|
|
6944
7046
|
when :gemfile
|
|
6945
7047
|
finalize_gemfile_template_source(recipe, content, destination_content, facts: facts, template_content: content)
|
|
6946
7048
|
when :appraisals
|
|
6947
|
-
merged = merge_appraisals_template_policy(content, facts: facts)
|
|
7049
|
+
merged = merge_appraisals_template_policy(content, facts: facts, target_path: recipe.fetch(:target_path))
|
|
6948
7050
|
if recipe.fetch(:target_path).to_s == "Appraisal.root.gemfile"
|
|
6949
7051
|
normalize_appraisal_root_templating_gate(merged, facts)
|
|
6950
7052
|
else
|
|
@@ -7236,6 +7338,7 @@ module Kettle
|
|
|
7236
7338
|
"FUNDING_TIDELIFT",
|
|
7237
7339
|
keep: funding_platform_enabled?(facts.dig(:funding, :platforms), "tidelift")
|
|
7238
7340
|
)
|
|
7341
|
+
processed = processed.gsub(/<!-- KJ:FUNDING_TIDELIFT:(?:START|END) -->\n?/, "")
|
|
7239
7342
|
star_history_enabled = facts.dig(:repository, :star_history, :enabled) == true
|
|
7240
7343
|
return processed if star_history_enabled
|
|
7241
7344
|
|
|
@@ -7252,6 +7355,12 @@ module Kettle
|
|
|
7252
7355
|
open_collective_enabled = !facts.dig(:funding, :open_collective_disabled)
|
|
7253
7356
|
processed = apply_markdown_conditional_block(content, "OPEN_COLLECTIVE", keep: open_collective_enabled)
|
|
7254
7357
|
processed = apply_markdown_conditional_block(processed, "NO_OPEN_COLLECTIVE", keep: !open_collective_enabled)
|
|
7358
|
+
processed = apply_markdown_conditional_block(
|
|
7359
|
+
processed,
|
|
7360
|
+
"FUNDING_TIDELIFT",
|
|
7361
|
+
keep: funding_platform_enabled?(facts.dig(:funding, :platforms), "tidelift")
|
|
7362
|
+
)
|
|
7363
|
+
processed = processed.gsub(/<!-- KJ:FUNDING_TIDELIFT:(?:START|END) -->\n?/, "")
|
|
7255
7364
|
funding_platforms = facts.dig(:funding, :platforms)
|
|
7256
7365
|
if facts.dig(:funding, :open_collective_disabled)
|
|
7257
7366
|
processed = remove_readme_badge_and_refs(
|
|
@@ -7624,7 +7733,7 @@ module Kettle
|
|
|
7624
7733
|
if file_type == :gemfile
|
|
7625
7734
|
return finalize_gemfile_template_source(recipe, template_content, destination_content, facts: facts, template_content: template_content)
|
|
7626
7735
|
end
|
|
7627
|
-
return merge_appraisals_template_policy(template_content, facts: facts) if file_type == :appraisals
|
|
7736
|
+
return merge_appraisals_template_policy(template_content, facts: facts, target_path: recipe.fetch(:target_path)) if file_type == :appraisals
|
|
7628
7737
|
return finalize_github_workflow_template(prune_github_workflow_matrix_by_min_ruby(template_content, facts), facts) if github_workflow_template_recipe?(recipe)
|
|
7629
7738
|
|
|
7630
7739
|
return template_content
|
|
@@ -7633,7 +7742,7 @@ module Kettle
|
|
|
7633
7742
|
if file_type == :gemfile
|
|
7634
7743
|
return finalize_gemfile_template_source(recipe, destination_content, destination_content, facts: facts, template_content: template_content)
|
|
7635
7744
|
end
|
|
7636
|
-
return merge_appraisals_template_policy(destination_content, facts: facts) if file_type == :appraisals
|
|
7745
|
+
return merge_appraisals_template_policy(destination_content, facts: facts, target_path: recipe.fetch(:target_path)) if file_type == :appraisals
|
|
7637
7746
|
|
|
7638
7747
|
return destination_content
|
|
7639
7748
|
end
|
|
@@ -7680,7 +7789,7 @@ module Kettle
|
|
|
7680
7789
|
output = merge_gemfile_eval_bucket_entries(template_content, output, destination_content: destination_content)
|
|
7681
7790
|
return finalize_gemfile_template_source(recipe, output, destination_content, facts: facts, template_content: template_content)
|
|
7682
7791
|
end
|
|
7683
|
-
return merge_appraisals_template_policy(output, facts: facts) if file_type == :appraisals
|
|
7792
|
+
return merge_appraisals_template_policy(output, facts: facts, target_path: recipe.fetch(:target_path)) if file_type == :appraisals
|
|
7684
7793
|
|
|
7685
7794
|
output = finalize_github_workflow_template(prune_github_workflow_matrix_by_min_ruby(output, facts), facts) if github_workflow_template_recipe?(recipe)
|
|
7686
7795
|
output = normalize_simplecov_template_source(output) if recipe.fetch(:target_path).to_s == ".simplecov"
|
|
@@ -7741,6 +7850,7 @@ module Kettle
|
|
|
7741
7850
|
output = preserve_destination_gemfile_source(output, destination_content)
|
|
7742
7851
|
end
|
|
7743
7852
|
output = inject_main_gemfile_recording_eval(output, facts)
|
|
7853
|
+
output = inject_main_gemfile_duel_eval(output, facts)
|
|
7744
7854
|
output = inject_main_gemfile_default_test_bundle(output, facts)
|
|
7745
7855
|
output = remove_stale_main_gemfile_tree_sitter_language_pack(output, template_content)
|
|
7746
7856
|
output = remove_stale_main_gemfile_direct_sibling_block(output, template_content)
|
|
@@ -9394,6 +9504,34 @@ module Kettle
|
|
|
9394
9504
|
ensure_trailing_newline(lines.join.gsub(/\n{3,}/, "\n\n"))
|
|
9395
9505
|
end
|
|
9396
9506
|
|
|
9507
|
+
# Duel for local development (`rake duel`), from its modular Gemfile, which
|
|
9508
|
+
# installs nothing below Ruby 3.4. CI loads it from the perf appraisal.
|
|
9509
|
+
def inject_main_gemfile_duel_eval(content, facts)
|
|
9510
|
+
return content unless facts.to_h.dig(:ci, :duel)
|
|
9511
|
+
return content if gemfile_eval_paths(content).include?(DUEL_MAIN_GEMFILE_EVAL_PATH)
|
|
9512
|
+
|
|
9513
|
+
lines = content.to_s.lines
|
|
9514
|
+
insert_at = main_gemfile_duel_insertion_index(content)
|
|
9515
|
+
unless insert_at
|
|
9516
|
+
lines << "\n" unless lines.empty? || lines.last.strip.empty?
|
|
9517
|
+
insert_at = lines.length
|
|
9518
|
+
end
|
|
9519
|
+
lines.insert(insert_at, "# Performance regression guards (Duel requires Ruby 3.4 or later)\n", %(eval_gemfile "#{DUEL_MAIN_GEMFILE_EVAL_PATH}"\n), "\n")
|
|
9520
|
+
ensure_trailing_newline(lines.join.gsub(/\n{3,}/, "\n\n"))
|
|
9521
|
+
end
|
|
9522
|
+
|
|
9523
|
+
# After the optional dependencies, before the extracted standard library
|
|
9524
|
+
# gems.
|
|
9525
|
+
def main_gemfile_duel_insertion_index(content)
|
|
9526
|
+
lines = content.to_s.lines
|
|
9527
|
+
call = ruby_call_records(content, :eval_gemfile).find do |candidate|
|
|
9528
|
+
ruby_string_argument(candidate).to_s == "gemfiles/modular/x_std_libs.gemfile"
|
|
9529
|
+
end
|
|
9530
|
+
return unless call
|
|
9531
|
+
|
|
9532
|
+
gemfile_eval_comment_start_line(lines, call.location.start_line) - 1
|
|
9533
|
+
end
|
|
9534
|
+
|
|
9397
9535
|
# The root Gemfile is the local-development bundle. Appraisals and CI use
|
|
9398
9536
|
# their own Gemfiles, so a project that needs a framework or application
|
|
9399
9537
|
# harness locally must declare that dependency separately in test_bundle.
|
|
@@ -10489,14 +10627,16 @@ module Kettle
|
|
|
10489
10627
|
version.split(".", -1).all? { |part| !part.empty? && part.each_char.all? { |char| char.between?("0", "9") } }
|
|
10490
10628
|
end
|
|
10491
10629
|
|
|
10492
|
-
def merge_appraisals_template_policy(content, facts:)
|
|
10630
|
+
def merge_appraisals_template_policy(content, facts:, target_path: nil)
|
|
10493
10631
|
package_name = facts.dig(:package, :name).to_s if facts
|
|
10494
10632
|
min_ruby = minimum_ruby_token(facts.dig(:ci, :test_min_ruby) || facts.dig(:rubygems, :min_ruby)) if facts
|
|
10495
10633
|
with_framework_appraisals = merge_framework_matrix_appraisals(content, facts)
|
|
10496
10634
|
with_standard_appraisal_gemfiles = inject_standard_appraisal_gemfiles(with_framework_appraisals, facts)
|
|
10635
|
+
with_standard_appraisal_gemfiles = inject_duel_appraisal(with_standard_appraisal_gemfiles, facts) if target_path.to_s == "Appraisals"
|
|
10497
10636
|
pruned = prune_appraisals_recording_entries(with_standard_appraisal_gemfiles, facts)
|
|
10498
10637
|
pruned = prune_appraisals_below_min_ruby(pruned, min_ruby)
|
|
10499
10638
|
pruned = remove_managed_stdlib_appraisal_gems(pruned)
|
|
10639
|
+
pruned = remove_duplicate_runtime_heads_from_head_appraisal(pruned)
|
|
10500
10640
|
pruned = migrate_legacy_byebug_appraisal_dependencies(pruned)
|
|
10501
10641
|
pruned = remove_gemfile_dependency_lines(pruned, [package_name])
|
|
10502
10642
|
remove_gemfile_percent_w_entries(pruned, [package_name])
|
|
@@ -10617,6 +10757,56 @@ module Kettle
|
|
|
10617
10757
|
end
|
|
10618
10758
|
end
|
|
10619
10759
|
|
|
10760
|
+
# Older generated Appraisals put runtime dependency HEADs in the `head`
|
|
10761
|
+
# appraisal alongside Ruby-version-compatible extracted stdlib versions.
|
|
10762
|
+
# The template tests those separately: `head` uses compatible versions,
|
|
10763
|
+
# while `dep-heads` tests runtime dependency HEADs. Remove the stale eval
|
|
10764
|
+
# only when the separate dependency-head appraisal is present.
|
|
10765
|
+
def remove_duplicate_runtime_heads_from_head_appraisal(content)
|
|
10766
|
+
parsed = appraisal_blocks(content)
|
|
10767
|
+
blocks = parsed.fetch(:blocks)
|
|
10768
|
+
head = blocks["head"]
|
|
10769
|
+
return content unless head && appraisal_x_stdlib_eval_gemfile_call(head)
|
|
10770
|
+
return content unless blocks["dep-heads"] && runtime_heads_eval_gemfile_call(blocks["dep-heads"])
|
|
10771
|
+
|
|
10772
|
+
call = runtime_heads_eval_gemfile_call(head)
|
|
10773
|
+
return content unless call
|
|
10774
|
+
|
|
10775
|
+
lines = head.lines
|
|
10776
|
+
first = call.location.start_line - 1
|
|
10777
|
+
last = ruby_node_source_end_line(call) - 1
|
|
10778
|
+
first -= 1 while first.positive? && gemfile_comment_line?(lines[first - 1])
|
|
10779
|
+
head = ensure_trailing_newline(lines.each_with_index.reject { |_line, index| (first..last).cover?(index) }.map(&:first).join.gsub(/\n{3,}/, "\n\n"))
|
|
10780
|
+
blocks["head"] = head
|
|
10781
|
+
ensure_trailing_newline(([parsed.fetch(:prelude).to_s.rstrip] + parsed.fetch(:order).map { |name| blocks.fetch(name).rstrip }).reject(&:empty?).join("\n\n"))
|
|
10782
|
+
end
|
|
10783
|
+
|
|
10784
|
+
def runtime_heads_eval_gemfile_call(block)
|
|
10785
|
+
ruby_call_records(block, :eval_gemfile).find do |call|
|
|
10786
|
+
ruby_string_argument(call).to_s.end_with?("modular/runtime_heads.gemfile")
|
|
10787
|
+
end
|
|
10788
|
+
end
|
|
10789
|
+
|
|
10790
|
+
# The perf appraisal, whose generated gemfiles/perf.gemfile the Perf
|
|
10791
|
+
# workflow bundles: the gemspec's dependencies and Duel. An existing perf
|
|
10792
|
+
# appraisal is the project's, and is kept as it is.
|
|
10793
|
+
def inject_duel_appraisal(content, facts)
|
|
10794
|
+
return content unless facts.to_h.dig(:ci, :duel)
|
|
10795
|
+
return content if appraisal_call_records(content).any? { |record| record.fetch(:name) == DUEL_APPRAISAL_NAME }
|
|
10796
|
+
|
|
10797
|
+
# (The comments are inside the block: Appraisals keeps no comment
|
|
10798
|
+
# between blocks.)
|
|
10799
|
+
block = <<~RUBY
|
|
10800
|
+
appraise "#{DUEL_APPRAISAL_NAME}" do
|
|
10801
|
+
# Performance regression guards, for .github/workflows/perf.yml
|
|
10802
|
+
# (Duel requires Ruby 3.4 or later)
|
|
10803
|
+
eval_gemfile "modular/duel.gemfile"
|
|
10804
|
+
eval_gemfile "modular/x_std_libs.gemfile"
|
|
10805
|
+
end
|
|
10806
|
+
RUBY
|
|
10807
|
+
ensure_trailing_newline([content.to_s.rstrip, block].reject(&:empty?).join("\n\n"))
|
|
10808
|
+
end
|
|
10809
|
+
|
|
10620
10810
|
def prune_appraisals_recording_entries(content, facts)
|
|
10621
10811
|
remove_indexes = Set.new
|
|
10622
10812
|
lines = content.to_s.lines
|
|
@@ -11114,9 +11304,16 @@ module Kettle
|
|
|
11114
11304
|
end
|
|
11115
11305
|
|
|
11116
11306
|
def gemspec_standard_legacy_version_loader_block(entrypoint_require:, namespace:, min_ruby:)
|
|
11117
|
-
|
|
11307
|
+
legacy_else =
|
|
11118
11308
|
if min_ruby >= REQUIRE_RELATIVE_MIN_RUBY
|
|
11119
|
-
|
|
11309
|
+
<<~RUBY.chomp
|
|
11310
|
+
# Ruby < 3.1: Kernel.load(path, module) is unavailable, but Module#module_eval with file
|
|
11311
|
+
# metadata works on Ruby 2.2+. Anonymous loading matters beyond coverage: evaluating this
|
|
11312
|
+
# gemspec (e.g. by bundler/setup in a subprocess) must not define top-level constants,
|
|
11313
|
+
# which isolated-loading specs assert on.
|
|
11314
|
+
version_path = "\#{__dir__}/lib/#{entrypoint_require}/version.rb"
|
|
11315
|
+
Module.new.tap { |mod| mod.module_eval(File.read(version_path), version_path, 1) }::#{namespace}::Version::VERSION
|
|
11316
|
+
RUBY
|
|
11120
11317
|
else
|
|
11121
11318
|
<<~RUBY.chomp
|
|
11122
11319
|
# NOTE: Use __FILE__ or __dir__ until removal of Ruby 1.x support
|
|
@@ -11124,6 +11321,7 @@ module Kettle
|
|
|
11124
11321
|
lib = File.expand_path("lib", File.dirname(__FILE__))
|
|
11125
11322
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
11126
11323
|
require "#{entrypoint_require}/version"
|
|
11324
|
+
#{namespace}::Version::VERSION
|
|
11127
11325
|
RUBY
|
|
11128
11326
|
end
|
|
11129
11327
|
|
|
@@ -11135,8 +11333,7 @@ module Kettle
|
|
|
11135
11333
|
# See: https://github.com/panorama-ed/memo_wise/pull/397
|
|
11136
11334
|
#{gemspec_modern_version_loader_expression(entrypoint_require: entrypoint_require, namespace: namespace)}
|
|
11137
11335
|
else
|
|
11138
|
-
#{
|
|
11139
|
-
#{namespace}::Version::VERSION
|
|
11336
|
+
#{legacy_else.gsub("\n", "\n ")}
|
|
11140
11337
|
end
|
|
11141
11338
|
RUBY
|
|
11142
11339
|
end
|
|
@@ -13934,9 +14131,6 @@ module Kettle
|
|
|
13934
14131
|
if !opencollective_disabled && funding_platform_enabled?(enabled_platforms, "open_collective")
|
|
13935
14132
|
urls << github_funding_platform_urls("open_collective", [open_collective_org]).first
|
|
13936
14133
|
end
|
|
13937
|
-
if funding_platform_enabled?(enabled_platforms, "tidelift")
|
|
13938
|
-
urls << github_funding_platform_urls("tidelift", ["rubygems/#{package_name}"]).first
|
|
13939
|
-
end
|
|
13940
14134
|
|
|
13941
14135
|
urls.compact.uniq.sort
|
|
13942
14136
|
end
|
|
@@ -13974,8 +14168,6 @@ module Kettle
|
|
|
13974
14168
|
"https://opencollective.com/#{handle}"
|
|
13975
14169
|
when "thanks_dev"
|
|
13976
14170
|
"https://thanks.dev/#{handle}"
|
|
13977
|
-
when "tidelift"
|
|
13978
|
-
"https://tidelift.com/funding/github/#{handle}"
|
|
13979
14171
|
end
|
|
13980
14172
|
end
|
|
13981
14173
|
end
|
|
@@ -13996,7 +14188,8 @@ module Kettle
|
|
|
13996
14188
|
next if opencollective_disabled && opencollective_disabled_file?(relative_path)
|
|
13997
14189
|
next if inactive_packaged_workflow?(relative_path, config)
|
|
13998
14190
|
next if generated_or_obsolete_github_workflow?(relative_path) &&
|
|
13999
|
-
!skip_packaged_workflow_template?(relative_path, config)
|
|
14191
|
+
!skip_packaged_workflow_template?(relative_path, config) &&
|
|
14192
|
+
!skip_duel_template?(relative_path, config)
|
|
14000
14193
|
|
|
14001
14194
|
relative_path
|
|
14002
14195
|
end.sort
|
|
@@ -14149,7 +14342,7 @@ module Kettle
|
|
|
14149
14342
|
path = kettle_jem_config_path(project_root)
|
|
14150
14343
|
return {} unless File.exist?(path)
|
|
14151
14344
|
|
|
14152
|
-
config = YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {}
|
|
14345
|
+
config = YAML.safe_load_file(path, permitted_classes: [Date], aliases: false) || {}
|
|
14153
14346
|
config = normalize_kettle_jem_config(config)
|
|
14154
14347
|
validate_kettle_jem_config!(config)
|
|
14155
14348
|
config
|
|
@@ -14175,9 +14368,38 @@ module Kettle
|
|
|
14175
14368
|
|
|
14176
14369
|
def kettle_jem_config_path(project_root)
|
|
14177
14370
|
canonical = File.join(project_root, KETTLE_CONFIG_PATH)
|
|
14371
|
+
legacy = File.join(project_root, LEGACY_KETTLE_CONFIG_PATH)
|
|
14372
|
+
if File.exist?(canonical) && File.exist?(legacy)
|
|
14373
|
+
raise Error, "Invalid kettle-jem config: both #{KETTLE_CONFIG_PATH} and legacy #{LEGACY_KETTLE_CONFIG_PATH} exist in #{project_root}. " \
|
|
14374
|
+
"The legacy file is silently ignored when both are present, so this combination is not supported. " \
|
|
14375
|
+
"Merge any unique settings from #{LEGACY_KETTLE_CONFIG_PATH} into #{KETTLE_CONFIG_PATH}, then delete #{LEGACY_KETTLE_CONFIG_PATH}."
|
|
14376
|
+
end
|
|
14178
14377
|
return canonical if File.exist?(canonical)
|
|
14179
14378
|
|
|
14180
|
-
|
|
14379
|
+
legacy
|
|
14380
|
+
end
|
|
14381
|
+
|
|
14382
|
+
# Legacy-only projects are migrated to the canonical config path with MOVE
|
|
14383
|
+
# semantics: the content lands at .structuredmerge/kettle-jem.yml and the
|
|
14384
|
+
# legacy .kettle-jem.yml is removed, so there is never a stale second
|
|
14385
|
+
# config that the loader silently ignores.
|
|
14386
|
+
def legacy_kettle_config_migration(project_root)
|
|
14387
|
+
canonical = File.join(project_root, KETTLE_CONFIG_PATH)
|
|
14388
|
+
legacy = File.join(project_root, LEGACY_KETTLE_CONFIG_PATH)
|
|
14389
|
+
return unless File.exist?(legacy)
|
|
14390
|
+
return if File.exist?(canonical)
|
|
14391
|
+
|
|
14392
|
+
{from: LEGACY_KETTLE_CONFIG_PATH, to: KETTLE_CONFIG_PATH}
|
|
14393
|
+
end
|
|
14394
|
+
|
|
14395
|
+
def apply_legacy_kettle_config_migration!(project_root)
|
|
14396
|
+
migration = legacy_kettle_config_migration(project_root)
|
|
14397
|
+
return unless migration
|
|
14398
|
+
|
|
14399
|
+
canonical = File.join(project_root, KETTLE_CONFIG_PATH)
|
|
14400
|
+
FileUtils.mkdir_p(File.dirname(canonical))
|
|
14401
|
+
FileUtils.mv(File.join(project_root, LEGACY_KETTLE_CONFIG_PATH), canonical)
|
|
14402
|
+
migration.merge(applied: true)
|
|
14181
14403
|
end
|
|
14182
14404
|
|
|
14183
14405
|
def kettle_jem_config_relative_path(project_root)
|
|
@@ -14739,7 +14961,7 @@ module Kettle
|
|
|
14739
14961
|
"KJ|CI:EXEC_CMD" => facts.dig(:ci, :exec_cmd).to_s,
|
|
14740
14962
|
"KJ|CI:OS_MATRIX_ENTRIES" => github_actions_current_os_matrix_entries(facts),
|
|
14741
14963
|
"KJ|CI:OS_TEST_STEPS" => github_actions_current_os_test_steps(facts),
|
|
14742
|
-
"KJ|README:
|
|
14964
|
+
"KJ|README:CURRENT_MRI_PLATFORMS" => readme_current_mri_platforms(facts),
|
|
14743
14965
|
"KJ|RAKE:TOP_LEVEL_DOCS_TASK" => rake_top_level_docs_task_token(facts).rstrip,
|
|
14744
14966
|
"KJ|GITHUB_ACTIONS:COVERAGE_UPLOAD_STEPS" => github_actions_coverage_steps(disabled_integrations: facts.dig(:integrations, :disabled))
|
|
14745
14967
|
}.merge(
|
|
@@ -14771,6 +14993,7 @@ module Kettle
|
|
|
14771
14993
|
readme_family_intro_and_backend_matrix(facts.fetch(:readme_style, {}))
|
|
14772
14994
|
tokens["KJ|README:DEV_TEST_STACK_TABLE"] = readme_dev_test_stack_table(package.fetch(:name).to_s)
|
|
14773
14995
|
tokens.merge!(readme_fossa_template_tokens(facts.fetch(:readme_style, {})))
|
|
14996
|
+
tokens.merge!(duel_template_tokens(facts))
|
|
14774
14997
|
tokens.merge!(rubyforum_template_tokens(facts.fetch(:rubyforum, {})))
|
|
14775
14998
|
tokens.merge!(version_gem_template_tokens(facts))
|
|
14776
14999
|
tokens.merge!(shim_template_tokens(facts.fetch(:shim, {})))
|
|
@@ -14814,66 +15037,17 @@ module Kettle
|
|
|
14814
15037
|
"#{package_name} / #{namespace}"
|
|
14815
15038
|
end
|
|
14816
15039
|
|
|
14817
|
-
#
|
|
14818
|
-
#
|
|
14819
|
-
#
|
|
14820
|
-
|
|
14821
|
-
# and every engine lane outside current.yml is Linux-only. Engine capability
|
|
14822
|
-
# notes come from Kettle::Rb::PlatformSupport and carry their citation.
|
|
14823
|
-
def readme_platform_support_table(facts)
|
|
14824
|
-
engines = facts.dig(:rubygems, :engines)
|
|
14825
|
-
engines = DEFAULT_ENGINES if engines.nil? || engines.empty?
|
|
15040
|
+
# Keep the platform detail within Compatibility. The compatibility badge
|
|
15041
|
+
# table already carries engine/version detail; this adds the OS platforms
|
|
15042
|
+
# exercised by the current MRI lane.
|
|
15043
|
+
def readme_current_mri_platforms(facts)
|
|
14826
15044
|
platforms = facts.dig(:ci, :platforms)
|
|
14827
15045
|
platforms = DEFAULT_PLATFORMS if platforms.nil? || platforms.empty?
|
|
14828
|
-
|
|
15046
|
+
platform_names = platforms.map { |platform| (platform == "macos") ? "macOS" : platform.capitalize }
|
|
15047
|
+
return platform_names.first if platform_names.one?
|
|
15048
|
+
return platform_names.join(" and ") if platform_names.length == 2
|
|
14829
15049
|
|
|
14830
|
-
|
|
14831
|
-
unsupported = "Unsupported"
|
|
14832
|
-
rows = []
|
|
14833
|
-
|
|
14834
|
-
unless ruby_versions.empty?
|
|
14835
|
-
rows << [
|
|
14836
|
-
"MRI #{ruby_versions.first}-#{ruby_versions.last}",
|
|
14837
|
-
parallel, unsupported, unsupported,
|
|
14838
|
-
"Versioned Ruby workflows run on Linux."
|
|
14839
|
-
]
|
|
14840
|
-
end
|
|
14841
|
-
rows << [
|
|
14842
|
-
"MRI current",
|
|
14843
|
-
parallel,
|
|
14844
|
-
platforms.include?("macos") ? parallel : unsupported,
|
|
14845
|
-
platforms.include?("windows") ? parallel : unsupported,
|
|
14846
|
-
"Current Ruby workflow covers the declared OS platforms."
|
|
14847
|
-
]
|
|
14848
|
-
rows << [
|
|
14849
|
-
"MRI head",
|
|
14850
|
-
parallel, unsupported, unsupported,
|
|
14851
|
-
"CI is allowed to fail while Ruby is in development."
|
|
14852
|
-
]
|
|
14853
|
-
|
|
14854
|
-
(engines - ["ruby"]).each do |engine|
|
|
14855
|
-
label = (engine == "jruby") ? "JRuby" : "TruffleRuby"
|
|
14856
|
-
windows_entry = Kettle::Rb::PlatformSupport.engine_os_entry(engine, "windows")
|
|
14857
|
-
note = if windows_entry && windows_entry.support == "unsupported"
|
|
14858
|
-
"#{windows_entry.note} ([source](#{windows_entry.citation}))"
|
|
14859
|
-
else
|
|
14860
|
-
"Engine workflows run on Linux; macOS and Windows are not exercised by CI."
|
|
14861
|
-
end
|
|
14862
|
-
rows << ["#{label} (all CI lanes)", parallel, unsupported, unsupported, note]
|
|
14863
|
-
end
|
|
14864
|
-
|
|
14865
|
-
[
|
|
14866
|
-
"Support tiers describe what this project's CI actually exercises, not what may",
|
|
14867
|
-
"work in an untested environment:",
|
|
14868
|
-
"",
|
|
14869
|
-
"- **Parallel support**: CI runs the test suite with multiple workers available.",
|
|
14870
|
-
"- **Unsupported**: the engine/platform combination is not covered by CI and is",
|
|
14871
|
-
" not a project compatibility claim.",
|
|
14872
|
-
"",
|
|
14873
|
-
"| Ruby engine/version | Linux | macOS | Windows | Notes |",
|
|
14874
|
-
"|---------------------|-------|-------|---------|-------|",
|
|
14875
|
-
*rows.map { |row| "| #{row.join(" | ")} |" }
|
|
14876
|
-
].join("\n")
|
|
15050
|
+
platform_names[0...-1].join(", ") + ", and #{platform_names.last}"
|
|
14877
15051
|
end
|
|
14878
15052
|
|
|
14879
15053
|
def readme_dev_test_stack_table(package_name)
|
|
@@ -14911,6 +15085,41 @@ module Kettle
|
|
|
14911
15085
|
}
|
|
14912
15086
|
end
|
|
14913
15087
|
|
|
15088
|
+
# The README badge and the Rake task, for a project with Duel on. They are
|
|
15089
|
+
# empty otherwise, which leaves its README and Rakefile as they were.
|
|
15090
|
+
def duel_template_tokens(facts)
|
|
15091
|
+
unless facts.dig(:ci, :duel)
|
|
15092
|
+
return {"KJ|RAKE:DUEL_TASK" => "", "KJ|README:DUEL_BADGE" => "", "KJ|README:DUEL_REFS" => ""}
|
|
15093
|
+
end
|
|
15094
|
+
|
|
15095
|
+
{
|
|
15096
|
+
"KJ|RAKE:DUEL_TASK" => rake_duel_task_token,
|
|
15097
|
+
"KJ|README:DUEL_BADGE" => README_DUEL_BADGE,
|
|
15098
|
+
"KJ|README:DUEL_REFS" => [
|
|
15099
|
+
"[⚔️duel]: https://github.com/sferik/duel-ruby",
|
|
15100
|
+
"[⚔️duel-img]: https://raw.githubusercontent.com/sferik/duel-ruby/main/assets/badge.svg"
|
|
15101
|
+
].join("\n")
|
|
15102
|
+
}
|
|
15103
|
+
end
|
|
15104
|
+
|
|
15105
|
+
def rake_duel_task_token
|
|
15106
|
+
<<~RAKE
|
|
15107
|
+
### PERFORMANCE TASKS
|
|
15108
|
+
# Duel's performance regression guards: rake duel compares this tree with
|
|
15109
|
+
# the main branch (see .duel.rb and duel/). Duel requires Ruby 3.4 or
|
|
15110
|
+
# later, so older Rubies get a stub task.
|
|
15111
|
+
begin
|
|
15112
|
+
require "duel/rake_task"
|
|
15113
|
+
Duel::RakeTask.new
|
|
15114
|
+
rescue LoadError
|
|
15115
|
+
desc("(stub) duel is unavailable")
|
|
15116
|
+
task("duel") do
|
|
15117
|
+
warn("NOTE: duel isn't installed, or is disabled for \#{RUBY_VERSION} in the current environment")
|
|
15118
|
+
end
|
|
15119
|
+
end
|
|
15120
|
+
RAKE
|
|
15121
|
+
end
|
|
15122
|
+
|
|
14914
15123
|
def readme_fossa_template_tokens(readme_style)
|
|
14915
15124
|
project = readme_style.fetch(:fossa_project, "").to_s
|
|
14916
15125
|
return {"KJ|README:FOSSA_BADGE" => "", "KJ|README:FOSSA_REFS" => ""} if project.empty?
|
|
@@ -15944,7 +16153,7 @@ module Kettle
|
|
|
15944
16153
|
end
|
|
15945
16154
|
|
|
15946
16155
|
def kettle_dev_local_gems(config)
|
|
15947
|
-
gems = %w[kettle-dev kettle-family kettle-rb kettle-test kettle-soup-cover kettle-changelog]
|
|
16156
|
+
gems = %w[kettle-deps-floor kettle-dev kettle-family kettle-rb kettle-test kettle-soup-cover kettle-changelog]
|
|
15948
16157
|
plugin_names = PluginLoader.normalize_plugin_names(plugin_names_from_config(config))
|
|
15949
16158
|
gems.concat(plugin_names.select { |plugin_name| plugin_name.start_with?("kettle-") })
|
|
15950
16159
|
gems.uniq.join(" ")
|
|
@@ -15953,7 +16162,7 @@ module Kettle
|
|
|
15953
16162
|
def main_gemfile_kettle_family_gem(package_name)
|
|
15954
16163
|
return "" if package_name.to_s == "kettle-family"
|
|
15955
16164
|
|
|
15956
|
-
%(gem "kettle-family", "~> 1.3", ">= 1.3.
|
|
16165
|
+
%(gem "kettle-family", "~> 1.3", ">= 1.3.36"\n)
|
|
15957
16166
|
end
|
|
15958
16167
|
|
|
15959
16168
|
def main_gemfile_nomono_bootstrap(package_name)
|
|
@@ -15966,7 +16175,7 @@ module Kettle
|
|
|
15966
16175
|
end
|
|
15967
16176
|
|
|
15968
16177
|
def nomono_gemfile_declaration
|
|
15969
|
-
%(gem "nomono", "~> 1.1", ">= 1.1.
|
|
16178
|
+
%(gem "nomono", "~> 1.1", ">= 1.1.17", require: false # ruby >= 3.2.0)
|
|
15970
16179
|
end
|
|
15971
16180
|
|
|
15972
16181
|
def local_gemfile_nomono_bootstrap(_package_name)
|
|
@@ -15974,11 +16183,16 @@ module Kettle
|
|
|
15974
16183
|
end
|
|
15975
16184
|
|
|
15976
16185
|
def nomono_bundler_bootstrap(lockfile_path)
|
|
16186
|
+
# Derive the activation floor from TEMPLATE_MANAGED_DEPENDENCIES (the same
|
|
16187
|
+
# source of truth as the main-Gemfile declaration) so the two can never
|
|
16188
|
+
# drift apart when the floor is bumped.
|
|
16189
|
+
nomono_managed = TEMPLATE_MANAGED_DEPENDENCIES.find { |dependency| dependency.fetch(:name) == "nomono" }
|
|
16190
|
+
nomono_floor_requirements = nomono_managed.fetch(:requirements).inspect
|
|
15977
16191
|
<<~RUBY.rstrip
|
|
15978
16192
|
# Bootstrapping nomono here cannot rely on a plain `gem "nomono", ...` line.
|
|
15979
16193
|
# Bundler records that dependency during Gemfile evaluation, but it does not
|
|
15980
16194
|
# activate that exact version before the immediate `require "nomono/bundler"`.
|
|
15981
|
-
nomono_activation_requirements =
|
|
16195
|
+
nomono_activation_requirements = #{nomono_floor_requirements}
|
|
15982
16196
|
nomono_requirement = Gem::Requirement.new(nomono_activation_requirements)
|
|
15983
16197
|
nomono_already_activated = Gem.loaded_specs["nomono"]
|
|
15984
16198
|
nomono_lockfile = File.expand_path(#{lockfile_path.inspect}, __dir__)
|
|
@@ -17503,7 +17717,8 @@ module Kettle
|
|
|
17503
17717
|
sponsors = readme_corporate_sponsors(config, env)
|
|
17504
17718
|
compact_hash(
|
|
17505
17719
|
entries: sponsors,
|
|
17506
|
-
row: readme_corporate_sponsors_row(sponsors)
|
|
17720
|
+
row: readme_corporate_sponsors_row(sponsors),
|
|
17721
|
+
detail: readme_corporate_sponsors_detail(sponsors)
|
|
17507
17722
|
)
|
|
17508
17723
|
end
|
|
17509
17724
|
|
|
@@ -17513,7 +17728,25 @@ module Kettle
|
|
|
17513
17728
|
readme_config["corporate_sponsors"] || readme_config["sponsors"]
|
|
17514
17729
|
)
|
|
17515
17730
|
inherited = normalize_readme_corporate_sponsors_json(env["KETTLE_JEM_CORPORATE_SPONSORS_JSON"])
|
|
17516
|
-
deduplicate_readme_corporate_sponsors(configured + inherited)
|
|
17731
|
+
sponsors = deduplicate_readme_corporate_sponsors(configured + inherited)
|
|
17732
|
+
today = Date.today
|
|
17733
|
+
sponsors
|
|
17734
|
+
.select { |sponsor| readme_corporate_sponsor_active?(sponsor, today) }
|
|
17735
|
+
.map do |sponsor|
|
|
17736
|
+
next sponsor unless sponsor[:members]
|
|
17737
|
+
|
|
17738
|
+
sponsor.merge(members: sponsor[:members].select { |member| readme_corporate_sponsor_active?(member, today) })
|
|
17739
|
+
end
|
|
17740
|
+
end
|
|
17741
|
+
|
|
17742
|
+
def readme_corporate_sponsor_active?(sponsor, today)
|
|
17743
|
+
since = sponsor[:since]
|
|
17744
|
+
return false if since && since > today
|
|
17745
|
+
|
|
17746
|
+
until_date = sponsor[:until]
|
|
17747
|
+
return false if until_date && until_date < today
|
|
17748
|
+
|
|
17749
|
+
true
|
|
17517
17750
|
end
|
|
17518
17751
|
|
|
17519
17752
|
def normalize_readme_corporate_sponsors_json(value)
|
|
@@ -17529,24 +17762,68 @@ module Kettle
|
|
|
17529
17762
|
def normalize_readme_corporate_sponsors(value)
|
|
17530
17763
|
Array(value).filter_map do |entry|
|
|
17531
17764
|
unless entry.is_a?(Hash)
|
|
17532
|
-
raise ArgumentError, "corporate sponsor entries must be mappings with name,
|
|
17765
|
+
raise ArgumentError, "corporate sponsor entries must be mappings with name, target_href, and image_src (legacy url and img_src are also accepted)"
|
|
17533
17766
|
end
|
|
17534
17767
|
|
|
17535
17768
|
name = entry["name"].to_s.strip
|
|
17536
|
-
url = entry["url"].to_s.strip
|
|
17537
|
-
img_src = entry["img_src"].to_s.strip
|
|
17769
|
+
url = (entry["target_href"] || entry["url"]).to_s.strip
|
|
17770
|
+
img_src = (entry["image_src"] || entry["img_src"]).to_s.strip
|
|
17538
17771
|
if name.empty? || url.empty? || img_src.empty?
|
|
17539
|
-
raise ArgumentError, "corporate sponsor entries require name,
|
|
17772
|
+
raise ArgumentError, "corporate sponsor entries require name, target_href, and image_src (legacy url and img_src are also accepted)"
|
|
17540
17773
|
end
|
|
17541
17774
|
|
|
17542
17775
|
{
|
|
17543
17776
|
name: name,
|
|
17544
17777
|
url: url,
|
|
17545
|
-
img_src: img_src
|
|
17546
|
-
|
|
17778
|
+
img_src: img_src,
|
|
17779
|
+
since: normalize_readme_corporate_sponsor_date(entry["since"], "since", name),
|
|
17780
|
+
until: normalize_readme_corporate_sponsor_date(entry["until"], "until", name),
|
|
17781
|
+
members: normalize_readme_corporate_sponsor_members(entry["members"], name)
|
|
17782
|
+
}.compact
|
|
17783
|
+
end
|
|
17784
|
+
end
|
|
17785
|
+
|
|
17786
|
+
# Members of an umbrella sponsor (e.g. the companies behind Alpha-Omega).
|
|
17787
|
+
# Members require name and url; img_src is optional because members render
|
|
17788
|
+
# as linked names in a caption, not as logos. Members may not themselves
|
|
17789
|
+
# have members — nesting stops at one level.
|
|
17790
|
+
def normalize_readme_corporate_sponsor_members(value, sponsor_name)
|
|
17791
|
+
return nil if value.nil?
|
|
17792
|
+
|
|
17793
|
+
Array(value).filter_map do |entry|
|
|
17794
|
+
unless entry.is_a?(Hash)
|
|
17795
|
+
raise ArgumentError, "corporate sponsor #{sponsor_name.inspect} members must be mappings with name and target_href (legacy url is also accepted)"
|
|
17796
|
+
end
|
|
17797
|
+
unless entry["members"].nil?
|
|
17798
|
+
raise ArgumentError, "corporate sponsor #{sponsor_name.inspect} members cannot themselves have members"
|
|
17799
|
+
end
|
|
17800
|
+
|
|
17801
|
+
name = entry["name"].to_s.strip
|
|
17802
|
+
url = (entry["target_href"] || entry["url"]).to_s.strip
|
|
17803
|
+
img_src = (entry["image_src"] || entry["img_src"]).to_s.strip
|
|
17804
|
+
if name.empty? || url.empty?
|
|
17805
|
+
raise ArgumentError, "corporate sponsor #{sponsor_name.inspect} members require name and target_href (legacy url is also accepted)"
|
|
17806
|
+
end
|
|
17807
|
+
|
|
17808
|
+
{
|
|
17809
|
+
name: name,
|
|
17810
|
+
url: url,
|
|
17811
|
+
img_src: img_src.empty? ? nil : img_src,
|
|
17812
|
+
since: normalize_readme_corporate_sponsor_date(entry["since"], "since", name),
|
|
17813
|
+
until: normalize_readme_corporate_sponsor_date(entry["until"], "until", name)
|
|
17814
|
+
}.compact
|
|
17547
17815
|
end
|
|
17548
17816
|
end
|
|
17549
17817
|
|
|
17818
|
+
def normalize_readme_corporate_sponsor_date(value, key, name)
|
|
17819
|
+
clean = value.to_s.strip
|
|
17820
|
+
return nil if clean.empty?
|
|
17821
|
+
|
|
17822
|
+
Date.iso8601(clean)
|
|
17823
|
+
rescue ArgumentError, TypeError
|
|
17824
|
+
raise ArgumentError, "corporate sponsor #{name.inspect} has invalid #{key} date #{clean.inspect} (expected ISO 8601, e.g. 2026-01-31)"
|
|
17825
|
+
end
|
|
17826
|
+
|
|
17550
17827
|
def deduplicate_readme_corporate_sponsors(sponsors)
|
|
17551
17828
|
sponsors
|
|
17552
17829
|
.group_by { |sponsor| [sponsor[:name], sponsor[:url], sponsor[:img_src]] }
|
|
@@ -17569,6 +17846,32 @@ module Kettle
|
|
|
17569
17846
|
%(<p><sub>#{label} #{logos} <a href="#-floss-funding">Become a sponsor</a></sub></p>)
|
|
17570
17847
|
end
|
|
17571
17848
|
|
|
17849
|
+
# Markdown detail block for the FLOSS Funding section: one bullet per
|
|
17850
|
+
# active sponsor with its logo and linked name. Umbrella sponsors append a
|
|
17851
|
+
# "backed by" caption of linked member names (text links, not logos, so
|
|
17852
|
+
# the backing relationship stays explicit in every rendering context).
|
|
17853
|
+
def readme_corporate_sponsors_detail(sponsors)
|
|
17854
|
+
return "" if sponsors.empty?
|
|
17855
|
+
|
|
17856
|
+
lines = sponsors.map do |sponsor|
|
|
17857
|
+
name = sponsor.fetch(:name)
|
|
17858
|
+
url = sponsor.fetch(:url)
|
|
17859
|
+
img_src = sponsor.fetch(:img_src)
|
|
17860
|
+
line = [
|
|
17861
|
+
%(- <a href="#{html_attribute_escape(url)}">),
|
|
17862
|
+
%(<img alt="#{html_attribute_escape(name)}" src="#{html_attribute_escape(img_src)}" height="32"/>),
|
|
17863
|
+
%(</a> [#{name}](#{url}))
|
|
17864
|
+
].join
|
|
17865
|
+
members = Array(sponsor[:members])
|
|
17866
|
+
unless members.empty?
|
|
17867
|
+
member_links = members.map { |member| "[#{member.fetch(:name)}](#{member.fetch(:url)})" }.join(", ")
|
|
17868
|
+
line = "#{line} — backed by #{member_links}"
|
|
17869
|
+
end
|
|
17870
|
+
line
|
|
17871
|
+
end
|
|
17872
|
+
lines.join("\n")
|
|
17873
|
+
end
|
|
17874
|
+
|
|
17572
17875
|
def readme_top_logo_options(config)
|
|
17573
17876
|
readme_logo_options_from_config(config, "top_logos", "top_logo_options", README_TOP_LOGO_DEFAULTS)
|
|
17574
17877
|
end
|
|
@@ -17869,7 +18172,8 @@ module Kettle
|
|
|
17869
18172
|
|
|
17870
18173
|
def readme_corporate_sponsor_template_tokens(readme_sponsors)
|
|
17871
18174
|
{
|
|
17872
|
-
"KJ|README:CORPORATE_SPONSORS" => readme_sponsors[:row].to_s
|
|
18175
|
+
"KJ|README:CORPORATE_SPONSORS" => readme_sponsors[:row].to_s,
|
|
18176
|
+
"KJ|README:CORPORATE_SPONSORS_DETAIL" => readme_sponsors[:detail].to_s
|
|
17873
18177
|
}
|
|
17874
18178
|
end
|
|
17875
18179
|
|
|
@@ -18946,6 +19250,7 @@ module Kettle
|
|
|
18946
19250
|
result["ruby"] = {} unless result["ruby"].is_a?(Hash)
|
|
18947
19251
|
result["ruby"]["test_minimum"] = config_test_min_ruby(result, facts.dig(:rubygems, :min_ruby)).to_s
|
|
18948
19252
|
result["resolved_licenses"] = license[:spdx]
|
|
19253
|
+
result["resolved_duel"] = true if duel_configured?(config) && duel_template_profile?(facts[:template_profile])
|
|
18949
19254
|
if facts[:template_profile].to_s == SHIM_TEMPLATE_PROFILE
|
|
18950
19255
|
result["templates"] = default_template_config.merge(result["templates"].is_a?(Hash) ? result["templates"] : {})
|
|
18951
19256
|
result["templates"]["profile"] = SHIM_TEMPLATE_PROFILE
|
|
@@ -19083,7 +19388,7 @@ module Kettle
|
|
|
19083
19388
|
relative_path = path.delete_prefix("#{project_root}/")
|
|
19084
19389
|
next unless preferred_template_source(PACKAGED_TEMPLATE_ROOT, relative_path)
|
|
19085
19390
|
|
|
19086
|
-
{target_path: relative_path} if skip_packaged_gemfile_template?(relative_path, config
|
|
19391
|
+
{target_path: relative_path} if skip_packaged_gemfile_template?(relative_path, config)
|
|
19087
19392
|
end
|
|
19088
19393
|
(gemfile_cleanups + disabled_integration_config_cleanups(project_root, config)).sort_by { |cleanup| cleanup.fetch(:target_path) }
|
|
19089
19394
|
end
|
|
@@ -19167,8 +19472,9 @@ module Kettle
|
|
|
19167
19472
|
end
|
|
19168
19473
|
return if source_path.to_s.empty? || target_path.to_s.empty?
|
|
19169
19474
|
return if skip_packaged_workflow_template?(target_path, config, include_patterns: include_patterns)
|
|
19475
|
+
return if skip_duel_template?(target_path, config)
|
|
19170
19476
|
return if skip_disabled_integration_template?(target_path, config)
|
|
19171
|
-
return if skip_packaged_gemfile_template?(target_path, config,
|
|
19477
|
+
return if skip_packaged_gemfile_template?(target_path, config, template_root_path: template_root.fetch(:path))
|
|
19172
19478
|
return if skip_packaged_license_template?(target_path, config)
|
|
19173
19479
|
return if skip_packaged_version_gem_entrypoint_template?(project_root, source_path, target_path, config)
|
|
19174
19480
|
return if template_root.fetch(:kind) == "packaged" && opencollective_disabled && opencollective_disabled_file?(target_path)
|
|
@@ -19404,10 +19710,18 @@ module Kettle
|
|
|
19404
19710
|
Gem::Version.new(version) < min_ruby
|
|
19405
19711
|
end
|
|
19406
19712
|
|
|
19407
|
-
def skip_packaged_gemfile_template?(target_path, config,
|
|
19713
|
+
def skip_packaged_gemfile_template?(target_path, config, template_root_path: PACKAGED_TEMPLATE_ROOT)
|
|
19408
19714
|
return true if target_path.to_s.include?("gemfiles/modular/recording/") && !appraisal_recording_enabled?(config)
|
|
19409
19715
|
|
|
19410
|
-
|
|
19716
|
+
inventory = packaged_x_std_lib_template_inventory(template_root_path)
|
|
19717
|
+
x_std_bucket = inventory.fetch(:targets)[target_path.to_s.delete_prefix("gemfiles/modular/")]
|
|
19718
|
+
if x_std_bucket
|
|
19719
|
+
min_ruby = config_test_min_ruby(config)
|
|
19720
|
+
return false unless min_ruby
|
|
19721
|
+
|
|
19722
|
+
relevant = Kettle::Rb::RubySeriesBuckets.relevant_buckets(inventory.fetch(:buckets), minimum: min_ruby)
|
|
19723
|
+
return !relevant.include?(x_std_bucket)
|
|
19724
|
+
end
|
|
19411
19725
|
|
|
19412
19726
|
version = packaged_gemfile_template_ruby_floor(target_path)
|
|
19413
19727
|
return false unless version
|
|
@@ -19418,19 +19732,50 @@ module Kettle
|
|
|
19418
19732
|
Gem::Version.new(version) < min_ruby
|
|
19419
19733
|
end
|
|
19420
19734
|
|
|
19421
|
-
def
|
|
19422
|
-
|
|
19423
|
-
|
|
19735
|
+
def packaged_x_std_lib_template_inventory(template_root_path)
|
|
19736
|
+
root_key = File.expand_path(template_root_path.to_s)
|
|
19737
|
+
if root_key == File.expand_path(PACKAGED_TEMPLATE_ROOT)
|
|
19738
|
+
# Process-lifetime memo of an immutable inventory; recomputation is
|
|
19739
|
+
# idempotent, so a racing rebuild is harmless.
|
|
19740
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
19741
|
+
@packaged_x_std_lib_template_inventory ||= build_packaged_x_std_lib_template_inventory(root_key)
|
|
19742
|
+
return @packaged_x_std_lib_template_inventory
|
|
19743
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
19744
|
+
end
|
|
19745
|
+
|
|
19746
|
+
build_packaged_x_std_lib_template_inventory(root_key)
|
|
19747
|
+
end
|
|
19748
|
+
|
|
19749
|
+
def build_packaged_x_std_lib_template_inventory(template_root_path)
|
|
19750
|
+
modular_root = File.join(template_root_path.to_s, "gemfiles/modular")
|
|
19751
|
+
seeds = Dir.glob(File.join(modular_root, "x_std_libs/**/*.gemfile.example"))
|
|
19752
|
+
aggregate = File.join(modular_root, "x_std_libs.gemfile.example")
|
|
19753
|
+
seeds << aggregate if File.file?(aggregate)
|
|
19424
19754
|
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
|
|
19430
|
-
|
|
19755
|
+
queue = seeds
|
|
19756
|
+
visited = Set.new
|
|
19757
|
+
targets = {}
|
|
19758
|
+
buckets = Set.new
|
|
19759
|
+
until queue.empty?
|
|
19760
|
+
source = queue.pop
|
|
19761
|
+
next unless File.file?(source)
|
|
19762
|
+
next unless visited.add?(source)
|
|
19763
|
+
|
|
19764
|
+
relative = source.delete_prefix("#{modular_root}/").delete_suffix(".example")
|
|
19765
|
+
bucket = relative.split("/").find { |segment| segment.match?(/\Ar\d+(?:\.\d+)?\z/) }
|
|
19766
|
+
if bucket
|
|
19767
|
+
targets[relative] = bucket
|
|
19768
|
+
buckets << bucket
|
|
19769
|
+
end
|
|
19770
|
+
|
|
19771
|
+
gemfile_eval_paths(File.read(source)).each do |reference|
|
|
19772
|
+
resolved = File.expand_path(reference, File.dirname(source))
|
|
19773
|
+
candidate = "#{resolved}.example"
|
|
19774
|
+
queue << candidate if candidate.start_with?("#{modular_root}/") && File.file?(candidate)
|
|
19775
|
+
end
|
|
19431
19776
|
end
|
|
19432
|
-
|
|
19433
|
-
|
|
19777
|
+
|
|
19778
|
+
{targets: targets, buckets: buckets.to_a}
|
|
19434
19779
|
end
|
|
19435
19780
|
|
|
19436
19781
|
def skip_disabled_integration_template?(target_path, config)
|
|
@@ -19869,6 +20214,27 @@ module Kettle
|
|
|
19869
20214
|
DecisionPolicy.value_to_boolean(workflows["recording"]) == true
|
|
19870
20215
|
end
|
|
19871
20216
|
|
|
20217
|
+
# Whether the project opts in to Duel's performance regression guards
|
|
20218
|
+
# with `workflows.duel: true`. Duel is off by default.
|
|
20219
|
+
def duel_configured?(config)
|
|
20220
|
+
workflows = config["workflows"]
|
|
20221
|
+
workflows = {} unless workflows.is_a?(Hash)
|
|
20222
|
+
|
|
20223
|
+
DecisionPolicy.value_to_boolean(workflows["duel"]) == true
|
|
20224
|
+
end
|
|
20225
|
+
|
|
20226
|
+
# Duel's templates belong to a gem's full template: the monorepo and shim
|
|
20227
|
+
# profiles have no Perf workflow to compare it in.
|
|
20228
|
+
def duel_template_profile?(template_profile)
|
|
20229
|
+
["", FULL_TEMPLATE_PROFILE].include?(normalize_template_profile(template_profile))
|
|
20230
|
+
end
|
|
20231
|
+
|
|
20232
|
+
# While Duel is off, its templates are neither written nor cleaned up, so
|
|
20233
|
+
# a project's own .duel.rb, perf.yml, or duel.gemfile is left as it is.
|
|
20234
|
+
def skip_duel_template?(target_path, config)
|
|
20235
|
+
DUEL_TEMPLATE_PATHS.include?(target_path.to_s) && config["resolved_duel"] != true
|
|
20236
|
+
end
|
|
20237
|
+
|
|
19872
20238
|
def project_recording_enabled?(project_root, config)
|
|
19873
20239
|
return true if appraisal_recording_enabled?(config)
|
|
19874
20240
|
|
|
@@ -19954,8 +20320,9 @@ module Kettle
|
|
|
19954
20320
|
end
|
|
19955
20321
|
end
|
|
19956
20322
|
|
|
19957
|
-
def synchronize_github_funding_yml(content, facts)
|
|
20323
|
+
def synchronize_github_funding_yml(content, facts, seed_if_empty: false)
|
|
19958
20324
|
output = content.to_s
|
|
20325
|
+
output = github_funding_yml_seed(facts) if seed_if_empty && output.strip.empty?
|
|
19959
20326
|
enabled_platforms = facts.fetch(:funding, {}).fetch(:platforms, {})
|
|
19960
20327
|
enabled_platforms = enabled_platforms.merge("open_collective" => false) if facts.fetch(:funding, {})[:open_collective_disabled]
|
|
19961
20328
|
FUNDING_YML_PLATFORMS.each do |platform|
|
|
@@ -19963,16 +20330,35 @@ module Kettle
|
|
|
19963
20330
|
|
|
19964
20331
|
output = remove_top_level_yaml_key_lines(output, platform)
|
|
19965
20332
|
end
|
|
19966
|
-
|
|
19967
|
-
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
lines << "tidelift: #{tidelift_value}\n"
|
|
19971
|
-
output = lines.join
|
|
19972
|
-
end
|
|
20333
|
+
# Tidelift is in limbo post-Sonar acquisition with no URLs to direct
|
|
20334
|
+
# people to, so the platform is no longer emitted. Strip any lingering
|
|
20335
|
+
# tidelift key from previously templated destinations.
|
|
20336
|
+
output = remove_top_level_yaml_key_lines(output, "tidelift")
|
|
19973
20337
|
ensure_trailing_newline(output)
|
|
19974
20338
|
end
|
|
19975
20339
|
|
|
20340
|
+
# Build first-run FUNDING.yml content structurally from facts (Psych dump
|
|
20341
|
+
# of the known platform keys), so cold-start scaffolds get a valid file
|
|
20342
|
+
# without line-munging the commented packaged template — that template
|
|
20343
|
+
# still reaches destinations unchanged via template_source_application on
|
|
20344
|
+
# configured runs, comments intact.
|
|
20345
|
+
def github_funding_yml_seed(facts)
|
|
20346
|
+
funding = facts.fetch(:funding, {})
|
|
20347
|
+
platform_tokens = funding.fetch(:platform_tokens, {})
|
|
20348
|
+
github_user = github_org_from_url(facts.dig(:package, :source_url)).to_s
|
|
20349
|
+
values = {
|
|
20350
|
+
"buy_me_a_coffee" => platform_tokens[:buymeacoffee].to_s,
|
|
20351
|
+
"github" => github_user.empty? ? nil : [github_user],
|
|
20352
|
+
"ko_fi" => platform_tokens[:kofi].to_s,
|
|
20353
|
+
"liberapay" => platform_tokens[:liberapay].to_s,
|
|
20354
|
+
"open_collective" => funding[:open_collective_org].to_s,
|
|
20355
|
+
"thanks_dev" => github_user.empty? ? nil : "u/gh/#{github_user}"
|
|
20356
|
+
}.reject { |_key, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
|
|
20357
|
+
return "" if values.empty?
|
|
20358
|
+
|
|
20359
|
+
"# These are supported funding model platforms\n\n#{Psych.dump(values)}"
|
|
20360
|
+
end
|
|
20361
|
+
|
|
19976
20362
|
def remove_top_level_yaml_key_lines(content, key)
|
|
19977
20363
|
lines = content.to_s.lines
|
|
19978
20364
|
document = Psych.parse_stream(content.to_s).children.first
|
|
@@ -20606,7 +20992,7 @@ module Kettle
|
|
|
20606
20992
|
steps << <<~YAML
|
|
20607
20993
|
- name: Upload coverage to QLTY
|
|
20608
20994
|
if: ${{ !env.ACT }}
|
|
20609
|
-
uses: qltysh/qlty-action/coverage@
|
|
20995
|
+
uses: qltysh/qlty-action/coverage@ebcd284bf32e36a030afe15f1e136c7be08b9506 # v2.5.0
|
|
20610
20996
|
with:
|
|
20611
20997
|
oidc: true
|
|
20612
20998
|
files: coverage/lcov.info
|
|
@@ -20649,7 +21035,7 @@ module Kettle
|
|
|
20649
21035
|
|
|
20650
21036
|
- name: Add Coverage PR Comment
|
|
20651
21037
|
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
|
|
20652
|
-
if: ${{ !env.ACT && github.event_name == 'pull_request' }}
|
|
21038
|
+
if: ${{ !env.ACT && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
|
|
20653
21039
|
with:
|
|
20654
21040
|
recreate: true
|
|
20655
21041
|
path: code-coverage-results.md
|
|
@@ -20901,19 +21287,20 @@ module Kettle
|
|
|
20901
21287
|
"actions/checkout" => "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1",
|
|
20902
21288
|
"actions/cache" => "actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0",
|
|
20903
21289
|
"appraisal-rb/setup-ruby-flash" => "appraisal-rb/setup-ruby-flash@6f8ad36ba7488db591541483afb5464d01e8861b # v2.14",
|
|
20904
|
-
"ruby/setup-ruby" => "ruby/setup-ruby@
|
|
21290
|
+
"ruby/setup-ruby" => "ruby/setup-ruby@de492cf36cbab201753d720ce4f4e336cd688c94 # v1.336.0",
|
|
20905
21291
|
"coverallsapp/github-action" => "coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8",
|
|
20906
|
-
"qltysh/qlty-action/coverage" => "qltysh/qlty-action/coverage@
|
|
21292
|
+
"qltysh/qlty-action/coverage" => "qltysh/qlty-action/coverage@ebcd284bf32e36a030afe15f1e136c7be08b9506 # v2.5.0",
|
|
20907
21293
|
"codecov/codecov-action" => "codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1",
|
|
20908
21294
|
"irongut/CodeCoverageSummary" => "irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0",
|
|
20909
21295
|
"marocchino/sticky-pull-request-comment" => "marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5",
|
|
20910
21296
|
"actions/upload-artifact" => "actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2",
|
|
21297
|
+
"actions/download-artifact" => "actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2",
|
|
20911
21298
|
"amancevice/setup-code-climate" => "amancevice/setup-code-climate@0daf2985a225e8ac15975b4d233010e94d65b76a # v2",
|
|
20912
21299
|
"actions/dependency-review-action" => "actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0",
|
|
20913
21300
|
"github/codeql-action/init" => "github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1",
|
|
20914
21301
|
"github/codeql-action/autobuild" => "github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1",
|
|
20915
21302
|
"github/codeql-action/analyze" => "github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1",
|
|
20916
|
-
"pozil/auto-assign-issue" => "pozil/auto-assign-issue@
|
|
21303
|
+
"pozil/auto-assign-issue" => "pozil/auto-assign-issue@d6e511896e5f1d2408e74db74416647d8ff80c8c # v4.1.0",
|
|
20917
21304
|
"apache/skywalking-eyes/dependency" => "apache/skywalking-eyes/dependency@a196742f472feaffafea537ce5a2a4c3c53a8de4 # v0.9.0",
|
|
20918
21305
|
"sarisia/actions-status-discord" => "sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0"
|
|
20919
21306
|
}
|