kettle-jem-appraisals 0.1.3 → 0.1.4
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 +20 -1
- data/README.md +102 -5
- data/lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb +106 -0
- data/lib/kettle/jem/appraisals/cli.rb +48 -13
- data/lib/kettle/jem/appraisals/config_file.rb +132 -0
- data/lib/kettle/jem/appraisals/version.rb +1 -1
- data/lib/kettle/jem/appraisals.rb +2 -0
- data/sig/kettle/jem/appraisals.rbs +43 -0
- data.tar.gz.sig +0 -0
- metadata +6 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a78ffff1b9845b058f35013adea659ae8a8b03f21c522494122255f1774f0913
|
|
4
|
+
data.tar.gz: 4411573fb6ebf9808efdf42d0d4be6496cdd18f07a9c637d326075a5314c2f14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9aa86d8864d7204613bf89db1f78811b944d7cbe842f2f5e6c4bd95448bbb500bcfcd201c6e5519532a85fa9acf3974592f642651f793fd7317db80f38823df
|
|
7
|
+
data.tar.gz: e07bd405386de50eb128ab77984ae8eb47786f80c180fa3a8e8e8ba76503e8d3bbb306906eb9cffb25166ffbb9fb30cd06a9296c4581d606792a49d8f89bd130
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,23 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [0.1.4] - 2026-09-14
|
|
34
|
+
|
|
35
|
+
- TAG: [v0.1.4][0.1.4t]
|
|
36
|
+
- COVERAGE: 94.54% -- 848/897 lines in 15 files
|
|
37
|
+
- BRANCH COVERAGE: 78.67% -- 225/286 branches in 15 files
|
|
38
|
+
- 91.86% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Generate `gemfiles/modular/activerecord_support.gemfile` (sqlite3 1.4.x) and `activerecord_support_modern.gemfile` (sqlite3 >= 2.1) with JRuby JDBC adapter blocks when `activerecord` is a tier1 gem, and wire the matching one into each generated appraisal by ActiveRecord version; opt out with `appraisal_matrix.activerecord_support: false`.
|
|
43
|
+
- Document collapsing onto standard `ruby-X-Y` appraisals (intentional duplicate avoidance), collapse policies, and shared `appraisal_gemfiles`.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Read and write `appraisal_matrix` in `.structuredmerge/kettle-jem.yml` (the file kettle-jem uses), falling back to a legacy root `.kettle-jem.yml`.
|
|
48
|
+
- Preserve comments and all other settings when writing the config; only changed top-level sections are rewritten.
|
|
49
|
+
|
|
33
50
|
## [0.1.3] - 2026-09-11
|
|
34
51
|
|
|
35
52
|
- TAG: [v0.1.3][0.1.3t]
|
|
@@ -177,7 +194,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
177
194
|
- kettle-jem-template-20260802-001 - Devcontainer JSON files now merge as JSONC,
|
|
178
195
|
preserving comments and trailing commas during template updates.
|
|
179
196
|
|
|
180
|
-
[Unreleased]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.
|
|
197
|
+
[Unreleased]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.4...HEAD
|
|
198
|
+
[0.1.4]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.3...v0.1.4
|
|
199
|
+
[0.1.4t]: https://github.com/appraisal-rb/kettle-jem-appraisals/releases/tag/v0.1.4
|
|
181
200
|
[0.1.3]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.2...v0.1.3
|
|
182
201
|
[0.1.3t]: https://github.com/appraisal-rb/kettle-jem-appraisals/releases/tag/v0.1.3
|
|
183
202
|
[0.1.2]: https://github.com/appraisal-rb/kettle-jem-appraisals/compare/v0.1.1...v0.1.2
|
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 `.kettle-jem.yml` and
|
|
68
|
+
This test floor is configured by `ruby.test_minimum` in `.structuredmerge/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
|
|
|
@@ -144,8 +144,16 @@ gem install kettle-jem-appraisals
|
|
|
144
144
|
|
|
145
145
|
## ⚙️ Configuration
|
|
146
146
|
|
|
147
|
-
Configuration lives
|
|
148
|
-
|
|
147
|
+
Configuration lives under the `appraisal_matrix` key of the project's
|
|
148
|
+
[kettle-jem](https://github.com/kettle-dev/kettle-jem) configuration file,
|
|
149
|
+
`.structuredmerge/kettle-jem.yml` — the same file kettle-jem reads when templating.
|
|
150
|
+
Projects that still have a legacy root `.kettle-jem.yml` (and no
|
|
151
|
+
`.structuredmerge/kettle-jem.yml`) are read from there instead.
|
|
152
|
+
Running `--scaffold` creates a starter `appraisal_matrix` from your gemspec.
|
|
153
|
+
|
|
154
|
+
Writes are surgical: `--scaffold` and `--resolve` only rewrite the top-level
|
|
155
|
+
sections they change (normally just `appraisal_matrix`), so comments and every
|
|
156
|
+
other kettle-jem setting in the file are preserved.
|
|
149
157
|
|
|
150
158
|
### Config schema
|
|
151
159
|
|
|
@@ -162,6 +170,19 @@ appraisal_matrix:
|
|
|
162
170
|
# Command to run in CI for each appraisal entry (default: "rake spec")
|
|
163
171
|
exec_cmd: "rake spec"
|
|
164
172
|
|
|
173
|
+
# How generated entries collapse onto kettle-jem's standard ruby-X-Y
|
|
174
|
+
# appraisals (default: unique). See "Collapsing onto standard appraisals".
|
|
175
|
+
standard_appraisal_role: runtime_dependency
|
|
176
|
+
|
|
177
|
+
# Shared support gemfiles evaluated by every generated entry (and injected
|
|
178
|
+
# by kettle-jem into standard test appraisals). See "Shared support gemfiles".
|
|
179
|
+
appraisal_gemfiles:
|
|
180
|
+
- modular/combustion.gemfile
|
|
181
|
+
|
|
182
|
+
# Generate and wire ActiveRecord database support gemfiles when activerecord
|
|
183
|
+
# is a tier1 gem (default: true). See "ActiveRecord database support".
|
|
184
|
+
activerecord_support: true
|
|
185
|
+
|
|
165
186
|
gems:
|
|
166
187
|
# Tier 1: primary dependencies whose versions drive the matrix.
|
|
167
188
|
# Each tier1 version is assigned to its optimal Ruby bucket.
|
|
@@ -311,6 +332,82 @@ Common gem abbreviations are applied automatically:
|
|
|
311
332
|
|
|
312
333
|
Examples: `kja-ar-7-1-oa-2-1-r3`, `kja-mail-2-8-r3` (tier1-only)
|
|
313
334
|
|
|
335
|
+
#### Collapsing onto standard appraisals
|
|
336
|
+
|
|
337
|
+
kettle-jem already generates one standard appraisal per supported Ruby series
|
|
338
|
+
(`ruby-2-7`, `ruby-3-1`, …), and each of those already has a CI job. A generated
|
|
339
|
+
`kja-*` entry whose Ruby bucket maps to one of those series would otherwise be a
|
|
340
|
+
**duplicate** appraisal (and duplicate CI job) for the same Ruby. To avoid that,
|
|
341
|
+
kettle-jem-appraisals intentionally **collapses** one generated entry per
|
|
342
|
+
standard series onto the standard name: the entry keeps its `kja-*` name
|
|
343
|
+
internally, but is emitted as `appraise "ruby-X-Y"`. kettle-jem then merges that
|
|
344
|
+
block with its own templated `ruby-X-Y` block, so the standard job runs against
|
|
345
|
+
the generated dependency versions. Every entry that is not collapsed keeps its
|
|
346
|
+
`kja-*` name, and stale-file cleanup still keys off the `kja-` prefix.
|
|
347
|
+
|
|
348
|
+
Which entry collapses is controlled by the collapse policy, read from
|
|
349
|
+
`appraisal_matrix.collapse.standard_appraisals`, `standard_appraisal_role`, or
|
|
350
|
+
`standard_appraisal_collapse` (first one set wins):
|
|
351
|
+
|
|
352
|
+
| Policy | Accepted values | Behavior |
|
|
353
|
+
|-------------------------|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
|
|
354
|
+
| **unique** (default) | anything not listed below | Collapse only when a single generated entry maps to the series; ambiguous series keep all entries as `kja-*`. |
|
|
355
|
+
| **required** | `required`, `runtime`, `runtime_dependency`, `dependency`, `substrate` | Always collapse one entry per series — the one with the highest tier1 (then tier2) version. Use when the tier1 gem is a runtime dependency every standard job needs. |
|
|
356
|
+
| **none** | `none`, `false`, `off`, `no`, `never` | Never collapse; every entry stays `kja-*`. |
|
|
357
|
+
|
|
358
|
+
For example, with `standard_appraisal_role: runtime_dependency` and an
|
|
359
|
+
`activerecord` tier1, the `r3.1` bucket's ActiveRecord 7.2 entry is emitted as
|
|
360
|
+
`ruby-3-1`, while an extra ActiveRecord 7.0 entry for the same bucket stays
|
|
361
|
+
`kja-ar-7-0-r2`.
|
|
362
|
+
|
|
363
|
+
### Shared support gemfiles
|
|
364
|
+
|
|
365
|
+
`appraisal_matrix.appraisal_gemfiles` (alias: `appraisal_eval_gemfiles`) lists
|
|
366
|
+
modular gemfiles that every generated entry should `eval_gemfile`, such as
|
|
367
|
+
test-only fixtures (`modular/combustion.gemfile`). Paths may be given with or
|
|
368
|
+
without the leading `gemfiles/`. kettle-jem reads the same key (or
|
|
369
|
+
`workflows.standard_appraisal_gemfiles`) to inject those gemfiles into its
|
|
370
|
+
standard test appraisals, so collapsed and non-collapsed jobs share them.
|
|
371
|
+
|
|
372
|
+
kettle-jem's `test_bundle.gemfiles` is separate: it controls the root
|
|
373
|
+
`Gemfile` used for local development and the locked-deps workflow, and is not
|
|
374
|
+
changed by `appraisal_matrix`.
|
|
375
|
+
|
|
376
|
+
### ActiveRecord database support
|
|
377
|
+
|
|
378
|
+
This gem's primary use case is gems that are tested against a spread of
|
|
379
|
+
ActiveRecord versions on MRI, JRuby, and TruffleRuby. Those test suites need a
|
|
380
|
+
database driver whose gem differs per engine and per ActiveRecord version:
|
|
381
|
+
`sqlite3` on MRI/TruffleRuby, `activerecord-jdbcsqlite3-adapter` (plus
|
|
382
|
+
`bigdecimal`) on JRuby. When `activerecord` is a tier1 gem, `--resolve`:
|
|
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
|
|
388
|
+
the matching adapter;
|
|
389
|
+
- 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).
|
|
392
|
+
|
|
393
|
+
Existing support gemfiles that do not carry the `# Generated by
|
|
394
|
+
kettle-jem-appraisals` header are treated as hand-maintained and left untouched.
|
|
395
|
+
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
|
|
397
|
+
via kettle-jem's `test_bundle.gemfiles`, and select the JDBC adapter in your spec
|
|
398
|
+
helper on JRuby:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
require "active_record"
|
|
402
|
+
if RUBY_PLATFORM == "java"
|
|
403
|
+
require "activerecord-jdbcsqlite3-adapter"
|
|
404
|
+
else
|
|
405
|
+
require "sqlite3"
|
|
406
|
+
end
|
|
407
|
+
adapter = (RUBY_PLATFORM == "java" && ActiveRecord.version < Gem::Version.new("7.2")) ? "jdbcsqlite3" : "sqlite3"
|
|
408
|
+
ActiveRecord::Base.establish_connection(adapter: adapter, database: ":memory:")
|
|
409
|
+
```
|
|
410
|
+
|
|
314
411
|
### Optimal bucket assignment
|
|
315
412
|
|
|
316
413
|
Rather than cross-producting every gem version with every Ruby series,
|
|
@@ -331,7 +428,7 @@ Example with `activerecord`:
|
|
|
331
428
|
|
|
332
429
|
### Two-step workflow
|
|
333
430
|
|
|
334
|
-
**Step 1 — Scaffold** reads your gemspec and populates `.kettle-jem.yml`:
|
|
431
|
+
**Step 1 — Scaffold** reads your gemspec and populates `appraisal_matrix` in `.structuredmerge/kettle-jem.yml`:
|
|
335
432
|
|
|
336
433
|
```sh
|
|
337
434
|
kettle-jem-appraisals --scaffold
|
|
@@ -659,7 +756,7 @@ Thanks for RTFM. ☺️
|
|
|
659
756
|
[📌gitmoji]: https://gitmoji.dev
|
|
660
757
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
661
758
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
662
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.
|
|
759
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.897-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
663
760
|
[🔐security]: https://github.com/appraisal-rb/kettle-jem-appraisals/blob/main/SECURITY.md
|
|
664
761
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
665
762
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Kettle
|
|
6
|
+
module Jem
|
|
7
|
+
module Appraisals
|
|
8
|
+
# Generates the modular gemfiles that give ActiveRecord test suites a
|
|
9
|
+
# database driver on every engine: +sqlite3+ on MRI/TruffleRuby and
|
|
10
|
+
# +activerecord-jdbcsqlite3-adapter+ (plus +bigdecimal+) on JRuby.
|
|
11
|
+
#
|
|
12
|
+
# Two variants exist because +sqlite3+ 2.x is only supported by
|
|
13
|
+
# ActiveRecord >= 7.2, while older ActiveRecord needs the 1.4 series.
|
|
14
|
+
# The JDBC adapter's release series tracks ActiveRecord (52.x, 61.x, 72.x…),
|
|
15
|
+
# so an open requirement lets Bundler pick the adapter matching the
|
|
16
|
+
# resolved ActiveRecord.
|
|
17
|
+
#
|
|
18
|
+
# Files that exist without the {GENERATED_HEADER} are treated as
|
|
19
|
+
# hand-maintained and are never overwritten.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# generator = ActiveRecordSupportGemfileGenerator.new(base_dir: "/path/to/gem")
|
|
23
|
+
# generator.generate
|
|
24
|
+
# generator.eval_path_for("7.1.6") #=> "modular/activerecord_support.gemfile"
|
|
25
|
+
class ActiveRecordSupportGemfileGenerator
|
|
26
|
+
# @return [String] header marking a support gemfile as generated (and safe to regenerate)
|
|
27
|
+
GENERATED_HEADER = "# Generated by kettle-jem-appraisals"
|
|
28
|
+
|
|
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"
|
|
34
|
+
|
|
35
|
+
# @return [Gem::Version] first ActiveRecord release that supports sqlite3 2.x
|
|
36
|
+
MODERN_ACTIVERECORD = Gem::Version.new("7.2")
|
|
37
|
+
|
|
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
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
# @param path [String] a gemfile path, with or without the leading +gemfiles/+
|
|
46
|
+
# @return [Boolean] whether +path+ names one of the ActiveRecord support gemfiles
|
|
47
|
+
def support_gemfile?(path)
|
|
48
|
+
normalized = path.to_s.strip.sub(%r{\Agemfiles/}, "")
|
|
49
|
+
VARIANTS.keys.any? { |variant| variant.delete_prefix("gemfiles/") == normalized }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String] absolute path to the project root
|
|
54
|
+
attr_reader :base_dir
|
|
55
|
+
|
|
56
|
+
# @param base_dir [String] path to the project root
|
|
57
|
+
def initialize(base_dir: Dir.pwd)
|
|
58
|
+
@base_dir = base_dir
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Writes both support gemfiles, skipping any hand-maintained file.
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash{String => Symbol}] relative path => +:written+ or +:kept+
|
|
64
|
+
def generate
|
|
65
|
+
VARIANTS.each_with_object({}) do |(relative_path, variant), results|
|
|
66
|
+
absolute = File.join(base_dir, relative_path)
|
|
67
|
+
if File.exist?(absolute) && !File.read(absolute).include?(GENERATED_HEADER)
|
|
68
|
+
results[relative_path] = :kept
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(absolute))
|
|
73
|
+
File.write(absolute, build_content(variant))
|
|
74
|
+
results[relative_path] = :written
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param activerecord_version [String, Gem::Version] the entry's ActiveRecord version
|
|
79
|
+
# @return [String] the matching support gemfile, relative to +gemfiles/+ (for +eval_gemfile+)
|
|
80
|
+
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/")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def build_content(variant)
|
|
88
|
+
sqlite3 = [%("sqlite3"), *variant.fetch(:sqlite3).map(&:inspect), "platforms: [:ruby]"].join(", ")
|
|
89
|
+
<<~GEMFILE
|
|
90
|
+
# frozen_string_literal: true
|
|
91
|
+
|
|
92
|
+
#{GENERATED_HEADER}
|
|
93
|
+
# Database drivers for #{variant.fetch(:label)}: sqlite3 on MRI/TruffleRuby,
|
|
94
|
+
# activerecord-jdbcsqlite3-adapter on JRuby (its release series tracks ActiveRecord).
|
|
95
|
+
gem #{sqlite3}
|
|
96
|
+
|
|
97
|
+
platforms :jruby do
|
|
98
|
+
gem "activerecord-jdbcsqlite3-adapter", ">= 1.3"
|
|
99
|
+
gem "bigdecimal", ">= 1"
|
|
100
|
+
end
|
|
101
|
+
GEMFILE
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -13,7 +13,7 @@ module Kettle
|
|
|
13
13
|
# or accepts explicit +--scaffold+ / +--resolve+ flags.
|
|
14
14
|
#
|
|
15
15
|
# In *scaffold* mode, reads the project gemspec, excludes standard-library
|
|
16
|
-
# gems, and writes a skeleton +.kettle-jem.yml+ with tier1 candidates.
|
|
16
|
+
# gems, and writes a skeleton +appraisal_matrix+ into +.structuredmerge/kettle-jem.yml+ with tier1 candidates.
|
|
17
17
|
#
|
|
18
18
|
# In *resolve* mode, queries RubyGems for version data, assigns gem
|
|
19
19
|
# versions to Ruby-series buckets, generates modular gemfiles, an
|
|
@@ -26,8 +26,11 @@ module Kettle
|
|
|
26
26
|
# cli = Kettle::Jem::Appraisals::CLI.new(["--scaffold"], project_dir: "/path/to/gem")
|
|
27
27
|
# cli.run
|
|
28
28
|
class CLI
|
|
29
|
-
# @return [String]
|
|
30
|
-
CONFIG_FILE =
|
|
29
|
+
# @return [String] canonical per-project kettle-jem configuration file (holds +appraisal_matrix+)
|
|
30
|
+
CONFIG_FILE = Kettle::Jem::KETTLE_CONFIG_PATH
|
|
31
|
+
|
|
32
|
+
# @return [String] legacy per-project configuration file, read when {CONFIG_FILE} is absent
|
|
33
|
+
LEGACY_CONFIG_FILE = Kettle::Jem::LEGACY_KETTLE_CONFIG_PATH
|
|
31
34
|
|
|
32
35
|
# @return [String] top-level key in the YAML config that holds the appraisal matrix
|
|
33
36
|
APPRAISAL_MATRIX_KEY = "appraisal_matrix"
|
|
@@ -125,10 +128,10 @@ module Kettle
|
|
|
125
128
|
config[APPRAISAL_MATRIX_KEY] = matrix
|
|
126
129
|
write_config(config)
|
|
127
130
|
|
|
128
|
-
puts " ✅ Wrote scaffold to #{
|
|
131
|
+
puts " ✅ Wrote scaffold to #{config_file.relative_path}"
|
|
129
132
|
puts ""
|
|
130
133
|
puts " Next steps:"
|
|
131
|
-
puts " 1. Edit #{
|
|
134
|
+
puts " 1. Edit #{config_file.relative_path} — arrange gems into tier1 and tier2"
|
|
132
135
|
puts " 2. Run `kettle-jem-appraisals` again (or `--resolve`) to generate matrix"
|
|
133
136
|
end
|
|
134
137
|
|
|
@@ -140,7 +143,7 @@ module Kettle
|
|
|
140
143
|
config = load_config
|
|
141
144
|
matrix = config[APPRAISAL_MATRIX_KEY]
|
|
142
145
|
unless matrix
|
|
143
|
-
warn " ❌ No #{APPRAISAL_MATRIX_KEY} in #{
|
|
146
|
+
warn " ❌ No #{APPRAISAL_MATRIX_KEY} in #{config_file.relative_path}. Run --scaffold first."
|
|
144
147
|
exit(1)
|
|
145
148
|
end
|
|
146
149
|
|
|
@@ -263,6 +266,7 @@ module Kettle
|
|
|
263
266
|
sub_resolver
|
|
264
267
|
)
|
|
265
268
|
annotate_extra_gemfiles(appraisal_entries, matrix_extra_gemfiles(matrix))
|
|
269
|
+
annotate_activerecord_support(appraisal_entries, tier1_gems, matrix)
|
|
266
270
|
annotate_standard_appraisal_collapses(appraisal_entries, bucket_ranges, matrix)
|
|
267
271
|
|
|
268
272
|
puts " 📊 Generated #{appraisal_entries.size} appraisal entries"
|
|
@@ -323,16 +327,16 @@ module Kettle
|
|
|
323
327
|
puts " 🗑️ Removed #{stale.size} stale gemfile(s)"
|
|
324
328
|
end
|
|
325
329
|
|
|
326
|
-
def
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
def config_file
|
|
331
|
+
@config_file ||= ConfigFile.new(project_dir: project_dir)
|
|
332
|
+
end
|
|
329
333
|
|
|
330
|
-
|
|
334
|
+
def load_config
|
|
335
|
+
config_file.load
|
|
331
336
|
end
|
|
332
337
|
|
|
333
338
|
def write_config(config)
|
|
334
|
-
|
|
335
|
-
File.write(path, YAML.dump(config))
|
|
339
|
+
config_file.write(config)
|
|
336
340
|
end
|
|
337
341
|
|
|
338
342
|
def find_gemspec
|
|
@@ -437,6 +441,7 @@ module Kettle
|
|
|
437
441
|
|
|
438
442
|
entries << {
|
|
439
443
|
name: GemAbbreviations.appraisal_name(t1_name, t1_ver, nil, nil, rs),
|
|
444
|
+
tier1_name: t1_name,
|
|
440
445
|
tier1_version: t1_ver,
|
|
441
446
|
tier1_gemfile: t1_gemfile,
|
|
442
447
|
tier2_gemfile: nil,
|
|
@@ -479,6 +484,8 @@ module Kettle
|
|
|
479
484
|
|
|
480
485
|
entries << {
|
|
481
486
|
name: GemAbbreviations.appraisal_name(t1_name, t1_ver, t2_name, t2_ver, rs),
|
|
487
|
+
tier1_name: t1_name,
|
|
488
|
+
tier2_name: t2_name,
|
|
482
489
|
tier1_version: t1_ver,
|
|
483
490
|
tier2_version: t2_ver,
|
|
484
491
|
tier1_gemfile: t1_gemfile,
|
|
@@ -591,7 +598,7 @@ module Kettle
|
|
|
591
598
|
return if extra_gemfiles.empty?
|
|
592
599
|
|
|
593
600
|
appraisal_entries.each do |entry|
|
|
594
|
-
entry[:extra_gemfiles] = extra_gemfiles
|
|
601
|
+
entry[:extra_gemfiles] = (Array(entry[:extra_gemfiles]) + extra_gemfiles).uniq
|
|
595
602
|
end
|
|
596
603
|
end
|
|
597
604
|
|
|
@@ -602,6 +609,34 @@ module Kettle
|
|
|
602
609
|
end.uniq
|
|
603
610
|
end
|
|
604
611
|
|
|
612
|
+
# Generates the ActiveRecord database support gemfiles and adds the one
|
|
613
|
+
# matching each ActiveRecord entry's version to its extra gemfiles.
|
|
614
|
+
# Skipped when disabled via +activerecord_support+, when activerecord is not
|
|
615
|
+
# a tier1 gem, or when +appraisal_gemfiles+ already names a support gemfile.
|
|
616
|
+
def annotate_activerecord_support(appraisal_entries, tier1_gems, matrix)
|
|
617
|
+
return unless activerecord_support_enabled?(matrix)
|
|
618
|
+
return unless tier1_gems.any? { |gem_config| gem_config["name"] == "activerecord" }
|
|
619
|
+
return if matrix_extra_gemfiles(matrix).any? { |path| ActiveRecordSupportGemfileGenerator.support_gemfile?(path) }
|
|
620
|
+
|
|
621
|
+
generator = ActiveRecordSupportGemfileGenerator.new(base_dir: project_dir)
|
|
622
|
+
generator.generate.each do |path, status|
|
|
623
|
+
puts " 🗄️ #{(status == :written) ? "Wrote" : "Kept hand-maintained"} #{path}"
|
|
624
|
+
end
|
|
625
|
+
appraisal_entries.each do |entry|
|
|
626
|
+
next unless entry[:tier1_name] == "activerecord" && entry[:tier1_version]
|
|
627
|
+
|
|
628
|
+
support = generator.eval_path_for(entry[:tier1_version])
|
|
629
|
+
entry[:extra_gemfiles] = [support, *Array(entry[:extra_gemfiles])].uniq
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def activerecord_support_enabled?(matrix)
|
|
634
|
+
value = matrix.is_a?(Hash) ? matrix["activerecord_support"] : nil
|
|
635
|
+
return true if value.nil?
|
|
636
|
+
|
|
637
|
+
!%w[false no off none never 0].include?(value.to_s.strip.downcase)
|
|
638
|
+
end
|
|
639
|
+
|
|
605
640
|
def annotate_standard_appraisal_collapses(appraisal_entries, bucket_ranges, matrix = {})
|
|
606
641
|
policy = standard_appraisal_collapse_policy(matrix)
|
|
607
642
|
by_standard = appraisal_entries.group_by do |entry|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Kettle
|
|
7
|
+
module Jem
|
|
8
|
+
module Appraisals
|
|
9
|
+
# Reads and surgically updates the project's kettle-jem configuration file.
|
|
10
|
+
#
|
|
11
|
+
# The +appraisal_matrix+ lives in the same file kettle-jem uses
|
|
12
|
+
# (+.structuredmerge/kettle-jem.yml+). That file is heavily commented and
|
|
13
|
+
# holds many other settings, so writes only replace the top-level sections
|
|
14
|
+
# whose values changed, leaving comments and every other section intact.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# config_file = ConfigFile.new(project_dir: "/path/to/gem")
|
|
18
|
+
# config = config_file.load
|
|
19
|
+
# config["appraisal_matrix"]["resolved_at"] = Time.now.to_i
|
|
20
|
+
# config_file.write(config)
|
|
21
|
+
class ConfigFile
|
|
22
|
+
# @return [String] canonical config path, relative to the project root
|
|
23
|
+
CANONICAL_PATH = Kettle::Jem::KETTLE_CONFIG_PATH
|
|
24
|
+
|
|
25
|
+
# @return [String] legacy config path, read only when the canonical file is absent
|
|
26
|
+
LEGACY_PATH = Kettle::Jem::LEGACY_KETTLE_CONFIG_PATH
|
|
27
|
+
|
|
28
|
+
# @return [String] absolute path to the project root
|
|
29
|
+
attr_reader :project_dir
|
|
30
|
+
|
|
31
|
+
# @param project_dir [String] path to the project root
|
|
32
|
+
def initialize(project_dir:)
|
|
33
|
+
@project_dir = project_dir
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The config file to read and write: the canonical file when present,
|
|
37
|
+
# otherwise an existing legacy file, otherwise the canonical location.
|
|
38
|
+
#
|
|
39
|
+
# @return [String] absolute path
|
|
40
|
+
def path
|
|
41
|
+
canonical = File.join(project_dir, CANONICAL_PATH)
|
|
42
|
+
return canonical if File.exist?(canonical)
|
|
43
|
+
|
|
44
|
+
legacy = File.join(project_dir, LEGACY_PATH)
|
|
45
|
+
return legacy if File.exist?(legacy)
|
|
46
|
+
|
|
47
|
+
canonical
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [String] {#path} relative to the project root, for messages
|
|
51
|
+
def relative_path
|
|
52
|
+
File.expand_path(path).delete_prefix("#{File.expand_path(project_dir)}/")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Hash] the parsed config, or an empty Hash when the file does not exist
|
|
56
|
+
def load
|
|
57
|
+
return {} unless File.exist?(path)
|
|
58
|
+
|
|
59
|
+
YAML.safe_load_file(path, permitted_classes: [Symbol]) || {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Writes +config+, replacing only the top-level sections that differ from
|
|
63
|
+
# the file on disk. Creates the file (and its directory) when missing.
|
|
64
|
+
#
|
|
65
|
+
# @param config [Hash] the full desired config
|
|
66
|
+
# @return [void]
|
|
67
|
+
def write(config)
|
|
68
|
+
target = path
|
|
69
|
+
unless File.exist?(target)
|
|
70
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
71
|
+
File.write(target, YAML.dump(config))
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
content = File.read(target)
|
|
76
|
+
current = YAML.safe_load(content, permitted_classes: [Symbol]) || {}
|
|
77
|
+
config.each do |key, value|
|
|
78
|
+
next if current.key?(key) && current[key] == value
|
|
79
|
+
|
|
80
|
+
content = upsert_section(content, key.to_s, value)
|
|
81
|
+
end
|
|
82
|
+
File.write(target, content)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def upsert_section(content, key, value)
|
|
88
|
+
block = YAML.dump({key => value}).delete_prefix("---\n")
|
|
89
|
+
range = section_range(content, key)
|
|
90
|
+
return "#{content.rstrip}\n\n#{block}" unless range
|
|
91
|
+
|
|
92
|
+
lines = content.lines
|
|
93
|
+
[*lines[0...range.begin], block, *lines[range.end..]].join
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Line range (0-based, end-exclusive) of the top-level +key+ section.
|
|
97
|
+
# A parsed section runs up to the next top-level key, so trailing blank
|
|
98
|
+
# lines and comment lines (commented-out settings, or comments that
|
|
99
|
+
# introduce the next section) are trimmed back out of the range and kept.
|
|
100
|
+
def section_range(content, key)
|
|
101
|
+
require "yaml/merge"
|
|
102
|
+
|
|
103
|
+
analysis = Yaml::Merge::FileAnalysis.new(content)
|
|
104
|
+
raise ArgumentError, "could not parse #{relative_path} as YAML" unless analysis.valid?
|
|
105
|
+
|
|
106
|
+
body = analysis.documents.first&.body_node
|
|
107
|
+
return unless body&.mapping?
|
|
108
|
+
|
|
109
|
+
lines = content.lines
|
|
110
|
+
pairs = body.mapping_pairs
|
|
111
|
+
pairs.each_with_index do |pair, index|
|
|
112
|
+
next unless pair.key_name == key
|
|
113
|
+
|
|
114
|
+
start_index = pair.start_line.to_i - 1
|
|
115
|
+
next_pair = pairs[index + 1]
|
|
116
|
+
end_index = next_pair ? next_pair.start_line.to_i - 1 : lines.length
|
|
117
|
+
end_index -= 1 while end_index > start_index + 1 && trailing_trivia?(lines[end_index - 1])
|
|
118
|
+
return start_index...end_index
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @return [Boolean] whether +line+ is blank or a comment (at any indentation)
|
|
125
|
+
def trailing_trivia?(line)
|
|
126
|
+
stripped = line.strip
|
|
127
|
+
stripped.empty? || stripped.start_with?("#")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -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.
|
|
9
|
+
VERSION = "0.1.4"
|
|
10
10
|
end
|
|
11
11
|
# Current gem version exposed at the traditional constant location.
|
|
12
12
|
VERSION = Version::VERSION # Traditional Constant Location
|
|
@@ -15,7 +15,9 @@ module Kettle
|
|
|
15
15
|
path.to_s.sub(VAR_HOME_PREFIX, "/home")
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
autoload :ActiveRecordSupportGemfileGenerator, "kettle/jem/appraisals/active_record_support_gemfile_generator"
|
|
18
19
|
autoload :CLI, "kettle/jem/appraisals/cli"
|
|
20
|
+
autoload :ConfigFile, "kettle/jem/appraisals/config_file"
|
|
19
21
|
autoload :GemAbbreviations, "kettle/jem/appraisals/gem_abbreviations"
|
|
20
22
|
autoload :GemVersionResolver, "kettle/jem/appraisals/gem_version_resolver"
|
|
21
23
|
autoload :MatrixBuilder, "kettle/jem/appraisals/matrix_builder"
|
|
@@ -10,8 +10,47 @@ module Kettle
|
|
|
10
10
|
def self.generate: (Array[Hash[Symbol, untyped]] matrix) -> String
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
class ActiveRecordSupportGemfileGenerator
|
|
14
|
+
GENERATED_HEADER: String
|
|
15
|
+
LEGACY_PATH: String
|
|
16
|
+
MODERN_PATH: String
|
|
17
|
+
MODERN_ACTIVERECORD: Gem::Version
|
|
18
|
+
VARIANTS: Hash[String, Hash[Symbol, untyped]]
|
|
19
|
+
|
|
20
|
+
attr_reader base_dir: String
|
|
21
|
+
|
|
22
|
+
def self.support_gemfile?: (String path) -> bool
|
|
23
|
+
def initialize: (?base_dir: String) -> void
|
|
24
|
+
def generate: () -> Hash[String, Symbol]
|
|
25
|
+
def eval_path_for: (String | Gem::Version activerecord_version) -> String
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def build_content: (Hash[Symbol, untyped] variant) -> String
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class ConfigFile
|
|
33
|
+
CANONICAL_PATH: String
|
|
34
|
+
LEGACY_PATH: String
|
|
35
|
+
|
|
36
|
+
attr_reader project_dir: String
|
|
37
|
+
|
|
38
|
+
def initialize: (project_dir: String) -> void
|
|
39
|
+
def path: () -> String
|
|
40
|
+
def relative_path: () -> String
|
|
41
|
+
def load: () -> Hash[String, untyped]
|
|
42
|
+
def write: (Hash[String, untyped] config) -> void
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def upsert_section: (String content, String key, untyped value) -> String
|
|
47
|
+
def section_range: (String content, String key) -> Range[Integer]?
|
|
48
|
+
def trailing_trivia?: (String line) -> bool
|
|
49
|
+
end
|
|
50
|
+
|
|
13
51
|
class CLI
|
|
14
52
|
CONFIG_FILE: String
|
|
53
|
+
LEGACY_CONFIG_FILE: String
|
|
15
54
|
APPRAISAL_MATRIX_KEY: String
|
|
16
55
|
DEFAULT_FRESHNESS_TTL: Integer
|
|
17
56
|
|
|
@@ -28,8 +67,12 @@ module Kettle
|
|
|
28
67
|
def has_versions?: (Hash[String, untyped] matrix) -> bool
|
|
29
68
|
def run_scaffold: () -> void
|
|
30
69
|
def run_resolve: () -> void
|
|
70
|
+
def config_file: () -> ConfigFile
|
|
31
71
|
def load_config: () -> Hash[String, untyped]
|
|
32
72
|
def write_config: (Hash[String, untyped] config) -> void
|
|
73
|
+
def annotate_extra_gemfiles: (Array[Hash[Symbol, untyped]] appraisal_entries, Array[String] extra_gemfiles) -> void
|
|
74
|
+
def annotate_activerecord_support: (Array[Hash[Symbol, untyped]] appraisal_entries, Array[Hash[String, untyped]] tier1_gems, Hash[String, untyped] matrix) -> void
|
|
75
|
+
def activerecord_support_enabled?: (Hash[String, untyped]? matrix) -> bool
|
|
33
76
|
def find_gemspec: () -> String?
|
|
34
77
|
def extract_runtime_deps: (String gemspec_path) -> Array[String]
|
|
35
78
|
def fresh?: (Hash[String, untyped] matrix) -> bool
|
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
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -296,8 +296,10 @@ files:
|
|
|
296
296
|
- exe/kettle-jem-appraisals
|
|
297
297
|
- lib/kettle-jem-appraisals.rb
|
|
298
298
|
- lib/kettle/jem/appraisals.rb
|
|
299
|
+
- lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb
|
|
299
300
|
- lib/kettle/jem/appraisals/appraisals_generator.rb
|
|
300
301
|
- lib/kettle/jem/appraisals/cli.rb
|
|
302
|
+
- lib/kettle/jem/appraisals/config_file.rb
|
|
301
303
|
- lib/kettle/jem/appraisals/gem_abbreviations.rb
|
|
302
304
|
- lib/kettle/jem/appraisals/gem_version_resolver.rb
|
|
303
305
|
- lib/kettle/jem/appraisals/matrix_builder.rb
|
|
@@ -313,10 +315,10 @@ licenses:
|
|
|
313
315
|
- AGPL-3.0-only
|
|
314
316
|
metadata:
|
|
315
317
|
homepage_uri: https://kettle-jem-appraisals.galtzo.com
|
|
316
|
-
source_code_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/tree/v0.1.
|
|
317
|
-
changelog_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/blob/v0.1.
|
|
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
|
|
318
320
|
bug_tracker_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/issues
|
|
319
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-jem-appraisals/0.1.
|
|
321
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-jem-appraisals/0.1.4
|
|
320
322
|
funding_uri: https://github.com/sponsors/pboling
|
|
321
323
|
wiki_uri: https://github.com/appraisal-rb/kettle-jem-appraisals/wiki
|
|
322
324
|
news_uri: https://www.railsbling.com/tags/kettle-jem-appraisals
|
metadata.gz.sig
CHANGED
|
Binary file
|