kettle-jem 7.1.28 → 7.1.29
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 +183 -1
- data/README.md +18 -2
- data/lib/kettle/jem/maintenance_changelog.rb +1 -1
- data/lib/kettle/jem/relative_paths.rb +61 -0
- data/lib/kettle/jem/tasks/install_task.rb +9 -5
- data/lib/kettle/jem/tasks/prepare_task.rb +29 -10
- data/lib/kettle/jem/tasks/template_task.rb +18 -3
- data/lib/kettle/jem/templates/.git-hooks/prepare-commit-msg.example +3 -4
- data/lib/kettle/jem/templates/.github/workflows/coverage.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/current.yml.example +8 -13
- data/lib/kettle/jem/templates/.github/workflows/dep-heads.yml.example +5 -5
- data/lib/kettle/jem/templates/.github/workflows/framework-ci.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/heads.yml.example +3 -63
- data/lib/kettle/jem/templates/.github/workflows/jruby-10.0.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/jruby-9.1.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/jruby-9.2.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/jruby-9.3.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/jruby-9.4.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/jruby.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/license-eye.yml.example +22 -1
- data/lib/kettle/jem/templates/.github/workflows/locked_deps.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/opencollective.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-2.3.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-2.4.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-2.5.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-2.6.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-2.7.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-3.0.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-3.1.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-3.2.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-3.3.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/ruby-3.4.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/style.yml.example +1 -1
- data/lib/kettle/jem/templates/.github/workflows/templating.yml.example +14 -1
- data/lib/kettle/jem/templates/.github/workflows/truffle.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/truffleruby-22.3.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/truffleruby-23.1.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/truffleruby-24.2.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/truffleruby-25.0.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/truffleruby-33.0.yml.example +2 -2
- data/lib/kettle/jem/templates/.github/workflows/unlocked_deps.yml.example +1 -1
- data/lib/kettle/jem/templates/.structuredmerge/kettle-jem.yml.example +19 -0
- data/lib/kettle/jem/templates/Appraisal.root.gemfile.example +2 -1
- data/lib/kettle/jem/templates/Appraisals.example +4 -1
- data/lib/kettle/jem/templates/CHANGELOG.transfer.md +6 -0
- data/lib/kettle/jem/templates/Gemfile.example +12 -3
- data/lib/kettle/jem/templates/README.md.example +5 -5
- data/lib/kettle/jem/templates/Rakefile.example +2 -0
- data/lib/kettle/jem/templates/bin/turbo_tests2.example +14 -0
- data/lib/kettle/jem/templates/gem.gemspec.example +6 -6
- data/lib/kettle/jem/templates/gemfiles/modular/coverage.gemfile.example +1 -1
- data/lib/kettle/jem/templates/gemfiles/modular/coverage_local.gemfile.example +10 -12
- data/lib/kettle/jem/templates/gemfiles/modular/documentation.gemfile.example +5 -5
- data/lib/kettle/jem/templates/gemfiles/modular/ostruct/r4/v0.6.gemfile.example +2 -0
- data/lib/kettle/jem/templates/gemfiles/modular/style.gemfile.example +15 -8
- data/lib/kettle/jem/templates/gemfiles/modular/style_local.gemfile.example +7 -1
- data/lib/kettle/jem/templates/gemfiles/modular/templating.gemfile.example +3 -2
- data/lib/kettle/jem/templates/gemfiles/modular/templating_local.gemfile.example +18 -19
- data/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r4/libs.gemfile.example +1 -0
- data/lib/kettle/jem/templates/mise.toml.example +1 -1
- data/lib/kettle/jem/templates/mise.toml.no-osc.example +1 -1
- data/lib/kettle/jem/templates/readme/partials/configuration.md.example +4 -3
- data/lib/kettle/jem/templates/shim/Gemfile.example +9 -3
- data/lib/kettle/jem/templates/shim/gem.gemspec.example +3 -3
- data/lib/kettle/jem/version.rb +1 -1
- data/lib/kettle/jem.rb +1156 -705
- data.tar.gz.sig +0 -0
- metadata +28 -67
- metadata.gz.sig +0 -0
- data/lib/kettle/jem/templates/.github/workflows/truffleruby-23.0.yml.example +0 -92
- data/lib/kettle/jem/templates/gemfiles/modular/json/truffleruby_23_0.gemfile.example +0 -6
data/lib/kettle/jem.rb
CHANGED
|
@@ -23,9 +23,12 @@ require "rbs"
|
|
|
23
23
|
require "kettle/dev"
|
|
24
24
|
require "kettle/changelog"
|
|
25
25
|
require "kettle/rb/compat_matrix"
|
|
26
|
+
require "kettle/rb/gem_floors"
|
|
27
|
+
require "kettle/rb/platform_support"
|
|
26
28
|
require_relative "jem/version"
|
|
27
29
|
require_relative "jem/license_txt_migrator"
|
|
28
30
|
require_relative "jem/maintenance_changelog"
|
|
31
|
+
require_relative "jem/relative_paths"
|
|
29
32
|
|
|
30
33
|
begin
|
|
31
34
|
require "kettle/drift"
|
|
@@ -57,11 +60,12 @@ module Kettle
|
|
|
57
60
|
# declarations in the destination and reconciles their complete requirement
|
|
58
61
|
# set in place before Bundler is allowed to evaluate that destination.
|
|
59
62
|
TEMPLATE_MANAGED_DEPENDENCIES = [
|
|
60
|
-
{name:
|
|
61
|
-
{name: "
|
|
63
|
+
{name: PACKAGE_NAME, requirements: ["~> 7.1", ">= 7.1.28"], bootstrap: false},
|
|
64
|
+
{name: "nomono", requirements: ["~> 1.1", ">= 1.1.7"], bootstrap: true},
|
|
65
|
+
{name: "kettle-dev", requirements: ["~> 3.1", ">= 3.1.11"], bootstrap: true},
|
|
62
66
|
{
|
|
63
67
|
name: "kettle-changelog",
|
|
64
|
-
requirements: ["~> 1.0", ">= 1.0.
|
|
68
|
+
requirements: ["~> 1.0", ">= 1.0.9"],
|
|
65
69
|
bootstrap: true,
|
|
66
70
|
ruby_requirement: ">= 4.0.0"
|
|
67
71
|
}
|
|
@@ -89,14 +93,99 @@ module Kettle
|
|
|
89
93
|
"kettle-test -I ../spec --options ../.rspec ../spec"
|
|
90
94
|
].freeze
|
|
91
95
|
DEFAULT_ENGINES = %w[ruby jruby truffleruby].freeze
|
|
96
|
+
# CI OS families a destination supports when it declares no `platforms:` key.
|
|
97
|
+
# Windows is opt-in only: Windows CI is materially slower and most gems do
|
|
98
|
+
# not need it, so a destination that wants it must declare it. The values
|
|
99
|
+
# are Kettle::Rb::PlatformSupport OS families, not Gem::Platform strings.
|
|
100
|
+
DEFAULT_PLATFORMS = %w[linux macos].freeze
|
|
101
|
+
# Matches GitHub Windows runner labels (windows-latest, windows-2022,
|
|
102
|
+
# windows-11-arm, ...) anywhere in a workflow file, which covers both a
|
|
103
|
+
# direct `runs-on:` and the `matrix.os` indirection. Validated against the
|
|
104
|
+
# packaged templates: their only Windows token is windows-latest, and
|
|
105
|
+
# `runs-on:` only ever appears as `${{ matrix.os }}` or an ubuntu label.
|
|
106
|
+
WINDOWS_RUNNER_LABEL_PATTERN = /windows-(?:latest|\d+)/i
|
|
107
|
+
# Documentation injected with `platforms:` by #migrate_platforms_config.
|
|
108
|
+
# Kept in sync with the packaged config template
|
|
109
|
+
# (templates/.structuredmerge/kettle-jem.yml.example).
|
|
110
|
+
PLATFORMS_CONFIG_DOCUMENTATION = [
|
|
111
|
+
"# List the OS platforms this project's CI exercises. kettle-jem uses this to",
|
|
112
|
+
"# generate the current.yml OS matrix and the README platform tier table.",
|
|
113
|
+
"# Windows applies only when declared: the `if: matrix.os` guards and the",
|
|
114
|
+
"# Windows-only test step are emitted solely for destinations that opt in.",
|
|
115
|
+
"#",
|
|
116
|
+
"# Supported values: linux, macos, windows",
|
|
117
|
+
"# Default (when key is absent): linux, macos",
|
|
118
|
+
"# These are CI OS families, a distinct vocabulary from Gem::Platform strings",
|
|
119
|
+
"# and Gemfile DSL platform symbols; never reuse them for",
|
|
120
|
+
"# `bundle lock --add-platform`."
|
|
121
|
+
].freeze
|
|
92
122
|
DEFAULT_OPENCOLLECTIVE_ORG = "galtzo-floss"
|
|
93
123
|
# These tools remain active, but their dependency ownership belongs to the
|
|
94
124
|
# generated modular Gemfiles rather than a destination gemspec.
|
|
95
125
|
EXTERNALIZED_GEMSPEC_DEVELOPMENT_DEPENDENCIES = %w[kettle-drift kettle-soup-cover rubocop-rspec yard-junk].freeze
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
126
|
+
# Gems superseded by a template-provided replacement, wrong everywhere
|
|
127
|
+
# regardless of whether the template places a replacement anywhere for
|
|
128
|
+
# this destination (case 2 of the dev-dependency conflict policy):
|
|
129
|
+
# unconditional removal, no review needed. gem-release is a legacy
|
|
130
|
+
# release tool - Kettle Dev owns release workflows. appraisal and
|
|
131
|
+
# turbo_tests were replaced by appraisal2 and turbo_tests2.
|
|
132
|
+
PROHIBITED_GEMSPEC_DEPENDENCIES = %w[gem-release appraisal turbo_tests].freeze
|
|
99
133
|
PROHIBITED_GEMFILE_DEPENDENCIES = PROHIBITED_GEMSPEC_DEPENDENCIES.freeze
|
|
134
|
+
# Git sources Bundler already defines. A Gemfile override (for example an
|
|
135
|
+
# SSH `git_source(:github)`) replaces Bundler's own HTTPS source and is
|
|
136
|
+
# recorded in Gemfile.lock, where CI cannot clone it: unconditional removal.
|
|
137
|
+
BUNDLER_BUILTIN_GIT_SOURCES = %w[github gist bitbucket].freeze
|
|
138
|
+
STRUCTUREDMERGE_GEMS = %w[
|
|
139
|
+
ast-crispr ast-crispr-markdown-markly ast-crispr-ruby-prism ast-merge ast-merge-git
|
|
140
|
+
ast-template bash-merge binary-merge citrus-toml-merge commonmarker-merge dotenv-merge
|
|
141
|
+
go-merge html-merge json-merge kettle-jem kramdown-merge markdown-merge markly-merge
|
|
142
|
+
parslet-toml-merge plain-merge prism-merge psych-merge rbs-merge ruby-merge rust-merge
|
|
143
|
+
smorg-rb toml-merge tree_haver typescript-merge yaml-merge zip-merge
|
|
144
|
+
].freeze
|
|
145
|
+
# Canonical resolutions for development-dependency gems whose handling is
|
|
146
|
+
# already known ahead of any specific destination project. Two
|
|
147
|
+
# categories today:
|
|
148
|
+
#
|
|
149
|
+
# :template_managed - the gem is always owned by a template modular
|
|
150
|
+
# Gemfile; a direct declaration is always a stale duplicate. Detected
|
|
151
|
+
# the same way as any other modular_dependency_conflicts entry (a
|
|
152
|
+
# direct declaration plus a modular declaration of the same gem
|
|
153
|
+
# name); when a newly surfaced conflict's gem matches one of these
|
|
154
|
+
# (and force_review isn't set), the real decided entry is written
|
|
155
|
+
# straight to dependency_conflicts.resolve instead of a placeholder
|
|
156
|
+
# "review" entry.
|
|
157
|
+
#
|
|
158
|
+
# :engine_incompatible - the gem doesn't support every engine the
|
|
159
|
+
# destination declares (config["engines"], falling back to
|
|
160
|
+
# DEFAULT_ENGINES) and has no template-managed modular home (below the
|
|
161
|
+
# template's inclusion threshold). Detected by #engine_dependency_conflicts, a
|
|
162
|
+
# separate engine-declaration-based check (comparing supported_engines
|
|
163
|
+
# against the destination's declared engines) rather than by
|
|
164
|
+
# modular_dependency_conflicts, since there's no modular-side
|
|
165
|
+
# declaration to match against. sqlite3 (the pilot entry) sets
|
|
166
|
+
# force_review: true because there's no single correct action across
|
|
167
|
+
# destinations - only the destination's own declared engines: decide
|
|
168
|
+
# it. A future entry in this category with force_review: false would
|
|
169
|
+
# auto-resolve via #engine_dependency_conflicts the same way
|
|
170
|
+
# :template_managed does via modular_dependency_conflicts (both
|
|
171
|
+
# funnel into the same auto_resolvable_known_gem_conflict? gate); its
|
|
172
|
+
# registry action would need to make sense with no modular
|
|
173
|
+
# counterpart to defer to, e.g. an unconditional removal.
|
|
174
|
+
KNOWN_GEM_CONFLICT_RESOLUTIONS = {
|
|
175
|
+
"debug" => {
|
|
176
|
+
category: :template_managed,
|
|
177
|
+
action: "remove_direct_gem",
|
|
178
|
+
reason: "debug is template-managed via gemfiles/modular/debug.gemfile (platform :mri); it ships a native C extension with no jruby/truffleruby release.",
|
|
179
|
+
force_review: false
|
|
180
|
+
}.freeze,
|
|
181
|
+
"sqlite3" => {
|
|
182
|
+
category: :engine_incompatible,
|
|
183
|
+
action: "review",
|
|
184
|
+
reason: "sqlite3 has no jruby-platform release on rubygems.org and is not template-managed; declare it in a modular gemfile with platforms: [:ruby] instead of a direct dependency (kettle-jem-appraisals generates gemfiles/modular/activerecord_support/*.gemfile, with the JDBC adapter on JRuby), or resolve per destination based on declared engines.",
|
|
185
|
+
supported_engines: %w[ruby].freeze,
|
|
186
|
+
force_review: true
|
|
187
|
+
}.freeze
|
|
188
|
+
}.freeze
|
|
100
189
|
FILE_DELETION_PRIMITIVES = %w[
|
|
101
190
|
supplied_obsolete_file_deletion
|
|
102
191
|
supplied_opt_in_workflow_deletion
|
|
@@ -423,7 +512,6 @@ module Kettle
|
|
|
423
512
|
TEMPLATE_SOURCE_APPLICATION_RECIPE = /\Atemplate_source_application_/
|
|
424
513
|
DECISION_NO_WRITE_ACTIONS = %w[keep skip].freeze
|
|
425
514
|
RUBY_TEMPLATE_POLICY_FILE_TYPES = %i[gemfile gemspec appraisals].freeze
|
|
426
|
-
GEMFILE_POLICY_SELF_DEPENDENCIES = %w[appraisal].freeze
|
|
427
515
|
TEMPLATE_CONTENT_PRIMITIVES = %w[
|
|
428
516
|
supplied_kettle_config_bootstrap
|
|
429
517
|
supplied_template_source_preference
|
|
@@ -484,6 +572,7 @@ module Kettle
|
|
|
484
572
|
KJ|MIN_RUBY
|
|
485
573
|
KJ|KETTLE_CHANGELOG_GEMFILE_DEPENDENCY
|
|
486
574
|
KJ|OPENCOLLECTIVE_ORG
|
|
575
|
+
KJ|RAKE:TOP_LEVEL_DOCS_TASK
|
|
487
576
|
KJ|README:COPYRIGHT_NOTICE
|
|
488
577
|
KJ|README:CORPORATE_SPONSORS
|
|
489
578
|
KJ|README:FAMILY_INTRO_BACKEND_MATRIX
|
|
@@ -533,39 +622,10 @@ module Kettle
|
|
|
533
622
|
}
|
|
534
623
|
])
|
|
535
624
|
README_TOP_LOGO_TYPES = %w[related_org ruby language org project affiliated_project].freeze
|
|
536
|
-
APPRAISAL_NAME_PREFIX = "kja"
|
|
537
|
-
APPRAISAL_GEM_ABBREVIATIONS = {
|
|
538
|
-
"activerecord" => "ar",
|
|
539
|
-
"actionmailer" => "am",
|
|
540
|
-
"actionpack" => "ap",
|
|
541
|
-
"activesupport" => "as",
|
|
542
|
-
"activejob" => "aj",
|
|
543
|
-
"actioncable" => "ac",
|
|
544
|
-
"actionview" => "av",
|
|
545
|
-
"activestorage" => "ast",
|
|
546
|
-
"actionmailbox" => "amb",
|
|
547
|
-
"actiontext" => "at",
|
|
548
|
-
"omniauth" => "oa",
|
|
549
|
-
"mongoid" => "mo",
|
|
550
|
-
"sequel" => "sq",
|
|
551
|
-
"couch_potato" => "cp",
|
|
552
|
-
"rom" => "rom",
|
|
553
|
-
"rom-sql" => "rsql"
|
|
554
|
-
}.freeze
|
|
555
|
-
APPRAISAL_WORKFLOW_LIFECYCLE_RANGES = Ractor.make_shareable({
|
|
556
|
-
"current" => {min: Gem::Version.new("3.4"), max: Gem::Version.new("3.99")},
|
|
557
|
-
"supported" => {min: Gem::Version.new("3.2"), max: Gem::Version.new("3.3")},
|
|
558
|
-
"legacy" => {min: Gem::Version.new("3.0"), max: Gem::Version.new("3.1")},
|
|
559
|
-
"unsupported" => {min: Gem::Version.new("2.6"), max: Gem::Version.new("2.7")},
|
|
560
|
-
"ancient" => {min: Gem::Version.new("2.3"), max: Gem::Version.new("2.5")}
|
|
561
|
-
})
|
|
562
|
-
APPRAISAL_ALWAYS_EXCLUDED_GEMS = %w[version_gem].freeze
|
|
563
|
-
APPRAISAL_VERSION_SELECTION_MODES = %w[major minor patch minor-minmax semver].freeze
|
|
564
625
|
DEFAULT_TEST_MINIMUM_RUBY = Gem::Version.new("2.4")
|
|
565
626
|
REQUIRE_RELATIVE_MIN_RUBY = Gem::Version.new("2.2").freeze
|
|
566
627
|
ANONYMOUS_VERSION_LOADER_MIN_RUBY = Gem::Version.new("2.2").freeze
|
|
567
628
|
MODERN_GEMSPEC_VERSION_LOADER_MIN_RUBY = Gem::Version.new("3.1").freeze
|
|
568
|
-
APPRAISAL_DEFAULT_FRESHNESS_TTL = 604_800
|
|
569
629
|
DECISION_ACTIONS = %w[create merge replace keep delete skip].freeze
|
|
570
630
|
DECISION_SEVERITIES = %w[advisory warning fatal].freeze
|
|
571
631
|
|
|
@@ -2609,6 +2669,74 @@ module Kettle
|
|
|
2609
2669
|
require "ast/crispr/ruby/prism"
|
|
2610
2670
|
@runtime_dependencies_loaded = true
|
|
2611
2671
|
end
|
|
2672
|
+
|
|
2673
|
+
# Grammars templating merges through tree_sitter_language_pack. Prefetching
|
|
2674
|
+
# them through TSLP's hot-load API downloads and loads the grammars up
|
|
2675
|
+
# front so the on-demand download cannot transiently fail inside a merge
|
|
2676
|
+
# and surface as a misleading "No parser registered" abort.
|
|
2677
|
+
TSLP_PREFETCH_LANGUAGES = %w[
|
|
2678
|
+
bash
|
|
2679
|
+
go
|
|
2680
|
+
html
|
|
2681
|
+
json
|
|
2682
|
+
json5
|
|
2683
|
+
markdown
|
|
2684
|
+
rbs
|
|
2685
|
+
ruby
|
|
2686
|
+
rust
|
|
2687
|
+
toml
|
|
2688
|
+
tsx
|
|
2689
|
+
typescript
|
|
2690
|
+
yaml
|
|
2691
|
+
].freeze
|
|
2692
|
+
|
|
2693
|
+
def tslp_prefetch_languages(env)
|
|
2694
|
+
override = env["KJ_TSLP_PREFETCH_LANGUAGES"]
|
|
2695
|
+
return TSLP_PREFETCH_LANGUAGES if override.nil? || override.to_s.strip.empty?
|
|
2696
|
+
|
|
2697
|
+
override.to_s.split(/[\s,]+/).map(&:downcase).uniq
|
|
2698
|
+
end
|
|
2699
|
+
|
|
2700
|
+
def prefetch_tslp_grammars(env: ENV, events: nil)
|
|
2701
|
+
override = env["KJ_TSLP_PREFETCH_LANGUAGES"]
|
|
2702
|
+
if DecisionPolicy.value_to_boolean(override) == false
|
|
2703
|
+
return {attempted: false, reason: "disabled via KJ_TSLP_PREFETCH_LANGUAGES"}
|
|
2704
|
+
end
|
|
2705
|
+
|
|
2706
|
+
languages = tslp_prefetch_languages(env)
|
|
2707
|
+
report = TreeHaver.prefetch_languages(languages)
|
|
2708
|
+
report[:languages] = languages
|
|
2709
|
+
if events
|
|
2710
|
+
emit_event(events, "diagnostic", {
|
|
2711
|
+
severity: report.fetch(:failures, {}).empty? ? "advisory" : "warning",
|
|
2712
|
+
category: "tslp_prefetch",
|
|
2713
|
+
message: tslp_prefetch_message(report)
|
|
2714
|
+
})
|
|
2715
|
+
end
|
|
2716
|
+
report
|
|
2717
|
+
rescue => error
|
|
2718
|
+
if events
|
|
2719
|
+
emit_event(events, "diagnostic", {
|
|
2720
|
+
severity: "warning",
|
|
2721
|
+
category: "tslp_prefetch",
|
|
2722
|
+
message: "TSLP grammar prefetch failed: #{error.class}: #{error.message}"
|
|
2723
|
+
})
|
|
2724
|
+
end
|
|
2725
|
+
{attempted: false, reason: "#{error.class}: #{error.message}"}
|
|
2726
|
+
end
|
|
2727
|
+
|
|
2728
|
+
def tslp_prefetch_message(report)
|
|
2729
|
+
return "TSLP grammar prefetch skipped: #{report[:reason]}" unless report.fetch(:attempted, false)
|
|
2730
|
+
|
|
2731
|
+
prefetched = Array(report[:prefetched])
|
|
2732
|
+
failures = report.fetch(:failures, {})
|
|
2733
|
+
message = "TSLP grammar prefetch loaded #{prefetched.length} grammar(s): #{prefetched.sort.join(", ")}"
|
|
2734
|
+
unless failures.empty?
|
|
2735
|
+
detail = failures.sort.map { |name, reason| "#{name}: #{reason}" }.join("; ")
|
|
2736
|
+
message += "; #{failures.length} failure(s): #{detail}"
|
|
2737
|
+
end
|
|
2738
|
+
message
|
|
2739
|
+
end
|
|
2612
2740
|
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
2613
2741
|
|
|
2614
2742
|
def display_path(path)
|
|
@@ -2650,546 +2778,6 @@ module Kettle
|
|
|
2650
2778
|
load(File.expand_path("jem/tasks.rb", __dir__))
|
|
2651
2779
|
end
|
|
2652
2780
|
|
|
2653
|
-
def appraisal_gem_abbreviation(gem_name)
|
|
2654
|
-
APPRAISAL_GEM_ABBREVIATIONS.fetch(gem_name.to_s, gem_name.to_s)
|
|
2655
|
-
end
|
|
2656
|
-
|
|
2657
|
-
def appraisal_format_version(version)
|
|
2658
|
-
version.to_s.tr(".", "-")
|
|
2659
|
-
end
|
|
2660
|
-
|
|
2661
|
-
def appraisal_name(tier1_gem:, tier1_version:, ruby_series:, tier2_gem: nil, tier2_version: nil)
|
|
2662
|
-
parts = [
|
|
2663
|
-
APPRAISAL_NAME_PREFIX,
|
|
2664
|
-
appraisal_gem_abbreviation(tier1_gem),
|
|
2665
|
-
appraisal_format_version(tier1_version)
|
|
2666
|
-
]
|
|
2667
|
-
unless tier2_gem.to_s.empty?
|
|
2668
|
-
parts << appraisal_gem_abbreviation(tier2_gem)
|
|
2669
|
-
parts << appraisal_format_version(tier2_version)
|
|
2670
|
-
end
|
|
2671
|
-
parts << ruby_series.to_s
|
|
2672
|
-
parts.join("-")
|
|
2673
|
-
end
|
|
2674
|
-
|
|
2675
|
-
def appraisal_modular_gemfile_path(gem_name:, version:, ruby_series:)
|
|
2676
|
-
File.join("gemfiles", "modular", gem_name.to_s, ruby_series.to_s, "v#{version}.gemfile")
|
|
2677
|
-
end
|
|
2678
|
-
|
|
2679
|
-
def appraisal_modular_gemfile_content(gem_name:, version:, sub_dependencies: {})
|
|
2680
|
-
lines = [
|
|
2681
|
-
"# frozen_string_literal: true",
|
|
2682
|
-
"",
|
|
2683
|
-
"# Generated by kettle-jem",
|
|
2684
|
-
"",
|
|
2685
|
-
%(gem "#{gem_name}", "#{appraisal_version_requirement(version)}")
|
|
2686
|
-
]
|
|
2687
|
-
sub_dependencies.each do |name, requirement|
|
|
2688
|
-
lines << %(gem "#{name}", "~> #{requirement}")
|
|
2689
|
-
end
|
|
2690
|
-
ensure_trailing_newline(lines.join("\n"))
|
|
2691
|
-
end
|
|
2692
|
-
|
|
2693
|
-
def appraisal_version_requirement(version)
|
|
2694
|
-
segments = version.to_s.split(".")
|
|
2695
|
-
(segments.length >= 3) ? "~> #{version}" : "~> #{version}.0"
|
|
2696
|
-
end
|
|
2697
|
-
|
|
2698
|
-
def appraisal_file_content(matrix_entries)
|
|
2699
|
-
lines = [
|
|
2700
|
-
"# frozen_string_literal: true",
|
|
2701
|
-
"",
|
|
2702
|
-
"# Generated by kettle-jem",
|
|
2703
|
-
"# Do not edit directly; regenerate from Kettle/Jem appraisal matrix metadata.",
|
|
2704
|
-
""
|
|
2705
|
-
]
|
|
2706
|
-
matrix_entries.each do |entry|
|
|
2707
|
-
lines << %(appraise "#{entry.fetch(:name)}" do)
|
|
2708
|
-
lines << %( eval_gemfile "#{appraisal_eval_gemfile_path(entry.fetch(:tier1_gemfile))}") if entry[:tier1_gemfile]
|
|
2709
|
-
lines << %( eval_gemfile "#{appraisal_eval_gemfile_path(entry.fetch(:tier2_gemfile))}") if entry[:tier2_gemfile]
|
|
2710
|
-
lines << %( eval_gemfile "#{appraisal_eval_gemfile_path(entry.fetch(:x_std_libs_gemfile))}") if entry[:x_std_libs_gemfile]
|
|
2711
|
-
lines << "end"
|
|
2712
|
-
lines << ""
|
|
2713
|
-
end
|
|
2714
|
-
lines.pop while lines.last == ""
|
|
2715
|
-
ensure_trailing_newline(lines.join("\n"))
|
|
2716
|
-
end
|
|
2717
|
-
|
|
2718
|
-
def appraisal_eval_gemfile_path(path)
|
|
2719
|
-
path.to_s.delete_prefix("gemfiles/")
|
|
2720
|
-
end
|
|
2721
|
-
|
|
2722
|
-
def appraisal_workflow_groups(matrix_entries, bucket_ranges:, exec_cmd: "kettle-test")
|
|
2723
|
-
grouped = Hash.new { |hash, key| hash[key] = [] }
|
|
2724
|
-
normalized_ranges = bucket_ranges.transform_values do |range|
|
|
2725
|
-
{
|
|
2726
|
-
floor: Gem::Version.new((range[:floor] || range["floor"]).to_s),
|
|
2727
|
-
ceiling: Gem::Version.new((range[:ceiling] || range["ceiling"]).to_s)
|
|
2728
|
-
}
|
|
2729
|
-
end
|
|
2730
|
-
matrix_entries.each do |entry|
|
|
2731
|
-
ruby_series = entry[:ruby_series] || entry["ruby_series"]
|
|
2732
|
-
range = normalized_ranges[ruby_series]
|
|
2733
|
-
next unless range
|
|
2734
|
-
|
|
2735
|
-
lifecycle = appraisal_workflow_lifecycle(range.fetch(:floor))
|
|
2736
|
-
grouped[lifecycle] << {
|
|
2737
|
-
ruby: appraisal_workflow_ruby(range.fetch(:floor), lifecycle),
|
|
2738
|
-
appraisal: entry[:name] || entry["name"],
|
|
2739
|
-
exec_cmd: exec_cmd,
|
|
2740
|
-
rubygems: "latest",
|
|
2741
|
-
bundler: "latest"
|
|
2742
|
-
}
|
|
2743
|
-
end
|
|
2744
|
-
grouped.transform_values { |entries| entries.sort_by { |entry| entry.fetch(:appraisal).to_s } }
|
|
2745
|
-
end
|
|
2746
|
-
|
|
2747
|
-
def appraisal_workflow_yaml_snippets(matrix_entries, bucket_ranges:, exec_cmd: "kettle-test")
|
|
2748
|
-
appraisal_workflow_groups(matrix_entries, bucket_ranges: bucket_ranges, exec_cmd: exec_cmd).transform_values do |entries|
|
|
2749
|
-
lines = ["strategy:", " matrix:", " include:"]
|
|
2750
|
-
entries.each do |entry|
|
|
2751
|
-
lines << %( - ruby: "#{entry.fetch(:ruby)}")
|
|
2752
|
-
lines << %( appraisal: "#{entry.fetch(:appraisal)}")
|
|
2753
|
-
lines << %( exec_cmd: "#{entry.fetch(:exec_cmd)}")
|
|
2754
|
-
lines << %( rubygems: "#{entry.fetch(:rubygems)}")
|
|
2755
|
-
lines << %( bundler: "#{entry.fetch(:bundler)}")
|
|
2756
|
-
end
|
|
2757
|
-
lines.join("\n")
|
|
2758
|
-
end
|
|
2759
|
-
end
|
|
2760
|
-
|
|
2761
|
-
def appraisal_workflow_lifecycle(ruby_floor)
|
|
2762
|
-
APPRAISAL_WORKFLOW_LIFECYCLE_RANGES.each do |name, range|
|
|
2763
|
-
return name if ruby_floor.between?(range.fetch(:min), range.fetch(:max))
|
|
2764
|
-
end
|
|
2765
|
-
(ruby_floor < APPRAISAL_WORKFLOW_LIFECYCLE_RANGES.fetch("ancient").fetch(:min)) ? "ancient" : "current"
|
|
2766
|
-
end
|
|
2767
|
-
|
|
2768
|
-
def appraisal_workflow_ruby(ruby_floor, lifecycle)
|
|
2769
|
-
return "ruby" if lifecycle == "current"
|
|
2770
|
-
|
|
2771
|
-
segments = ruby_floor.segments
|
|
2772
|
-
"#{segments[0]}.#{segments[1] || 0}"
|
|
2773
|
-
end
|
|
2774
|
-
|
|
2775
|
-
def appraisal_x_stdlib_exclusions(template_content)
|
|
2776
|
-
gems = ruby_call_records(template_content, :eval_gemfile).filter_map do |call|
|
|
2777
|
-
path = ruby_string_argument(call).to_s
|
|
2778
|
-
next unless path.start_with?("../")
|
|
2779
|
-
|
|
2780
|
-
path.delete_prefix("../").split("/").first
|
|
2781
|
-
end
|
|
2782
|
-
(gems + APPRAISAL_ALWAYS_EXCLUDED_GEMS).uniq.sort
|
|
2783
|
-
end
|
|
2784
|
-
|
|
2785
|
-
def appraisal_select_versions(version_metadata, mode:, requirements: nil)
|
|
2786
|
-
mode = mode.to_s
|
|
2787
|
-
raise ArgumentError, "invalid appraisal version selection mode: #{mode}" unless APPRAISAL_VERSION_SELECTION_MODES.include?(mode)
|
|
2788
|
-
|
|
2789
|
-
versions = appraisal_filtered_versions(version_metadata, requirements: requirements)
|
|
2790
|
-
return versions if mode == "patch"
|
|
2791
|
-
|
|
2792
|
-
by_major = appraisal_minor_versions_by_major(versions)
|
|
2793
|
-
return [] if by_major.empty?
|
|
2794
|
-
|
|
2795
|
-
current_major = by_major.last.fetch(:major)
|
|
2796
|
-
case mode
|
|
2797
|
-
when "major"
|
|
2798
|
-
by_major.map { |entry| entry.fetch(:minors).last }
|
|
2799
|
-
when "minor"
|
|
2800
|
-
by_major.flat_map { |entry| entry.fetch(:minors) }
|
|
2801
|
-
when "minor-minmax"
|
|
2802
|
-
by_major.flat_map do |entry|
|
|
2803
|
-
minors = entry.fetch(:minors)
|
|
2804
|
-
(entry.fetch(:major) < current_major) ? [minors.first, minors.last].uniq : minors
|
|
2805
|
-
end
|
|
2806
|
-
when "semver"
|
|
2807
|
-
by_major.flat_map do |entry|
|
|
2808
|
-
(entry.fetch(:major) < current_major) ? [entry.fetch(:minors).last] : entry.fetch(:minors)
|
|
2809
|
-
end
|
|
2810
|
-
end
|
|
2811
|
-
end
|
|
2812
|
-
|
|
2813
|
-
def appraisal_matrix_entries(tier1_gems:, tier2_gems: [])
|
|
2814
|
-
entries = []
|
|
2815
|
-
tier1_gems.each do |tier1|
|
|
2816
|
-
tier1_name = tier1[:name] || tier1["name"]
|
|
2817
|
-
assignments = tier1[:assignments] || tier1["assignments"] || []
|
|
2818
|
-
assignments.each do |assignment|
|
|
2819
|
-
tier1_version = assignment[:version] || assignment["version"]
|
|
2820
|
-
ruby_series = assignment[:bucket] || assignment["bucket"] || assignment[:ruby_series] || assignment["ruby_series"]
|
|
2821
|
-
if tier2_gems.empty?
|
|
2822
|
-
entries << appraisal_matrix_entry(
|
|
2823
|
-
tier1_name: tier1_name,
|
|
2824
|
-
tier1_version: tier1_version,
|
|
2825
|
-
ruby_series: ruby_series
|
|
2826
|
-
)
|
|
2827
|
-
else
|
|
2828
|
-
tier2_gems.each do |tier2|
|
|
2829
|
-
tier2_name = tier2[:name] || tier2["name"]
|
|
2830
|
-
Array(tier2[:versions] || tier2["versions"]).each do |tier2_version|
|
|
2831
|
-
entries << appraisal_matrix_entry(
|
|
2832
|
-
tier1_name: tier1_name,
|
|
2833
|
-
tier1_version: tier1_version,
|
|
2834
|
-
ruby_series: ruby_series,
|
|
2835
|
-
tier2_name: tier2_name,
|
|
2836
|
-
tier2_version: tier2_version
|
|
2837
|
-
)
|
|
2838
|
-
end
|
|
2839
|
-
end
|
|
2840
|
-
end
|
|
2841
|
-
end
|
|
2842
|
-
end
|
|
2843
|
-
entries
|
|
2844
|
-
end
|
|
2845
|
-
|
|
2846
|
-
def appraisal_matrix_entry(tier1_name:, tier1_version:, ruby_series:, tier2_name: nil, tier2_version: nil)
|
|
2847
|
-
{
|
|
2848
|
-
name: appraisal_name(
|
|
2849
|
-
tier1_gem: tier1_name,
|
|
2850
|
-
tier1_version: tier1_version,
|
|
2851
|
-
tier2_gem: tier2_name,
|
|
2852
|
-
tier2_version: tier2_version,
|
|
2853
|
-
ruby_series: ruby_series
|
|
2854
|
-
),
|
|
2855
|
-
tier1_gemfile: appraisal_modular_gemfile_path(gem_name: tier1_name, version: tier1_version, ruby_series: ruby_series),
|
|
2856
|
-
tier2_gemfile: tier2_name ? appraisal_modular_gemfile_path(gem_name: tier2_name, version: tier2_version, ruby_series: ruby_series) : nil,
|
|
2857
|
-
x_std_libs_gemfile: File.join("gemfiles", "modular", "x_std_libs", ruby_series.to_s, "libs.gemfile"),
|
|
2858
|
-
ruby_series: ruby_series.to_s
|
|
2859
|
-
}
|
|
2860
|
-
end
|
|
2861
|
-
|
|
2862
|
-
def appraisal_filtered_versions(version_metadata, requirements:)
|
|
2863
|
-
requirement = requirements ? Gem::Requirement.new(Array(requirements)) : nil
|
|
2864
|
-
version_metadata.filter_map do |entry|
|
|
2865
|
-
number = entry[:number] || entry["number"]
|
|
2866
|
-
next if number.to_s.empty?
|
|
2867
|
-
next if entry[:prerelease] || entry["prerelease"]
|
|
2868
|
-
next if requirement && !requirement.satisfied_by?(Gem::Version.new(number))
|
|
2869
|
-
|
|
2870
|
-
number.to_s
|
|
2871
|
-
end.sort_by { |version| Gem::Version.new(version) }
|
|
2872
|
-
end
|
|
2873
|
-
|
|
2874
|
-
def appraisal_minor_versions_by_major(versions)
|
|
2875
|
-
versions.map do |version|
|
|
2876
|
-
gem_version = Gem::Version.new(version)
|
|
2877
|
-
segments = gem_version.segments
|
|
2878
|
-
{
|
|
2879
|
-
major: segments[0],
|
|
2880
|
-
minor: "#{segments[0]}.#{segments[1] || 0}"
|
|
2881
|
-
}
|
|
2882
|
-
end.uniq.group_by { |entry| entry.fetch(:major) }.map do |major, entries|
|
|
2883
|
-
{
|
|
2884
|
-
major: major,
|
|
2885
|
-
minors: entries.map { |entry| entry.fetch(:minor) }.sort_by { |minor| Gem::Version.new(minor) }
|
|
2886
|
-
}
|
|
2887
|
-
end.sort_by { |entry| entry.fetch(:major) }
|
|
2888
|
-
end
|
|
2889
|
-
|
|
2890
|
-
def appraisal_find_ruby_seams(version_metadata)
|
|
2891
|
-
minors = appraisal_latest_patch_by_minor(version_metadata)
|
|
2892
|
-
seams = []
|
|
2893
|
-
previous = nil
|
|
2894
|
-
minors.sort_by { |minor, _entry| Gem::Version.new(minor) }.each do |minor, entry|
|
|
2895
|
-
min_ruby = Gem::Version.new((entry[:min_ruby] || entry["min_ruby"]).to_s)
|
|
2896
|
-
min_ruby = [min_ruby, DEFAULT_TEST_MINIMUM_RUBY].max
|
|
2897
|
-
if previous.nil? || min_ruby > previous
|
|
2898
|
-
seams << {version: minor, min_ruby: min_ruby}
|
|
2899
|
-
end
|
|
2900
|
-
previous = min_ruby
|
|
2901
|
-
end
|
|
2902
|
-
seams
|
|
2903
|
-
end
|
|
2904
|
-
|
|
2905
|
-
def appraisal_ruby_series(version_metadata, project_min_ruby: nil)
|
|
2906
|
-
floors = appraisal_find_ruby_seams(version_metadata).map { |seam| seam.fetch(:min_ruby) }
|
|
2907
|
-
if project_min_ruby
|
|
2908
|
-
floor = Gem::Version.new(project_min_ruby.to_s)
|
|
2909
|
-
floors.reject! { |version| version < floor }
|
|
2910
|
-
floors << floor unless floors.include?(floor)
|
|
2911
|
-
end
|
|
2912
|
-
floors = [Gem::Version.new("3.2")] if floors.empty?
|
|
2913
|
-
appraisal_minor_versions_to_buckets(floors.map { |version| appraisal_minor_key(version) }.uniq.sort)
|
|
2914
|
-
end
|
|
2915
|
-
|
|
2916
|
-
def appraisal_assign_version_buckets(selected_versions:, seams:, buckets:, bucket_ranges:, all_versions:)
|
|
2917
|
-
return [] if selected_versions.empty? || buckets.empty?
|
|
2918
|
-
|
|
2919
|
-
normalized_ranges = appraisal_normalized_bucket_ranges(bucket_ranges)
|
|
2920
|
-
version_min_rubies = appraisal_version_min_ruby_map(all_versions, seams)
|
|
2921
|
-
assignments = selected_versions.sort_by { |version| Gem::Version.new(version) }.filter_map do |version|
|
|
2922
|
-
min_ruby = version_min_rubies[version]
|
|
2923
|
-
next unless min_ruby
|
|
2924
|
-
|
|
2925
|
-
next_seam = appraisal_next_seam_ruby(version, min_ruby, all_versions, version_min_rubies)
|
|
2926
|
-
bucket = next_seam ? appraisal_bucket_below(next_seam, buckets, normalized_ranges) : buckets.last
|
|
2927
|
-
{version: version, bucket: bucket} if bucket
|
|
2928
|
-
end
|
|
2929
|
-
appraisal_fill_bucket_gaps(assignments, buckets, normalized_ranges, version_min_rubies, all_versions)
|
|
2930
|
-
end
|
|
2931
|
-
|
|
2932
|
-
def appraisal_latest_patch_by_minor(version_metadata)
|
|
2933
|
-
version_metadata.each_with_object({}) do |entry, latest|
|
|
2934
|
-
number = (entry[:number] || entry["number"]).to_s
|
|
2935
|
-
next if number.empty?
|
|
2936
|
-
|
|
2937
|
-
version = Gem::Version.new(number)
|
|
2938
|
-
minor = "#{version.segments[0]}.#{version.segments[1] || 0}"
|
|
2939
|
-
current = latest[minor]
|
|
2940
|
-
latest[minor] = entry if current.nil? || version > Gem::Version.new((current[:number] || current["number"]).to_s)
|
|
2941
|
-
end
|
|
2942
|
-
end
|
|
2943
|
-
|
|
2944
|
-
def appraisal_minor_versions_to_buckets(minor_versions)
|
|
2945
|
-
by_major = minor_versions.group_by { |minor| minor.split(".").first.to_i }
|
|
2946
|
-
buckets = []
|
|
2947
|
-
ranges = {}
|
|
2948
|
-
by_major.each do |major, minors|
|
|
2949
|
-
sorted = minors.sort_by { |minor| Gem::Version.new(minor) }
|
|
2950
|
-
sorted.each_with_index do |minor, index|
|
|
2951
|
-
bucket = (index == sorted.length - 1) ? "r#{major}" : "r#{major}.#{[sorted[index + 1].split(".").last.to_i - 1, minor.split(".").last.to_i].max}"
|
|
2952
|
-
next if ranges.key?(bucket)
|
|
2953
|
-
|
|
2954
|
-
buckets << bucket
|
|
2955
|
-
ceiling = if index == sorted.length - 1
|
|
2956
|
-
"#{major}.99"
|
|
2957
|
-
else
|
|
2958
|
-
bucket.split(".").last ? "#{major}.#{bucket.split(".").last}" : "#{major}.99"
|
|
2959
|
-
end
|
|
2960
|
-
ranges[bucket] = {floor: Gem::Version.new(minor), ceiling: Gem::Version.new(ceiling)}
|
|
2961
|
-
end
|
|
2962
|
-
end
|
|
2963
|
-
{buckets: buckets.sort_by { |bucket| appraisal_bucket_sort_key(bucket) }, bucket_ranges: ranges}
|
|
2964
|
-
end
|
|
2965
|
-
|
|
2966
|
-
def appraisal_minor_key(version)
|
|
2967
|
-
segments = Gem::Version.new(version.to_s).segments
|
|
2968
|
-
"#{segments[0]}.#{segments[1] || 0}"
|
|
2969
|
-
end
|
|
2970
|
-
|
|
2971
|
-
def appraisal_bucket_sort_key(bucket)
|
|
2972
|
-
match = bucket.to_s.match(/\Ar(\d+)(?:\.(\d+))?\z/)
|
|
2973
|
-
[match[1].to_i, match[2] ? match[2].to_i : 999]
|
|
2974
|
-
end
|
|
2975
|
-
|
|
2976
|
-
def appraisal_normalized_bucket_ranges(bucket_ranges)
|
|
2977
|
-
bucket_ranges.transform_values do |range|
|
|
2978
|
-
{
|
|
2979
|
-
floor: Gem::Version.new((range[:floor] || range["floor"]).to_s),
|
|
2980
|
-
ceiling: Gem::Version.new((range[:ceiling] || range["ceiling"]).to_s)
|
|
2981
|
-
}
|
|
2982
|
-
end
|
|
2983
|
-
end
|
|
2984
|
-
|
|
2985
|
-
def appraisal_version_min_ruby_map(all_versions, seams)
|
|
2986
|
-
sorted_seams = seams.sort_by { |seam| Gem::Version.new(seam[:version] || seam["version"]) }
|
|
2987
|
-
seam_index = 0
|
|
2988
|
-
current = nil
|
|
2989
|
-
all_versions.sort_by { |version| Gem::Version.new(version) }.each_with_object({}) do |version, map|
|
|
2990
|
-
while seam_index < sorted_seams.length && Gem::Version.new(sorted_seams[seam_index][:version] || sorted_seams[seam_index]["version"]) <= Gem::Version.new(version)
|
|
2991
|
-
current = Gem::Version.new((sorted_seams[seam_index][:min_ruby] || sorted_seams[seam_index]["min_ruby"]).to_s)
|
|
2992
|
-
current = [current, DEFAULT_TEST_MINIMUM_RUBY].max
|
|
2993
|
-
seam_index += 1
|
|
2994
|
-
end
|
|
2995
|
-
map[version] = current if current
|
|
2996
|
-
end
|
|
2997
|
-
end
|
|
2998
|
-
|
|
2999
|
-
def appraisal_next_seam_ruby(version, min_ruby, all_versions, version_min_rubies)
|
|
3000
|
-
found = false
|
|
3001
|
-
all_versions.sort_by { |candidate| Gem::Version.new(candidate) }.each do |candidate|
|
|
3002
|
-
found ||= Gem::Version.new(candidate) >= Gem::Version.new(version)
|
|
3003
|
-
next unless found
|
|
3004
|
-
|
|
3005
|
-
candidate_ruby = version_min_rubies[candidate]
|
|
3006
|
-
return candidate_ruby if candidate_ruby && candidate_ruby > min_ruby
|
|
3007
|
-
end
|
|
3008
|
-
nil
|
|
3009
|
-
end
|
|
3010
|
-
|
|
3011
|
-
def appraisal_bucket_below(ruby_floor, buckets, bucket_ranges)
|
|
3012
|
-
buckets.filter_map do |bucket|
|
|
3013
|
-
range = bucket_ranges[bucket]
|
|
3014
|
-
next unless range && range.fetch(:ceiling) < ruby_floor
|
|
3015
|
-
|
|
3016
|
-
[bucket, range.fetch(:ceiling)]
|
|
3017
|
-
end.max_by(&:last)&.first
|
|
3018
|
-
end
|
|
3019
|
-
|
|
3020
|
-
def appraisal_fill_bucket_gaps(assignments, buckets, bucket_ranges, version_min_rubies, all_versions)
|
|
3021
|
-
covered = assignments.map { |assignment| assignment.fetch(:bucket) }
|
|
3022
|
-
(buckets - covered).each do |bucket|
|
|
3023
|
-
range = bucket_ranges[bucket]
|
|
3024
|
-
next unless range
|
|
3025
|
-
|
|
3026
|
-
filler = all_versions.sort_by { |version| Gem::Version.new(version) }.reverse.find do |version|
|
|
3027
|
-
ruby = version_min_rubies[version]
|
|
3028
|
-
ruby&.between?(range.fetch(:floor), range.fetch(:ceiling))
|
|
3029
|
-
end
|
|
3030
|
-
filler ||= all_versions.sort_by { |version| Gem::Version.new(version) }.reverse.find do |version|
|
|
3031
|
-
ruby = version_min_rubies[version]
|
|
3032
|
-
ruby && ruby <= range.fetch(:ceiling)
|
|
3033
|
-
end
|
|
3034
|
-
assignments << {version: filler, bucket: bucket, filler: true} if filler
|
|
3035
|
-
end
|
|
3036
|
-
assignments.sort_by { |assignment| bucket_ranges.fetch(assignment.fetch(:bucket)).fetch(:floor) }
|
|
3037
|
-
end
|
|
3038
|
-
|
|
3039
|
-
def appraisal_resolve_sub_dependencies(parent_gem:, parent_version:, parent_versions:, dependency_versions:, ruby_min: nil, excluded_gems: [])
|
|
3040
|
-
parent = appraisal_latest_version_matching(parent_versions, parent_version)
|
|
3041
|
-
return {} unless parent
|
|
3042
|
-
|
|
3043
|
-
ruby_floor = Gem::Version.new(ruby_min.to_s) unless ruby_min.to_s.empty?
|
|
3044
|
-
excluded = excluded_gems.map(&:to_s)
|
|
3045
|
-
Array(parent[:runtime_dependencies] || parent["runtime_dependencies"]).each_with_object({}) do |dependency, resolved|
|
|
3046
|
-
name = (dependency[:name] || dependency["name"]).to_s
|
|
3047
|
-
next if name.empty? || excluded.include?(name)
|
|
3048
|
-
|
|
3049
|
-
requirement = begin
|
|
3050
|
-
Gem::Requirement.new(dependency[:requirements] || dependency["requirements"] || ">= 0")
|
|
3051
|
-
rescue ArgumentError
|
|
3052
|
-
Gem::Requirement.default
|
|
3053
|
-
end
|
|
3054
|
-
selected = appraisal_select_dependency_version(
|
|
3055
|
-
Array(dependency_versions[name] || dependency_versions[name.to_sym]),
|
|
3056
|
-
requirement: requirement,
|
|
3057
|
-
ruby_min: ruby_floor
|
|
3058
|
-
)
|
|
3059
|
-
resolved[name] = selected if selected
|
|
3060
|
-
end
|
|
3061
|
-
end
|
|
3062
|
-
|
|
3063
|
-
def appraisal_latest_version_matching(version_metadata, requested_version)
|
|
3064
|
-
prefix = "#{requested_version}."
|
|
3065
|
-
version_metadata.select do |entry|
|
|
3066
|
-
number = (entry[:number] || entry["number"]).to_s
|
|
3067
|
-
number == requested_version.to_s || number.start_with?(prefix)
|
|
3068
|
-
end.max_by { |entry| Gem::Version.new((entry[:number] || entry["number"]).to_s) }
|
|
3069
|
-
end
|
|
3070
|
-
|
|
3071
|
-
def appraisal_select_dependency_version(version_metadata, requirement:, ruby_min:)
|
|
3072
|
-
compatible = version_metadata.select do |entry|
|
|
3073
|
-
number = (entry[:number] || entry["number"]).to_s
|
|
3074
|
-
!number.empty? && requirement.satisfied_by?(Gem::Version.new(number))
|
|
3075
|
-
end.sort_by { |entry| Gem::Version.new((entry[:number] || entry["number"]).to_s) }
|
|
3076
|
-
return if compatible.empty?
|
|
3077
|
-
|
|
3078
|
-
if ruby_min
|
|
3079
|
-
selected = compatible.reverse.find do |entry|
|
|
3080
|
-
min_ruby = entry[:min_ruby] || entry["min_ruby"]
|
|
3081
|
-
min_ruby.to_s.empty? || Gem::Version.new(min_ruby.to_s) <= ruby_min
|
|
3082
|
-
end
|
|
3083
|
-
return (selected || compatible.first).then { |entry| entry[:number] || entry["number"] }
|
|
3084
|
-
end
|
|
3085
|
-
compatible.last.then { |entry| entry[:number] || entry["number"] }
|
|
3086
|
-
end
|
|
3087
|
-
|
|
3088
|
-
def appraisal_stale_gemfile_paths(existing_paths:, current_entries:)
|
|
3089
|
-
current_names = current_entries.map { |entry| (entry[:name] || entry["name"]).to_s }.to_set
|
|
3090
|
-
existing_paths.map(&:to_s).select do |path|
|
|
3091
|
-
basename = File.basename(path, ".gemfile")
|
|
3092
|
-
path.start_with?("gemfiles/#{APPRAISAL_NAME_PREFIX}-") &&
|
|
3093
|
-
path.end_with?(".gemfile") &&
|
|
3094
|
-
!current_names.include?(basename)
|
|
3095
|
-
end.sort
|
|
3096
|
-
end
|
|
3097
|
-
|
|
3098
|
-
def appraisal_extract_runtime_dependencies(gemspec_content)
|
|
3099
|
-
gemspec_dependency_records(gemspec_content)
|
|
3100
|
-
.reject { |record| record.fetch(:kind) == "add_development_dependency" }
|
|
3101
|
-
.map { |record| record.fetch(:name) }
|
|
3102
|
-
.uniq
|
|
3103
|
-
end
|
|
3104
|
-
|
|
3105
|
-
def appraisal_scaffold_config(gemspec_content:, existing_config: {}, exclusions: [], default_mode: "semver", freshness_ttl: APPRAISAL_DEFAULT_FRESHNESS_TTL)
|
|
3106
|
-
excluded = exclusions.map(&:to_s).to_set
|
|
3107
|
-
runtime_dependencies = appraisal_extract_runtime_dependencies(gemspec_content)
|
|
3108
|
-
tier1 = runtime_dependencies.reject { |name| excluded.include?(name) }.map { |name| {"name" => name} }
|
|
3109
|
-
config = deep_string_key_hash(existing_config)
|
|
3110
|
-
matrix = config["appraisal_matrix"] || {}
|
|
3111
|
-
gems = matrix["gems"] || {}
|
|
3112
|
-
|
|
3113
|
-
matrix["mode"] ||= default_mode
|
|
3114
|
-
matrix["freshness_ttl"] ||= freshness_ttl
|
|
3115
|
-
gems["tier1"] = tier1
|
|
3116
|
-
gems["tier2"] ||= []
|
|
3117
|
-
matrix["gems"] = gems
|
|
3118
|
-
config["appraisal_matrix"] = matrix
|
|
3119
|
-
config
|
|
3120
|
-
end
|
|
3121
|
-
|
|
3122
|
-
def appraisal_matrix_has_versions?(matrix)
|
|
3123
|
-
gems = deep_string_key_hash(matrix || {}).fetch("gems", {})
|
|
3124
|
-
%w[tier1 tier2].any? do |tier|
|
|
3125
|
-
Array(gems[tier]).any? { |gem_config| Array(gem_config["versions"]).any? }
|
|
3126
|
-
end
|
|
3127
|
-
end
|
|
3128
|
-
|
|
3129
|
-
def appraisal_matrix_fresh?(matrix, now: Time.now.to_i)
|
|
3130
|
-
resolved_at = (matrix || {})[:resolved_at] || (matrix || {})["resolved_at"]
|
|
3131
|
-
return false unless resolved_at
|
|
3132
|
-
|
|
3133
|
-
ttl = (matrix || {})[:freshness_ttl] || (matrix || {})["freshness_ttl"] || APPRAISAL_DEFAULT_FRESHNESS_TTL
|
|
3134
|
-
(now.to_i - resolved_at.to_i) < ttl.to_i
|
|
3135
|
-
end
|
|
3136
|
-
|
|
3137
|
-
def appraisal_time_ago(timestamp, now: Time.now.to_i)
|
|
3138
|
-
return "unknown" unless timestamp
|
|
3139
|
-
|
|
3140
|
-
seconds = now.to_i - timestamp.to_i
|
|
3141
|
-
return "#{seconds / 60}m" if seconds < 3600
|
|
3142
|
-
return "#{seconds / 3600}h" if seconds < 86_400
|
|
3143
|
-
|
|
3144
|
-
"#{seconds / 86_400}d"
|
|
3145
|
-
end
|
|
3146
|
-
|
|
3147
|
-
def appraisal_all_versions_for(resolver:, gem_name:, mode:, requirements: nil, include_versions: nil, exclude_versions: nil)
|
|
3148
|
-
base_versions = if mode.to_s == "patch"
|
|
3149
|
-
resolver.versions(gem_name, requirements: requirements).map { |entry| entry[:number] || entry["number"] }
|
|
3150
|
-
else
|
|
3151
|
-
resolver.minor_versions_by_major(gem_name, requirements: requirements).flat_map { |entry| entry[:minors] || entry["minors"] }
|
|
3152
|
-
end
|
|
3153
|
-
appraisal_finalize_versions(base_versions, include_versions: include_versions, exclude_versions: exclude_versions)
|
|
3154
|
-
end
|
|
3155
|
-
|
|
3156
|
-
def appraisal_finalize_versions(base_versions, include_versions: nil, exclude_versions: nil)
|
|
3157
|
-
merged = appraisal_sort_versions(Array(base_versions) + Array(include_versions))
|
|
3158
|
-
excluded = Array(exclude_versions).map(&:to_s).to_set
|
|
3159
|
-
return merged if excluded.empty?
|
|
3160
|
-
|
|
3161
|
-
appraisal_sort_versions(merged.reject { |version| excluded.include?(version) })
|
|
3162
|
-
end
|
|
3163
|
-
|
|
3164
|
-
def appraisal_compatible_version_for_bucket?(resolver:, gem_name:, version:, ruby_series:, bucket_ranges:)
|
|
3165
|
-
range = bucket_ranges[ruby_series] || bucket_ranges[ruby_series.to_s]
|
|
3166
|
-
return true unless range
|
|
3167
|
-
|
|
3168
|
-
ceiling = Gem::Version.new((range[:ceiling] || range["ceiling"]).to_s)
|
|
3169
|
-
exact_version = appraisal_latest_minor_patch(resolver: resolver, gem_name: gem_name, version: version)
|
|
3170
|
-
min_ruby = resolver.min_ruby_version(gem_name, exact_version)
|
|
3171
|
-
min_ruby.nil? || Gem::Version.new(min_ruby.to_s) <= ceiling
|
|
3172
|
-
rescue
|
|
3173
|
-
true
|
|
3174
|
-
end
|
|
3175
|
-
|
|
3176
|
-
def appraisal_latest_minor_patch(resolver:, gem_name:, version:)
|
|
3177
|
-
all_versions = resolver.versions(gem_name)
|
|
3178
|
-
prefix = "#{version}."
|
|
3179
|
-
matching = all_versions.select do |entry|
|
|
3180
|
-
number = (entry[:number] || entry["number"]).to_s
|
|
3181
|
-
number == version.to_s || number.start_with?(prefix)
|
|
3182
|
-
end
|
|
3183
|
-
return version.to_s if matching.empty?
|
|
3184
|
-
|
|
3185
|
-
latest = matching.max_by { |entry| Gem::Version.new((entry[:number] || entry["number"]).to_s) }
|
|
3186
|
-
(latest[:number] || latest["number"]).to_s
|
|
3187
|
-
end
|
|
3188
|
-
|
|
3189
|
-
def appraisal_sort_versions(values)
|
|
3190
|
-
values.compact.map(&:to_s).reject(&:empty?).uniq.sort_by { |version| Gem::Version.new(version) }
|
|
3191
|
-
end
|
|
3192
|
-
|
|
3193
2781
|
def deep_string_key_hash(value)
|
|
3194
2782
|
case value
|
|
3195
2783
|
when Hash
|
|
@@ -3274,6 +2862,7 @@ module Kettle
|
|
|
3274
2862
|
}
|
|
3275
2863
|
bootstrap = kettle_config_bootstrap_facts(project_root, env, template_selection: template_selection)
|
|
3276
2864
|
bootstrap[:licenses] = configured_or_detected_licenses if bootstrap && !configured_or_detected_licenses.empty?
|
|
2865
|
+
bootstrap[:platforms] = detected_os_platforms(project_root) if bootstrap && !detected_os_platforms(project_root).empty?
|
|
3277
2866
|
facts[:kettle_config_bootstrap] = bootstrap if bootstrap
|
|
3278
2867
|
facts[:author] = author unless author.empty?
|
|
3279
2868
|
facts[:copyright] = copyright unless copyright.empty?
|
|
@@ -3521,12 +3110,21 @@ module Kettle
|
|
|
3521
3110
|
facts[:version_gem] = version_gem_facts unless version_gem_facts.empty?
|
|
3522
3111
|
facts[:shim] = shim unless shim.empty?
|
|
3523
3112
|
facts[:repository] = repository unless repository.empty?
|
|
3524
|
-
|
|
3113
|
+
# facts[:ci] itself isn't assembled until later in this method, but
|
|
3114
|
+
# shunted_gemfile_block needs the project's effective CI test floor
|
|
3115
|
+
# (already resolved into project_runtime, above) rather than the
|
|
3116
|
+
# gemspec's own, often much older, required_ruby_version.
|
|
3117
|
+
generated_blocks = generated_blocks_facts(
|
|
3118
|
+
gemspec_metadata,
|
|
3119
|
+
facts.merge(project_root: File.expand_path(project_root), ci: {test_min_ruby: project_runtime.fetch(:test_min_ruby).to_s}),
|
|
3120
|
+
run_options
|
|
3121
|
+
)
|
|
3525
3122
|
facts[:generated_blocks] = generated_blocks unless generated_blocks.empty?
|
|
3526
3123
|
bootstrap = kettle_config_bootstrap_facts(project_root, env, template_selection: template_selection)
|
|
3527
3124
|
if bootstrap
|
|
3528
3125
|
min_ruby_token = minimum_ruby_token(min_ruby)
|
|
3529
3126
|
bootstrap[:licenses] = gemspec_license_spdx unless gemspec_license_spdx.empty?
|
|
3127
|
+
bootstrap[:platforms] = detected_os_platforms(project_root) unless detected_os_platforms(project_root).empty?
|
|
3530
3128
|
bootstrap[:gemspec_path] = File.basename(gemspec_path) if gemspec_path
|
|
3531
3129
|
bootstrap[:min_ruby] = min_ruby_token unless min_ruby_token.empty?
|
|
3532
3130
|
bootstrap[:test_min_ruby] = config_test_min_ruby(kettle_config, min_ruby).to_s
|
|
@@ -3593,6 +3191,7 @@ module Kettle
|
|
|
3593
3191
|
default_branch: "main",
|
|
3594
3192
|
exec_cmd: github_actions_exec_cmd(kettle_config, env),
|
|
3595
3193
|
engine_exec_cmds: github_actions_engine_exec_cmds(kettle_config),
|
|
3194
|
+
platforms: ci_os_platforms_facts(kettle_config, project_root),
|
|
3596
3195
|
recording: project_recording_enabled?(project_root, kettle_config),
|
|
3597
3196
|
ruby_versions: github_actions_ruby_versions(project_runtime.fetch(:test_min_ruby)),
|
|
3598
3197
|
test_min_ruby: project_runtime.fetch(:test_min_ruby).to_s,
|
|
@@ -3882,7 +3481,7 @@ module Kettle
|
|
|
3882
3481
|
dependencies = extract_gemspec_development_dependencies(gemspec)
|
|
3883
3482
|
return if dependencies.empty?
|
|
3884
3483
|
|
|
3885
|
-
floor = shunted_effective_floor(facts.dig(:rubygems, :min_ruby))
|
|
3484
|
+
floor = shunted_effective_floor(facts.dig(:ci, :test_min_ruby) || facts.dig(:rubygems, :min_ruby))
|
|
3886
3485
|
project_root = facts[:project_root] || facts["project_root"] || run_options[:project_root] || run_options["project_root"]
|
|
3887
3486
|
shunted = dependencies.filter_map do |dependency|
|
|
3888
3487
|
next if shunted_dependency_has_modular_override?(project_root, dependency)
|
|
@@ -3951,11 +3550,11 @@ module Kettle
|
|
|
3951
3550
|
|
|
3952
3551
|
def shunted_effective_floor(min_ruby)
|
|
3953
3552
|
floor = minimum_ruby_token(min_ruby)
|
|
3954
|
-
versions = [
|
|
3553
|
+
versions = [DEFAULT_TEST_MINIMUM_RUBY]
|
|
3955
3554
|
versions << Gem::Version.new(floor) unless floor.to_s.empty?
|
|
3956
3555
|
versions.max
|
|
3957
3556
|
rescue ArgumentError
|
|
3958
|
-
|
|
3557
|
+
DEFAULT_TEST_MINIMUM_RUBY
|
|
3959
3558
|
end
|
|
3960
3559
|
|
|
3961
3560
|
def shunted_gemfile_managed_block(dependencies)
|
|
@@ -3978,6 +3577,7 @@ module Kettle
|
|
|
3978
3577
|
events = event_stream_from_options(run_options)
|
|
3979
3578
|
emit_event(events, "run_start", mode: "plan", project_root: project_root.to_s)
|
|
3980
3579
|
with_event_phase(events, "runtime_dependencies") { ensure_runtime_dependencies! }
|
|
3580
|
+
with_event_phase(events, "tslp_prefetch") { prefetch_tslp_grammars(env: env, events: events) }
|
|
3981
3581
|
with_event_phase(events, "preflight") { preflight_project!(project_root) }
|
|
3982
3582
|
template_selection = with_event_phase(events, "template_selection") { template_selection_for(env, run_options) }
|
|
3983
3583
|
checksum_mode = with_event_phase(events, "checksum_mode") { checksum_mode_for(env, run_options) }
|
|
@@ -4194,6 +3794,10 @@ module Kettle
|
|
|
4194
3794
|
# Config migrations are implemented during template application. They must
|
|
4195
3795
|
# run even when the template source and destination checksums still match.
|
|
4196
3796
|
return false if report.fetch(:relative_path, "").to_s == KETTLE_CONFIG_PATH
|
|
3797
|
+
# Appraisal.root.gemfile has facts-dependent structural normalization in
|
|
3798
|
+
# its accepted-template finalizer, so matching source/destination hashes
|
|
3799
|
+
# do not prove that the final output is already normalized.
|
|
3800
|
+
return false if report.fetch(:relative_path, "").to_s == "Appraisal.root.gemfile"
|
|
4197
3801
|
|
|
4198
3802
|
primitive = report.dig(:request_envelope, :request, :recipe_name).to_s
|
|
4199
3803
|
primitive == "supplied_template_source_application"
|
|
@@ -4227,6 +3831,12 @@ module Kettle
|
|
|
4227
3831
|
if template_file_type_for_relative_path(report.fetch(:relative_path).to_s) == :appraisals
|
|
4228
3832
|
payload[:appraisals_template_policy_fingerprint_version] = APPRAISALS_TEMPLATE_POLICY_FINGERPRINT_VERSION
|
|
4229
3833
|
end
|
|
3834
|
+
if report.fetch(:relative_path).to_s.start_with?("gemfiles/modular/")
|
|
3835
|
+
conflict_policy = modular_dependency_conflict_decisions(
|
|
3836
|
+
kettle_jem_config(project_root)
|
|
3837
|
+
).select { |decision| decision.fetch("modular") == report.fetch(:relative_path).to_s }
|
|
3838
|
+
payload[:modular_dependency_conflict_policy] = conflict_policy
|
|
3839
|
+
end
|
|
4230
3840
|
if template_file_type_for_relative_path(report.fetch(:relative_path).to_s) == :gemspec
|
|
4231
3841
|
payload[:gemspec_template_policy_fingerprint_version] = GEMSPEC_TEMPLATE_POLICY_FINGERPRINT_VERSION
|
|
4232
3842
|
end
|
|
@@ -4721,19 +4331,11 @@ module Kettle
|
|
|
4721
4331
|
report[:changed_files] = (report.fetch(:changed_files, []) + report.fetch(:post_apply_steps).flat_map do |step|
|
|
4722
4332
|
reported_post_apply_changed_files(step)
|
|
4723
4333
|
end).uniq.sort
|
|
4724
|
-
report[:duplicate_drift] =
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
reason: "skip_drift_check"
|
|
4730
|
-
}
|
|
4731
|
-
else
|
|
4732
|
-
duplicate_drift_report(
|
|
4733
|
-
project_root: project_root,
|
|
4734
|
-
template_root: template_root_path(project_root, config: kettle_jem_config(project_root)),
|
|
4735
|
-
run_options: run_options
|
|
4736
|
-
)
|
|
4334
|
+
report[:duplicate_drift] = if DecisionPolicy.value_to_boolean((run_options || {})[:defer_drift_check])
|
|
4335
|
+
{available: false, skipped: true, reason: "deferred_until_postprocessing"}
|
|
4336
|
+
else
|
|
4337
|
+
with_event_phase(events, "duplicate_drift") do
|
|
4338
|
+
duplicate_drift_check_result(project_root: project_root, run_options: run_options)
|
|
4737
4339
|
end
|
|
4738
4340
|
end
|
|
4739
4341
|
report[:phase_timings] = events.phase_timings if events.respond_to?(:phase_timings)
|
|
@@ -4741,6 +4343,25 @@ module Kettle
|
|
|
4741
4343
|
report
|
|
4742
4344
|
end
|
|
4743
4345
|
|
|
4346
|
+
def finalize_duplicate_drift(project_root:, report:, run_options: {})
|
|
4347
|
+
events = event_stream_from_options(run_options)
|
|
4348
|
+
drift = with_event_phase(events, "duplicate_drift") do
|
|
4349
|
+
duplicate_drift_check_result(project_root: project_root, run_options: run_options)
|
|
4350
|
+
end
|
|
4351
|
+
report.merge(duplicate_drift: drift, phase_timings: events.phase_timings)
|
|
4352
|
+
end
|
|
4353
|
+
|
|
4354
|
+
def duplicate_drift_check_result(project_root:, run_options: {})
|
|
4355
|
+
return {available: false, skipped: true, reason: "skip_drift_check"} if
|
|
4356
|
+
DecisionPolicy.value_to_boolean((run_options || {})[:skip_drift_check])
|
|
4357
|
+
|
|
4358
|
+
duplicate_drift_report(
|
|
4359
|
+
project_root: project_root,
|
|
4360
|
+
template_root: template_root_path(project_root, config: kettle_jem_config(project_root)),
|
|
4361
|
+
run_options: run_options
|
|
4362
|
+
)
|
|
4363
|
+
end
|
|
4364
|
+
|
|
4744
4365
|
def event_stream(io, types: nil)
|
|
4745
4366
|
Kettle::Ndjson.event_stream(
|
|
4746
4367
|
io,
|
|
@@ -5483,6 +5104,7 @@ module Kettle
|
|
|
5483
5104
|
ensure_trailing_newline(content.to_s.empty? ? %(source "https://gem.coop"\n) : content.to_s).dup
|
|
5484
5105
|
)
|
|
5485
5106
|
updated = remove_gemfile_dependency_blocks(updated, PROHIBITED_GEMFILE_DEPENDENCIES)
|
|
5107
|
+
updated = remove_gemfile_builtin_git_source_overrides(updated).dup
|
|
5486
5108
|
monorepo_root_gemfile_dependency_lines.each do |line|
|
|
5487
5109
|
next if gemfile_declares_gem?(updated, line.fetch(:name))
|
|
5488
5110
|
|
|
@@ -5494,17 +5116,17 @@ module Kettle
|
|
|
5494
5116
|
|
|
5495
5117
|
def monorepo_root_gemfile_dependency_lines
|
|
5496
5118
|
[
|
|
5497
|
-
{name: "appraisal2", source: %(gem "appraisal2", "~> 3.2", ">= 3.2.
|
|
5119
|
+
{name: "appraisal2", source: %(gem "appraisal2", "~> 3.2", ">= 3.2.5"\n)},
|
|
5498
5120
|
{name: "bundler-audit", source: %(gem "bundler-audit", "~> 0.9.3"\n)},
|
|
5499
|
-
{name: "kettle-dev", source: %(gem "kettle-dev", "~> 3.1", ">= 3.1.
|
|
5500
|
-
{name: "kettle-drift", source: %(gem "kettle-drift", "~> 1.0", ">= 1.0.
|
|
5501
|
-
{name: "kettle-family", source: %(gem "kettle-family", "~> 1.3", ">= 1.3.
|
|
5121
|
+
{name: "kettle-dev", source: %(gem "kettle-dev", "~> 3.1", ">= 3.1.11"\n)},
|
|
5122
|
+
{name: "kettle-drift", source: %(gem "kettle-drift", "~> 1.0", ">= 1.0.16"\n)},
|
|
5123
|
+
{name: "kettle-family", source: %(gem "kettle-family", "~> 1.3", ">= 1.3.10"\n)},
|
|
5502
5124
|
{name: PACKAGE_NAME, source: kettle_jem_dependency_source},
|
|
5503
|
-
{name: "kettle-test", source: %(gem "kettle-test", "~> 2.0", ">= 2.0.
|
|
5125
|
+
{name: "kettle-test", source: %(gem "kettle-test", "~> 2.0", ">= 2.0.24"\n)},
|
|
5504
5126
|
{name: "rake", source: %(gem "rake", "~> 13.0"\n)},
|
|
5505
5127
|
{name: "rspec", source: %(gem "rspec", "~> 3.0"\n)},
|
|
5506
|
-
{name: "stone_checksums", source: %(gem "stone_checksums", "~> 1.0", ">= 1.0.
|
|
5507
|
-
{name: "turbo_tests2", source: %(gem "turbo_tests2", "~> 3.2", ">= 3.2.
|
|
5128
|
+
{name: "stone_checksums", source: %(gem "stone_checksums", "~> 1.0", ">= 1.0.9"\n)},
|
|
5129
|
+
{name: "turbo_tests2", source: %(gem "turbo_tests2", "~> 3.2", ">= 3.2.14"\n)}
|
|
5508
5130
|
].freeze
|
|
5509
5131
|
end
|
|
5510
5132
|
|
|
@@ -5546,6 +5168,24 @@ module Kettle
|
|
|
5546
5168
|
end.sort
|
|
5547
5169
|
end
|
|
5548
5170
|
|
|
5171
|
+
# CI OS families evidenced by the destination's existing workflows. The
|
|
5172
|
+
# heuristic exists so re-templating a repo that already runs Windows CI
|
|
5173
|
+
# does not silently drop that coverage when the `platforms:` key is
|
|
5174
|
+
# injected or generated from. It only ever ADDS windows to the default:
|
|
5175
|
+
# the key's absent-means-default semantics already cover linux and macos,
|
|
5176
|
+
# and every packaged workflow is Linux-only except current.yml, which the
|
|
5177
|
+
# template always ships with ubuntu and macos lanes. Returns [] when the
|
|
5178
|
+
# destination has no workflow directory, so callers fall back cleanly.
|
|
5179
|
+
def detected_os_platforms(project_root)
|
|
5180
|
+
workflows_dir = File.join(project_root.to_s, ".github", "workflows")
|
|
5181
|
+
return [] unless Dir.exist?(workflows_dir)
|
|
5182
|
+
|
|
5183
|
+
windows = Dir.glob(File.join(workflows_dir, "*.{yml,yaml}")).any? do |path|
|
|
5184
|
+
File.read(path).match?(WINDOWS_RUNNER_LABEL_PATTERN)
|
|
5185
|
+
end
|
|
5186
|
+
windows ? DEFAULT_PLATFORMS + ["windows"] : DEFAULT_PLATFORMS.dup
|
|
5187
|
+
end
|
|
5188
|
+
|
|
5549
5189
|
def spdx_from_basename(basename)
|
|
5550
5190
|
return "LicenseRef-Big-Time-Public-License" if basename.to_s == "Big-Time-Public-License"
|
|
5551
5191
|
|
|
@@ -5985,6 +5625,7 @@ module Kettle
|
|
|
5985
5625
|
def readme_template_tokens(facts)
|
|
5986
5626
|
{
|
|
5987
5627
|
"KJ|CB:USER" => "",
|
|
5628
|
+
"KJ|DEFAULT_TEST_MINIMUM_RUBY" => DEFAULT_TEST_MINIMUM_RUBY.to_s,
|
|
5988
5629
|
"KJ|FUNDING:BUYMEACOFFEE" => "",
|
|
5989
5630
|
"KJ|FUNDING:KOFI" => "",
|
|
5990
5631
|
"KJ|FUNDING:LIBERAPAY" => "",
|
|
@@ -6960,7 +6601,7 @@ module Kettle
|
|
|
6960
6601
|
def gemfile_policy_operations(template_content, original, final, request)
|
|
6961
6602
|
package_name = runtime_context_value(request, :package, :name).to_s
|
|
6962
6603
|
deleted = gemfile_dependency_names("#{template_content}\n#{original}") - gemfile_dependency_names(final)
|
|
6963
|
-
expected =
|
|
6604
|
+
expected = PROHIBITED_GEMFILE_DEPENDENCIES.dup
|
|
6964
6605
|
expected << package_name unless package_name.empty?
|
|
6965
6606
|
[
|
|
6966
6607
|
{
|
|
@@ -7213,7 +6854,11 @@ module Kettle
|
|
|
7213
6854
|
destination_content: original
|
|
7214
6855
|
)
|
|
7215
6856
|
return with_readme_timing("readme.append_used_link_definitions") do
|
|
7216
|
-
appended = append_used_markdown_link_definitions(
|
|
6857
|
+
appended = append_used_markdown_link_definitions(
|
|
6858
|
+
processed,
|
|
6859
|
+
resolved,
|
|
6860
|
+
fallback_definition_source: original
|
|
6861
|
+
)
|
|
7217
6862
|
postprocess_readme_content(
|
|
7218
6863
|
appended,
|
|
7219
6864
|
facts,
|
|
@@ -7225,7 +6870,7 @@ module Kettle
|
|
|
7225
6870
|
if strategy.empty? || strategy == "merge"
|
|
7226
6871
|
merged = merge_config_template_source(recipe, resolved, original, facts: facts, env: env)
|
|
7227
6872
|
merged = preserve_mise_project_settings(recipe, merged, original, project_root: project_root, facts: facts) if template_file_type(recipe) == :toml
|
|
7228
|
-
return finalize_template_source_content(recipe, sync_kettle_config_env_overrides(merged, env)) if recipe.fetch(:target_path) == KETTLE_CONFIG_PATH
|
|
6873
|
+
return finalize_template_source_content(recipe, sync_kettle_config_env_overrides(merged, env, project_root: project_root)) if recipe.fetch(:target_path) == KETTLE_CONFIG_PATH
|
|
7229
6874
|
|
|
7230
6875
|
merged = postprocess_funding_markdown_content(merged, facts) if recipe.fetch(:target_path) == "FUNDING.md"
|
|
7231
6876
|
|
|
@@ -7240,7 +6885,7 @@ module Kettle
|
|
|
7240
6885
|
if strategy == "accept_template"
|
|
7241
6886
|
accepted = finalize_accepted_template_source(recipe, resolved, original, facts: facts, project_root: project_root)
|
|
7242
6887
|
accepted = preserve_github_workflow_project_settings(recipe, accepted, original, project_root: project_root) if github_workflow_template_recipe?(recipe)
|
|
7243
|
-
accepted = sync_kettle_config_env_overrides(accepted, env) if recipe.fetch(:target_path) == KETTLE_CONFIG_PATH
|
|
6888
|
+
accepted = sync_kettle_config_env_overrides(accepted, env, project_root: project_root) if recipe.fetch(:target_path) == KETTLE_CONFIG_PATH
|
|
7244
6889
|
accepted = postprocess_funding_markdown_content(accepted, facts) if recipe.fetch(:target_path) == "FUNDING.md"
|
|
7245
6890
|
if github_workflow_template_recipe?(recipe)
|
|
7246
6891
|
accepted = finalize_github_workflow_template(accepted, facts)
|
|
@@ -7297,7 +6942,12 @@ module Kettle
|
|
|
7297
6942
|
when :gemfile
|
|
7298
6943
|
finalize_gemfile_template_source(recipe, content, destination_content, facts: facts, template_content: content)
|
|
7299
6944
|
when :appraisals
|
|
7300
|
-
merge_appraisals_template_policy(content, facts: facts)
|
|
6945
|
+
merged = merge_appraisals_template_policy(content, facts: facts)
|
|
6946
|
+
if recipe.fetch(:target_path).to_s == "Appraisal.root.gemfile"
|
|
6947
|
+
normalize_appraisal_root_templating_gate(merged, facts)
|
|
6948
|
+
else
|
|
6949
|
+
merged
|
|
6950
|
+
end
|
|
7301
6951
|
when :gemspec
|
|
7302
6952
|
package_name = facts.dig(:package, :name).to_s if facts
|
|
7303
6953
|
receiver = gemspec_block_param(content) || "spec"
|
|
@@ -7788,12 +7438,14 @@ module Kettle
|
|
|
7788
7438
|
[content.to_s.rstrip, "", missing_sources.join.rstrip, ""].join("\n")
|
|
7789
7439
|
end
|
|
7790
7440
|
|
|
7791
|
-
def append_used_markdown_link_definitions(content, definition_source)
|
|
7441
|
+
def append_used_markdown_link_definitions(content, definition_source, fallback_definition_source: nil)
|
|
7792
7442
|
owners = ReadmePostProcessor.markdown_structural_owners(content, :link_definitions, :inline_references)
|
|
7793
7443
|
existing = owners.fetch(:link_definitions).map { |owner| owner.label.to_s }.to_set
|
|
7794
7444
|
referenced = owners.fetch(:inline_references).flat_map(&:labels).map(&:to_s).to_set
|
|
7795
|
-
available =
|
|
7796
|
-
|
|
7445
|
+
available = [fallback_definition_source, definition_source].compact.each_with_object({}) do |source, definitions|
|
|
7446
|
+
ReadmePostProcessor.markdown_link_definition_owners(source).each do |owner|
|
|
7447
|
+
definitions[owner.label.to_s] = owner
|
|
7448
|
+
end
|
|
7797
7449
|
end
|
|
7798
7450
|
missing = referenced.filter_map do |label|
|
|
7799
7451
|
next if existing.include?(label)
|
|
@@ -8075,6 +7727,10 @@ module Kettle
|
|
|
8075
7727
|
preserve_self_word_entries: local_gemfile_template_recipe?(recipe)
|
|
8076
7728
|
)
|
|
8077
7729
|
end
|
|
7730
|
+
output = remove_gemfile_builtin_git_source_overrides(output)
|
|
7731
|
+
if recipe.fetch(:target_path).to_s == "Appraisal.root.gemfile"
|
|
7732
|
+
output = normalize_appraisal_root_templating_gate(output, facts)
|
|
7733
|
+
end
|
|
8078
7734
|
if recipe.fetch(:target_path).to_s == "Gemfile"
|
|
8079
7735
|
# A merged Gemfile retains its project-specific source; an accepted
|
|
8080
7736
|
# template owns the complete source declaration.
|
|
@@ -8266,6 +7922,126 @@ module Kettle
|
|
|
8266
7922
|
output
|
|
8267
7923
|
end
|
|
8268
7924
|
|
|
7925
|
+
def normalize_appraisal_root_templating_gate(content, facts)
|
|
7926
|
+
has_structuredmerge_dependency = package_runtime_dependency_names(facts).any? do |name|
|
|
7927
|
+
STRUCTUREDMERGE_GEMS.include?(name.to_s)
|
|
7928
|
+
end
|
|
7929
|
+
result = prism_parse_success(content)
|
|
7930
|
+
return content unless result
|
|
7931
|
+
|
|
7932
|
+
nodes = []
|
|
7933
|
+
result.value.breadth_first_search_all { |node| nodes << node }
|
|
7934
|
+
template_eval = nodes.find do |node|
|
|
7935
|
+
node.is_a?(::Prism::CallNode) &&
|
|
7936
|
+
node.name == :eval_gemfile &&
|
|
7937
|
+
ruby_string_argument(node) == "gemfiles/modular/templating.gemfile"
|
|
7938
|
+
end
|
|
7939
|
+
template_gate = nodes.find do |node|
|
|
7940
|
+
node.is_a?(::Prism::IfNode) &&
|
|
7941
|
+
node.location.slice.to_s.include?("K_JEM_TEMPLATING") &&
|
|
7942
|
+
template_eval &&
|
|
7943
|
+
template_eval.location.start_offset > node.location.start_offset &&
|
|
7944
|
+
template_eval.location.end_offset < node.location.end_offset
|
|
7945
|
+
end
|
|
7946
|
+
generator_only = nodes.find do |node|
|
|
7947
|
+
node.is_a?(::Prism::CallNode) && node.name == :generator_only && node.block
|
|
7948
|
+
end
|
|
7949
|
+
style_comment = "# The style toolchain includes dependencies that require Ruby 3.3 or newer."
|
|
7950
|
+
style_gate_anchor = nodes.find do |node|
|
|
7951
|
+
gemfile_conditional_node?(node) && node.location.slice.to_s.include?("Gem::Version")
|
|
7952
|
+
end
|
|
7953
|
+
style_comment_attached = if style_gate_anchor
|
|
7954
|
+
content.lines[style_gate_anchor.location.start_line - 2].to_s.strip == style_comment
|
|
7955
|
+
else
|
|
7956
|
+
false
|
|
7957
|
+
end
|
|
7958
|
+
|
|
7959
|
+
if has_structuredmerge_dependency
|
|
7960
|
+
template_gate_is_nested = template_gate && generator_only &&
|
|
7961
|
+
template_gate.location.start_offset > generator_only.block.location.start_offset &&
|
|
7962
|
+
template_gate.location.end_offset < generator_only.block.location.end_offset
|
|
7963
|
+
if template_gate_is_nested
|
|
7964
|
+
return content if style_comment_attached
|
|
7965
|
+
|
|
7966
|
+
comment_line_index = content.lines.index { |line| line.strip == style_comment }
|
|
7967
|
+
return content unless comment_line_index
|
|
7968
|
+
|
|
7969
|
+
content = replace_source_range_lines(content, comment_line_index + 1, comment_line_index + 1, "")
|
|
7970
|
+
result = prism_parse_success(content)
|
|
7971
|
+
nodes = []
|
|
7972
|
+
result&.value&.breadth_first_search_all { |node| nodes << node }
|
|
7973
|
+
anchor = nodes.find do |node|
|
|
7974
|
+
gemfile_conditional_node?(node) && node.location.slice.to_s.include?("Gem::Version")
|
|
7975
|
+
end
|
|
7976
|
+
return content unless anchor
|
|
7977
|
+
|
|
7978
|
+
return insert_lines_before(content, anchor.location.start_line, "#{style_comment}\n")
|
|
7979
|
+
end
|
|
7980
|
+
|
|
7981
|
+
if template_gate
|
|
7982
|
+
content = replace_source_range_lines(
|
|
7983
|
+
content,
|
|
7984
|
+
template_gate.location.start_line,
|
|
7985
|
+
expand_line_range_through_following_blanks(content, ruby_node_source_end_line(template_gate)),
|
|
7986
|
+
""
|
|
7987
|
+
)
|
|
7988
|
+
elsif template_eval
|
|
7989
|
+
content = replace_source_range_lines(
|
|
7990
|
+
content,
|
|
7991
|
+
template_eval.location.start_line,
|
|
7992
|
+
ruby_node_source_end_line(template_eval),
|
|
7993
|
+
""
|
|
7994
|
+
)
|
|
7995
|
+
end
|
|
7996
|
+
|
|
7997
|
+
result = prism_parse_success(content)
|
|
7998
|
+
return content unless result
|
|
7999
|
+
|
|
8000
|
+
nodes = []
|
|
8001
|
+
result.value.breadth_first_search_all { |node| nodes << node }
|
|
8002
|
+
gate = <<~RUBY
|
|
8003
|
+
if respond_to?(:generator_only)
|
|
8004
|
+
generator_only do
|
|
8005
|
+
if ENV.fetch("K_JEM_TEMPLATING", "false").casecmp("true").zero?
|
|
8006
|
+
eval_gemfile "gemfiles/modular/templating.gemfile"
|
|
8007
|
+
end
|
|
8008
|
+
end
|
|
8009
|
+
end
|
|
8010
|
+
RUBY
|
|
8011
|
+
anchor = nodes.find do |node|
|
|
8012
|
+
gemfile_conditional_node?(node) && node.location.slice.to_s.include?("Gem::Version")
|
|
8013
|
+
end
|
|
8014
|
+
return content unless anchor
|
|
8015
|
+
|
|
8016
|
+
lines = content.lines
|
|
8017
|
+
anchor_line_index = anchor.location.start_line - 1
|
|
8018
|
+
preceding_line = lines[anchor_line_index - 1]
|
|
8019
|
+
if preceding_line&.strip == style_comment
|
|
8020
|
+
comment_start = lines.take(anchor_line_index - 1).sum(&:bytesize)
|
|
8021
|
+
replace_source_offsets(content, [{
|
|
8022
|
+
start_offset: comment_start,
|
|
8023
|
+
end_offset: anchor.location.start_offset,
|
|
8024
|
+
replacement: "#{gate}#{preceding_line}"
|
|
8025
|
+
}])
|
|
8026
|
+
else
|
|
8027
|
+
replace_source_offsets(content, [{
|
|
8028
|
+
start_offset: anchor.location.start_offset,
|
|
8029
|
+
end_offset: anchor.location.start_offset,
|
|
8030
|
+
replacement: gate
|
|
8031
|
+
}])
|
|
8032
|
+
end
|
|
8033
|
+
else
|
|
8034
|
+
return content unless template_gate
|
|
8035
|
+
|
|
8036
|
+
replace_source_range_lines(
|
|
8037
|
+
content,
|
|
8038
|
+
template_gate.location.start_line,
|
|
8039
|
+
expand_line_range_through_following_blanks(content, ruby_node_source_end_line(template_gate)),
|
|
8040
|
+
""
|
|
8041
|
+
)
|
|
8042
|
+
end
|
|
8043
|
+
end
|
|
8044
|
+
|
|
8269
8045
|
def ensure_main_gemfile_nomono_bootstrap(content, template_content)
|
|
8270
8046
|
return content if gemfile_declares_gem?(content, "nomono")
|
|
8271
8047
|
|
|
@@ -8976,6 +8752,19 @@ module Kettle
|
|
|
8976
8752
|
def normalize_local_gemfile_nomono_bootstrap(content)
|
|
8977
8753
|
output = remove_obsolete_local_gemfile_nomono_activation(content)
|
|
8978
8754
|
bootstrap = "#{local_gemfile_nomono_bootstrap(nil)}\n\n"
|
|
8755
|
+
marker = local_gemfile_nomono_bootstrap_marker(output)
|
|
8756
|
+
activation_assignments = prism_descendants(output).select do |node|
|
|
8757
|
+
node.is_a?(::Prism::LocalVariableWriteNode) && node.name == :nomono_activation_requirements
|
|
8758
|
+
end
|
|
8759
|
+
if marker && activation_assignments.any?
|
|
8760
|
+
first_line = activation_assignments.map { |node| node.location.start_line }.min
|
|
8761
|
+
first_line = preceding_comment_block_start_line(output.lines, first_line)
|
|
8762
|
+
output = replace_source_range_lines(output, first_line, marker.fetch(:start_line) - 1, bootstrap) if first_line < marker.fetch(:start_line)
|
|
8763
|
+
output = remove_local_gemfile_nomono_loader_blocks_after_marker(output)
|
|
8764
|
+
output = remove_redundant_nomono_bundler_requires_after_marker(output)
|
|
8765
|
+
return ensure_trailing_newline(output.rstrip)
|
|
8766
|
+
end
|
|
8767
|
+
|
|
8979
8768
|
require_records = ruby_call_records(output, :require).filter_map do |call|
|
|
8980
8769
|
next unless ruby_string_argument(call) == "nomono/bundler"
|
|
8981
8770
|
|
|
@@ -8999,6 +8788,82 @@ module Kettle
|
|
|
8999
8788
|
end
|
|
9000
8789
|
end
|
|
9001
8790
|
|
|
8791
|
+
def local_gemfile_nomono_bootstrap_marker(content)
|
|
8792
|
+
result = prism_parse_success(content)
|
|
8793
|
+
body = result&.value&.statements&.body || []
|
|
8794
|
+
local_gems_assignment = body.find do |node|
|
|
8795
|
+
node.is_a?(::Prism::LocalVariableWriteNode) && %i[local_gems structuredmerge_local_gems].include?(node.name)
|
|
8796
|
+
end
|
|
8797
|
+
[
|
|
8798
|
+
local_gems_assignment_record(content),
|
|
8799
|
+
local_gems_assignment && {start_line: local_gems_assignment.location.start_line},
|
|
8800
|
+
word_array_assignment_record(content, :structuredmerge_local_gems)
|
|
8801
|
+
].compact.min_by { |record| record.fetch(:start_line) }
|
|
8802
|
+
end
|
|
8803
|
+
|
|
8804
|
+
def remove_local_gemfile_nomono_loader_blocks_after_marker(content)
|
|
8805
|
+
marker = local_gemfile_nomono_bootstrap_marker(content)
|
|
8806
|
+
return content unless marker
|
|
8807
|
+
|
|
8808
|
+
result = prism_parse_success(content)
|
|
8809
|
+
body = result&.value&.statements&.body || []
|
|
8810
|
+
ranges = body.each_with_index.filter_map do |node, index|
|
|
8811
|
+
next unless node.is_a?(::Prism::LocalVariableWriteNode) && %i[nomono_activation_requirements nomono_local_loader].include?(node.name)
|
|
8812
|
+
next unless node.location.start_line > marker.fetch(:start_line)
|
|
8813
|
+
|
|
8814
|
+
loader = if node.name == :nomono_local_loader
|
|
8815
|
+
node
|
|
8816
|
+
else
|
|
8817
|
+
loader_index = (index + 1...body.length).find do |candidate_index|
|
|
8818
|
+
candidate = body[candidate_index]
|
|
8819
|
+
candidate.is_a?(::Prism::LocalVariableWriteNode) && candidate.name == :nomono_local_loader
|
|
8820
|
+
end
|
|
8821
|
+
loader_index && body[loader_index]
|
|
8822
|
+
end
|
|
8823
|
+
next unless loader
|
|
8824
|
+
next if node.name == :nomono_local_loader && body[0...index].any? do |candidate|
|
|
8825
|
+
candidate.is_a?(::Prism::LocalVariableWriteNode) && candidate.name == :nomono_activation_requirements &&
|
|
8826
|
+
candidate.location.start_line > marker.fetch(:start_line)
|
|
8827
|
+
end
|
|
8828
|
+
|
|
8829
|
+
following = body[(body.index(loader) + 1)..].to_a.find do |candidate|
|
|
8830
|
+
candidate.is_a?(::Prism::IfNode) && prism_descendants(candidate.slice).any? do |descendant|
|
|
8831
|
+
descendant.is_a?(::Prism::CallNode) && descendant.name == :require
|
|
8832
|
+
end
|
|
8833
|
+
end
|
|
8834
|
+
{
|
|
8835
|
+
start_line: (node.name == :nomono_activation_requirements) ?
|
|
8836
|
+
preceding_comment_block_start_line(content.to_s.lines, node.location.start_line) : node.location.start_line,
|
|
8837
|
+
end_line: ruby_node_source_end_line(following || loader)
|
|
8838
|
+
}
|
|
8839
|
+
end
|
|
8840
|
+
ranges.sort_by { |range| -range.fetch(:start_line) }.reduce(content.to_s) do |output, range|
|
|
8841
|
+
replace_source_range_lines(output, range.fetch(:start_line), range.fetch(:end_line), "")
|
|
8842
|
+
end
|
|
8843
|
+
end
|
|
8844
|
+
|
|
8845
|
+
def remove_redundant_nomono_bundler_requires_after_marker(content)
|
|
8846
|
+
marker = local_gemfile_nomono_bootstrap_marker(content)
|
|
8847
|
+
return content unless marker
|
|
8848
|
+
|
|
8849
|
+
records = ruby_call_records(content, :require).filter_map do |call|
|
|
8850
|
+
next unless ruby_string_argument(call) == "nomono/bundler"
|
|
8851
|
+
next unless call.location.start_line > marker.fetch(:start_line)
|
|
8852
|
+
|
|
8853
|
+
{start_line: call.location.start_line, end_line: ruby_node_source_end_line(call)}
|
|
8854
|
+
end
|
|
8855
|
+
records.sort_by { |record| -record.fetch(:start_line) }.reduce(content.to_s) do |output, record|
|
|
8856
|
+
replace_source_range_lines(output, record.fetch(:start_line), record.fetch(:end_line), "")
|
|
8857
|
+
end
|
|
8858
|
+
end
|
|
8859
|
+
|
|
8860
|
+
def prism_descendants(content)
|
|
8861
|
+
result = prism_parse_success(content)
|
|
8862
|
+
return [] unless result
|
|
8863
|
+
|
|
8864
|
+
result.value.breadth_first_search_all { true }
|
|
8865
|
+
end
|
|
8866
|
+
|
|
9002
8867
|
def remove_obsolete_local_gemfile_nomono_activation(content)
|
|
9003
8868
|
records = obsolete_local_gemfile_nomono_activation_records(content)
|
|
9004
8869
|
return content if records.empty?
|
|
@@ -9414,7 +9279,7 @@ module Kettle
|
|
|
9414
9279
|
|
|
9415
9280
|
def merge_gemfile_template_policy(content, facts:, template_content: nil, preserve_self_word_entries: false)
|
|
9416
9281
|
package_name = facts.dig(:package, :name).to_s if facts
|
|
9417
|
-
removable_gems = [
|
|
9282
|
+
removable_gems = [*PROHIBITED_GEMFILE_DEPENDENCIES]
|
|
9418
9283
|
removable_gems << package_name unless package_name.to_s.empty?
|
|
9419
9284
|
removable_gems.concat(package_runtime_dependency_names(facts))
|
|
9420
9285
|
removable_gems << "version_gem" unless version_gem_enabled?(facts)
|
|
@@ -9431,7 +9296,16 @@ module Kettle
|
|
|
9431
9296
|
|
|
9432
9297
|
def package_runtime_dependency_names(facts)
|
|
9433
9298
|
dependencies = facts.to_h.dig(:package, :runtime_dependencies)
|
|
9434
|
-
Array(dependencies).
|
|
9299
|
+
Array(dependencies).filter_map do |dependency|
|
|
9300
|
+
name = if dependency.respond_to?(:name)
|
|
9301
|
+
dependency.name
|
|
9302
|
+
elsif dependency.is_a?(Hash)
|
|
9303
|
+
dependency[:name] || dependency["name"]
|
|
9304
|
+
else
|
|
9305
|
+
dependency
|
|
9306
|
+
end
|
|
9307
|
+
name.to_s unless name.to_s.empty?
|
|
9308
|
+
end.uniq
|
|
9435
9309
|
end
|
|
9436
9310
|
|
|
9437
9311
|
def inject_main_gemfile_recording_eval(content, facts)
|
|
@@ -9652,6 +9526,22 @@ module Kettle
|
|
|
9652
9526
|
ensure_trailing_newline(lines.each_with_index.reject { |_line, index| remove_indexes.include?(index) }.map(&:first).join.gsub(/\n{3,}/, "\n\n"))
|
|
9653
9527
|
end
|
|
9654
9528
|
|
|
9529
|
+
# Bundler already provides these git sources; see BUNDLER_BUILTIN_GIT_SOURCES.
|
|
9530
|
+
def remove_gemfile_builtin_git_source_overrides(content)
|
|
9531
|
+
remove_indexes = Set.new
|
|
9532
|
+
ruby_call_records(content, :git_source).each do |call|
|
|
9533
|
+
argument = call.arguments&.arguments&.first
|
|
9534
|
+
next unless argument.is_a?(::Prism::SymbolNode)
|
|
9535
|
+
next unless BUNDLER_BUILTIN_GIT_SOURCES.include?(argument.unescaped.to_s)
|
|
9536
|
+
|
|
9537
|
+
(call.location.start_line..ruby_node_source_end_line(call)).each { |line_number| remove_indexes << (line_number - 1) }
|
|
9538
|
+
end
|
|
9539
|
+
return content if remove_indexes.empty?
|
|
9540
|
+
|
|
9541
|
+
lines = content.to_s.lines.each_with_index.reject { |_line, index| remove_indexes.include?(index) }.map(&:first)
|
|
9542
|
+
ensure_trailing_newline(lines.join.sub(/\A\n+/, "").gsub(/\n{3,}/, "\n\n"))
|
|
9543
|
+
end
|
|
9544
|
+
|
|
9655
9545
|
def gemfile_gem_call_records(content)
|
|
9656
9546
|
ruby_call_records(content, :gem).filter_map do |call|
|
|
9657
9547
|
name = ruby_string_argument(call)
|
|
@@ -9669,21 +9559,53 @@ module Kettle
|
|
|
9669
9559
|
# A dependency owned directly by the project must not also be introduced by
|
|
9670
9560
|
# a generated modular Gemfile. On first bootstrap, write a reviewable
|
|
9671
9561
|
# resolution list; later runs require an explicit removal operation.
|
|
9562
|
+
#
|
|
9563
|
+
# Also surfaces case 3 of the dev-dependency conflict policy (engine
|
|
9564
|
+
# incompatibility, e.g. sqlite3 on jruby): see #engine_dependency_conflicts.
|
|
9565
|
+
# Both conflict sources share the same dependency_conflicts.resolve list,
|
|
9566
|
+
# decision-matching, and review gate.
|
|
9672
9567
|
def validate_modular_dependency_conflicts!(project_root, recipe_reports)
|
|
9673
|
-
|
|
9568
|
+
config = kettle_jem_config(project_root)
|
|
9569
|
+
conflicts = modular_dependency_conflicts(project_root, recipe_reports) +
|
|
9570
|
+
engine_dependency_conflicts(project_root, recipe_reports, config)
|
|
9674
9571
|
return if conflicts.empty?
|
|
9675
9572
|
|
|
9676
|
-
config = kettle_jem_config(project_root)
|
|
9677
9573
|
decisions = modular_dependency_conflict_decisions(config)
|
|
9678
9574
|
unresolved = conflicts.reject { |conflict| modular_dependency_conflict_decided?(decisions, conflict) }
|
|
9679
9575
|
if unresolved.empty?
|
|
9680
9576
|
return
|
|
9681
9577
|
end
|
|
9682
9578
|
|
|
9579
|
+
# A conflict is "newly surfaced" only if no entry exists yet in
|
|
9580
|
+
# dependency_conflicts.resolve for its exact gem/direct/modular triple
|
|
9581
|
+
# (regardless of whether that entry's action/reason are filled in or
|
|
9582
|
+
# valid). Newly surfaced conflicts get a placeholder entry written for
|
|
9583
|
+
# review and do not block this run. A conflict that already has an
|
|
9584
|
+
# entry but is still undecided (e.g. the placeholder was never
|
|
9585
|
+
# completed, or reviewed was never flipped to true) has already had
|
|
9586
|
+
# its chance to be reviewed, so it blocks the run instead of silently
|
|
9587
|
+
# regenerating the same placeholder forever.
|
|
9588
|
+
surfaced_entries = modular_dependency_conflict_entries(config)
|
|
9589
|
+
newly_surfaced, previously_surfaced = unresolved.partition do |conflict|
|
|
9590
|
+
!modular_dependency_conflict_previously_surfaced?(surfaced_entries, conflict)
|
|
9591
|
+
end
|
|
9592
|
+
|
|
9593
|
+
# Among newly surfaced conflicts, gems with a canonical, pre-decided
|
|
9594
|
+
# resolution (KNOWN_GEM_CONFLICT_RESOLUTIONS) don't need a human in
|
|
9595
|
+
# the loop: write the real decided entry straight away instead of a
|
|
9596
|
+
# placeholder "review" entry. Everything else still goes through the
|
|
9597
|
+
# placeholder/review flow.
|
|
9598
|
+
canonical_surfaced, review_needed_surfaced = newly_surfaced.partition do |conflict|
|
|
9599
|
+
auto_resolvable_known_gem_conflict?(conflict)
|
|
9600
|
+
end
|
|
9601
|
+
|
|
9683
9602
|
bootstrap_report = recipe_reports.find { |report| report.fetch(:relative_path, "").to_s == KETTLE_CONFIG_PATH }
|
|
9684
|
-
if bootstrap_report &&
|
|
9685
|
-
|
|
9686
|
-
|
|
9603
|
+
if bootstrap_report && newly_surfaced.any?
|
|
9604
|
+
content = bootstrap_report.fetch(:final_content, "")
|
|
9605
|
+
content = add_dependency_conflict_resolved_entries(content, canonical_surfaced) if canonical_surfaced.any?
|
|
9606
|
+
content = add_dependency_conflict_review_list(content, review_needed_surfaced) if review_needed_surfaced.any?
|
|
9607
|
+
bootstrap_report[:final_content] = content
|
|
9608
|
+
return if previously_surfaced.empty?
|
|
9687
9609
|
end
|
|
9688
9610
|
|
|
9689
9611
|
details = unresolved.sort_by { |conflict| conflict.values_at(:name, :direct, :modular) }.map do |conflict|
|
|
@@ -9692,13 +9614,21 @@ module Kettle
|
|
|
9692
9614
|
raise Error,
|
|
9693
9615
|
"direct dependencies also declared by templated modular Gemfiles: #{details}. " \
|
|
9694
9616
|
"Review dependency_conflicts.resolve in #{KETTLE_CONFIG_PATH}; each entry needs gem, direct, modular, action, and reason. " \
|
|
9695
|
-
"Supported actions: remove_modular_gem, remove_x_std_lib_eval, keep_both. " \
|
|
9617
|
+
"Supported actions: remove_modular_gem, remove_x_std_lib_eval, remove_direct_gem, keep_both. " \
|
|
9696
9618
|
"keep_both is valid only when the direct and modular requirements overlap."
|
|
9697
9619
|
end
|
|
9698
9620
|
|
|
9621
|
+
# Paths of a project's own directly-authored dependency manifests: the
|
|
9622
|
+
# root Gemfile plus any gemspec, whether already on disk or only present
|
|
9623
|
+
# as a pending recipe report (not yet written) for this templating run.
|
|
9624
|
+
def direct_dependency_paths(project_root, recipe_reports)
|
|
9625
|
+
report_paths = recipe_reports.map { |report| report.fetch(:relative_path, "").to_s }
|
|
9626
|
+
["Gemfile", *Dir.glob(File.join(project_root.to_s, "*.gemspec")).map { |path| File.basename(path) }, *report_paths.select { |path| path.end_with?(".gemspec") }]
|
|
9627
|
+
end
|
|
9628
|
+
|
|
9699
9629
|
def modular_dependency_conflicts(project_root, recipe_reports)
|
|
9700
9630
|
report_paths = recipe_reports.map { |report| report.fetch(:relative_path, "").to_s }
|
|
9701
|
-
paths =
|
|
9631
|
+
paths = direct_dependency_paths(project_root, recipe_reports)
|
|
9702
9632
|
direct = dependency_file_contents(project_root, recipe_reports, paths)
|
|
9703
9633
|
modular_paths = [
|
|
9704
9634
|
*Dir.glob(File.join(project_root.to_s, "gemfiles/modular/**/*.gemfile")).map do |path|
|
|
@@ -9716,6 +9646,12 @@ module Kettle
|
|
|
9716
9646
|
relative = path.delete_prefix("gemfiles/modular/")
|
|
9717
9647
|
!relative.include?("/") || relative.start_with?("x_std_libs/")
|
|
9718
9648
|
end
|
|
9649
|
+
# shunted.gemfile is derived from the gemspec's own dependencies
|
|
9650
|
+
# (#shunted_gemfile_block): it's expected to redeclare a gem the
|
|
9651
|
+
# gemspec already declares, so it isn't a real direct-vs-modular
|
|
9652
|
+
# authoring conflict. Every other consumer of the packaged modular
|
|
9653
|
+
# gemfile list excludes it the same way.
|
|
9654
|
+
modular_paths = modular_paths.reject { |path| path == "gemfiles/modular/shunted.gemfile" }
|
|
9719
9655
|
modular = dependency_file_contents(project_root, recipe_reports, modular_paths)
|
|
9720
9656
|
direct.flat_map do |direct_path, content|
|
|
9721
9657
|
direct_records = dependency_records_for(direct_path, content)
|
|
@@ -9736,6 +9672,41 @@ module Kettle
|
|
|
9736
9672
|
end
|
|
9737
9673
|
end
|
|
9738
9674
|
|
|
9675
|
+
# Case 3 of the dev-dependency conflict policy: a direct dependency whose
|
|
9676
|
+
# KNOWN_GEM_CONFLICT_RESOLUTIONS entry is :engine_incompatible and
|
|
9677
|
+
# doesn't support one or more of the destination's declared engines
|
|
9678
|
+
# (config["engines"], falling back to DEFAULT_ENGINES — see
|
|
9679
|
+
# #enabled_ruby_engines). Unlike #modular_dependency_conflicts, this
|
|
9680
|
+
# needs no modular-side declaration to match against: these are gems
|
|
9681
|
+
# below the template's inclusion threshold with no template-managed
|
|
9682
|
+
# modular home (sqlite3 is the pilot; see KNOWN_GEM_CONFLICT_RESOLUTIONS), so the
|
|
9683
|
+
# incompatibility itself — not a direct-vs-modular duplicate — is the
|
|
9684
|
+
# conflict. The :modular field is a fixed, human-readable placeholder
|
|
9685
|
+
# (there is no modular file to name) rather than the specific
|
|
9686
|
+
# unsupported-engine list, so a decision stays matched across runs even
|
|
9687
|
+
# if the destination's declared engines change without actually
|
|
9688
|
+
# resolving the incompatibility.
|
|
9689
|
+
def engine_dependency_conflicts(project_root, recipe_reports, config)
|
|
9690
|
+
declared_engines = enabled_ruby_engines(config)
|
|
9691
|
+
paths = direct_dependency_paths(project_root, recipe_reports)
|
|
9692
|
+
direct = dependency_file_contents(project_root, recipe_reports, paths)
|
|
9693
|
+
direct.flat_map do |direct_path, content|
|
|
9694
|
+
dependency_records_for(direct_path, content).filter_map do |record|
|
|
9695
|
+
resolution = known_gem_conflict_resolution(record.fetch(:name))
|
|
9696
|
+
next unless resolution && resolution.fetch(:category) == :engine_incompatible
|
|
9697
|
+
next if (declared_engines - resolution.fetch(:supported_engines, [])).empty?
|
|
9698
|
+
|
|
9699
|
+
{
|
|
9700
|
+
name: record.fetch(:name),
|
|
9701
|
+
direct: direct_path,
|
|
9702
|
+
modular: "(engine-incompatible; no modular home)",
|
|
9703
|
+
direct_requirements: record.fetch(:requirements, []),
|
|
9704
|
+
modular_requirements: []
|
|
9705
|
+
}
|
|
9706
|
+
end
|
|
9707
|
+
end
|
|
9708
|
+
end
|
|
9709
|
+
|
|
9739
9710
|
def dependency_records_for(path, content)
|
|
9740
9711
|
if path == "Gemfile" || path.end_with?(".gemfile")
|
|
9741
9712
|
gemfile_gem_call_records(content)
|
|
@@ -9771,13 +9742,34 @@ module Kettle
|
|
|
9771
9742
|
config.dig("dependency_conflicts", "reviewed") == true
|
|
9772
9743
|
end
|
|
9773
9744
|
|
|
9745
|
+
# Every entry currently in dependency_conflicts.resolve, keyed loosely
|
|
9746
|
+
# (unlike #modular_dependency_conflict_decisions, incomplete entries such
|
|
9747
|
+
# as an unfinished "action: review" placeholder are included) so callers
|
|
9748
|
+
# can tell "this conflict was already surfaced for review" apart from
|
|
9749
|
+
# "this conflict has never been seen before".
|
|
9750
|
+
def modular_dependency_conflict_entries(config)
|
|
9751
|
+
Array(config.dig("dependency_conflicts", "resolve")).filter_map do |entry|
|
|
9752
|
+
next unless entry.is_a?(Hash)
|
|
9753
|
+
|
|
9754
|
+
entry.each_with_object({}) { |(key, value), result| result[key.to_s] = value.to_s }
|
|
9755
|
+
end
|
|
9756
|
+
end
|
|
9757
|
+
|
|
9758
|
+
def modular_dependency_conflict_previously_surfaced?(entries, conflict)
|
|
9759
|
+
entries.any? do |entry|
|
|
9760
|
+
entry["gem"] == conflict.fetch(:name) &&
|
|
9761
|
+
entry["direct"] == conflict.fetch(:direct) &&
|
|
9762
|
+
entry["modular"] == conflict.fetch(:modular)
|
|
9763
|
+
end
|
|
9764
|
+
end
|
|
9765
|
+
|
|
9774
9766
|
def modular_dependency_conflict_decided?(decisions, conflict)
|
|
9775
9767
|
decisions.any? do |decision|
|
|
9776
9768
|
decision.fetch("gem") == conflict.fetch(:name) &&
|
|
9777
9769
|
decision.fetch("direct") == conflict.fetch(:direct) &&
|
|
9778
9770
|
decision.fetch("modular") == conflict.fetch(:modular) &&
|
|
9779
9771
|
case decision.fetch("action")
|
|
9780
|
-
when "remove_modular_gem", "remove_x_std_lib_eval"
|
|
9772
|
+
when "remove_modular_gem", "remove_x_std_lib_eval", "remove_direct_gem"
|
|
9781
9773
|
true
|
|
9782
9774
|
when "keep_both"
|
|
9783
9775
|
compatible_dependency_requirements?(conflict.fetch(:direct_requirements), conflict.fetch(:modular_requirements))
|
|
@@ -9807,38 +9799,118 @@ module Kettle
|
|
|
9807
9799
|
false
|
|
9808
9800
|
end
|
|
9809
9801
|
|
|
9802
|
+
# A gem qualifies for automatic (no-human-review) resolution when it has
|
|
9803
|
+
# a KNOWN_GEM_CONFLICT_RESOLUTIONS entry in
|
|
9804
|
+
# AUTO_RESOLVABLE_KNOWN_GEM_CONFLICT_CATEGORIES and not explicitly
|
|
9805
|
+
# flagged force_review — see #auto_resolvable_known_gem_conflict? below.
|
|
9806
|
+
def known_gem_conflict_resolution(name)
|
|
9807
|
+
KNOWN_GEM_CONFLICT_RESOLUTIONS[name.to_s]
|
|
9808
|
+
end
|
|
9809
|
+
|
|
9810
|
+
AUTO_RESOLVABLE_KNOWN_GEM_CONFLICT_CATEGORIES = %i[template_managed engine_incompatible].freeze
|
|
9811
|
+
|
|
9812
|
+
def auto_resolvable_known_gem_conflict?(conflict)
|
|
9813
|
+
resolution = known_gem_conflict_resolution(conflict.fetch(:name))
|
|
9814
|
+
return false unless resolution
|
|
9815
|
+
|
|
9816
|
+
AUTO_RESOLVABLE_KNOWN_GEM_CONFLICT_CATEGORIES.include?(resolution.fetch(:category)) &&
|
|
9817
|
+
!resolution.fetch(:force_review, false)
|
|
9818
|
+
end
|
|
9819
|
+
|
|
9820
|
+
def dependency_conflict_entry_lines(conflict, action:, reason:)
|
|
9821
|
+
[
|
|
9822
|
+
" - gem: #{conflict.fetch(:name)}\n",
|
|
9823
|
+
" direct: #{conflict.fetch(:direct)}\n",
|
|
9824
|
+
" modular: #{conflict.fetch(:modular)}\n",
|
|
9825
|
+
" action: #{action}\n",
|
|
9826
|
+
" reason: #{reason.to_s.empty? ? "\"\"" : reason.to_s.inspect}\n"
|
|
9827
|
+
]
|
|
9828
|
+
end
|
|
9829
|
+
|
|
9830
|
+
# Writes a placeholder "action: review" entry for each newly surfaced
|
|
9831
|
+
# conflict. When dependency_conflicts: already exists, this only adds
|
|
9832
|
+
# entries for these specific conflicts and resets reviewed: to false
|
|
9833
|
+
# (a new, unreviewed conflict invalidates a prior reviewed: true) —
|
|
9834
|
+
# existing entries, including ones a human already wrote, are left
|
|
9835
|
+
# exactly as they are.
|
|
9810
9836
|
def add_dependency_conflict_review_list(content, conflicts)
|
|
9837
|
+
return content if conflicts.empty?
|
|
9838
|
+
|
|
9839
|
+
new_entry_lines = conflicts.sort_by { |conflict| conflict.values_at(:name, :direct, :modular) }.flat_map do |conflict|
|
|
9840
|
+
dependency_conflict_entry_lines(conflict, action: "review", reason: "")
|
|
9841
|
+
end
|
|
9842
|
+
|
|
9843
|
+
insert_dependency_conflict_resolve_entries(content, new_entry_lines, reset_reviewed: true)
|
|
9844
|
+
end
|
|
9845
|
+
|
|
9846
|
+
# Writes a fully decided entry (real action + reason, straight from
|
|
9847
|
+
# KNOWN_GEM_CONFLICT_RESOLUTIONS) for each newly surfaced conflict whose
|
|
9848
|
+
# gem is canonical. Unlike add_dependency_conflict_review_list, these
|
|
9849
|
+
# need no human decision, so reviewed: is left exactly as it already is
|
|
9850
|
+
# instead of being reset to false.
|
|
9851
|
+
def add_dependency_conflict_resolved_entries(content, conflicts)
|
|
9852
|
+
return content if conflicts.empty?
|
|
9853
|
+
|
|
9854
|
+
new_entry_lines = conflicts.sort_by { |conflict| conflict.values_at(:name, :direct, :modular) }.flat_map do |conflict|
|
|
9855
|
+
resolution = known_gem_conflict_resolution(conflict.fetch(:name))
|
|
9856
|
+
dependency_conflict_entry_lines(conflict, action: resolution.fetch(:action), reason: resolution.fetch(:reason))
|
|
9857
|
+
end
|
|
9858
|
+
|
|
9859
|
+
insert_dependency_conflict_resolve_entries(content, new_entry_lines, reset_reviewed: false)
|
|
9860
|
+
end
|
|
9861
|
+
|
|
9862
|
+
# Shared splicing logic behind both writers above: finds (or creates)
|
|
9863
|
+
# the dependency_conflicts: block and appends new_entry_lines under
|
|
9864
|
+
# resolve:, optionally resetting reviewed: to false along the way.
|
|
9865
|
+
def insert_dependency_conflict_resolve_entries(content, new_entry_lines, reset_reviewed:)
|
|
9811
9866
|
lines = content.to_s.lines
|
|
9812
9867
|
index = lines.index { |line| line.match?(/\Adependency_conflicts:\s*\z/) }
|
|
9813
9868
|
unless index
|
|
9814
9869
|
lines << "\n" unless lines.empty? || lines.last.strip.empty?
|
|
9815
|
-
lines.concat([
|
|
9816
|
-
|
|
9870
|
+
lines.concat([
|
|
9871
|
+
"dependency_conflicts:\n",
|
|
9872
|
+
" # Review each entry and choose a supported action.\n",
|
|
9873
|
+
" # keep_both is for a broad direct requirement plus a compatible modular narrowing.\n",
|
|
9874
|
+
" reviewed: false\n",
|
|
9875
|
+
" resolve:\n",
|
|
9876
|
+
*new_entry_lines
|
|
9877
|
+
])
|
|
9878
|
+
return ensure_trailing_newline(lines.join)
|
|
9817
9879
|
end
|
|
9818
9880
|
|
|
9819
9881
|
block_end = (index + 1...lines.length).find { |line_index| lines[line_index].match?(/\A\S/) } || lines.length
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
"
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9882
|
+
block_lines = lines[index...block_end]
|
|
9883
|
+
|
|
9884
|
+
if reset_reviewed
|
|
9885
|
+
reviewed_index = block_lines.index { |line| line.match?(/\A\s*reviewed:\s*\S/) }
|
|
9886
|
+
if reviewed_index
|
|
9887
|
+
block_lines[reviewed_index] = block_lines[reviewed_index].sub(/reviewed:\s*\S+/, "reviewed: false")
|
|
9888
|
+
else
|
|
9889
|
+
insert_at = 1
|
|
9890
|
+
insert_at += 1 while block_lines[insert_at]&.match?(/\A\s*#/)
|
|
9891
|
+
block_lines.insert(insert_at, " reviewed: false\n")
|
|
9892
|
+
end
|
|
9893
|
+
end
|
|
9894
|
+
|
|
9895
|
+
resolve_index = block_lines.index { |line| line.match?(/\A\s*resolve:\s*(\[\s*\])?\s*\z/) }
|
|
9896
|
+
if resolve_index
|
|
9897
|
+
if block_lines[resolve_index].match?(/\[\s*\]\s*\z/)
|
|
9898
|
+
indent = block_lines[resolve_index][/\A\s*/]
|
|
9899
|
+
block_lines[resolve_index] = "#{indent}resolve:\n"
|
|
9900
|
+
end
|
|
9901
|
+
block_lines.insert(resolve_index + 1, *new_entry_lines)
|
|
9902
|
+
else
|
|
9903
|
+
block_lines << " resolve:\n"
|
|
9904
|
+
block_lines.concat(new_entry_lines)
|
|
9835
9905
|
end
|
|
9836
|
-
|
|
9906
|
+
|
|
9907
|
+
ensure_trailing_newline([*lines[0...index], *block_lines, *lines[block_end..].to_a].join)
|
|
9837
9908
|
end
|
|
9838
9909
|
|
|
9839
9910
|
def apply_modular_dependency_conflict_resolutions(project_root, decisions)
|
|
9840
9911
|
decisions.each_with_object([]) do |decision, changed_files|
|
|
9841
|
-
|
|
9912
|
+
relative_path = (decision.fetch("action") == "remove_direct_gem") ? decision.fetch("direct") : decision.fetch("modular")
|
|
9913
|
+
path = File.join(project_root.to_s, relative_path)
|
|
9842
9914
|
next unless File.file?(path)
|
|
9843
9915
|
|
|
9844
9916
|
before = File.read(path)
|
|
@@ -9847,13 +9919,33 @@ module Kettle
|
|
|
9847
9919
|
remove_gemfile_dependency_blocks(before, [decision.fetch("gem")])
|
|
9848
9920
|
when "remove_x_std_lib_eval"
|
|
9849
9921
|
remove_x_std_lib_eval_for_gem(before, decision.fetch("gem"))
|
|
9922
|
+
when "remove_direct_gem"
|
|
9923
|
+
remove_dependency_conflict_direct_declaration(before, relative_path, decision.fetch("gem"))
|
|
9850
9924
|
when "keep_both"
|
|
9851
9925
|
before
|
|
9852
9926
|
end
|
|
9853
9927
|
next if after == before
|
|
9854
9928
|
|
|
9855
9929
|
File.write(path, after)
|
|
9856
|
-
changed_files <<
|
|
9930
|
+
changed_files << relative_path
|
|
9931
|
+
end
|
|
9932
|
+
end
|
|
9933
|
+
|
|
9934
|
+
# The "direct" side of a dependency_conflicts entry is either a Gemfile
|
|
9935
|
+
# (or a modular .gemfile) or a .gemspec, mirroring the same dispatch
|
|
9936
|
+
# #dependency_records_for uses to detect the conflict in the first
|
|
9937
|
+
# place. The gemspec branch has to discover its own receiver (the
|
|
9938
|
+
# Gem::Specification.new block's parameter name) since, unlike the
|
|
9939
|
+
# gemspec-merge pipeline, there's no already-known template_receiver
|
|
9940
|
+
# here — this runs standalone, against whatever's on disk.
|
|
9941
|
+
def remove_dependency_conflict_direct_declaration(content, relative_path, gem_name)
|
|
9942
|
+
if relative_path == "Gemfile" || relative_path.end_with?(".gemfile")
|
|
9943
|
+
remove_gemfile_dependency_blocks(content, [gem_name])
|
|
9944
|
+
else
|
|
9945
|
+
receiver = gemspec_block_param(content)
|
|
9946
|
+
return content unless receiver
|
|
9947
|
+
|
|
9948
|
+
remove_gemspec_dependency_lines(content, receiver: receiver, names: [gem_name])
|
|
9857
9949
|
end
|
|
9858
9950
|
end
|
|
9859
9951
|
|
|
@@ -9958,12 +10050,23 @@ module Kettle
|
|
|
9958
10050
|
TEMPLATE_MANAGED_DEPENDENCIES.find { |dependency| dependency.fetch(:name) == name.to_s }
|
|
9959
10051
|
end
|
|
9960
10052
|
|
|
9961
|
-
def template_managed_dependency_requirements(name, version_module: Version)
|
|
9962
|
-
return
|
|
10053
|
+
def template_managed_dependency_requirements(name, env: ENV, version_module: Version)
|
|
10054
|
+
return kettle_jem_template_dependency_requirements(env: env, version_module: version_module) if name.to_s == PACKAGE_NAME
|
|
9963
10055
|
|
|
9964
10056
|
template_managed_dependency(name)&.fetch(:requirements)
|
|
9965
10057
|
end
|
|
9966
10058
|
|
|
10059
|
+
def kettle_jem_template_dependency_requirements(env: ENV, version_module: Version)
|
|
10060
|
+
return kettle_jem_dependency_requirements(version_module: version_module) unless local_structuredmerge_path_mode?(env)
|
|
10061
|
+
|
|
10062
|
+
template_managed_dependency(PACKAGE_NAME).fetch(:requirements)
|
|
10063
|
+
end
|
|
10064
|
+
|
|
10065
|
+
def local_structuredmerge_path_mode?(env)
|
|
10066
|
+
value = (env || {}).fetch("STRUCTUREDMERGE_DEV", "false").to_s.strip
|
|
10067
|
+
value != "" && !DecisionPolicy.falsey?(value)
|
|
10068
|
+
end
|
|
10069
|
+
|
|
9967
10070
|
def template_managed_dependency_names(bootstrap: nil, ruby_version: RUBY_VERSION)
|
|
9968
10071
|
TEMPLATE_MANAGED_DEPENDENCIES.filter_map do |dependency|
|
|
9969
10072
|
next if !bootstrap.nil? && dependency.fetch(:bootstrap) != bootstrap
|
|
@@ -9984,12 +10087,12 @@ module Kettle
|
|
|
9984
10087
|
Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(ruby_version))
|
|
9985
10088
|
end
|
|
9986
10089
|
|
|
9987
|
-
def reconcile_template_managed_dependencies(source)
|
|
10090
|
+
def reconcile_template_managed_dependencies(source, env: ENV)
|
|
9988
10091
|
replacements = ruby_call_records(source, nil).filter_map do |call|
|
|
9989
10092
|
next unless template_managed_dependency_call?(call)
|
|
9990
10093
|
|
|
9991
10094
|
name = ruby_string_argument(call)
|
|
9992
|
-
requirements = template_managed_dependency_requirements(name)
|
|
10095
|
+
requirements = template_managed_dependency_requirements(name, env: env)
|
|
9993
10096
|
next unless requirements
|
|
9994
10097
|
|
|
9995
10098
|
requirement_nodes = Array(call.arguments&.arguments).drop(1).reject do |argument|
|
|
@@ -11128,14 +11231,59 @@ module Kettle
|
|
|
11128
11231
|
|
|
11129
11232
|
def remove_gemspec_dependency_lines(content, receiver:, names:, runtime_only: false, development_only: false)
|
|
11130
11233
|
wanted = names.map(&:to_s).to_set
|
|
11131
|
-
|
|
11132
|
-
.select { |record| wanted.include?(record.fetch(:name)) }
|
|
11133
|
-
.select { |record| !runtime_only || record.fetch(:kind) != "add_development_dependency" }
|
|
11134
|
-
.select { |record| !development_only || record.fetch(:kind) == "add_development_dependency" }
|
|
11135
|
-
.to_h { |record| [record.fetch(:start_line), record.merge(replacement: "")] }
|
|
11136
|
-
return content if records_by_line.empty?
|
|
11234
|
+
return content if wanted.empty?
|
|
11137
11235
|
|
|
11138
|
-
|
|
11236
|
+
allowed_kinds = gemspec_dependency_call_kinds(runtime_only: runtime_only, development_only: development_only)
|
|
11237
|
+
targets = wanted.map do |name|
|
|
11238
|
+
ast_crispr_gemspec_dependency_delete_target(receiver: receiver, name: name, allowed_kinds: allowed_kinds)
|
|
11239
|
+
end
|
|
11240
|
+
|
|
11241
|
+
result = Ast::Crispr::DeleteBatch.call(content: content, targets: targets, source_label: "gemspec")
|
|
11242
|
+
return content unless result.changed
|
|
11243
|
+
|
|
11244
|
+
ensure_trailing_newline(result.updated_content.gsub(/\n{3,}/, "\n\n"))
|
|
11245
|
+
end
|
|
11246
|
+
|
|
11247
|
+
def gemspec_dependency_call_kinds(runtime_only: false, development_only: false)
|
|
11248
|
+
kinds = %w[add_dependency add_runtime_dependency add_development_dependency]
|
|
11249
|
+
kinds = kinds.reject { |kind| kind == "add_development_dependency" } if runtime_only
|
|
11250
|
+
kinds = kinds.select { |kind| kind == "add_development_dependency" } if development_only
|
|
11251
|
+
kinds.map(&:to_sym)
|
|
11252
|
+
end
|
|
11253
|
+
|
|
11254
|
+
# Removes a single named gemspec dependency call, along with its owned
|
|
11255
|
+
# leading comment (if any) — matching by call name and the same
|
|
11256
|
+
# receiver, kind, and first-argument matching #gemspec_dependency_records
|
|
11257
|
+
# uses, but locating the call anywhere in the tree via the :all_statements
|
|
11258
|
+
# owner scope (not just the top level), and deleting it with
|
|
11259
|
+
# Ast::Crispr::DeleteBatch instead of raw line-range replacement so its
|
|
11260
|
+
# owned comments come with it rather than being left dangling.
|
|
11261
|
+
def ast_crispr_gemspec_dependency_delete_target(receiver:, name:, allowed_kinds:)
|
|
11262
|
+
Ast::Crispr::Ruby::Prism::Selectors.owner_filter(
|
|
11263
|
+
id: "remove_gemspec_dependency_#{receiver}_#{name}",
|
|
11264
|
+
owner_scope: :all_statements,
|
|
11265
|
+
limit: {none_or_one: true}
|
|
11266
|
+
) do |context, owner|
|
|
11267
|
+
next false unless owner.respond_to?(:receiver) && owner.respond_to?(:name)
|
|
11268
|
+
next false unless owner.receiver&.slice == receiver.to_s
|
|
11269
|
+
next false unless allowed_kinds.include?(owner.name)
|
|
11270
|
+
|
|
11271
|
+
argument = owner.arguments&.arguments&.first
|
|
11272
|
+
next false unless ruby_static_string_value(argument) == name
|
|
11273
|
+
|
|
11274
|
+
leading = context.comment_regions_for(owner, region: :leading, owner_scope: :all_statements)
|
|
11275
|
+
start_line = leading.any? ? leading.map { |comment| comment.location.start_line }.min : owner.location.start_line
|
|
11276
|
+
Ast::Crispr::Match.new(
|
|
11277
|
+
node: owner,
|
|
11278
|
+
start_line: start_line,
|
|
11279
|
+
end_line: owner.location.end_line,
|
|
11280
|
+
metadata: {
|
|
11281
|
+
start_boundary: (leading.any? ? :comment_region_start : :owner_start),
|
|
11282
|
+
end_boundary: :owner_end,
|
|
11283
|
+
payload_kind: :structural_owner_body
|
|
11284
|
+
}
|
|
11285
|
+
)
|
|
11286
|
+
end
|
|
11139
11287
|
end
|
|
11140
11288
|
|
|
11141
11289
|
def remove_ruby_comment_lines_containing(content, text)
|
|
@@ -11667,8 +11815,8 @@ module Kettle
|
|
|
11667
11815
|
|
|
11668
11816
|
case expression.receiver&.slice
|
|
11669
11817
|
when "enumerate_package_files"
|
|
11670
|
-
|
|
11671
|
-
|
|
11818
|
+
arguments = Array(expression.arguments&.arguments)
|
|
11819
|
+
arguments.length == 1 && !ruby_static_string_value(arguments.first).nil?
|
|
11672
11820
|
when "enumerate_package_glob"
|
|
11673
11821
|
generated_gemspec_package_glob_call?(expression)
|
|
11674
11822
|
else
|
|
@@ -12495,11 +12643,13 @@ module Kettle
|
|
|
12495
12643
|
content = resolve_template_tokens(content, tokens, scan_unresolved: false)
|
|
12496
12644
|
bootstrap_licenses = Array(recipe[:bootstrap_licenses]).map(&:to_s).reject(&:empty?)
|
|
12497
12645
|
content = replace_kettle_config_bootstrap_licenses(content, bootstrap_licenses) unless bootstrap_licenses.empty?
|
|
12646
|
+
bootstrap_platforms = Array(recipe[:bootstrap_platforms]).map(&:to_s).reject(&:empty?)
|
|
12647
|
+
content = replace_kettle_config_bootstrap_platforms(content, bootstrap_platforms) unless bootstrap_platforms.empty?
|
|
12498
12648
|
content = replace_kettle_config_bootstrap_project_emoji(content, recipe[:bootstrap_project_emoji]) unless recipe[:bootstrap_project_emoji].to_s.empty?
|
|
12499
12649
|
content = replace_kettle_config_bootstrap_rubyforum(content, recipe[:bootstrap_rubyforum]) if recipe[:bootstrap_rubyforum].is_a?(Hash)
|
|
12500
12650
|
content = apply_kettle_config_bootstrap_profile(content, recipe[:bootstrap_template_profile], recipe[:bootstrap_gemspec_path])
|
|
12501
12651
|
content = add_shim_bootstrap_config(content, recipe[:bootstrap_shim]) if recipe[:bootstrap_shim].is_a?(Hash)
|
|
12502
|
-
assert_no_unresolved_template_tokens_in_yaml_values(sync_kettle_config_env_overrides(content, env), KETTLE_CONFIG_PATH)
|
|
12652
|
+
assert_no_unresolved_template_tokens_in_yaml_values(sync_kettle_config_env_overrides(content, env, project_root: project_root), KETTLE_CONFIG_PATH)
|
|
12503
12653
|
end
|
|
12504
12654
|
|
|
12505
12655
|
def replace_kettle_config_bootstrap_project_emoji(content, emoji)
|
|
@@ -12517,6 +12667,14 @@ module Kettle
|
|
|
12517
12667
|
raise Error, "Could not replace licenses block in .kettle-jem.yml bootstrap template"
|
|
12518
12668
|
end
|
|
12519
12669
|
|
|
12670
|
+
def replace_kettle_config_bootstrap_platforms(content, platforms)
|
|
12671
|
+
platform_block = ["platforms:", *platforms.map { |platform| " - #{platform}" }].join("\n")
|
|
12672
|
+
updated = replace_yaml_node_lines(content, "platforms", "#{platform_block}\n")
|
|
12673
|
+
return updated unless updated == content
|
|
12674
|
+
|
|
12675
|
+
raise Error, "Could not replace platforms block in .kettle-jem.yml bootstrap template"
|
|
12676
|
+
end
|
|
12677
|
+
|
|
12520
12678
|
def replace_kettle_config_bootstrap_rubyforum(content, rubyforum)
|
|
12521
12679
|
updated = content
|
|
12522
12680
|
family_tag = rubyforum[:family_tag].to_s
|
|
@@ -12556,7 +12714,7 @@ module Kettle
|
|
|
12556
12714
|
nil
|
|
12557
12715
|
end
|
|
12558
12716
|
|
|
12559
|
-
def sync_kettle_config_env_overrides(content, env)
|
|
12717
|
+
def sync_kettle_config_env_overrides(content, env, project_root: nil)
|
|
12560
12718
|
synced = KETTLE_CONFIG_ENV_SYNC_PATHS.reduce(content.to_s) do |updated, (path, env_key)|
|
|
12561
12719
|
value = env_sync_value(env, env_key)
|
|
12562
12720
|
next updated unless present_template_token_value?(value)
|
|
@@ -12568,9 +12726,36 @@ module Kettle
|
|
|
12568
12726
|
synced = sync_kettle_config_internal_values(synced)
|
|
12569
12727
|
synced = migrate_readme_logo_config(synced)
|
|
12570
12728
|
synced = prune_legacy_kettle_config_keys(synced)
|
|
12729
|
+
synced = migrate_platforms_config(synced, project_root) if project_root
|
|
12571
12730
|
sync_kettle_config_documentation_comments(synced)
|
|
12572
12731
|
end
|
|
12573
12732
|
|
|
12733
|
+
# Injects the `platforms:` key into a pre-existing config that predates it.
|
|
12734
|
+
# The injected value is computed from repo evidence (existing Windows CI)
|
|
12735
|
+
# rather than defaulted, so a destination that already runs Windows CI does
|
|
12736
|
+
# not silently lose that coverage on its next re-template. Mirrors the
|
|
12737
|
+
# licenses bootstrap: config wins when present, evidence only fills an
|
|
12738
|
+
# absent key. The block lands adjacent to `engines:`, which it documents by
|
|
12739
|
+
# analogy, and carries its own documentation because the packaged config
|
|
12740
|
+
# documents every key with a preceding comment block.
|
|
12741
|
+
def migrate_platforms_config(content, project_root)
|
|
12742
|
+
config = YAML.safe_load(content.to_s, permitted_classes: [], aliases: true) || {}
|
|
12743
|
+
return content if config.key?("platforms")
|
|
12744
|
+
|
|
12745
|
+
platforms = detected_os_platforms(project_root)
|
|
12746
|
+
platforms = DEFAULT_PLATFORMS if platforms.empty?
|
|
12747
|
+
block = [
|
|
12748
|
+
*PLATFORMS_CONFIG_DOCUMENTATION,
|
|
12749
|
+
"platforms:",
|
|
12750
|
+
*platforms.map { |platform| " - #{platform}" },
|
|
12751
|
+
""
|
|
12752
|
+
].join("\n")
|
|
12753
|
+
|
|
12754
|
+
insert_yaml_block_after_top_level_key(content, "engines", block)
|
|
12755
|
+
rescue Psych::Exception
|
|
12756
|
+
content
|
|
12757
|
+
end
|
|
12758
|
+
|
|
12574
12759
|
def normalize_kettle_config_optional_scalars(content)
|
|
12575
12760
|
config = YAML.safe_load(content.to_s, permitted_classes: [], aliases: true) || {}
|
|
12576
12761
|
return content unless config.key?("min_divergence_threshold")
|
|
@@ -12950,6 +13135,26 @@ module Kettle
|
|
|
12950
13135
|
content
|
|
12951
13136
|
end
|
|
12952
13137
|
|
|
13138
|
+
# Inserts a pre-indented block (comment lines plus a key with a block
|
|
13139
|
+
# sequence) immediately after the given top-level key's value span, so an
|
|
13140
|
+
# injected key lands adjacent to the analogous key it documents by
|
|
13141
|
+
# analogy. Falls back to appending at end of file when the anchor key is
|
|
13142
|
+
# absent. Only column-zero keys match: a nested key of the same name under
|
|
13143
|
+
# `files:` must not become the anchor.
|
|
13144
|
+
def insert_yaml_block_after_top_level_key(content, key, block)
|
|
13145
|
+
lines = content.to_s.lines
|
|
13146
|
+
yaml_mapping_nodes(content).each do |mapping|
|
|
13147
|
+
mapping.children.each_slice(2) do |key_node, value_node|
|
|
13148
|
+
next unless key_node.is_a?(Psych::Nodes::Scalar) && key_node.value.to_s == key.to_s
|
|
13149
|
+
next unless key_node.start_column.zero?
|
|
13150
|
+
|
|
13151
|
+
end_line = value_node.end_line
|
|
13152
|
+
return [*lines[0...end_line], block, *lines[end_line..].to_a].join
|
|
13153
|
+
end
|
|
13154
|
+
end
|
|
13155
|
+
"#{content.to_s.sub(/\n*\z/, "\n")}\n#{block}"
|
|
13156
|
+
end
|
|
13157
|
+
|
|
12953
13158
|
def apply_kettle_config_bootstrap_profile(content, profile, gemspec_path)
|
|
12954
13159
|
return content if profile.to_s.empty?
|
|
12955
13160
|
return replace_yaml_scalar_path(content, %w[templates profile], FULL_TEMPLATE_PROFILE) if profile.to_s == FULL_TEMPLATE_PROFILE
|
|
@@ -13601,6 +13806,13 @@ module Kettle
|
|
|
13601
13806
|
engines.map { |engine| engine.to_s.strip.downcase }.reject(&:empty?).uniq
|
|
13602
13807
|
end
|
|
13603
13808
|
|
|
13809
|
+
def os_platforms_config(config)
|
|
13810
|
+
platforms = config["platforms"]
|
|
13811
|
+
return unless platforms.is_a?(Array)
|
|
13812
|
+
|
|
13813
|
+
platforms.map { |platform| platform.to_s.strip.downcase }.reject(&:empty?).uniq
|
|
13814
|
+
end
|
|
13815
|
+
|
|
13604
13816
|
def funding_urls(project_root, package_name, funding_uri: nil, opencollective_disabled: false, open_collective_org: nil, enabled_platforms: nil)
|
|
13605
13817
|
urls = [funding_uri]
|
|
13606
13818
|
path = File.join(project_root, ".github", "FUNDING.yml")
|
|
@@ -14419,6 +14631,10 @@ module Kettle
|
|
|
14419
14631
|
package.fetch(:name).to_s
|
|
14420
14632
|
),
|
|
14421
14633
|
"KJ|CI:EXEC_CMD" => facts.dig(:ci, :exec_cmd).to_s,
|
|
14634
|
+
"KJ|CI:OS_MATRIX_ENTRIES" => github_actions_current_os_matrix_entries(facts),
|
|
14635
|
+
"KJ|CI:OS_TEST_STEPS" => github_actions_current_os_test_steps(facts),
|
|
14636
|
+
"KJ|README:PLATFORM_SUPPORT_TABLE" => readme_platform_support_table(facts),
|
|
14637
|
+
"KJ|RAKE:TOP_LEVEL_DOCS_TASK" => rake_top_level_docs_task_token(facts).rstrip,
|
|
14422
14638
|
"KJ|GITHUB_ACTIONS:COVERAGE_UPLOAD_STEPS" => github_actions_coverage_steps(disabled_integrations: facts.dig(:integrations, :disabled))
|
|
14423
14639
|
}.merge(
|
|
14424
14640
|
rubocop_template_tokens(rubygems[:min_ruby], ruby_style: facts.fetch(:ruby_style, {}))
|
|
@@ -14459,27 +14675,27 @@ module Kettle
|
|
|
14459
14675
|
def kettle_changelog_gemfile_dependency_token(package_name)
|
|
14460
14676
|
return "" if package_name == "kettle-changelog"
|
|
14461
14677
|
|
|
14462
|
-
|
|
14463
|
-
# Release lockfile/build commands set this dependency-specific switch so
|
|
14464
|
-
# the development tool cannot pull unpublished family gems into resolution.
|
|
14465
|
-
kettle_changelog_skip = ENV.fetch("KETTLE_DEV_SKIP_CHANGELOG_DEPENDENCY", "false").downcase
|
|
14466
|
-
kettle_changelog_skip = %w[true 1 yes on].include?(kettle_changelog_skip)
|
|
14467
|
-
kettle_changelog_local = ENV.fetch("KETTLE_DEV_DEV", "false").downcase
|
|
14468
|
-
kettle_changelog_local = !%w[false 0 no off].include?(kettle_changelog_local)
|
|
14469
|
-
unless kettle_changelog_skip
|
|
14470
|
-
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
end
|
|
14482
|
-
|
|
14678
|
+
[
|
|
14679
|
+
"# Release lockfile/build commands set this dependency-specific switch so",
|
|
14680
|
+
"# the development tool cannot pull unpublished family gems into resolution.",
|
|
14681
|
+
"kettle_changelog_skip = ENV.fetch(\"KETTLE_DEV_SKIP_CHANGELOG_DEPENDENCY\", \"false\").downcase",
|
|
14682
|
+
"kettle_changelog_skip = %w[true 1 yes on].include?(kettle_changelog_skip)",
|
|
14683
|
+
"kettle_changelog_local = ENV.fetch(\"KETTLE_DEV_DEV\", \"false\").downcase",
|
|
14684
|
+
"kettle_changelog_local = !%w[false 0 no off].include?(kettle_changelog_local)",
|
|
14685
|
+
"unless kettle_changelog_skip",
|
|
14686
|
+
" if kettle_changelog_local",
|
|
14687
|
+
indent_source(nomono_bundler_bootstrap("../../Gemfile.lock"), 4),
|
|
14688
|
+
" eval_nomono_gems(",
|
|
14689
|
+
" gems: [\"kettle-changelog\"],",
|
|
14690
|
+
" prefix: \"KETTLE_DEV\",",
|
|
14691
|
+
" path_env: \"KETTLE_DEV_DEV\",",
|
|
14692
|
+
" root: [\"src\", \"my\", \"kettle-dev\"]",
|
|
14693
|
+
" )",
|
|
14694
|
+
" elsif Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(\"4.0.0\")",
|
|
14695
|
+
%( gem "kettle-changelog", #{template_managed_dependency("kettle-changelog").fetch(:requirements).map(&:inspect).join(", ")}),
|
|
14696
|
+
" end",
|
|
14697
|
+
"end"
|
|
14698
|
+
].join("\n")
|
|
14483
14699
|
end
|
|
14484
14700
|
|
|
14485
14701
|
def readme_title_token(package, rubygems)
|
|
@@ -14492,6 +14708,68 @@ module Kettle
|
|
|
14492
14708
|
"#{package_name} / #{namespace}"
|
|
14493
14709
|
end
|
|
14494
14710
|
|
|
14711
|
+
# The README platform tier table, generated from the same facts that drive
|
|
14712
|
+
# the workflows, so it cannot drift from CI again (spec slice 1038). Rows
|
|
14713
|
+
# mirror the generated workflow set: versioned MRI workflows are Linux-only,
|
|
14714
|
+
# the current.yml OS matrix covers the declared platforms for MRI current,
|
|
14715
|
+
# and every engine lane outside current.yml is Linux-only. Engine capability
|
|
14716
|
+
# notes come from Kettle::Rb::PlatformSupport and carry their citation.
|
|
14717
|
+
def readme_platform_support_table(facts)
|
|
14718
|
+
engines = facts.dig(:rubygems, :engines)
|
|
14719
|
+
engines = DEFAULT_ENGINES if engines.nil? || engines.empty?
|
|
14720
|
+
platforms = facts.dig(:ci, :platforms)
|
|
14721
|
+
platforms = DEFAULT_PLATFORMS if platforms.nil? || platforms.empty?
|
|
14722
|
+
ruby_versions = facts.dig(:ci, :ruby_versions).to_a.map(&:to_s).reject(&:empty?)
|
|
14723
|
+
|
|
14724
|
+
parallel = "Parallel support"
|
|
14725
|
+
unsupported = "Unsupported"
|
|
14726
|
+
rows = []
|
|
14727
|
+
|
|
14728
|
+
unless ruby_versions.empty?
|
|
14729
|
+
rows << [
|
|
14730
|
+
"MRI #{ruby_versions.first}-#{ruby_versions.last}",
|
|
14731
|
+
parallel, unsupported, unsupported,
|
|
14732
|
+
"Versioned Ruby workflows run on Linux."
|
|
14733
|
+
]
|
|
14734
|
+
end
|
|
14735
|
+
rows << [
|
|
14736
|
+
"MRI current",
|
|
14737
|
+
parallel,
|
|
14738
|
+
platforms.include?("macos") ? parallel : unsupported,
|
|
14739
|
+
platforms.include?("windows") ? parallel : unsupported,
|
|
14740
|
+
"Current Ruby workflow covers the declared OS platforms."
|
|
14741
|
+
]
|
|
14742
|
+
rows << [
|
|
14743
|
+
"MRI head",
|
|
14744
|
+
parallel, unsupported, unsupported,
|
|
14745
|
+
"CI is allowed to fail while Ruby is in development."
|
|
14746
|
+
]
|
|
14747
|
+
|
|
14748
|
+
(engines - ["ruby"]).each do |engine|
|
|
14749
|
+
label = (engine == "jruby") ? "JRuby" : "TruffleRuby"
|
|
14750
|
+
windows_entry = Kettle::Rb::PlatformSupport.engine_os_entry(engine, "windows")
|
|
14751
|
+
note = if windows_entry && windows_entry.support == "unsupported"
|
|
14752
|
+
"#{windows_entry.note} ([source](#{windows_entry.citation}))"
|
|
14753
|
+
else
|
|
14754
|
+
"Engine workflows run on Linux; macOS and Windows are not exercised by CI."
|
|
14755
|
+
end
|
|
14756
|
+
rows << ["#{label} (all CI lanes)", parallel, unsupported, unsupported, note]
|
|
14757
|
+
end
|
|
14758
|
+
|
|
14759
|
+
[
|
|
14760
|
+
"Support tiers describe what this project's CI actually exercises, not what may",
|
|
14761
|
+
"work in an untested environment:",
|
|
14762
|
+
"",
|
|
14763
|
+
"- **Parallel support**: CI runs the test suite with multiple workers available.",
|
|
14764
|
+
"- **Unsupported**: the engine/platform combination is not covered by CI and is",
|
|
14765
|
+
" not a project compatibility claim.",
|
|
14766
|
+
"",
|
|
14767
|
+
"| Ruby engine/version | Linux | macOS | Windows | Notes |",
|
|
14768
|
+
"|---------------------|-------|-------|---------|-------|",
|
|
14769
|
+
*rows.map { |row| "| #{row.join(" | ")} |" }
|
|
14770
|
+
].join("\n")
|
|
14771
|
+
end
|
|
14772
|
+
|
|
14495
14773
|
def readme_dev_test_stack_table(package_name)
|
|
14496
14774
|
rows = README_DEV_TEST_STACK_GEMS.reject { |gem| gem.fetch(:name) == package_name.to_s }.map do |gem|
|
|
14497
14775
|
name = gem.fetch(:name)
|
|
@@ -14761,7 +15039,7 @@ module Kettle
|
|
|
14761
15039
|
"[sm-family-fixtures]: https://github.com/structuredmerge/structuredmerge-fixtures",
|
|
14762
15040
|
"[sm-family-go]: https://github.com/structuredmerge/structuredmerge-go",
|
|
14763
15041
|
"[sm-family-ruby]: https://github.com/structuredmerge/structuredmerge-ruby",
|
|
14764
|
-
"[sm-family-rust]: https://github.com/structuredmerge/structuredmerge
|
|
15042
|
+
"[sm-family-rust]: https://github.com/structuredmerge/structuredmerge",
|
|
14765
15043
|
"[sm-family-typescript]: https://github.com/structuredmerge/structuredmerge-typescript"
|
|
14766
15044
|
].join("\n")
|
|
14767
15045
|
end
|
|
@@ -15170,7 +15448,7 @@ module Kettle
|
|
|
15170
15448
|
compact_hash(
|
|
15171
15449
|
freeze_token: config.dig("defaults", "freeze_token").to_s.empty? ? "kettle-jem" : config.dig("defaults", "freeze_token").to_s,
|
|
15172
15450
|
kettle_jem_version: VERSION,
|
|
15173
|
-
kettle_jem_dependency_arguments:
|
|
15451
|
+
kettle_jem_dependency_arguments: kettle_jem_template_dependency_requirements(env: env).map(&:inspect).join(", "),
|
|
15174
15452
|
template_run_date: run_timestamp.strftime("%Y-%m-%d"),
|
|
15175
15453
|
template_run_year: run_timestamp.year.to_s,
|
|
15176
15454
|
kettle_dev_local_gems: kettle_dev_local_gems(config),
|
|
@@ -15314,7 +15592,7 @@ module Kettle
|
|
|
15314
15592
|
dev_env = "#{prefix}_DEV"
|
|
15315
15593
|
root_literal = ruby_array_literal(direct_sibling_nomono_root_parts(workspace_slug, repository))
|
|
15316
15594
|
word_array = names.map { |name| " #{name}" }.join("\n")
|
|
15317
|
-
|
|
15595
|
+
nomono_bootstrap = nomono_bundler_bootstrap("Gemfile.lock")
|
|
15318
15596
|
|
|
15319
15597
|
blocks << <<~RUBY.rstrip
|
|
15320
15598
|
# Direct sibling dependencies (env-switched via #{dev_env})
|
|
@@ -15331,7 +15609,7 @@ module Kettle
|
|
|
15331
15609
|
ENV.fetch("K_JEM_TEMPLATING", "false").casecmp("true").zero?)
|
|
15332
15610
|
direct_sibling_dev_was_set = ENV.key?("#{dev_env}")
|
|
15333
15611
|
direct_sibling_dev_original = ENV.fetch("#{dev_env}", nil)
|
|
15334
|
-
#{
|
|
15612
|
+
#{nomono_bootstrap}
|
|
15335
15613
|
begin
|
|
15336
15614
|
ENV["#{dev_env}"] = File.expand_path("..", __dir__) if direct_sibling_templating && !direct_sibling_local
|
|
15337
15615
|
|
|
@@ -15518,7 +15796,7 @@ module Kettle
|
|
|
15518
15796
|
end
|
|
15519
15797
|
|
|
15520
15798
|
def kettle_dev_local_gems(config)
|
|
15521
|
-
gems = %w[kettle-dev kettle-family kettle-test kettle-soup-cover kettle-changelog]
|
|
15799
|
+
gems = %w[kettle-dev kettle-family kettle-rb kettle-test kettle-soup-cover kettle-changelog]
|
|
15522
15800
|
plugin_names = PluginLoader.normalize_plugin_names(plugin_names_from_config(config))
|
|
15523
15801
|
gems.concat(plugin_names.select { |plugin_name| plugin_name.start_with?("kettle-") })
|
|
15524
15802
|
gems.uniq.join(" ")
|
|
@@ -15527,7 +15805,7 @@ module Kettle
|
|
|
15527
15805
|
def main_gemfile_kettle_family_gem(package_name)
|
|
15528
15806
|
return "" if package_name.to_s == "kettle-family"
|
|
15529
15807
|
|
|
15530
|
-
%(gem "kettle-family", "~> 1.3", ">= 1.3.
|
|
15808
|
+
%(gem "kettle-family", "~> 1.3", ">= 1.3.10"\n)
|
|
15531
15809
|
end
|
|
15532
15810
|
|
|
15533
15811
|
def main_gemfile_nomono_bootstrap(package_name)
|
|
@@ -15540,25 +15818,40 @@ module Kettle
|
|
|
15540
15818
|
end
|
|
15541
15819
|
|
|
15542
15820
|
def nomono_gemfile_declaration
|
|
15543
|
-
%(gem "nomono", "~> 1.1", ">= 1.1.
|
|
15821
|
+
%(gem "nomono", "~> 1.1", ">= 1.1.7", require: false # ruby >= 3.2.0)
|
|
15544
15822
|
end
|
|
15545
15823
|
|
|
15546
15824
|
def local_gemfile_nomono_bootstrap(_package_name)
|
|
15825
|
+
nomono_bundler_bootstrap("../../Gemfile.lock")
|
|
15826
|
+
end
|
|
15827
|
+
|
|
15828
|
+
def nomono_bundler_bootstrap(lockfile_path)
|
|
15547
15829
|
<<~RUBY.rstrip
|
|
15548
15830
|
# Bootstrapping nomono here cannot rely on a plain `gem "nomono", ...` line.
|
|
15549
15831
|
# Bundler records that dependency during Gemfile evaluation, but it does not
|
|
15550
15832
|
# activate that exact version before the immediate `require "nomono/bundler"`.
|
|
15551
|
-
nomono_activation_requirements = ["~> 1.1", ">= 1.1.
|
|
15833
|
+
nomono_activation_requirements = ["~> 1.1", ">= 1.1.7"]
|
|
15552
15834
|
nomono_requirement = Gem::Requirement.new(nomono_activation_requirements)
|
|
15553
15835
|
nomono_already_activated = Gem.loaded_specs["nomono"]
|
|
15554
|
-
nomono_lockfile = File.expand_path(
|
|
15555
|
-
|
|
15836
|
+
nomono_lockfile = File.expand_path(#{lockfile_path.inspect}, __dir__)
|
|
15837
|
+
nomono_locked_spec = nil
|
|
15838
|
+
if File.file?(nomono_lockfile)
|
|
15556
15839
|
require "bundler"
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
15560
|
-
|
|
15561
|
-
|
|
15840
|
+
nomono_locked_spec = Bundler::LockfileParser
|
|
15841
|
+
.new(Bundler.read_file(nomono_lockfile))
|
|
15842
|
+
.specs
|
|
15843
|
+
.find { |spec| spec.name == "nomono" }
|
|
15844
|
+
end
|
|
15845
|
+
nomono_local_loader = if nomono_locked_spec && nomono_locked_spec.source.is_a?(Bundler::Source::Path)
|
|
15846
|
+
File.expand_path(
|
|
15847
|
+
File.join(nomono_locked_spec.source.path, "lib", "nomono", "bundler"),
|
|
15848
|
+
File.dirname(nomono_lockfile)
|
|
15849
|
+
)
|
|
15850
|
+
end
|
|
15851
|
+
if nomono_local_loader && File.file?("\#{nomono_local_loader}.rb")
|
|
15852
|
+
require nomono_local_loader
|
|
15853
|
+
else
|
|
15854
|
+
if !nomono_already_activated || !nomono_requirement.satisfied_by?(nomono_already_activated.version)
|
|
15562
15855
|
nomono_locked_installed = nomono_locked_spec &&
|
|
15563
15856
|
Gem::Specification.find_all_by_name("nomono").any? { |spec| spec.version == nomono_locked_spec.version }
|
|
15564
15857
|
nomono_locked = nomono_locked_spec &&
|
|
@@ -15566,9 +15859,9 @@ module Kettle
|
|
|
15566
15859
|
nomono_requirement.satisfied_by?(nomono_locked_spec.version)
|
|
15567
15860
|
nomono_activation_requirements = ["= \#{nomono_locked_spec.version}"] if nomono_locked
|
|
15568
15861
|
end
|
|
15862
|
+
Kernel.send(:gem, "nomono", *nomono_activation_requirements)
|
|
15863
|
+
require "nomono/bundler"
|
|
15569
15864
|
end
|
|
15570
|
-
Kernel.send(:gem, "nomono", *nomono_activation_requirements)
|
|
15571
|
-
require "nomono/bundler"
|
|
15572
15865
|
RUBY
|
|
15573
15866
|
end
|
|
15574
15867
|
|
|
@@ -15791,7 +16084,10 @@ module Kettle
|
|
|
15791
16084
|
next false if argument.to_s.empty?
|
|
15792
16085
|
|
|
15793
16086
|
if call.name == :require_relative
|
|
15794
|
-
|
|
16087
|
+
Kettle::Dev::Paths.same?(
|
|
16088
|
+
File.join(project_root, File.dirname(entrypoint_path), "#{argument}.rb"),
|
|
16089
|
+
version_absolute_path
|
|
16090
|
+
)
|
|
15795
16091
|
else
|
|
15796
16092
|
argument == version_require_path
|
|
15797
16093
|
end
|
|
@@ -17440,6 +17736,38 @@ module Kettle
|
|
|
17440
17736
|
}
|
|
17441
17737
|
end
|
|
17442
17738
|
|
|
17739
|
+
# Top-level `docs` task, emitted only for the profiles that need it.
|
|
17740
|
+
#
|
|
17741
|
+
# The gate is inverted from the obvious reading. At a monorepo root the
|
|
17742
|
+
# `family` namespace loads (the root has a `gems/` directory) and
|
|
17743
|
+
# `family:docs` exists, and there is no `docs/` directory, so `rake docs`
|
|
17744
|
+
# fails loudly with "Don't know how to build task 'docs'". That is an honest
|
|
17745
|
+
# error which points the reader at `family:docs`; adding a delegating task
|
|
17746
|
+
# there would only create a second path to behavior the family task already
|
|
17747
|
+
# owns.
|
|
17748
|
+
#
|
|
17749
|
+
# Everywhere else the failure is silent, and that is what this fixes. A
|
|
17750
|
+
# generated `docs/` directory makes Rake resolve the bare name `docs` as an
|
|
17751
|
+
# implicit file task whose target already exists, so `rake docs` exits 0
|
|
17752
|
+
# having regenerated nothing. Verified per profile: standalone and
|
|
17753
|
+
# monorepo-subgem both have `docs/` and no top-level `docs` task;
|
|
17754
|
+
# monorepo-root has neither.
|
|
17755
|
+
#
|
|
17756
|
+
# Document generation is owned by `yard`, which kettle-dev installs and
|
|
17757
|
+
# registers as a `default` prerequisite, so this delegates rather than
|
|
17758
|
+
# reimplementing it.
|
|
17759
|
+
def rake_top_level_docs_task_token(facts)
|
|
17760
|
+
return "" if monorepo_root_template_profile?(facts)
|
|
17761
|
+
|
|
17762
|
+
<<~RAKE
|
|
17763
|
+
# Without this, a generated docs/ directory makes Rake resolve the bare
|
|
17764
|
+
# name `docs` as an up-to-date file task, so `rake docs` exits 0 having
|
|
17765
|
+
# regenerated nothing.
|
|
17766
|
+
desc "Generate YARD documentation"
|
|
17767
|
+
task docs: "yard"
|
|
17768
|
+
RAKE
|
|
17769
|
+
end
|
|
17770
|
+
|
|
17443
17771
|
def gemspec_template_facts(config)
|
|
17444
17772
|
includes = Array(config.dig("gemspec", "package_files", "include"))
|
|
17445
17773
|
.map { |path| path.to_s.strip }
|
|
@@ -18637,6 +18965,7 @@ module Kettle
|
|
|
18637
18965
|
"KJ|RUBYFORUM:PROJECT_TAG" => bootstrap.dig(:rubyforum, :project_tag).to_s
|
|
18638
18966
|
}
|
|
18639
18967
|
recipe[:bootstrap_licenses] = Array(bootstrap[:licenses]).map(&:to_s).reject(&:empty?)
|
|
18968
|
+
recipe[:bootstrap_platforms] = Array(bootstrap[:platforms]).map(&:to_s).reject(&:empty?)
|
|
18640
18969
|
recipe[:bootstrap_template_profile] = bootstrap[:template_profile].to_s unless bootstrap[:template_profile].to_s.empty?
|
|
18641
18970
|
recipe[:bootstrap_gemspec_path] = bootstrap[:gemspec_path].to_s unless bootstrap[:gemspec_path].to_s.empty?
|
|
18642
18971
|
recipe[:bootstrap_project_emoji] = bootstrap[:project_emoji].to_s unless bootstrap[:project_emoji].to_s.empty?
|
|
@@ -18956,6 +19285,27 @@ module Kettle
|
|
|
18956
19285
|
(engines.nil? || engines.empty?) ? DEFAULT_ENGINES : engines
|
|
18957
19286
|
end
|
|
18958
19287
|
|
|
19288
|
+
# CI OS families the destination supports, from the `platforms:` config key,
|
|
19289
|
+
# falling back to DEFAULT_PLATFORMS when the key is absent or empty. Named
|
|
19290
|
+
# to stay clear of the funding-platforms vocabulary (#funding_platform_enabled?).
|
|
19291
|
+
def enabled_os_platforms(config)
|
|
19292
|
+
platforms = os_platforms_config(config)
|
|
19293
|
+
(platforms.nil? || platforms.empty?) ? DEFAULT_PLATFORMS : platforms
|
|
19294
|
+
end
|
|
19295
|
+
|
|
19296
|
+
# Configured → detected → default. Facts are computed BEFORE the config
|
|
19297
|
+
# migration runs in the same templating pass, so a missing `platforms:` key
|
|
19298
|
+
# must fold repo evidence in here: generating from the default alone would
|
|
19299
|
+
# silently drop existing Windows CI coverage on the first run, even though
|
|
19300
|
+
# the migration writes the correct key in that same run. Mirrors
|
|
19301
|
+
# configured_or_detected_licenses.
|
|
19302
|
+
def ci_os_platforms_facts(kettle_config, project_root)
|
|
19303
|
+
platforms = os_platforms_config(kettle_config)
|
|
19304
|
+
platforms = detected_os_platforms(project_root) if platforms.nil? || platforms.empty?
|
|
19305
|
+
platforms = DEFAULT_PLATFORMS if platforms.nil? || platforms.empty?
|
|
19306
|
+
platforms
|
|
19307
|
+
end
|
|
19308
|
+
|
|
18959
19309
|
def config_min_ruby(config)
|
|
18960
19310
|
value = config.dig("rubygems", "min_ruby") || config["min_ruby"]
|
|
18961
19311
|
version = value.to_s[/\d+\.\d+(?:\.\d+)?/]
|
|
@@ -19053,6 +19403,7 @@ module Kettle
|
|
|
19053
19403
|
}
|
|
19054
19404
|
gemfile_entry[:default] = true if entry[:default]
|
|
19055
19405
|
gemfile_entry[:replaces] = entry[:replaces] unless entry.fetch(:replaces, []).empty?
|
|
19406
|
+
gemfile_entry.merge!(framework_matrix_security(framework_gem, entry))
|
|
19056
19407
|
gemfile_entry
|
|
19057
19408
|
end.uniq { |entry| entry.fetch(:path) }
|
|
19058
19409
|
default_gemfiles = gemfiles.select { |entry| entry[:default] }
|
|
@@ -19238,6 +19589,32 @@ module Kettle
|
|
|
19238
19589
|
"~> #{normalized}"
|
|
19239
19590
|
end
|
|
19240
19591
|
|
|
19592
|
+
# Security data from kettle-rb (Kettle::Rb::GemFloors) for a framework
|
|
19593
|
+
# matrix version of a tracked gem. The series' security floor is added
|
|
19594
|
+
# when the version's requirement still admits the series' first release,
|
|
19595
|
+
# and advisories that no release in the series fixes are recorded so the
|
|
19596
|
+
# generated gemfile can warn about them.
|
|
19597
|
+
def framework_matrix_security(gem_name, entry)
|
|
19598
|
+
return {} unless Kettle::Rb::GemFloors.tracked?(gem_name)
|
|
19599
|
+
|
|
19600
|
+
series = Kettle::Rb::GemFloors.series_for(entry.fetch(:label))
|
|
19601
|
+
return {} unless series
|
|
19602
|
+
|
|
19603
|
+
security = {}
|
|
19604
|
+
first_release = Gem::Version.new("#{series}.0")
|
|
19605
|
+
floor = Kettle::Rb::GemFloors.floor(gem_name, series)
|
|
19606
|
+
requirement = Gem::Requirement.new(*entry.fetch(:requirement).split(/\s*,\s*/))
|
|
19607
|
+
if floor && Gem::Version.new(floor) > first_release && requirement.satisfied_by?(first_release)
|
|
19608
|
+
security[:security_floor] = floor
|
|
19609
|
+
end
|
|
19610
|
+
unpatched = Kettle::Rb::GemFloors.unpatched_advisories(gem_name, series)
|
|
19611
|
+
security[:unpatched_advisories] = unpatched unless unpatched.empty?
|
|
19612
|
+
security[:security_series] = series unless security.empty?
|
|
19613
|
+
security
|
|
19614
|
+
rescue ArgumentError
|
|
19615
|
+
{}
|
|
19616
|
+
end
|
|
19617
|
+
|
|
19241
19618
|
def github_actions_coverage_config(config, env = ENV)
|
|
19242
19619
|
workflows = config["workflows"]
|
|
19243
19620
|
return {} unless workflows.is_a?(Hash)
|
|
@@ -19263,6 +19640,19 @@ module Kettle
|
|
|
19263
19640
|
)
|
|
19264
19641
|
end
|
|
19265
19642
|
|
|
19643
|
+
# The binstub the generated Windows-only test step invokes. The step
|
|
19644
|
+
# bypasses Appraisal and runs under gemfiles/current.gemfile, but the
|
|
19645
|
+
# command itself is the destination's own configured test command
|
|
19646
|
+
# (ci.exec_cmd) resolved to its binstub -- never a hardcoded binstub of
|
|
19647
|
+
# any one repo. The packaged template once hardcoded bin/turbo_tests2,
|
|
19648
|
+
# which was correct for exactly one destination and inconsistent even
|
|
19649
|
+
# there, since that destination's own exec_cmd is kettle-test.
|
|
19650
|
+
def github_actions_windows_exec_binstub(exec_cmd)
|
|
19651
|
+
token = exec_cmd.to_s.strip.split(/\s+/).last.to_s
|
|
19652
|
+
token = token.sub(%r{\Abin/}, "")
|
|
19653
|
+
token.empty? ? "kettle-test" : token
|
|
19654
|
+
end
|
|
19655
|
+
|
|
19266
19656
|
# A generated workflow normally uses one command for every matrix entry.
|
|
19267
19657
|
# Legacy engines occasionally need a narrower command while retaining the
|
|
19268
19658
|
# rest of the generated workflow (setup, retries, cache keys, and pins).
|
|
@@ -19709,7 +20099,7 @@ module Kettle
|
|
|
19709
20099
|
def github_actions_setup_ruby_steps(indent:)
|
|
19710
20100
|
yaml = <<~YAML
|
|
19711
20101
|
- name: Setup Ruby & RubyGems
|
|
19712
|
-
uses: appraisal-rb/setup-ruby-flash@
|
|
20102
|
+
uses: appraisal-rb/setup-ruby-flash@6f8ad36ba7488db591541483afb5464d01e8861b # v2.14
|
|
19713
20103
|
with:
|
|
19714
20104
|
ruby-version: "${{ matrix.ruby }}"
|
|
19715
20105
|
rubygems: "${{ matrix.rubygems }}"
|
|
@@ -19758,7 +20148,13 @@ module Kettle
|
|
|
19758
20148
|
lines << %(ENV[#{key.inspect}] = #{value.inspect})
|
|
19759
20149
|
end
|
|
19760
20150
|
lines << ""
|
|
19761
|
-
|
|
20151
|
+
unpatched = gemfile.fetch(:unpatched_advisories, [])
|
|
20152
|
+
unless unpatched.empty?
|
|
20153
|
+
lines << "# kettle-rb: no #{gemfile.fetch(:gem)} #{gemfile.fetch(:security_series)} release fixes #{unpatched.join(", ")}"
|
|
20154
|
+
end
|
|
20155
|
+
requirements = [gemfile.fetch(:requirement)]
|
|
20156
|
+
requirements << ">= #{gemfile.fetch(:security_floor)}" if gemfile[:security_floor]
|
|
20157
|
+
lines << %(gem #{[gemfile.fetch(:gem), *requirements].map(&:inspect).join(", ")})
|
|
19762
20158
|
"#{lines.join("\n")}\n"
|
|
19763
20159
|
end
|
|
19764
20160
|
|
|
@@ -19821,7 +20217,7 @@ module Kettle
|
|
|
19821
20217
|
" uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1",
|
|
19822
20218
|
"",
|
|
19823
20219
|
" - name: Setup Ruby & RubyGems",
|
|
19824
|
-
" uses: appraisal-rb/setup-ruby-flash@
|
|
20220
|
+
" uses: appraisal-rb/setup-ruby-flash@6f8ad36ba7488db591541483afb5464d01e8861b # v2.14",
|
|
19825
20221
|
" with:",
|
|
19826
20222
|
" ruby-version: \"${{ matrix.ruby }}\"",
|
|
19827
20223
|
" rubygems: \"${{ matrix.rubygems }}\"",
|
|
@@ -19873,6 +20269,61 @@ module Kettle
|
|
|
19873
20269
|
update_github_actions_pins(updated)
|
|
19874
20270
|
end
|
|
19875
20271
|
|
|
20272
|
+
# The current.yml OS matrix entries, generated from the destination's
|
|
20273
|
+
# declared CI OS families (facts[:ci][:platforms]). current.yml is
|
|
20274
|
+
# strategy: accept_template, so it is replaced wholesale and the matrix
|
|
20275
|
+
# must be generated from facts rather than pruned from a destination file
|
|
20276
|
+
# (spec slice 1038). Runner labels come from Kettle::Rb::PlatformSupport,
|
|
20277
|
+
# and an unknown family raises rather than silently dropping a lane, since
|
|
20278
|
+
# a typo'd family would otherwise produce no CI for that platform at all.
|
|
20279
|
+
def github_actions_current_os_matrix_entries(facts)
|
|
20280
|
+
platforms = facts.dig(:ci, :platforms)
|
|
20281
|
+
platforms = DEFAULT_PLATFORMS if platforms.nil? || platforms.empty?
|
|
20282
|
+
exec_cmd = facts.dig(:ci, :exec_cmd).to_s
|
|
20283
|
+
|
|
20284
|
+
platforms.map do |platform|
|
|
20285
|
+
label = Kettle::Rb::PlatformSupport.runner_label(platform)
|
|
20286
|
+
raise Error, "Unknown CI OS family in platforms config: #{platform.inspect} (expected one of #{Kettle::Rb::PlatformSupport::OS_FAMILIES.join(", ")})" unless label
|
|
20287
|
+
|
|
20288
|
+
[
|
|
20289
|
+
%( - ruby: "ruby"),
|
|
20290
|
+
%( appraisal: "current"),
|
|
20291
|
+
%( os: #{label}),
|
|
20292
|
+
%( exec_cmd: "#{exec_cmd}"),
|
|
20293
|
+
" rubygems: latest",
|
|
20294
|
+
" bundler: latest"
|
|
20295
|
+
].join("\n")
|
|
20296
|
+
end.join("\n")
|
|
20297
|
+
end
|
|
20298
|
+
|
|
20299
|
+
# The current.yml test steps, generated from the same platforms facts. The
|
|
20300
|
+
# Windows-only guard and step exist only when windows is declared, so no
|
|
20301
|
+
# tautological `if:` survives in a linux+macos destination (spec slice
|
|
20302
|
+
# 1038). The Windows step bypasses Appraisal and invokes the destination's
|
|
20303
|
+
# own configured test command, resolved to its binstub from
|
|
20304
|
+
# facts[:ci][:exec_cmd] -- never a hardcoded binstub of any one repo.
|
|
20305
|
+
def github_actions_current_os_test_steps(facts)
|
|
20306
|
+
platforms = facts.dig(:ci, :platforms)
|
|
20307
|
+
platforms = DEFAULT_PLATFORMS if platforms.nil? || platforms.empty?
|
|
20308
|
+
windows = platforms.include?("windows")
|
|
20309
|
+
windows_label = Kettle::Rb::PlatformSupport.runner_label("windows")
|
|
20310
|
+
|
|
20311
|
+
lines = [" - name: Tests for ${{ matrix.os }} ${{ matrix.ruby }} via ${{ matrix.exec_cmd }}"]
|
|
20312
|
+
lines << %( if: matrix.os != '#{windows_label}') if windows
|
|
20313
|
+
lines << " run: bundle exec appraisal ${{ matrix.appraisal }} ${{ matrix.exec_cmd }}"
|
|
20314
|
+
return lines.join("\n") unless windows
|
|
20315
|
+
|
|
20316
|
+
binstub = github_actions_windows_exec_binstub(facts.dig(:ci, :exec_cmd))
|
|
20317
|
+
(lines + [
|
|
20318
|
+
"",
|
|
20319
|
+
" - name: Tests for #{windows_label} ${{ matrix.ruby }} via ruby bin/#{binstub}",
|
|
20320
|
+
%( if: matrix.os == '#{windows_label}'),
|
|
20321
|
+
" env:",
|
|
20322
|
+
" BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/current.gemfile",
|
|
20323
|
+
" run: ruby -rbundler/setup bin/#{binstub}"
|
|
20324
|
+
]).join("\n")
|
|
20325
|
+
end
|
|
20326
|
+
|
|
19876
20327
|
def github_actions_push_branches_yaml(content, default_branch:, indent: " ")
|
|
19877
20328
|
github_actions_push_branches(content, default_branch: default_branch).map do |branch|
|
|
19878
20329
|
"#{indent}- #{branch.inspect}"
|
|
@@ -19970,7 +20421,7 @@ module Kettle
|
|
|
19970
20421
|
steps << <<~YAML
|
|
19971
20422
|
- name: Upload coverage to QLTY
|
|
19972
20423
|
if: ${{ !env.ACT }}
|
|
19973
|
-
uses: qltysh/qlty-action/coverage@
|
|
20424
|
+
uses: qltysh/qlty-action/coverage@c9b09987143d1e4ac955f4803c7bea742102feb5 # v2.4.0
|
|
19974
20425
|
with:
|
|
19975
20426
|
oidc: true
|
|
19976
20427
|
files: coverage/lcov.info
|
|
@@ -19984,7 +20435,7 @@ module Kettle
|
|
|
19984
20435
|
steps << <<~YAML
|
|
19985
20436
|
- name: Upload coverage to CodeCov
|
|
19986
20437
|
if: ${{ !env.ACT }}
|
|
19987
|
-
uses: codecov/codecov-action@
|
|
20438
|
+
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
|
19988
20439
|
with:
|
|
19989
20440
|
use_oidc: true
|
|
19990
20441
|
disable_search: true
|
|
@@ -20264,19 +20715,19 @@ module Kettle
|
|
|
20264
20715
|
{
|
|
20265
20716
|
"actions/checkout" => "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1",
|
|
20266
20717
|
"actions/cache" => "actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0",
|
|
20267
|
-
"appraisal-rb/setup-ruby-flash" => "appraisal-rb/setup-ruby-flash@
|
|
20268
|
-
"ruby/setup-ruby" => "ruby/setup-ruby@
|
|
20718
|
+
"appraisal-rb/setup-ruby-flash" => "appraisal-rb/setup-ruby-flash@6f8ad36ba7488db591541483afb5464d01e8861b # v2.14",
|
|
20719
|
+
"ruby/setup-ruby" => "ruby/setup-ruby@e81a8fa391b11a595c1b7ed84177cc0fe02faf83 # v1.328.0",
|
|
20269
20720
|
"coverallsapp/github-action" => "coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8",
|
|
20270
|
-
"qltysh/qlty-action/coverage" => "qltysh/qlty-action/coverage@
|
|
20271
|
-
"codecov/codecov-action" => "codecov/codecov-action@
|
|
20721
|
+
"qltysh/qlty-action/coverage" => "qltysh/qlty-action/coverage@c9b09987143d1e4ac955f4803c7bea742102feb5 # v2.4.0",
|
|
20722
|
+
"codecov/codecov-action" => "codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1",
|
|
20272
20723
|
"irongut/CodeCoverageSummary" => "irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0",
|
|
20273
20724
|
"marocchino/sticky-pull-request-comment" => "marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5",
|
|
20274
|
-
"actions/upload-artifact" => "actions/upload-artifact@
|
|
20725
|
+
"actions/upload-artifact" => "actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2",
|
|
20275
20726
|
"amancevice/setup-code-climate" => "amancevice/setup-code-climate@0daf2985a225e8ac15975b4d233010e94d65b76a # v2",
|
|
20276
20727
|
"actions/dependency-review-action" => "actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0",
|
|
20277
|
-
"github/codeql-action/init" => "github/codeql-action/init@
|
|
20278
|
-
"github/codeql-action/autobuild" => "github/codeql-action/autobuild@
|
|
20279
|
-
"github/codeql-action/analyze" => "github/codeql-action/analyze@
|
|
20728
|
+
"github/codeql-action/init" => "github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3",
|
|
20729
|
+
"github/codeql-action/autobuild" => "github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3",
|
|
20730
|
+
"github/codeql-action/analyze" => "github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3",
|
|
20280
20731
|
"pozil/auto-assign-issue" => "pozil/auto-assign-issue@af6beea6bdf1e8eb373f061c5bc168681fc6d011 # v4.0.1",
|
|
20281
20732
|
"apache/skywalking-eyes/dependency" => "apache/skywalking-eyes/dependency@a196742f472feaffafea537ce5a2a4c3c53a8de4 # v0.9.0",
|
|
20282
20733
|
"sarisia/actions-status-discord" => "sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0"
|