kettle-jem-appraisals 0.1.4 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a78ffff1b9845b058f35013adea659ae8a8b03f21c522494122255f1774f0913
4
- data.tar.gz: 4411573fb6ebf9808efdf42d0d4be6496cdd18f07a9c637d326075a5314c2f14
3
+ metadata.gz: bb5a39f890084799496748b7329463f78f41ece95e22ea867acaf93e4a9f90e4
4
+ data.tar.gz: 19be289c164635ed3dd05c9628b82a1d03f823edd34ae958e15362d3df188bf9
5
5
  SHA512:
6
- metadata.gz: c9aa86d8864d7204613bf89db1f78811b944d7cbe842f2f5e6c4bd95448bbb500bcfcd201c6e5519532a85fa9acf3974592f642651f793fd7317db80f38823df
7
- data.tar.gz: e07bd405386de50eb128ab77984ae8eb47786f80c180fa3a8e8e8ba76503e8d3bbb306906eb9cffb25166ffbb9fb30cd06a9296c4581d606792a49d8f89bd130
6
+ metadata.gz: f8d5f93acaf50f246d5725c7f32dd8e996c71e5c10192591c5da0beab84600166b26caf328c948aeef1b79181c8d48b23ae90535e5a79d97fbb792447fc047cf
7
+ data.tar.gz: 206fbde205782a4caf791b7527c30b32375d8cb5fc31b6811ed333e0dbc6027603dd4fa7da47ce24969f7eb6eb5d323d377e3d50562c09001eea5520271852cc
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,31 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [0.1.5] - 2026-09-19
34
+
35
+ - TAG: [v0.1.5][0.1.5t]
36
+ - COVERAGE: 95.68% -- 975/1019 lines in 15 files
37
+ - BRANCH COVERAGE: 83.93% -- 282/336 branches in 15 files
38
+ - 92.13% documented
39
+
40
+ ### Added
41
+
42
+ - Security floors from kettle-rb `Kettle::Rb::GemFloors` in generated per-version gemfiles for tracked gems and their tracked sub-dependencies (e.g. `gem "activerecord", "~> 7.1.0", ">= 7.1.6"`)
43
+ - `--resolve` warns, and generated gemfiles carry a `# kettle-rb:` comment, when a series has advisories no release fixes or an exact patch version is below its security floor
44
+ - Runtime dependency on kettle-rb `>= 0.1.14`
45
+
46
+ ### Changed
47
+
48
+ - ActiveRecord support gemfiles take their sqlite3 requirements from kettle-rb `Kettle::Rb::GemDependencies` instead of hardcoded ranges, are written per requirement under `gemfiles/modular/activerecord_support/` (e.g. `v6.0.gemfile` for ActiveRecord 6.0–7.0), and only for the ActiveRecord versions in the matrix
49
+ - `activerecord_support.gemfile` / `activerecord_support_modern.gemfile` generated by 0.1.4 and earlier are removed (hand-maintained ones are kept)
50
+
51
+ ### Fixed
52
+
53
+ - `--resolve` now exits non-zero when `bin/appraisal generate` fails, instead of reporting "Resolve complete"
54
+ - `-h`/`--help` prints usage instead of running scaffold mode, and unknown options are rejected before any file is written
55
+ - A selected version whose optimal Ruby bucket falls below the project's Ruby floor (e.g. activerecord 5.2 for a gem requiring Ruby >= 2.5) is assigned to the lowest bucket that can run it instead of being silently dropped; selected versions that still cannot be assigned are reported
56
+ - `semver` mode includes the lowest minor allowed by a gem's `requirements` (e.g. activerecord 7.0 for `>= 7.0`)
57
+
33
58
  ## [0.1.4] - 2026-09-14
34
59
 
35
60
  - TAG: [v0.1.4][0.1.4t]
@@ -194,7 +219,9 @@ Please file a bug if you notice a violation of semantic versioning.
194
219
  - kettle-jem-template-20260802-001 - Devcontainer JSON files now merge as JSONC,
195
220
  preserving comments and trailing commas during template updates.
196
221
 
197
- [Unreleased]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.4...HEAD
222
+ [Unreleased]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.5...HEAD
223
+ [0.1.5]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.4...v0.1.5
224
+ [0.1.5t]: https://github.com/appraisal-rb/kettle-jem-appraisals/releases/tag/v0.1.5
198
225
  [0.1.4]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.3...v0.1.4
199
226
  [0.1.4t]: https://github.com/appraisal-rb/kettle-jem-appraisals/releases/tag/v0.1.4
200
227
  [0.1.3]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.2...v0.1.3
data/README.md CHANGED
@@ -381,19 +381,24 @@ database driver whose gem differs per engine and per ActiveRecord version:
381
381
  `sqlite3` on MRI/TruffleRuby, `activerecord-jdbcsqlite3-adapter` (plus
382
382
  `bigdecimal`) on JRuby. When `activerecord` is a tier1 gem, `--resolve`:
383
383
 
384
- - writes `gemfiles/modular/activerecord_support.gemfile` (`sqlite3` 1.4.x, for
385
- ActiveRecord < 7.2) and `gemfiles/modular/activerecord_support_modern.gemfile`
386
- (`sqlite3` >= 2.1, for ActiveRecord >= 7.2), each with a JRuby platform block
387
- for the JDBC adapter, whose release series tracks ActiveRecord so Bundler picks
384
+ - writes one support gemfile under `gemfiles/modular/activerecord_support/` for
385
+ each `sqlite3` requirement the matrix's ActiveRecord versions need. The
386
+ requirements come from [kettle-rb][kettle-rb]'s `Kettle::Rb::GemDependencies`
387
+ (what each ActiveRecord series' sqlite3 adapter declares), and each file is
388
+ named after the oldest ActiveRecord series sharing the requirement (for
389
+ example `v6.0.gemfile` covers 6.0–7.0 with `sqlite3 ~> 1.4`, and `v8.0.gemfile`
390
+ covers 8.0+ with `sqlite3 >= 2.1`). Each file has a JRuby platform block for
391
+ the JDBC adapter, whose release series tracks ActiveRecord so Bundler picks
388
392
  the matching adapter;
389
393
  - adds the matching support gemfile to each generated entry, based on that
390
- entry's ActiveRecord version (unless `appraisal_gemfiles` already lists one of
391
- them).
394
+ entry's ActiveRecord version (unless `appraisal_gemfiles` already lists one);
395
+ - removes `activerecord_support.gemfile` / `activerecord_support_modern.gemfile`
396
+ written by kettle-jem-appraisals 0.1.4 and earlier.
392
397
 
393
398
  Existing support gemfiles that do not carry the `# Generated by
394
399
  kettle-jem-appraisals` header are treated as hand-maintained and left untouched.
395
400
  Set `activerecord_support: false` to opt out entirely (for example, when the
396
- suite uses PostgreSQL). Point the root `Gemfile` at the modern support gemfile
401
+ suite uses PostgreSQL). Point the root `Gemfile` at the newest support gemfile
397
402
  via kettle-jem's `test_bundle.gemfiles`, and select the JDBC adapter in your spec
398
403
  helper on JRuby:
399
404
 
@@ -408,6 +413,21 @@ adapter = (RUBY_PLATFORM == "java" && ActiveRecord.version < Gem::Version.new("7
408
413
  ActiveRecord::Base.establish_connection(adapter: adapter, database: ":memory:")
409
414
  ```
410
415
 
416
+ ### Security floors
417
+
418
+ Per-version gemfiles for gems tracked by kettle-rb's `Kettle::Rb::GemFloors`
419
+ (activemodel, activerecord, activesupport, sqlite3) require the minor series'
420
+ security floor, e.g. `gem "activerecord", "~> 7.1.0", ">= 7.1.6"`: the newest
421
+ patch that keeps the series' original minimum Ruby, never below a patched
422
+ advisory. Tracked sub-dependencies get their floor too. Exact patch versions
423
+ (`patch` mode) stay pinned.
424
+
425
+ When a series has advisories that no release fixes (for example activerecord
426
+ 5.2–7.0 and CVE-2025-55193), or an exact patch version is below its floor,
427
+ `--resolve` warns and the generated gemfile carries a `# kettle-rb:` comment.
428
+ Support gemfiles likewise note sqlite3 advisories that no allowed sqlite3
429
+ release fixes. Update kettle-rb to pick up new advisories.
430
+
411
431
  ### Optimal bucket assignment
412
432
 
413
433
  Rather than cross-producting every gem version with every Ruby series,
@@ -450,14 +470,17 @@ This generates:
450
470
 
451
471
  ### CLI flags
452
472
 
453
- | Flag | Description |
454
- |--------------|---------------------------------------------|
455
- | `--scaffold` | Force scaffold mode (even if config exists) |
456
- | `--resolve` | Force resolve mode |
457
- | `--force` | Bypass freshness TTL and re-resolve |
473
+ | Flag | Description |
474
+ |-------------------|---------------------------------------------|
475
+ | `--scaffold` | Force scaffold mode (even if config exists) |
476
+ | `--resolve` | Force resolve mode |
477
+ | `--force` | Bypass freshness TTL and re-resolve |
478
+ | `-v`, `--version` | Print the version and exit |
479
+ | `-h`, `--help` | Print usage and exit |
458
480
 
459
- Without flags, the CLI auto-detects: scaffold if no versions are configured,
460
- resolve otherwise.
481
+ Without a mode flag, the CLI auto-detects: scaffold if no versions are configured,
482
+ resolve otherwise. Unknown options exit with an error before any file is written.
483
+ `--resolve` exits non-zero when `bin/appraisal generate` fails.
461
484
 
462
485
  ### Re-resolving
463
486
 
@@ -651,6 +674,7 @@ Thanks for RTFM. ☺️
651
674
  [✉️ruby-forum]: https://www.rubyforum.org/tag/appraisal-rb
652
675
 
653
676
  [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
677
+ [kettle-rb]: https://github.com/kettle-dev/kettle-rb
654
678
  [⛳️gem-namespace]: https://github.com/appraisal-rb/kettle-jem-appraisals
655
679
  [⛳️namespace-img]: https://img.shields.io/badge/namespace-Kettle::Jem::Appraisals-3C2D2D.svg?style=square&logo=ruby&logoColor=white
656
680
  [⛳️gem-name]: https://clickgems.clickhouse.com/dashboard/kettle-jem-appraisals
@@ -756,7 +780,7 @@ Thanks for RTFM. ☺️
756
780
  [📌gitmoji]: https://gitmoji.dev
757
781
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
758
782
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
759
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.897-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
783
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.019-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
760
784
  [🔐security]: https://github.com/appraisal-rb/kettle-jem-appraisals/blob/main/SECURITY.md
761
785
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
762
786
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "kettle/rb"
4
5
 
5
6
  module Kettle
6
7
  module Jem
@@ -9,8 +10,13 @@ module Kettle
9
10
  # database driver on every engine: +sqlite3+ on MRI/TruffleRuby and
10
11
  # +activerecord-jdbcsqlite3-adapter+ (plus +bigdecimal+) on JRuby.
11
12
  #
12
- # Two variants exist because +sqlite3+ 2.x is only supported by
13
- # ActiveRecord >= 7.2, while older ActiveRecord needs the 1.4 series.
13
+ # The sqlite3 requirement comes from +Kettle::Rb::GemDependencies+, which
14
+ # records what each ActiveRecord series' sqlite3 adapter declares. One
15
+ # support gemfile is written per run of consecutive ActiveRecord series
16
+ # sharing a requirement, named after the oldest series in the run
17
+ # (+gemfiles/modular/activerecord_support/v6.0.gemfile+ covers 6.0–7.0).
18
+ # ActiveRecord newer than the recorded series uses the newest run.
19
+ #
14
20
  # The JDBC adapter's release series tracks ActiveRecord (52.x, 61.x, 72.x…),
15
21
  # so an open requirement lets Bundler pick the adapter matching the
16
22
  # resolved ActiveRecord.
@@ -20,33 +26,32 @@ module Kettle
20
26
  #
21
27
  # @example
22
28
  # generator = ActiveRecordSupportGemfileGenerator.new(base_dir: "/path/to/gem")
23
- # generator.generate
24
- # generator.eval_path_for("7.1.6") #=> "modular/activerecord_support.gemfile"
29
+ # generator.generate(["7.1", "8.0"])
30
+ # generator.eval_path_for("7.1.6") #=> "modular/activerecord_support/v7.1.gemfile"
25
31
  class ActiveRecordSupportGemfileGenerator
26
32
  # @return [String] header marking a support gemfile as generated (and safe to regenerate)
27
33
  GENERATED_HEADER = "# Generated by kettle-jem-appraisals"
28
34
 
29
- # @return [String] support gemfile for ActiveRecord below {MODERN_ACTIVERECORD}
30
- LEGACY_PATH = "gemfiles/modular/activerecord_support.gemfile"
31
-
32
- # @return [String] support gemfile for ActiveRecord at or above {MODERN_ACTIVERECORD}
33
- MODERN_PATH = "gemfiles/modular/activerecord_support_modern.gemfile"
35
+ # @return [String] directory holding the support gemfiles
36
+ SUPPORT_DIR = "gemfiles/modular/activerecord_support"
34
37
 
35
- # @return [Gem::Version] first ActiveRecord release that supports sqlite3 2.x
36
- MODERN_ACTIVERECORD = Gem::Version.new("7.2")
38
+ # @return [String] support gemfile for ActiveRecord older than every recorded series
39
+ UNKNOWN_PATH = "#{SUPPORT_DIR}/unknown.gemfile"
37
40
 
38
- # @return [Hash{String => Hash}] per-variant sqlite3 requirements and description
39
- VARIANTS = {
40
- LEGACY_PATH => {sqlite3: [">= 1.4", "< 1.5"], label: "ActiveRecord < 7.2"},
41
- MODERN_PATH => {sqlite3: [">= 2.1"], label: "ActiveRecord >= 7.2"}
42
- }.freeze
41
+ # @return [Array<String>] support gemfiles written by kettle-jem-appraisals 0.1.4 and earlier
42
+ LEGACY_PATHS = [
43
+ "gemfiles/modular/activerecord_support.gemfile",
44
+ "gemfiles/modular/activerecord_support_modern.gemfile"
45
+ ].freeze
43
46
 
44
47
  class << self
45
48
  # @param path [String] a gemfile path, with or without the leading +gemfiles/+
46
- # @return [Boolean] whether +path+ names one of the ActiveRecord support gemfiles
49
+ # @return [Boolean] whether +path+ names an ActiveRecord support gemfile
47
50
  def support_gemfile?(path)
48
51
  normalized = path.to_s.strip.sub(%r{\Agemfiles/}, "")
49
- VARIANTS.keys.any? { |variant| variant.delete_prefix("gemfiles/") == normalized }
52
+ return true if normalized.start_with?("#{SUPPORT_DIR.delete_prefix("gemfiles/")}/")
53
+
54
+ LEGACY_PATHS.any? { |legacy| legacy.delete_prefix("gemfiles/") == normalized }
50
55
  end
51
56
  end
52
57
 
@@ -58,40 +63,121 @@ module Kettle
58
63
  @base_dir = base_dir
59
64
  end
60
65
 
61
- # Writes both support gemfiles, skipping any hand-maintained file.
66
+ # Writes the support gemfile needed by each ActiveRecord version, skipping
67
+ # hand-maintained files, and removes support gemfiles generated by
68
+ # kettle-jem-appraisals 0.1.4 and earlier.
62
69
  #
63
- # @return [Hash{String => Symbol}] relative path => +:written+ or +:kept+
64
- def generate
65
- VARIANTS.each_with_object({}) do |(relative_path, variant), results|
70
+ # @param activerecord_versions [Array<String>] ActiveRecord versions in the matrix
71
+ # @return [Hash{String => Symbol}] relative path => +:written+, +:kept+, or +:removed+
72
+ def generate(activerecord_versions)
73
+ results = {}
74
+ activerecord_versions.map { |version| group_for(version) }.uniq.each do |group|
75
+ relative_path = path_for(group)
66
76
  absolute = File.join(base_dir, relative_path)
67
- if File.exist?(absolute) && !File.read(absolute).include?(GENERATED_HEADER)
77
+ if hand_maintained?(absolute)
68
78
  results[relative_path] = :kept
69
79
  next
70
80
  end
71
81
 
72
82
  FileUtils.mkdir_p(File.dirname(absolute))
73
- File.write(absolute, build_content(variant))
83
+ File.write(absolute, build_content(group))
74
84
  results[relative_path] = :written
75
85
  end
86
+
87
+ LEGACY_PATHS.each do |relative_path|
88
+ absolute = File.join(base_dir, relative_path)
89
+ next unless File.exist?(absolute) && !hand_maintained?(absolute)
90
+
91
+ FileUtils.rm_f(absolute)
92
+ results[relative_path] = :removed
93
+ end
94
+ results
76
95
  end
77
96
 
78
97
  # @param activerecord_version [String, Gem::Version] the entry's ActiveRecord version
79
98
  # @return [String] the matching support gemfile, relative to +gemfiles/+ (for +eval_gemfile+)
80
99
  def eval_path_for(activerecord_version)
81
- path = (Gem::Version.new(activerecord_version.to_s) >= MODERN_ACTIVERECORD) ? MODERN_PATH : LEGACY_PATH
82
- path.delete_prefix("gemfiles/")
100
+ path_for(group_for(activerecord_version)).delete_prefix("gemfiles/")
101
+ end
102
+
103
+ # @param activerecord_version [String, Gem::Version]
104
+ # @return [Array<String>] the sqlite3 requirements for that ActiveRecord version
105
+ def sqlite3_requirements(activerecord_version)
106
+ group = group_for(activerecord_version)
107
+ group ? group.first.requirements : []
108
+ end
109
+
110
+ # Advisories that affect every sqlite3 series the requirements allow.
111
+ #
112
+ # @param activerecord_version [String, Gem::Version]
113
+ # @return [Array<String>] advisory ids; empty when a fixed sqlite3 release is allowed
114
+ def unfixed_sqlite3_advisories(activerecord_version)
115
+ requirement = Gem::Requirement.new(*sqlite3_requirements(activerecord_version))
116
+ allowed = Kettle::Rb::GemFloors.entries_for("sqlite3").select { |entry|
117
+ requirement.satisfied_by?(Gem::Version.new(entry.floor))
118
+ }
119
+ return [] if allowed.empty? || allowed.any? { |entry| entry.unpatched_advisories.empty? }
120
+
121
+ allowed.flat_map(&:unpatched_advisories).uniq.sort
83
122
  end
84
123
 
85
124
  private
86
125
 
87
- def build_content(variant)
88
- sqlite3 = [%("sqlite3"), *variant.fetch(:sqlite3).map(&:inspect), "platforms: [:ruby]"].join(", ")
126
+ # Runs of consecutive ActiveRecord series with identical sqlite3 requirements.
127
+ def groups
128
+ @groups ||= Kettle::Rb::GemDependencies.entries_for("activerecord", "sqlite3")
129
+ .sort_by { |entry| Gem::Version.new(entry.series) }
130
+ .chunk_while { |older, newer| older.requirements == newer.requirements }
131
+ .to_a
132
+ end
133
+
134
+ def group_for(activerecord_version)
135
+ series = Kettle::Rb::GemFloors.series_for(activerecord_version)
136
+ return unless series
137
+
138
+ version = Gem::Version.new(series)
139
+ groups.reverse.find { |group| Gem::Version.new(group.first.series) <= version }
140
+ end
141
+
142
+ def path_for(group)
143
+ group ? "#{SUPPORT_DIR}/v#{group.first.series}.gemfile" : UNKNOWN_PATH
144
+ end
145
+
146
+ def hand_maintained?(absolute)
147
+ File.exist?(absolute) && !File.read(absolute).include?(GENERATED_HEADER)
148
+ end
149
+
150
+ def label(group)
151
+ return "ActiveRecord without recorded sqlite3 requirements" unless group
152
+
153
+ first = group.first.series
154
+ last = group.last.series
155
+ range =
156
+ if group.equal?(groups.last)
157
+ "#{first}+"
158
+ elsif first == last
159
+ first
160
+ else
161
+ "#{first}–#{last}"
162
+ end
163
+ "ActiveRecord #{range}"
164
+ end
165
+
166
+ def build_content(group)
167
+ requirements = group ? group.first.requirements : []
168
+ sqlite3 = [%("sqlite3"), *requirements.map(&:inspect), "platforms: [:ruby]"].join(", ")
169
+ notes = []
170
+ notes << "# sqlite3 requirement from kettle-rb: #{group.last.source}" if group
171
+ unfixed = group ? unfixed_sqlite3_advisories(group.first.series) : []
172
+ notes << "# kettle-rb: no sqlite3 release allowed here fixes #{unfixed.join(", ")}" unless unfixed.empty?
173
+
89
174
  <<~GEMFILE
90
175
  # frozen_string_literal: true
91
176
 
92
177
  #{GENERATED_HEADER}
93
- # Database drivers for #{variant.fetch(:label)}: sqlite3 on MRI/TruffleRuby,
178
+ # Database drivers for #{label(group)}: sqlite3 on MRI/TruffleRuby,
94
179
  # activerecord-jdbcsqlite3-adapter on JRuby (its release series tracks ActiveRecord).
180
+ #{notes.join("\n")}
95
181
  gem #{sqlite3}
96
182
 
97
183
  platforms :jruby do
@@ -59,10 +59,49 @@ module Kettle
59
59
  end
60
60
  end
61
61
 
62
+ # @return [Array<String>] command-line options accepted by {#run}
63
+ OPTIONS = %w[--scaffold --resolve --force -v --version -h --help].freeze
64
+
65
+ # @return [String] usage text printed by +--help+
66
+ USAGE = <<~USAGE
67
+ Usage: kettle-jem-appraisals [--scaffold | --resolve] [--force]
68
+
69
+ Generates an Appraisal matrix for the gem in the current directory.
70
+ Without a mode flag, scaffolds appraisal_matrix in #{CONFIG_FILE} when it
71
+ has no resolved versions, and resolves it otherwise.
72
+
73
+ Options:
74
+ --scaffold Force scaffold mode (even if config exists)
75
+ --resolve Force resolve mode
76
+ --force Bypass freshness TTL and re-resolve
77
+ -v, --version Print the version and exit
78
+ -h, --help Print this help and exit
79
+ USAGE
80
+
62
81
  # Detects the appropriate mode and dispatches to scaffold or resolve.
63
82
  #
83
+ # Prints usage for +-h+/+--help+ and the version for +-v+/+--version+
84
+ # without touching any files, and exits when given an unknown option.
85
+ #
64
86
  # @return [void]
65
87
  def run
88
+ if args.include?("-h") || args.include?("--help")
89
+ puts USAGE
90
+ return
91
+ end
92
+
93
+ if args.include?("-v") || args.include?("--version")
94
+ puts Version::VERSION
95
+ return
96
+ end
97
+
98
+ unknown = args - OPTIONS
99
+ unless unknown.empty?
100
+ warn "Unknown option(s): #{unknown.join(" ")}"
101
+ warn USAGE
102
+ exit(1)
103
+ end
104
+
66
105
  mode = detect_mode
67
106
  case mode
68
107
  when :scaffold
@@ -267,6 +306,7 @@ module Kettle
267
306
  )
268
307
  annotate_extra_gemfiles(appraisal_entries, matrix_extra_gemfiles(matrix))
269
308
  annotate_activerecord_support(appraisal_entries, tier1_gems, matrix)
309
+ warn_security_advisories(appraisal_entries)
270
310
  annotate_standard_appraisal_collapses(appraisal_entries, bucket_ranges, matrix)
271
311
 
272
312
  puts " 📊 Generated #{appraisal_entries.size} appraisal entries"
@@ -295,19 +335,27 @@ module Kettle
295
335
  config[APPRAISAL_MATRIX_KEY] = matrix
296
336
  write_config(config)
297
337
 
298
- # Run bin/appraisal generate
299
- if File.exist?(File.join(project_dir, "bin", "appraisal"))
300
- puts " 🔧 Running bin/appraisal generate..."
301
- system("bin/appraisal", "generate", chdir: project_dir)
302
- else
303
- puts " ⚠️ bin/appraisal not found — run `bundle binstubs appraisal2` then `bin/appraisal generate`"
304
- end
338
+ run_appraisal_generate
305
339
 
306
340
  puts " ✅ Resolve complete"
307
341
  end
308
342
 
309
343
  # ── Helpers ──────────────────────────────────────────────────────
310
344
 
345
+ # Runs +bin/appraisal generate+ when the binstub exists, exiting when it fails.
346
+ def run_appraisal_generate
347
+ unless File.exist?(File.join(project_dir, "bin", "appraisal"))
348
+ puts " ⚠️ bin/appraisal not found — run `bundle binstubs appraisal2` then `bin/appraisal generate`"
349
+ return
350
+ end
351
+
352
+ puts " 🔧 Running bin/appraisal generate..."
353
+ return if system("bin/appraisal", "generate", chdir: project_dir)
354
+
355
+ warn " ❌ bin/appraisal generate failed (#{$?.inspect}); Appraisals and gemfiles/ may be incomplete"
356
+ exit(1)
357
+ end
358
+
311
359
  # Removes stale kja-* flat gemfiles from gemfiles/ that are no longer
312
360
  # in the current matrix. Only touches files matching the PREFIX pattern.
313
361
  def cleanup_stale_gemfiles(current_entries)
@@ -393,6 +441,14 @@ module Kettle
393
441
  # that version is the best choice).
394
442
  #
395
443
  # Tier2 versions are all compatible versions for each tier1's bucket.
444
+ # Reports selected versions that could not be assigned to any Ruby bucket.
445
+ def warn_unassigned_versions(gem_name, versions, assignments)
446
+ missing = versions - assignments.map { |assignment| assignment[:version] }
447
+ return if missing.empty?
448
+
449
+ warn " ⚠️ #{gem_name} #{missing.join(", ")} not assigned to any Ruby bucket (unknown required_ruby_version?); left out of the matrix"
450
+ end
451
+
396
452
  def build_matrix(tier1_gems, tier2_gems, ruby_series, bucket_ranges, all_seams, all_versions_by_gem, resolver, builder, gemfile_gen, sub_resolver)
397
453
  entries = []
398
454
 
@@ -415,6 +471,7 @@ module Kettle
415
471
  puts " ⚠️ No bucket assignments for #{t1_name}, falling back to latest bucket"
416
472
  t1_assignments = t1_versions.map { |v| {version: v, bucket: ruby_series.last} }
417
473
  end
474
+ warn_unassigned_versions(t1_name, t1_versions, t1_assignments)
418
475
 
419
476
  # Show assignments
420
477
  t1_assignments.each do |a|
@@ -609,8 +666,9 @@ module Kettle
609
666
  end.uniq
610
667
  end
611
668
 
612
- # Generates the ActiveRecord database support gemfiles and adds the one
613
- # matching each ActiveRecord entry's version to its extra gemfiles.
669
+ # Generates the ActiveRecord database support gemfiles the matrix needs and
670
+ # adds the one matching each ActiveRecord entry's version to its extra gemfiles.
671
+ # Warns when no sqlite3 release an entry's support gemfile allows fixes an advisory.
614
672
  # Skipped when disabled via +activerecord_support+, when activerecord is not
615
673
  # a tier1 gem, or when +appraisal_gemfiles+ already names a support gemfile.
616
674
  def annotate_activerecord_support(appraisal_entries, tier1_gems, matrix)
@@ -618,16 +676,43 @@ module Kettle
618
676
  return unless tier1_gems.any? { |gem_config| gem_config["name"] == "activerecord" }
619
677
  return if matrix_extra_gemfiles(matrix).any? { |path| ActiveRecordSupportGemfileGenerator.support_gemfile?(path) }
620
678
 
679
+ activerecord_entries = appraisal_entries.select { |entry| entry[:tier1_name] == "activerecord" && entry[:tier1_version] }
680
+ versions = activerecord_entries.map { |entry| entry[:tier1_version] }
621
681
  generator = ActiveRecordSupportGemfileGenerator.new(base_dir: project_dir)
622
- generator.generate.each do |path, status|
623
- puts " 🗄️ #{(status == :written) ? "Wrote" : "Kept hand-maintained"} #{path}"
682
+ generator.generate(versions).each do |path, status|
683
+ label = {written: "Wrote", kept: "Kept hand-maintained", removed: "Removed stale generated"}.fetch(status)
684
+ puts " 🗄️ #{label} #{path}"
624
685
  end
625
- appraisal_entries.each do |entry|
626
- next unless entry[:tier1_name] == "activerecord" && entry[:tier1_version]
627
-
686
+ activerecord_entries.each do |entry|
628
687
  support = generator.eval_path_for(entry[:tier1_version])
629
688
  entry[:extra_gemfiles] = [support, *Array(entry[:extra_gemfiles])].uniq
630
689
  end
690
+ versions.group_by { |version| generator.eval_path_for(version) }.each do |path, grouped|
691
+ unfixed = generator.unfixed_sqlite3_advisories(grouped.first)
692
+ next if unfixed.empty?
693
+
694
+ warn " ⚠️ kettle-rb: no sqlite3 release allowed for activerecord #{grouped.uniq.join(", ")} (#{path}) fixes #{unfixed.join(", ")}"
695
+ end
696
+ end
697
+
698
+ # Warns about tracked gem versions in the matrix whose minor series has
699
+ # advisories no release fixes, or whose exact patch version is below the
700
+ # series' security floor (from +Kettle::Rb::GemFloors+).
701
+ def warn_security_advisories(appraisal_entries)
702
+ pairs = appraisal_entries.flat_map { |entry|
703
+ [[entry[:tier1_name], entry[:tier1_version]], [entry[:tier2_name], entry[:tier2_version]]]
704
+ }
705
+ messages = pairs.filter_map { |name, version| [name, version] if name && version && Kettle::Rb::GemFloors.tracked?(name) }.flat_map { |name, version|
706
+ notes = []
707
+ unpatched = Kettle::Rb::GemFloors.unpatched_advisories(name, version)
708
+ notes << "no #{name} #{Kettle::Rb::GemFloors.series_for(version)} release fixes #{unpatched.join(", ")}" unless unpatched.empty?
709
+ floor = Kettle::Rb::GemFloors.floor(name, version)
710
+ if floor && version.to_s.split(".").length >= 3 && Gem::Version.new(floor) > Gem::Version.new(version.to_s)
711
+ notes << "#{name} #{version} is pinned below the security floor #{floor}"
712
+ end
713
+ notes
714
+ }
715
+ messages.uniq.each { |message| warn " ⚠️ kettle-rb: #{message}" }
631
716
  end
632
717
 
633
718
  def activerecord_support_enabled?(matrix)
@@ -115,7 +115,11 @@ module Kettle
115
115
  next_seam_ruby = find_next_seam_ruby(ver, ver_min_ruby, all_versions, version_min_ruby)
116
116
 
117
117
  bucket = if next_seam_ruby
118
- find_bucket_below(next_seam_ruby, buckets, bucket_ranges)
118
+ # When the next seam is at or below the project's Ruby floor, no
119
+ # bucket sits below it; test this version on the lowest bucket
120
+ # that can run it instead of dropping it.
121
+ find_bucket_below(next_seam_ruby, buckets, bucket_ranges) ||
122
+ find_lowest_runnable_bucket(ver_min_ruby, buckets, bucket_ranges)
119
123
  else
120
124
  # This is in the latest seam range — catch-all bucket
121
125
  buckets.last
@@ -130,6 +134,14 @@ module Kettle
130
134
 
131
135
  private
132
136
 
137
+ # Finds the lowest bucket whose Ruby ceiling can run a version requiring +min_ruby+.
138
+ def find_lowest_runnable_bucket(min_ruby, buckets, bucket_ranges)
139
+ buckets.find { |bucket|
140
+ range = bucket_ranges[bucket]
141
+ range && range[:ceiling] >= min_ruby
142
+ }
143
+ end
144
+
133
145
  # One entry per major version (the latest minor of each).
134
146
  def select_major(by_major)
135
147
  by_major.map { |entry| entry[:minors].last }
@@ -190,6 +202,9 @@ module Kettle
190
202
  end
191
203
  end
192
204
 
205
+ # Explicit requirements declare the oldest supported version, so test it.
206
+ versions << by_major.first[:minors].first if requirements && !requirements.empty?
207
+
193
208
  versions.uniq.sort_by { |v| Gem::Version.new(v) }
194
209
  end
195
210
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "kettle/rb"
4
+
3
5
  module Kettle
4
6
  module Jem
5
7
  module Appraisals
@@ -8,6 +10,11 @@ module Kettle
8
10
  # Creates per-version gemfiles organized by gem name and Ruby series:
9
11
  # +gemfiles/modular/{gem_name}/{ruby_series}/v{version}.gemfile+
10
12
  #
13
+ # Gems tracked by +Kettle::Rb::GemFloors+ get their minor series' security
14
+ # floor (e.g. +"~> 7.1.0", ">= 7.1.6"+). Exact patch versions stay pinned.
15
+ # A comment notes advisories that no release in the series fixes, and an
16
+ # exact patch version below the floor.
17
+ #
11
18
  # @example
12
19
  # gen = ModularGemfileGenerator.new(base_dir: "/path/to/project")
13
20
  # gen.generate(gem_name: "activerecord", version: "7.1", ruby_series: "r3")
@@ -66,15 +73,11 @@ module Kettle
66
73
  private
67
74
 
68
75
  def build_content(gem_name, version, sub_deps)
69
- lines = [
70
- "# frozen_string_literal: true",
71
- "",
72
- "# Generated by kettle-jem-appraisals",
73
- "gem #{gem_name.inspect}, #{version_requirement(version).inspect}"
74
- ]
76
+ lines = header_lines
77
+ lines.concat(gem_lines(gem_name, version, pinned_requirements(gem_name, version)))
75
78
 
76
79
  sub_deps.each do |dep_name, dep_version|
77
- lines << "gem #{dep_name.inspect}, \"~> #{dep_version}\""
80
+ lines.concat(gem_lines(dep_name, dep_version, floored_requirements(dep_name, dep_version, ["~> #{dep_version}"])))
78
81
  end
79
82
 
80
83
  lines << ""
@@ -82,19 +85,64 @@ module Kettle
82
85
  end
83
86
 
84
87
  def build_tier2_content(gem_name, version)
85
- lines = [
88
+ lines = header_lines
89
+ lines.concat(gem_lines(gem_name, version, pinned_requirements(gem_name, version)))
90
+ lines << ""
91
+ "#{lines.join("\n")}\n"
92
+ end
93
+
94
+ def header_lines
95
+ [
86
96
  "# frozen_string_literal: true",
87
97
  "",
88
- "# Generated by kettle-jem-appraisals",
89
- "gem #{gem_name.inspect}, #{version_requirement(version).inspect}",
90
- ""
98
+ "# Generated by kettle-jem-appraisals"
91
99
  ]
92
- "#{lines.join("\n")}\n"
100
+ end
101
+
102
+ def gem_lines(gem_name, version, requirements)
103
+ advisory_notes(gem_name, version, requirements) + ["gem #{[gem_name, *requirements].map(&:inspect).join(", ")}"]
104
+ end
105
+
106
+ # Minor versions get the series' security floor; exact patch versions stay pinned.
107
+ def pinned_requirements(gem_name, version)
108
+ requirement = version_requirement(version)
109
+ return [requirement] if exact_patch?(version)
110
+
111
+ floored_requirements(gem_name, version, [requirement])
112
+ end
113
+
114
+ def floored_requirements(gem_name, version, requirements)
115
+ floor = floor_above(gem_name, version)
116
+ floor ? requirements + [">= #{floor}"] : requirements
117
+ end
118
+
119
+ def advisory_notes(gem_name, version, requirements)
120
+ notes = []
121
+ unpatched = Kettle::Rb::GemFloors.unpatched_advisories(gem_name, version)
122
+ unless unpatched.empty?
123
+ series = Kettle::Rb::GemFloors.series_for(version)
124
+ notes << "# kettle-rb: no #{gem_name} #{series} release fixes #{unpatched.join(", ")}"
125
+ end
126
+
127
+ floor = floor_above(gem_name, version)
128
+ if floor && !requirements.include?(">= #{floor}")
129
+ notes << "# kettle-rb: #{gem_name} #{version} is below the #{gem_name} security floor #{floor}"
130
+ end
131
+ notes
132
+ end
133
+
134
+ # @return [String, nil] the security floor when it is newer than +version+
135
+ def floor_above(gem_name, version)
136
+ floor = Kettle::Rb::GemFloors.floor(gem_name, version)
137
+ floor if floor && Gem::Version.new(floor) > Gem::Version.new(version)
138
+ end
139
+
140
+ def exact_patch?(version)
141
+ version.to_s.split(".").length >= 3
93
142
  end
94
143
 
95
144
  def version_requirement(version)
96
- segments = version.to_s.split(".")
97
- if segments.length >= 3
145
+ if exact_patch?(version)
98
146
  "~> #{version}"
99
147
  else
100
148
  "~> #{version}.0"
@@ -6,7 +6,7 @@ module Kettle
6
6
  # Version namespace for this gem.
7
7
  module Version
8
8
  # Current gem version.
9
- VERSION = "0.1.4"
9
+ VERSION = "0.1.5"
10
10
  end
11
11
  # Current gem version exposed at the traditional constant location.
12
12
  VERSION = Version::VERSION # Traditional Constant Location
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "kettle/jem"
4
+ require "kettle/rb"
4
5
 
5
6
  module Kettle
6
7
  module Jem
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-jem-appraisals
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -57,6 +57,26 @@ dependencies:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: 7.1.28
60
+ - !ruby/object:Gem::Dependency
61
+ name: kettle-rb
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.1'
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 0.1.14
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.1'
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 0.1.14
60
80
  - !ruby/object:Gem::Dependency
61
81
  name: version_gem
62
82
  requirement: !ruby/object:Gem::Requirement
@@ -315,10 +335,10 @@ licenses:
315
335
  - AGPL-3.0-only
316
336
  metadata:
317
337
  homepage_uri: https://kettle-jem-appraisals.galtzo.com
318
- source_code_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/tree/v0.1.4
319
- changelog_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/blob/v0.1.4/CHANGELOG.md
338
+ source_code_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/tree/v0.1.5
339
+ changelog_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/blob/v0.1.5/CHANGELOG.md
320
340
  bug_tracker_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/issues
321
- documentation_uri: https://www.rubydoc.info/gems/kettle-jem-appraisals/0.1.4
341
+ documentation_uri: https://www.rubydoc.info/gems/kettle-jem-appraisals/0.1.5
322
342
  funding_uri: https://github.com/sponsors/pboling
323
343
  wiki_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/wiki
324
344
  news_uri: https://www.railsbling.com/tags/kettle-jem-appraisals
metadata.gz.sig CHANGED
Binary file