kettle-jem-appraisals 0.1.4 → 0.1.6

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: 55c26a21775e32a64b50f493b789920cdcb15a9f142be11204c06c6c012bb411
4
+ data.tar.gz: 94556e63d682de6d157ffe647a78cda96cd3fcafc362bcf4f1234a23ed8dd8b9
5
5
  SHA512:
6
- metadata.gz: c9aa86d8864d7204613bf89db1f78811b944d7cbe842f2f5e6c4bd95448bbb500bcfcd201c6e5519532a85fa9acf3974592f642651f793fd7317db80f38823df
7
- data.tar.gz: e07bd405386de50eb128ab77984ae8eb47786f80c180fa3a8e8e8ba76503e8d3bbb306906eb9cffb25166ffbb9fb30cd06a9296c4581d606792a49d8f89bd130
6
+ metadata.gz: 35de41e63c518c3b0501e00b957f553a61eccecd4367d1861a12c4a4afc9ef4f788a5bfb640610d1810e576f1ee59cafb684895a2cecd91b40af3d87268421c7
7
+ data.tar.gz: 958ffe7e6d18a08a5b9241ba58c90047d6610bc7fce5f8cdef64a0287c617c7f882ca8b1829e056990e73ea970d5d067edc05bf7b5b3eccb85a0f86519b5cd5f
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,67 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [0.1.6] - 2026-09-29
34
+
35
+ - TAG: [v0.1.6][0.1.6t]
36
+ - COVERAGE: 95.69% -- 976/1020 lines in 15 files
37
+ - BRANCH COVERAGE: 83.93% -- 282/336 branches in 15 files
38
+ - 92.13% documented
39
+
40
+ ### Added
41
+
42
+ - kettle-jem-template-20260913-001 - Templating now also surfaces a review
43
+ entry in `dependency_conflicts.resolve` when a direct development
44
+ dependency doesn't support one or more of this project's declared
45
+ `engines:` and has no template-managed modular home (e.g. `sqlite3` on
46
+ `jruby`). Review each entry and pick a resolution per the project's own
47
+ engine support needs.
48
+
49
+ ### Changed
50
+
51
+ - [kc] kettle-jem/prepare: updated 14 project files:
52
+ - dependencies (14)
53
+
54
+ - [kc] kettle-jem/template: updated 17 project files:
55
+ - code and tests (2)
56
+ - dependencies (2)
57
+ - documentation (1)
58
+ - other (2)
59
+ - workflows (10)
60
+
61
+ ### Fixed
62
+
63
+ - Restore the released kettle-jem dependency floor after local templating raised it to an unreleased version.
64
+
65
+ - Skip the style-only RuboCop toolchain in the Ruby 3.2 appraisal bundle.
66
+
67
+ - Launch the generated appraisal binstub through the active Ruby interpreter so matrix resolution works on Windows.
68
+
69
+ ## [0.1.5] - 2026-09-19
70
+
71
+ - TAG: [v0.1.5][0.1.5t]
72
+ - COVERAGE: 95.68% -- 975/1019 lines in 15 files
73
+ - BRANCH COVERAGE: 83.93% -- 282/336 branches in 15 files
74
+ - 92.13% documented
75
+
76
+ ### Added
77
+
78
+ - 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"`)
79
+ - `--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
80
+ - Runtime dependency on kettle-rb `>= 0.1.14`
81
+
82
+ ### Changed
83
+
84
+ - 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
85
+ - `activerecord_support.gemfile` / `activerecord_support_modern.gemfile` generated by 0.1.4 and earlier are removed (hand-maintained ones are kept)
86
+
87
+ ### Fixed
88
+
89
+ - `--resolve` now exits non-zero when `bin/appraisal generate` fails, instead of reporting "Resolve complete"
90
+ - `-h`/`--help` prints usage instead of running scaffold mode, and unknown options are rejected before any file is written
91
+ - 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
92
+ - `semver` mode includes the lowest minor allowed by a gem's `requirements` (e.g. activerecord 7.0 for `>= 7.0`)
93
+
33
94
  ## [0.1.4] - 2026-09-14
34
95
 
35
96
  - TAG: [v0.1.4][0.1.4t]
@@ -194,7 +255,11 @@ Please file a bug if you notice a violation of semantic versioning.
194
255
  - kettle-jem-template-20260802-001 - Devcontainer JSON files now merge as JSONC,
195
256
  preserving comments and trailing commas during template updates.
196
257
 
197
- [Unreleased]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.4...HEAD
258
+ [Unreleased]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.6...HEAD
259
+ [0.1.6]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.5...v0.1.6
260
+ [0.1.6t]: https://github.com/appraisal-rb/kettle-jem-appraisals/releases/tag/v0.1.6
261
+ [0.1.5]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.4...v0.1.5
262
+ [0.1.5t]: https://github.com/appraisal-rb/kettle-jem-appraisals/releases/tag/v0.1.5
198
263
  [0.1.4]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.3...v0.1.4
199
264
  [0.1.4t]: https://github.com/appraisal-rb/kettle-jem-appraisals/releases/tag/v0.1.4
200
265
  [0.1.3]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.2...v0.1.3
data/README.md CHANGED
@@ -65,7 +65,7 @@ targets **`appraisal`** (which does not support `eval_gemfile`, and only support
65
65
 
66
66
  Compatible with MRI Ruby 4.0.0+, and JRuby.
67
67
  CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
68
- This test floor is configured by `ruby.test_minimum` in `.structuredmerge/kettle-jem.yml` and
68
+ This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
69
69
  may be higher than the gem's runtime compatibility floor when legacy Rubies are
70
70
  not practical for the current toolchain.
71
71
 
@@ -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.020-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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
  require "fileutils"
5
+ require "rbconfig"
5
6
  require "rubygems/specification"
6
7
 
7
8
  module Kettle
@@ -59,10 +60,49 @@ module Kettle
59
60
  end
60
61
  end
61
62
 
63
+ # @return [Array<String>] command-line options accepted by {#run}
64
+ OPTIONS = %w[--scaffold --resolve --force -v --version -h --help].freeze
65
+
66
+ # @return [String] usage text printed by +--help+
67
+ USAGE = <<~USAGE
68
+ Usage: kettle-jem-appraisals [--scaffold | --resolve] [--force]
69
+
70
+ Generates an Appraisal matrix for the gem in the current directory.
71
+ Without a mode flag, scaffolds appraisal_matrix in #{CONFIG_FILE} when it
72
+ has no resolved versions, and resolves it otherwise.
73
+
74
+ Options:
75
+ --scaffold Force scaffold mode (even if config exists)
76
+ --resolve Force resolve mode
77
+ --force Bypass freshness TTL and re-resolve
78
+ -v, --version Print the version and exit
79
+ -h, --help Print this help and exit
80
+ USAGE
81
+
62
82
  # Detects the appropriate mode and dispatches to scaffold or resolve.
63
83
  #
84
+ # Prints usage for +-h+/+--help+ and the version for +-v+/+--version+
85
+ # without touching any files, and exits when given an unknown option.
86
+ #
64
87
  # @return [void]
65
88
  def run
89
+ if args.include?("-h") || args.include?("--help")
90
+ puts USAGE
91
+ return
92
+ end
93
+
94
+ if args.include?("-v") || args.include?("--version")
95
+ puts Version::VERSION
96
+ return
97
+ end
98
+
99
+ unknown = args - OPTIONS
100
+ unless unknown.empty?
101
+ warn "Unknown option(s): #{unknown.join(" ")}"
102
+ warn USAGE
103
+ exit(1)
104
+ end
105
+
66
106
  mode = detect_mode
67
107
  case mode
68
108
  when :scaffold
@@ -267,6 +307,7 @@ module Kettle
267
307
  )
268
308
  annotate_extra_gemfiles(appraisal_entries, matrix_extra_gemfiles(matrix))
269
309
  annotate_activerecord_support(appraisal_entries, tier1_gems, matrix)
310
+ warn_security_advisories(appraisal_entries)
270
311
  annotate_standard_appraisal_collapses(appraisal_entries, bucket_ranges, matrix)
271
312
 
272
313
  puts " 📊 Generated #{appraisal_entries.size} appraisal entries"
@@ -295,19 +336,27 @@ module Kettle
295
336
  config[APPRAISAL_MATRIX_KEY] = matrix
296
337
  write_config(config)
297
338
 
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
339
+ run_appraisal_generate
305
340
 
306
341
  puts " ✅ Resolve complete"
307
342
  end
308
343
 
309
344
  # ── Helpers ──────────────────────────────────────────────────────
310
345
 
346
+ # Runs +bin/appraisal generate+ when the binstub exists, exiting when it fails.
347
+ def run_appraisal_generate
348
+ unless File.exist?(File.join(project_dir, "bin", "appraisal"))
349
+ puts " ⚠️ bin/appraisal not found — run `bundle binstubs appraisal2` then `bin/appraisal generate`"
350
+ return
351
+ end
352
+
353
+ puts " 🔧 Running bin/appraisal generate..."
354
+ return if system(RbConfig.ruby, "bin/appraisal", "generate", chdir: project_dir)
355
+
356
+ warn " ❌ bin/appraisal generate failed (#{$?.inspect}); Appraisals and gemfiles/ may be incomplete"
357
+ exit(1)
358
+ end
359
+
311
360
  # Removes stale kja-* flat gemfiles from gemfiles/ that are no longer
312
361
  # in the current matrix. Only touches files matching the PREFIX pattern.
313
362
  def cleanup_stale_gemfiles(current_entries)
@@ -393,6 +442,14 @@ module Kettle
393
442
  # that version is the best choice).
394
443
  #
395
444
  # Tier2 versions are all compatible versions for each tier1's bucket.
445
+ # Reports selected versions that could not be assigned to any Ruby bucket.
446
+ def warn_unassigned_versions(gem_name, versions, assignments)
447
+ missing = versions - assignments.map { |assignment| assignment[:version] }
448
+ return if missing.empty?
449
+
450
+ warn " ⚠️ #{gem_name} #{missing.join(", ")} not assigned to any Ruby bucket (unknown required_ruby_version?); left out of the matrix"
451
+ end
452
+
396
453
  def build_matrix(tier1_gems, tier2_gems, ruby_series, bucket_ranges, all_seams, all_versions_by_gem, resolver, builder, gemfile_gen, sub_resolver)
397
454
  entries = []
398
455
 
@@ -415,6 +472,7 @@ module Kettle
415
472
  puts " ⚠️ No bucket assignments for #{t1_name}, falling back to latest bucket"
416
473
  t1_assignments = t1_versions.map { |v| {version: v, bucket: ruby_series.last} }
417
474
  end
475
+ warn_unassigned_versions(t1_name, t1_versions, t1_assignments)
418
476
 
419
477
  # Show assignments
420
478
  t1_assignments.each do |a|
@@ -609,8 +667,9 @@ module Kettle
609
667
  end.uniq
610
668
  end
611
669
 
612
- # Generates the ActiveRecord database support gemfiles and adds the one
613
- # matching each ActiveRecord entry's version to its extra gemfiles.
670
+ # Generates the ActiveRecord database support gemfiles the matrix needs and
671
+ # adds the one matching each ActiveRecord entry's version to its extra gemfiles.
672
+ # Warns when no sqlite3 release an entry's support gemfile allows fixes an advisory.
614
673
  # Skipped when disabled via +activerecord_support+, when activerecord is not
615
674
  # a tier1 gem, or when +appraisal_gemfiles+ already names a support gemfile.
616
675
  def annotate_activerecord_support(appraisal_entries, tier1_gems, matrix)
@@ -618,16 +677,43 @@ module Kettle
618
677
  return unless tier1_gems.any? { |gem_config| gem_config["name"] == "activerecord" }
619
678
  return if matrix_extra_gemfiles(matrix).any? { |path| ActiveRecordSupportGemfileGenerator.support_gemfile?(path) }
620
679
 
680
+ activerecord_entries = appraisal_entries.select { |entry| entry[:tier1_name] == "activerecord" && entry[:tier1_version] }
681
+ versions = activerecord_entries.map { |entry| entry[:tier1_version] }
621
682
  generator = ActiveRecordSupportGemfileGenerator.new(base_dir: project_dir)
622
- generator.generate.each do |path, status|
623
- puts " 🗄️ #{(status == :written) ? "Wrote" : "Kept hand-maintained"} #{path}"
683
+ generator.generate(versions).each do |path, status|
684
+ label = {written: "Wrote", kept: "Kept hand-maintained", removed: "Removed stale generated"}.fetch(status)
685
+ puts " 🗄️ #{label} #{path}"
624
686
  end
625
- appraisal_entries.each do |entry|
626
- next unless entry[:tier1_name] == "activerecord" && entry[:tier1_version]
627
-
687
+ activerecord_entries.each do |entry|
628
688
  support = generator.eval_path_for(entry[:tier1_version])
629
689
  entry[:extra_gemfiles] = [support, *Array(entry[:extra_gemfiles])].uniq
630
690
  end
691
+ versions.group_by { |version| generator.eval_path_for(version) }.each do |path, grouped|
692
+ unfixed = generator.unfixed_sqlite3_advisories(grouped.first)
693
+ next if unfixed.empty?
694
+
695
+ warn " ⚠️ kettle-rb: no sqlite3 release allowed for activerecord #{grouped.uniq.join(", ")} (#{path}) fixes #{unfixed.join(", ")}"
696
+ end
697
+ end
698
+
699
+ # Warns about tracked gem versions in the matrix whose minor series has
700
+ # advisories no release fixes, or whose exact patch version is below the
701
+ # series' security floor (from +Kettle::Rb::GemFloors+).
702
+ def warn_security_advisories(appraisal_entries)
703
+ pairs = appraisal_entries.flat_map { |entry|
704
+ [[entry[:tier1_name], entry[:tier1_version]], [entry[:tier2_name], entry[:tier2_version]]]
705
+ }
706
+ messages = pairs.filter_map { |name, version| [name, version] if name && version && Kettle::Rb::GemFloors.tracked?(name) }.flat_map { |name, version|
707
+ notes = []
708
+ unpatched = Kettle::Rb::GemFloors.unpatched_advisories(name, version)
709
+ notes << "no #{name} #{Kettle::Rb::GemFloors.series_for(version)} release fixes #{unpatched.join(", ")}" unless unpatched.empty?
710
+ floor = Kettle::Rb::GemFloors.floor(name, version)
711
+ if floor && version.to_s.split(".").length >= 3 && Gem::Version.new(floor) > Gem::Version.new(version.to_s)
712
+ notes << "#{name} #{version} is pinned below the security floor #{floor}"
713
+ end
714
+ notes
715
+ }
716
+ messages.uniq.each { |message| warn " ⚠️ kettle-rb: #{message}" }
631
717
  end
632
718
 
633
719
  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.6"
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.6
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
@@ -86,7 +106,7 @@ dependencies:
86
106
  version: '3.1'
87
107
  - - ">="
88
108
  - !ruby/object:Gem::Version
89
- version: 3.1.0
109
+ version: 3.1.5
90
110
  type: :development
91
111
  prerelease: false
92
112
  version_requirements: !ruby/object:Gem::Requirement
@@ -96,7 +116,7 @@ dependencies:
96
116
  version: '3.1'
97
117
  - - ">="
98
118
  - !ruby/object:Gem::Version
99
- version: 3.1.0
119
+ version: 3.1.5
100
120
  - !ruby/object:Gem::Dependency
101
121
  name: bundler-audit
102
122
  requirement: !ruby/object:Gem::Requirement
@@ -174,7 +194,7 @@ dependencies:
174
194
  version: '3.2'
175
195
  - - ">="
176
196
  - !ruby/object:Gem::Version
177
- version: 3.2.4
197
+ version: 3.2.5
178
198
  type: :development
179
199
  prerelease: false
180
200
  version_requirements: !ruby/object:Gem::Requirement
@@ -184,7 +204,7 @@ dependencies:
184
204
  version: '3.2'
185
205
  - - ">="
186
206
  - !ruby/object:Gem::Version
187
- version: 3.2.4
207
+ version: 3.2.5
188
208
  - !ruby/object:Gem::Dependency
189
209
  name: kettle-test
190
210
  requirement: !ruby/object:Gem::Requirement
@@ -194,7 +214,7 @@ dependencies:
194
214
  version: '2.0'
195
215
  - - ">="
196
216
  - !ruby/object:Gem::Version
197
- version: 2.0.21
217
+ version: 2.0.23
198
218
  type: :development
199
219
  prerelease: false
200
220
  version_requirements: !ruby/object:Gem::Requirement
@@ -204,7 +224,7 @@ dependencies:
204
224
  version: '2.0'
205
225
  - - ">="
206
226
  - !ruby/object:Gem::Version
207
- version: 2.0.21
227
+ version: 2.0.23
208
228
  - !ruby/object:Gem::Dependency
209
229
  name: turbo_tests2
210
230
  requirement: !ruby/object:Gem::Requirement
@@ -214,7 +234,7 @@ dependencies:
214
234
  version: '3.2'
215
235
  - - ">="
216
236
  - !ruby/object:Gem::Version
217
- version: 3.2.7
237
+ version: 3.2.12
218
238
  type: :development
219
239
  prerelease: false
220
240
  version_requirements: !ruby/object:Gem::Requirement
@@ -224,7 +244,7 @@ dependencies:
224
244
  version: '3.2'
225
245
  - - ">="
226
246
  - !ruby/object:Gem::Version
227
- version: 3.2.7
247
+ version: 3.2.12
228
248
  - !ruby/object:Gem::Dependency
229
249
  name: ruby-progressbar
230
250
  requirement: !ruby/object:Gem::Requirement
@@ -248,7 +268,7 @@ dependencies:
248
268
  version: '1.0'
249
269
  - - ">="
250
270
  - !ruby/object:Gem::Version
251
- version: 1.0.8
271
+ version: 1.0.9
252
272
  type: :development
253
273
  prerelease: false
254
274
  version_requirements: !ruby/object:Gem::Requirement
@@ -258,7 +278,7 @@ dependencies:
258
278
  version: '1.0'
259
279
  - - ">="
260
280
  - !ruby/object:Gem::Version
261
- version: 1.0.8
281
+ version: 1.0.9
262
282
  - !ruby/object:Gem::Dependency
263
283
  name: gitmoji-regex
264
284
  requirement: !ruby/object:Gem::Requirement
@@ -268,7 +288,7 @@ dependencies:
268
288
  version: '2.0'
269
289
  - - ">="
270
290
  - !ruby/object:Gem::Version
271
- version: 2.0.12
291
+ version: 2.0.13
272
292
  type: :development
273
293
  prerelease: false
274
294
  version_requirements: !ruby/object:Gem::Requirement
@@ -278,7 +298,7 @@ dependencies:
278
298
  version: '2.0'
279
299
  - - ">="
280
300
  - !ruby/object:Gem::Version
281
- version: 2.0.12
301
+ version: 2.0.13
282
302
  description: "\U0001F372 Kettle::Jem::Appraisals auto-generates CI test matrices from
283
303
  RubyGems API data. Scaffolds tier1/tier2 gem lists from gemspec, resolves version
284
304
  spreads per mode (major/minor/minor-minmax/semver), generates modular gemfiles and
@@ -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.6
339
+ changelog_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/blob/v0.1.6/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.6
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
@@ -349,7 +369,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
349
369
  - !ruby/object:Gem::Version
350
370
  version: '0'
351
371
  requirements: []
352
- rubygems_version: 4.0.17
372
+ rubygems_version: 4.0.20
353
373
  specification_version: 4
354
374
  summary: "\U0001F372 Auto-generate Appraisal matrices for kettle-jem managed gems"
355
375
  test_files: []
metadata.gz.sig CHANGED
Binary file