appraisal2 3.1.3 → 3.2.0

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: 5697b5d78bd1b3ef5881a7e0bdce99ae1d786406652771d072f42e9d131c3d13
4
- data.tar.gz: af85c613932969ec699dc5297a8dc4f7d3601d6ce898aa3d5750eba68d72febc
3
+ metadata.gz: 3ea29429e7d6b15deb35e182590258883e32ef6fecfe015c4d39d932fd4bcf96
4
+ data.tar.gz: 11bafabfd834fd35254e0777feaa1180be8c6f08b6ea89e92e3615cfefc2b392
5
5
  SHA512:
6
- metadata.gz: ce5998ea1956670ef493180dcd23ec3026d9468ff87849424a6c491578be7260a277009cb1be39cb29ede5c68507116b61c0cb8473cbe520c5d426f547c55aff
7
- data.tar.gz: eed7065a3a876f9416cdc12e6915bdc692c6327dacc13e027b400fc2a120ac06cc1d31b6afb574868c48b76f935212d5938a7adadfc0cd55d44a230cd2e4e57e
6
+ metadata.gz: 3289da2711ffa8539b1a34aba8346209e077d3fd86cf1bdb1b726bea7093bc8ae5e2f4bc62c8aa68e03783266f6c77bdb743a2dc550a4fb14fb3744714943957
7
+ data.tar.gz: bf38ac3a50e473895fb994b5e0e2abbcc437e18b1ec9a2f46acb232bfefce945eddbc5201c7cb1ef2ac739ed556ff3e1b1c1da739c32b3525f23bfff01c58a98
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -22,11 +22,73 @@ Please file a bug if you notice a violation of semantic versioning.
22
22
 
23
23
  ### Changed
24
24
 
25
+ ### Deprecated
26
+
27
+ ### Removed
28
+
29
+ ### Fixed
30
+
31
+ ### Security
32
+
33
+ ## [3.2.0] - 2026-07-18
34
+
35
+ - TAG: [v3.2.0][3.2.0t]
36
+ - COVERAGE: 91.00% -- 910/1000 lines in 29 files
37
+ - BRANCH COVERAGE: 77.54% -- 183/236 branches in 29 files
38
+ - 42.33% documented
39
+
40
+ ### Added
41
+
42
+ - `appraisal generate`, `install`, `update`, `generate-install`, and
43
+ `generate-update` can now process appraisals in parallel with
44
+ `--appraisal-jobs` / `-n`, or `APPRAISAL_JOBS`. Appraisal-level
45
+ parallelism defaults to 2 workers; use `-n 1` to run serially. Bundler older
46
+ than 2.1 falls back to serial processing.
47
+
48
+ ### Changed
49
+
50
+ - Full dependency updates now run `bundle update --all` to avoid Bundler's
51
+ deprecated implicit full-update form.
52
+ - Acceptance specs now opt into expensive dummy-gem and bundle fixture setup
53
+ through metadata, and reuse a per-worker cached default fixture stage.
54
+ - Generated CI workflows now cache `.rspec_status` and pass it to
55
+ `kettle-test` so `turbo_tests2` can balance workers with historical example
56
+ timings.
57
+ - `generate-install` and `generate-update` now reuse the parsed Appraisals set
58
+ for their generate and install/update phases.
59
+
60
+ - kettle-jem-template-20260716-001 - Shim gemspec manifests now include
61
+ `LICENSE.md` instead of nonexistent `LICENSE.txt`.
62
+ - kettle-jem-template-20260716-002 - Generated gemspec manifests now ship fewer
63
+ repository-only files by default to reduce downstream distro packaging churn.
64
+
65
+ ### Fixed
66
+
67
+ - Appraisal command execution now checks whether the `bundle` executable can
68
+ boot before falling back to RubyGems spec probing and Bundler installation,
69
+ preserving TruffleRuby's engine-shipped Bundler in isolated subprocesses.
70
+ - Acceptance fixture Bundler selection now detects TruffleRuby's shipped
71
+ Bundler without reusing a newer Bundler activated by the appraised suite.
72
+ - Acceptance fixture binstubs now pin the selected Bundler version before
73
+ loading generated `bin/bundle` handoff code.
74
+ - Bundler-backed appraisal installs now set `BUNDLE_JOBS` explicitly so the
75
+ configured Appraisal job count also controls Bundler's installer worker
76
+ count in subprocesses.
77
+ - JRuby and TruffleRuby workflows now correctly skip ordinary pull request
78
+ branches unless the branch targets `jruby/`, `truffleruby/`, or `engines/`.
79
+
80
+ ## [3.1.4] - 2026-07-01
81
+
82
+ - TAG: [v3.1.4][3.1.4t]
83
+ - COVERAGE: 90.28% -- 827/916 lines in 29 files
84
+ - BRANCH COVERAGE: 79.31% -- 161/203 branches in 29 files
85
+ - 42.25% documented
86
+
87
+ ### Changed
88
+
25
89
  - Clarified README Bundler path caching guidance and removed hard paragraph
26
90
  wraps from prose.
27
91
 
28
- ### Deprecated
29
-
30
92
  ### Removed
31
93
 
32
94
  - Removed unused legacy root and fixture Gemfiles for Ruby 1.8, 2.0, and 2.1.
@@ -40,7 +102,7 @@ Please file a bug if you notice a violation of semantic versioning.
40
102
  - Disabled `ruby/setup-ruby` Bundler caching in the JRuby 9.3 workflow and
41
103
  run a serial Bundler install to avoid legacy fallback index failures.
42
104
 
43
- ### Security
105
+ - Package configured license files in gem release file lists.
44
106
 
45
107
  ## [3.1.3] - 2026-06-22
46
108
 
@@ -414,7 +476,11 @@ Please file a bug if you notice a violation of semantic versioning.
414
476
  - code coverage tracked with Coveralls, QLTY.sh, and the kettle-soup-cover gem
415
477
  - other minor fixes and improvements
416
478
 
417
- [Unreleased]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.3...HEAD
479
+ [Unreleased]: https://github.com/appraisal-rb/appraisal2/compare/v3.2.0...HEAD
480
+ [3.2.0]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.4...v3.2.0
481
+ [3.2.0t]: https://github.com/appraisal-rb/appraisal2/releases/tag/v3.2.0
482
+ [3.1.4]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.3...v3.1.4
483
+ [3.1.4t]: https://github.com/appraisal-rb/appraisal2/releases/tag/v3.1.4
418
484
  [3.1.3]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.2...v3.1.3
419
485
  [3.1.3t]: https://github.com/appraisal-rb/appraisal2/releases/tag/v3.1.3
420
486
  [3.1.2]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.1...v3.1.2
data/README.md CHANGED
@@ -259,6 +259,21 @@ Use the command that matches the lifecycle you want:
259
259
  | `generate-install` | Yes | Yes, via install | First setup, or after intentional Appraisals changes |
260
260
  | `generate-update` | Yes | Yes, via update | Regenerate gemfiles, then update dependency locks |
261
261
 
262
+ Appraisal2 can process appraisals in parallel with `--appraisal-jobs` / `-n`,
263
+ or with `APPRAISAL_JOBS` when no CLI value is provided. `generate`,
264
+ `generate-install`, and `generate-update` use this for generation. `install`,
265
+ `update`, `generate-install`, and `generate-update` use it to fan out dependency
266
+ resolution across appraisals. External commands run across all appraisals, such
267
+ as `bundle exec appraisal rake test`, use `APPRAISAL_JOBS`. Appraisal-level
268
+ parallelism defaults to 2 workers; use `-n 1` to run serially. When Appraisal2
269
+ is running under Bundler older than 2.1, Appraisal2 falls back to serial
270
+ processing because those Bundler versions do not provide the modern environment
271
+ helpers used to isolate parallel Bundler subprocesses.
272
+
273
+ Install commands keep `--jobs` / `-j` for the selected gem manager. With
274
+ Bundler, `-j` controls `bundle install --jobs`, while `-n` controls how many
275
+ appraisal gemfiles Appraisal2 processes at the same time.
276
+
262
277
  The deprecated rake task `rake appraisal:install` now delegates to `appraisal generate-install`, preserving its historical generate-and-install behavior while the CLI commands remain explicit.
263
278
 
264
279
  ### Command Options
@@ -270,12 +285,27 @@ Built-in dependency commands support the following options:
270
285
  | Option | Description |
271
286
  |--------|-------------|
272
287
  | `--gem-manager`, `-g` | Gem manager to use: `bundler` (default) or `ore`; applies to `install`, `update`, `generate-install`, and `generate-update` |
273
- | `--jobs`, `-j` | Install gems in parallel using the given number of workers; applies to `install` and `generate-install` |
288
+ | `--appraisal-jobs`, `-n` | Process appraisals in parallel for `generate`, `install`, `update`, `generate-install`, and `generate-update` (default: 2; use `-n 1` for serial execution; Bundler < 2.1 falls back to serial processing) |
289
+ | `--jobs`, `-j` | Pass a parallel job count to the selected gem manager; applies to `install` and `generate-install` |
274
290
  | `--retry` | Retry network and git requests that have failed; applies to `install` and `generate-install` (default: 1) |
275
291
  | `--without` | A space-separated list of groups to skip during installation; applies to `install` and `generate-install` |
276
292
  | `--full-index` | Run bundle install with the full-index argument; applies to `install` and `generate-install` |
277
293
  | `--path` | Install gems in the specified directory; applies to `install` and `generate-install` |
278
294
 
295
+ ```bash
296
+ # Process two appraisals at a time, using the default Appraisal worker count
297
+ bundle exec appraisal generate-install
298
+
299
+ # Process four appraisals at a time
300
+ bundle exec appraisal generate-install -n 4
301
+
302
+ # Process two appraisals at a time, and give each Bundler install four jobs
303
+ bundle exec appraisal generate-install -n 2 -j 4
304
+
305
+ # Run Appraisal serially when parallel appraisal processing is not desired
306
+ bundle exec appraisal generate-install -n 1
307
+ ```
308
+
279
309
  ### Using Commands with Named Appraisals
280
310
 
281
311
  #### Using Appraisal's built-in commands with named appraisals
@@ -752,7 +782,7 @@ Thanks for RTFM. ☺️
752
782
  [📌gitmoji]: https://gitmoji.dev
753
783
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
754
784
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
755
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.909-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
785
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.000-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
756
786
  [🔐security]: https://github.com/appraisal-rb/appraisal2/blob/main/SECURITY.md
757
787
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
758
788
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -94,7 +94,7 @@ module Appraisal
94
94
  end
95
95
 
96
96
  def gemfile_path
97
- gemfile_root.mkdir unless gemfile_root.exist?
97
+ FileUtils.mkdir_p(gemfile_root)
98
98
 
99
99
  gemfile_root.join(gemfile_name).to_s
100
100
  end
data/lib/appraisal/cli.rb CHANGED
@@ -2,17 +2,27 @@
2
2
 
3
3
  require "thor"
4
4
  require "fileutils"
5
+ require "thread"
5
6
 
6
7
  module Appraisal
7
8
  class CLI < Thor
9
+ MINIMUM_PARALLEL_BUNDLER_VERSION = Gem::Version.new("2.1.0")
10
+
8
11
  default_task :install
9
12
  map ["-v", "--version"] => "version"
13
+ map "generate-install" => "generate_install"
14
+ map "generate-update" => "generate_update"
10
15
 
11
16
  class_option "gem-manager",
12
17
  :aliases => "-g",
13
18
  :type => :string,
14
19
  :default => "bundler",
15
20
  :desc => "Gem manager to use for install/update (bundler or ore)"
21
+ class_option "appraisal-jobs",
22
+ :aliases => "-n",
23
+ :type => :numeric,
24
+ :banner => "SIZE",
25
+ :desc => "Process appraisals in parallel using the given number of workers."
16
26
 
17
27
  class << self
18
28
  # Override help command to print out usage
@@ -59,7 +69,7 @@ module Appraisal
59
69
  :type => :numeric,
60
70
  :default => 1,
61
71
  :banner => "SIZE",
62
- :desc => "Install gems in parallel using the given number of workers."
72
+ :desc => "Pass the given parallel job count to the selected gem manager."
63
73
  method_option "retry",
64
74
  :type => :numeric,
65
75
  :default => 1,
@@ -78,11 +88,7 @@ module Appraisal
78
88
  "Bundler will remember this option."
79
89
 
80
90
  def install
81
- install_options = options.to_h
82
- AppraisalFile.each do |appraisal|
83
- appraisal.install(install_options)
84
- appraisal.relativize
85
- end
91
+ install_appraisals(appraisals, install_options, appraisal_jobs(options))
86
92
  end
87
93
 
88
94
  desc "generate-install", "Generate gemfiles, then resolve and install dependencies for each appraisal"
@@ -91,7 +97,7 @@ module Appraisal
91
97
  :type => :numeric,
92
98
  :default => 1,
93
99
  :banner => "SIZE",
94
- :desc => "Install gems in parallel using the given number of workers."
100
+ :desc => "Pass the given parallel job count to the selected gem manager."
95
101
  method_option "retry",
96
102
  :type => :numeric,
97
103
  :default => 1,
@@ -109,15 +115,14 @@ module Appraisal
109
115
  :desc => "Install gems in the specified directory. " \
110
116
  "Bundler will remember this option."
111
117
  def generate_install
112
- invoke(:generate, [], {})
113
- invoke(:install, [], options.to_h)
118
+ appraisal_list = appraisals
119
+ generate_appraisals(appraisal_list, appraisal_jobs(options))
120
+ install_appraisals(appraisal_list, install_options, appraisal_jobs(options))
114
121
  end
115
122
 
116
123
  desc "generate", "Generate a gemfile for each appraisal"
117
124
  def generate
118
- AppraisalFile.each do |appraisal|
119
- appraisal.write_gemfile
120
- end
125
+ generate_appraisals(appraisals, appraisal_jobs(options))
121
126
  end
122
127
 
123
128
  desc "clean", "Remove all generated gemfiles and lockfiles from gemfiles folder"
@@ -127,17 +132,14 @@ module Appraisal
127
132
 
128
133
  desc "update [LIST_OF_GEMS]", "Update dependencies for each generated appraisal gemfile"
129
134
  def update(*gems)
130
- gem_manager = options["gem-manager"] || options[:gem_manager]
131
- update_options = gem_manager ? {:gem_manager => gem_manager} : {}
132
- AppraisalFile.each do |appraisal|
133
- appraisal.update(gems, update_options)
134
- end
135
+ update_appraisals(appraisals, gems, update_options, appraisal_jobs(options))
135
136
  end
136
137
 
137
138
  desc "generate-update [LIST_OF_GEMS]", "Generate gemfiles, then update dependencies for each appraisal"
138
139
  def generate_update(*gems)
139
- invoke(:generate, [], {})
140
- update(*gems)
140
+ appraisal_list = appraisals
141
+ generate_appraisals(appraisal_list, appraisal_jobs(options))
142
+ update_appraisals(appraisal_list, gems, update_options, appraisal_jobs(options))
141
143
  end
142
144
 
143
145
  desc "list", "List the names of the defined appraisals"
@@ -152,7 +154,99 @@ module Appraisal
152
154
 
153
155
  private
154
156
 
157
+ def appraisals
158
+ AppraisalFile.new.appraisals
159
+ end
160
+
161
+ def generate_appraisals(appraisal_list, jobs)
162
+ each_appraisal(appraisal_list, jobs) do |appraisal|
163
+ appraisal.write_gemfile
164
+ end
165
+ end
166
+
167
+ def install_appraisals(appraisal_list, install_options, jobs)
168
+ each_appraisal(appraisal_list, jobs) do |appraisal|
169
+ appraisal.install(install_options.dup)
170
+ appraisal.relativize
171
+ end
172
+ end
173
+
174
+ def update_appraisals(appraisal_list, gems, update_options, jobs)
175
+ each_appraisal(appraisal_list, jobs) do |appraisal|
176
+ appraisal.update(gems, update_options.dup)
177
+ end
178
+ end
179
+
180
+ def each_appraisal(appraisal_list, jobs)
181
+ jobs = worker_count(jobs, appraisal_list.length)
182
+ return appraisal_list.each { |appraisal| yield(appraisal) } if jobs <= 1
183
+
184
+ queue = Queue.new
185
+ errors = []
186
+ mutex = Mutex.new
187
+
188
+ appraisal_list.each { |appraisal| queue << appraisal }
189
+ jobs.times { queue << nil }
190
+
191
+ # rubocop:disable ThreadSafety/NewThread
192
+ threads = Array.new(jobs) do
193
+ Thread.new do
194
+ while (appraisal = queue.pop)
195
+ begin
196
+ yield(appraisal)
197
+ rescue Exception => error # rubocop:disable Lint/RescueException
198
+ mutex.synchronize { errors << error }
199
+ end
200
+ end
201
+ end
202
+ end
203
+ # rubocop:enable ThreadSafety/NewThread
204
+ threads.each(&:join)
205
+
206
+ raise errors.first unless errors.empty?
207
+ end
208
+
209
+ def worker_count(jobs, item_count)
210
+ jobs = jobs.to_i
211
+ jobs = 1 if jobs < 1
212
+ jobs = 1 unless parallel_appraisals_supported?
213
+ return jobs if item_count.zero?
214
+
215
+ [jobs, item_count].min
216
+ end
217
+
218
+ def parallel_appraisals_supported?
219
+ # Parallel install/update workers depend on Appraisal::Command being able
220
+ # to build independent Bundler subprocess environments. Bundler 2.1 is the
221
+ # floor for the modern original_env/unbundled_env API split that replaced
222
+ # clean_env, so older Bundler versions keep the historical serial path.
223
+ bundler_version = Gem::Specification.find_all_by_name("bundler").map(&:version).max
224
+ return false unless bundler_version
225
+
226
+ bundler_version >= MINIMUM_PARALLEL_BUNDLER_VERSION
227
+ end
228
+
229
+ def appraisal_jobs(command_options)
230
+ command_options["appraisal-jobs"] || command_options[:appraisal_jobs] || ENV["APPRAISAL_JOBS"] || 2
231
+ end
232
+
233
+ def install_options
234
+ options.to_h.reject { |key, _value| key.to_s == "appraisal-jobs" }
235
+ end
236
+
237
+ def update_options
238
+ gem_manager = options["gem-manager"] || options[:gem_manager]
239
+ gem_manager ? {:gem_manager => gem_manager} : {}
240
+ end
241
+
155
242
  def method_missing(name, *args)
243
+ case name.to_s
244
+ when "generate-install"
245
+ return generate_install
246
+ when "generate-update"
247
+ return generate_update(*args)
248
+ end
249
+
156
250
  matching_appraisal = AppraisalFile.new.appraisals.detect do |appraisal|
157
251
  appraisal.name == name.to_s
158
252
  end
@@ -213,7 +307,7 @@ module Appraisal
213
307
  Command.new(actual_args, :gemfile => matching_appraisal.gemfile_path).run
214
308
  end
215
309
  else
216
- AppraisalFile.each do |appraisal|
310
+ each_appraisal(AppraisalFile.new.appraisals, appraisal_jobs(options)) do |appraisal|
217
311
  Command.new(ARGV, :gemfile => appraisal.gemfile_path).run
218
312
  end
219
313
  end
@@ -240,6 +334,17 @@ module Appraisal
240
334
  options[:jobs] = Regexp.last_match(1).to_i
241
335
  when /^-j(\d+)$/
242
336
  options[:jobs] = Regexp.last_match(1).to_i
337
+ when /^-j$/
338
+ options[:jobs] = args[index + 1].to_i
339
+ skip_next = true
340
+ when /^--appraisal-jobs=\d+$/
341
+ nil
342
+ when /^--appraisal-jobs$/
343
+ skip_next = true
344
+ when /^-n\d+$/
345
+ nil
346
+ when /^-n$/
347
+ skip_next = true
243
348
  when /^--retry=(\d+)$/
244
349
  options[:retry] = Regexp.last_match(1).to_i
245
350
  when /^--without=(.+)$/
@@ -271,6 +376,10 @@ module Appraisal
271
376
  # Next arg should be the value
272
377
  options[:gem_manager] = args[index + 1]
273
378
  skip_next = true
379
+ when /^--appraisal-jobs$/
380
+ skip_next = true
381
+ when /^-n$/
382
+ skip_next = true
274
383
  when /^-/
275
384
  # Other options are not gems, but we don't handle them all here
276
385
  # For now, just skip them to be safe if they start with -