next_rails 1.5.0 → 1.6.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: a5a701cf48bef3ed14539987e1bad23b85972578c0f9db2194a7a073679d44ac
4
- data.tar.gz: 0bbb57bb3f628b4638e61e1ec40ba84365142ba236f520dbb39bfa879009623c
3
+ metadata.gz: 7ef709432bba364ea112d6d8774d0c51b8bf1959b48ede86437b7ae801c7ea3b
4
+ data.tar.gz: f568512b09f8d16199ec4fb1800964f6a8067a3fb3d36eeaf48a335e117d56c5
5
5
  SHA512:
6
- metadata.gz: c879317b36a0afe079c194700fc2007b0fb7a3d4e5a68fa22c75c12f04dbc457dd0883e2641543e8a474fec3a795176d2cba0e4f39afbc9d728fb459096d911f
7
- data.tar.gz: 17bb529e77fc546e39916c9264d7b02a6d51f21586aefd2aee26e2d6815ae5bb5ebb7ad79a33653add34e32158a79887e747949853271e2af0cc4541b818b9bf
6
+ metadata.gz: a2b296e5306c8ab34b7f5b38801785ecfb3a6039a6a48310533ffc02033634a9d67ca2c03f6531c0dc05725a44cffe06d9dc65071f947bc6ec5c6bdd2892a6f9
7
+ data.tar.gz: cba5e3a34b3127fdcb2bfa28775250b5d7b2f8e0bdb5fb453976c6304f34ca1e272c4b6abee61eb12fe01c0e0cb4c22ea838d7d94164c40f57d7e039ecd7db51
@@ -13,7 +13,7 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
15
  matrix:
16
- ruby-version: ["3.4", "3.3", "3.2", "3.1", "3.0", "2.7", "2.6", "2.5", "2.4", "2.3"]
16
+ ruby-version: ["4.0", "3.4", "3.3", "3.2", "3.1", "3.0", "2.7", "2.6", "2.5", "2.4", "2.3"]
17
17
 
18
18
  steps:
19
19
  - uses: actions/checkout@v4
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --format documentation
2
2
  --color
3
3
  --require spec_helper
4
+ --order=random
data/CHANGELOG.md CHANGED
@@ -1,9 +1,17 @@
1
- # main [(unreleased)](https://github.com/fastruby/next_rails/compare/v1.5.0...main)
1
+ # main [(unreleased)](https://github.com/fastruby/next_rails/compare/v1.6.0...main)
2
2
 
3
3
  - [BUGFIX: example](https://github.com/fastruby/next_rails/pull/<number>)
4
4
 
5
5
  * Your changes/patches go here.
6
6
 
7
+ # v1.6.0 / 2026-05-07 [(commits)](https://github.com/fastruby/next_rails/compare/v1.5.0...v1.6.0)
8
+
9
+ - [CHORE: Drop `rainbow` gem dependency in favor of a native `NextRails::Tint` ANSI wrapper](https://github.com/fastruby/next_rails/pull/183)
10
+ - [BUGFIX: Compare mode now checks only buckets the current process ran, fixing parallel test support](https://github.com/fastruby/next_rails/pull/179)
11
+ - [FEATURE: Add `deprecations merge` command to combine parallel CI shards](https://github.com/fastruby/next_rails/pull/177)
12
+ - [FEATURE: Add parallel CI support for DeprecationTracker](https://github.com/fastruby/next_rails/pull/176)
13
+ - [CHORE: Add Ruby 4.0 to the test matrix](https://github.com/fastruby/next_rails/pull/178)
14
+
7
15
  # v1.5.0 / 2026-04-01 [(commits)](https://github.com/fastruby/next_rails/compare/v1.4.8...v1.5.0)
8
16
 
9
17
  - [FEATURE: Add `NextRails.current?` as the inverse of `NextRails.next?`](https://github.com/fastruby/next_rails/pull/174)
data/CONTRIBUTING.md CHANGED
@@ -34,6 +34,10 @@ To run all of the tests, simply run:
34
34
  bundle exec rake
35
35
  ```
36
36
 
37
+ ### Testing against specific Ruby versions
38
+
39
+ See the [Docker Development Environment for Old Ruby Versions](https://github.com/fastruby/next_rails/wiki/Docker-Development-Environment-for-Old-Ruby-Versions) wiki page for setup and examples.
40
+
37
41
  ## A word on the changelog
38
42
 
39
43
  You may also notice that we have a changelog in the form of [CHANGELOG.md](CHANGELOG.md). We use a format based on [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
data/README.md CHANGED
@@ -1,83 +1,117 @@
1
1
  # Next Rails
2
2
 
3
3
  [![Continuous Integration](https://github.com/fastruby/next_rails/actions/workflows/main.yml/badge.svg)](https://github.com/fastruby/next_rails/actions/workflows/main.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/next_rails.svg)](https://rubygems.org/gems/next_rails)
5
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
4
6
 
5
- This is a toolkit to upgrade your next Rails application. It will help you
6
- set up dual booting, track deprecation warnings, and get a report on outdated
7
- dependencies for any Rails application.
7
+ A toolkit to upgrade your next Rails application. It helps you set up **dual booting**, **track deprecation warnings**, and get a **compatibility report** on outdated dependencies for any Rails application. [Learn more](https://www.fastruby.io/blog/next-rails-gem.html).
8
8
 
9
- This project is a fork of [`ten_years_rails`](https://github.com/clio/ten_years_rails)
9
+ ## Features
10
10
 
11
- ## History
11
+ - **Dual Boot** — Run your app against two sets of dependencies (e.g. Rails 7.1 and Rails 7.2) side by side
12
+ - **Deprecation Tracking** — Capture and compare deprecation warnings across test runs (RSpec & Minitest)
13
+ - **Bundle Report** — Check gem compatibility with a target Rails or Ruby version
14
+ - **Ruby Check** — Find the minimum Ruby version compatible with a target Rails version
12
15
 
13
- This gem started as a companion to the "[Ten Years of Rails Upgrades](https://www.youtube.com/watch?v=6aCfc0DkSFo)"
14
- conference talk by Jordan Raine.
16
+ ## Installation
15
17
 
16
- > You'll find various utilities that we use at Clio to help us prepare for and
17
- > complete Rails upgrades.
18
+ Add this line to your application's Gemfile:
18
19
 
19
- > These scripts are still early days and may not work in every environment or app.
20
+ > [!WARNING]
21
+ > We recommend adding `next_rails` in the root of your Gemfile, not inside a group.
22
+ > This ensures `NextRails.next?` and `NextRails.current?` are available everywhere in your codebase.
20
23
 
21
- > I wouldn't recommend adding this to your Gemfile long-term. Rather, try out
22
- > the scripts and use them as a point of reference. Feel free to tweak them to
23
- > better fit your environment.
24
+ ```ruby
25
+ gem 'next_rails'
26
+ ```
24
27
 
25
- ## Usage
28
+ Then run:
26
29
 
27
- ### `bundle_report`
30
+ ```bash
31
+ bundle install
32
+ ```
28
33
 
29
- Learn about your Gemfile and see what needs updating.
34
+ ## Dual Boot
30
35
 
31
- ```bash
32
- # Show all out-of-date gems
33
- bundle_report outdated
36
+ We recommend upgrading **one minor version at a time** (e.g. 7.1 → 7.2, not 6.1 → 7.0). This keeps changes small and manageable.
34
37
 
35
- # Show five oldest, out-of-date gems
36
- bundle_report outdated | head -n 5
38
+ ### Setup
37
39
 
38
- # Show all out-of-date gems in machine readable JSON format
39
- bundle_report outdated --json
40
+ > [!NOTE]
41
+ > The `next_rails --init` command will add a `next?` helper method to the top of your Gemfile, which you can use to conditionally set gem versions.
40
42
 
41
- # Show gems that don't work with Rails 5.2.0
42
- bundle_report compatibility --rails-version=5.2.0
43
+ ```bash
44
+ # Initialize dual boot (creates Gemfile.next and Gemfile.next.lock)
45
+ next_rails --init
43
46
 
44
- # Show gems that don't work with Ruby 3.0
45
- bundle_report compatibility --ruby-version=3.0
47
+ # Edit your Gemfile to conditionally set gem versions using `next?`
48
+ vim Gemfile
46
49
 
47
- # Find minimum compatible ruby version with Rails 7.0.0
48
- bundle_report ruby_check --rails-version=7.0.0
50
+ # Install dependencies for the next version
51
+ next bundle install
49
52
 
50
- # Show the usual help message
51
- bundle_report --help
53
+ # Start your server using the next Gemfile
54
+ next rails s
52
55
  ```
53
56
 
54
- ### Application usage
57
+ ### Conditional code
55
58
 
56
- Every now and then it will be necessary to add code like this to your
57
- application:
59
+ When your Gemfile targets two versions, you may need to branch application code as well:
58
60
 
59
61
  ```ruby
60
62
  if NextRails.next?
61
- # Do things "the Rails 7 way"
63
+ # Do things "the Rails 7.2 way"
62
64
  else
63
- # Do things "the Rails 6.1 way"
65
+ # Do things "the Rails 7.1 way"
64
66
  end
65
67
  ```
66
68
 
67
- The `NextRails.next?` method will use your environment
68
- (e.g. `ENV['BUNDLE_GEMFILE]`) to determine whether your application is
69
- running with the next set of dependencies or the current set of dependencies.
69
+ Or use `NextRails.current?` for the inverse check:
70
+
71
+ ```ruby
72
+ if NextRails.current?
73
+ # Do things "the Rails 7.1 way"
74
+ else
75
+ # Do things "the Rails 7.2 way"
76
+ end
77
+ ```
70
78
 
71
- This might come in handy if you need to inject
72
- [Ruby or Rails shims](https://www.fastruby.io/blog/rails/upgrades/rails-upgrade-shims.html).
79
+ Both methods check your environment (e.g. `ENV['BUNDLE_GEMFILE']`) to determine which dependency set is active. This is useful for injecting [Ruby or Rails shims](https://www.fastruby.io/blog/rails/upgrades/rails-upgrade-shims.html).
73
80
 
74
- ### Deprecation tracking
81
+ ## Bundle Report
75
82
 
76
- If you're using RSpec, add this snippet to `rails_helper.rb` or `spec_helper.rb` (whichever loads Rails).
83
+ Inspect your Gemfile and check compatibility with a target Rails or Ruby version.
84
+
85
+ ```bash
86
+ # Show all out-of-date gems
87
+ bundle_report outdated
88
+
89
+ # Show all out-of-date gems in JSON format
90
+ bundle_report outdated --json
91
+
92
+ # Show gems incompatible with Rails 7.2
93
+ bundle_report compatibility --rails-version=7.2
94
+
95
+ # Show gems incompatible with Ruby 3.3
96
+ bundle_report compatibility --ruby-version=3.3
97
+
98
+ # Find minimum Ruby version compatible with Rails 7.2
99
+ bundle_report ruby_check --rails-version=7.2
100
+
101
+ # Help
102
+ bundle_report --help
103
+ ```
104
+
105
+ ## Deprecation Tracking
106
+
107
+ Track deprecation warnings in your test suite so you can monitor and fix them incrementally.
108
+
109
+ ### RSpec
110
+
111
+ Add to `rails_helper.rb` or `spec_helper.rb`:
77
112
 
78
113
  ```ruby
79
114
  RSpec.configure do |config|
80
- # Tracker deprecation messages in each file
81
115
  if ENV["DEPRECATION_TRACKER"]
82
116
  DeprecationTracker.track_rspec(
83
117
  config,
@@ -89,10 +123,11 @@ RSpec.configure do |config|
89
123
  end
90
124
  ```
91
125
 
92
- If using minitest, add this somewhere close to the top of your `test_helper.rb`:
126
+ ### Minitest
127
+
128
+ Add near the top of `test_helper.rb`:
93
129
 
94
130
  ```ruby
95
- # Tracker deprecation messages in each file
96
131
  if ENV["DEPRECATION_TRACKER"]
97
132
  DeprecationTracker.track_minitest(
98
133
  shitlist_path: "test/support/deprecation_warning.shitlist.json",
@@ -102,105 +137,125 @@ if ENV["DEPRECATION_TRACKER"]
102
137
  end
103
138
  ```
104
139
 
105
- > Keep in mind this is currently not compatible with the `minitest/parallel_fork` gem!
140
+ > [!NOTE]
141
+ > This is currently not compatible with the `minitest/parallel_fork` gem.
106
142
 
107
- Once you have that, you can start using deprecation tracking in your tests:
143
+ ### Running deprecation tracking
108
144
 
109
145
  ```bash
110
- # Run your tests and save the deprecations to the shitlist
146
+ # Save current deprecations to the shitlist
111
147
  DEPRECATION_TRACKER=save rspec
112
- # Run your tests and raise an error when the deprecations change
148
+
149
+ # Fail if deprecations have changed since the last save
113
150
  DEPRECATION_TRACKER=compare rspec
114
151
  ```
115
152
 
116
- #### `deprecations` command
153
+ ### Parallel CI support
117
154
 
118
- Once you have stored your deprecations, you can use `deprecations` to display common warnings, run specs, or update the shitlist file.
155
+ When running tests across parallel CI nodes, each node can write to its own shard file to avoid conflicts. The tracker auto-detects the node index from common CI environment variables (`CI_NODE_INDEX`, `CIRCLE_NODE_INDEX`, `BUILDKITE_PARALLEL_JOB`, `SEMAPHORE_JOB_INDEX`, `CI_NODE_INDEX` for GitLab), or you can set it explicitly via the `node_index` option.
119
156
 
120
- ```bash
121
- deprecations info
122
- deprecations info --pattern "ActiveRecord::Base"
123
- deprecations run
124
- deprecations --help # For more options and examples
157
+ #### RSpec
158
+
159
+ ```ruby
160
+ RSpec.configure do |config|
161
+ if ENV["DEPRECATION_TRACKER"]
162
+ DeprecationTracker.track_rspec(
163
+ config,
164
+ node_index: ENV["CI_NODE_INDEX"]
165
+ )
166
+ end
167
+ end
125
168
  ```
126
169
 
127
- Right now, the path to the shitlist is hardcoded so make sure you store yours at `spec/support/deprecation_warning.shitlist.json`.
170
+ The `node_index` option is only used in save mode. When set, the tracker writes to a shard file (e.g. `deprecation_warning.shitlist.node-0.json`) instead of the canonical file. Compare mode does not support `node_index` and will raise an error if passed—compare should only run after merging shards on the final canonical shitlist.
128
171
 
129
- #### `next_rails` command
172
+ #### Merging shards
130
173
 
131
- You can use `next_rails` to fetch the version of the gem installed.
174
+ After all parallel nodes finish saving, merge shards into the canonical file:
132
175
 
133
176
  ```bash
134
- next_rails --version
135
- next_rails --help # For more options and examples
136
- ```
177
+ # Merge all shard files and remove them afterwards
178
+ deprecations merge --delete-shards
137
179
 
138
- ### Dual-boot Rails next
180
+ # Or use --next to merge shards for the next Rails version
181
+ deprecations merge --next --delete-shards
182
+ ```
139
183
 
140
- This command helps you dual-boot your application.
184
+ You can also merge shards programmatically:
141
185
 
142
- ```bash
143
- next_rails --init # Create Gemfile.next and Gemfile.next.lock
144
- vim Gemfile # Tweak your dependencies conditionally using `next?`
145
- next bundle install # Install new gems
146
- next rails s # Start server using Gemfile.next
186
+ ```ruby
187
+ DeprecationTracker.merge_shards(
188
+ "spec/support/deprecation_warning.shitlist.json",
189
+ delete_shards: true
190
+ )
147
191
  ```
148
192
 
149
- ## Installation
193
+ #### Example CI workflow
150
194
 
151
- Add this line to your application's Gemfile
195
+ ```yaml
196
+ # 1. Save phase — each parallel node writes its own shard
197
+ # (runs on every node)
198
+ DEPRECATION_TRACKER=save CI_NODE_INDEX=$NODE bundle exec rspec <subset>
152
199
 
153
- > NOTE: If you add this gem to a group, make sure it is the test env group
200
+ # 2. Merge phase fan-in step, runs once after all nodes finish
201
+ deprecations merge --delete-shards
154
202
 
155
- ```ruby
156
- gem 'next_rails'
203
+ # 3. Compare phase — each parallel node checks only its own buckets
204
+ # against the merged canonical file (no CI_NODE_INDEX needed)
205
+ DEPRECATION_TRACKER=compare bundle exec rspec <subset>
157
206
  ```
158
207
 
159
- And then execute:
208
+ ### `deprecations` command
160
209
 
161
- $ bundle
210
+ View, filter, and manage stored deprecation warnings:
162
211
 
163
- Or install it yourself as:
212
+ ```bash
213
+ deprecations info
214
+ deprecations info --pattern "ActiveRecord::Base"
215
+ deprecations merge --delete-shards
216
+ deprecations run
217
+ deprecations --help
218
+ ```
164
219
 
165
- $ gem install next_rails
220
+ ## CLI Reference
166
221
 
167
- ## Setup
222
+ ```bash
223
+ bundle exec next_rails --init # Set up dual boot
224
+ bundle exec next_rails --version # Show gem version
225
+ bundle exec next_rails --help # Show help
226
+ ```
168
227
 
169
- Execute:
228
+ ## Contributing
170
229
 
171
- $ next_rails --init
230
+ Bug reports and pull requests are welcome! See the [Contributing guide](CONTRIBUTING.md) for setup instructions and guidelines.
172
231
 
173
- Init will create a Gemfile.next and an initialized Gemfile.next.lock.
174
- The Gemfile.next.lock is initialized with the contents of your existing
175
- Gemfile.lock lock file. We initialize the Gemfile.next.lock to prevent
176
- major version jumps when running the next version of Rails.
232
+ ## Releases
177
233
 
178
- ## Contributing
234
+ `next_rails` follows [Semantic Versioning](https://semver.org). Given a version number `MAJOR.MINOR.PATCH`, we increment the:
179
235
 
180
- Have a fix for a problem you've been running into or an idea for a new feature you think would be useful? Want to see how you can support `next_rails`?
236
+ - **MAJOR** version for incompatible API changes
237
+ - **MINOR** version for backwards-compatible new functionality
238
+ - **PATCH** version for backwards-compatible bug fixes
181
239
 
182
- Take a look at the [Contributing document](CONTRIBUTING.md) for instructions to set up the repo on your machine!
240
+ ### Steps to release a new version
183
241
 
184
- ## Releases
242
+ 1. Update the version number in `lib/next_rails/version.rb`
243
+ 2. Update `CHANGELOG.md` with the appropriate headers and entries
244
+ 3. Commit your changes to a `release/v1.x.x` branch
245
+ 4. Push your changes and submit a pull request `Release v1.x.x`
246
+ 5. Merge your pull request to the `main` branch
247
+ 6. Tag the latest version on `main`: `git tag v1.x.x`
248
+ 7. Push the tag to GitHub: `git push --tags`
249
+ 8. Build the gem: `gem build next_rails.gemspec`
250
+ 9. Push to RubyGems: `gem push next_rails-1.x.x.gem`
185
251
 
186
- `next_rails` adheres to [semver](https://semver.org). So given a version number MAJOR.MINOR.PATCH, we will increment the:
252
+ ## Maintainers
187
253
 
188
- 1. MAJOR version when you make incompatible API changes,
189
- 2. MINOR version when you add functionality in a backwards compatible manner, and
190
- 3. PATCH version when you make backwards compatible bug fixes.
254
+ Maintained by [OmbuLabs / FastRuby.io](https://www.fastruby.io).
191
255
 
192
- Here are the steps to release a new version:
256
+ ## History
193
257
 
194
- 1. Update the `version.rb` file with the proper version number
195
- 2. Update `CHANGELOG.md` to have the right headers
196
- 3. Commit your changes to a `release/v-1-1-0` branch
197
- 4. Push your changes and submit a pull request
198
- 5. Merge your pull request to the `main` branch
199
- 6. Git tag the latest version of the `main` branch (`git tag v1.1.0`)
200
- 7. Push tags to GitHub (`git push --tags`)
201
- 8. Build the gem (`gem build next_rails.gemspec`)
202
- 9. Push the .gem package to Rubygems.org (`gem push next_rails-1.1.0.gem`)
203
- 10. You are all done!
258
+ This gem started as a fork of [`ten_years_rails`](https://github.com/clio/ten_years_rails), a companion to the "[Ten Years of Rails Upgrades](https://www.youtube.com/watch?v=6aCfc0DkSFo)" conference talk by Jordan Raine.
204
259
 
205
260
  ## License
206
261
 
data/exe/deprecations CHANGED
@@ -3,6 +3,7 @@ require "json"
3
3
  require "rainbow"
4
4
  require "optparse"
5
5
  require "set"
6
+ require_relative "../lib/deprecation_tracker/shard_merger"
6
7
 
7
8
  def run_tests(deprecation_warnings, opts = {})
8
9
  tracker_mode = opts[:tracker_mode]
@@ -49,6 +50,7 @@ option_parser = OptionParser.new do |opts|
49
50
  bin/deprecations --next info # Show top ten deprecations for Rails 5
50
51
  bin/deprecations --pattern "ActiveRecord::Base" --verbose info # Show full details on deprecations matching pattern
51
52
  bin/deprecations --tracker-mode save --pattern "pass" run # Run tests that output deprecations matching pattern and update shitlist
53
+ bin/deprecations merge --delete-shards # Merge parallel CI shards and remove shard files
52
54
 
53
55
  Modes:
54
56
  info
@@ -57,6 +59,9 @@ option_parser = OptionParser.new do |opts|
57
59
  run
58
60
  Run tests that are known to cause deprecation warnings. Use --pattern to filter what tests are run.
59
61
 
62
+ merge
63
+ Merge parallel CI shard files into the canonical shitlist. Use with --delete-shards to remove shard files after merging.
64
+
60
65
  Options:
61
66
  MESSAGE
62
67
 
@@ -76,6 +81,10 @@ option_parser = OptionParser.new do |opts|
76
81
  options[:verbose] = true
77
82
  end
78
83
 
84
+ opts.on("--delete-shards", "Delete shard files after merging") do
85
+ options[:delete_shards] = true
86
+ end
87
+
79
88
  opts.on_tail("-h", "--help", "Prints this help") do
80
89
  puts opts
81
90
  exit
@@ -87,24 +96,34 @@ option_parser.parse!
87
96
  options[:mode] = ARGV.last
88
97
  path = options[:next] ? "spec/support/deprecation_warning.next.shitlist.json" : "spec/support/deprecation_warning.shitlist.json"
89
98
 
90
- pattern_string = options.fetch(:pattern, ".+")
91
- pattern = /#{pattern_string}/
92
-
93
- deprecation_warnings = JSON.parse(File.read(path)).each_with_object({}) do |(test_file, messages), hash|
94
- filtered_messages = messages.select {|message| message.match(pattern) }
95
- hash[test_file] = filtered_messages if !filtered_messages.empty?
96
- end
99
+ case options[:mode]
100
+ when "merge"
101
+ output = DeprecationTracker::ShardMerger.new(path, delete_shards: !!options[:delete_shards]).merge
102
+ shards = output[:shards]
103
+ result = output[:result]
104
+ total_messages = result.values.map(&:size).reduce(0, :+)
105
+ puts "Merged #{shards} shard files into #{path} (#{result.size} buckets, #{total_messages} deprecation messages)"
106
+ when "run", "info"
107
+ pattern_string = options.fetch(:pattern, ".+")
108
+ pattern = /#{pattern_string}/
109
+
110
+ deprecation_warnings = JSON.parse(File.read(path)).each_with_object({}) do |(test_file, messages), hash|
111
+ filtered_messages = messages.select {|message| message.match(pattern) }
112
+ hash[test_file] = filtered_messages if !filtered_messages.empty?
113
+ end
97
114
 
98
- if deprecation_warnings.empty?
99
- abort "No test files with deprecations matching #{pattern.inspect}."
100
- exit 2
101
- end
115
+ if deprecation_warnings.empty?
116
+ abort "No test files with deprecations matching #{pattern.inspect}."
117
+ exit 2
118
+ end
102
119
 
103
- case options.fetch(:mode, "info")
104
- when "run" then run_tests(deprecation_warnings, next_mode: options[:next], tracker_mode: options[:tracker_mode])
105
- when "info" then print_info(deprecation_warnings, verbose: options[:verbose])
120
+ if options[:mode] == "run"
121
+ run_tests(deprecation_warnings, next_mode: options[:next], tracker_mode: options[:tracker_mode])
122
+ else
123
+ print_info(deprecation_warnings, verbose: options[:verbose])
124
+ end
106
125
  when nil
107
- STDERR.puts Rainbow("Must pass a mode: run or info").red
126
+ STDERR.puts Rainbow("Must pass a mode: run, info, or merge").red
108
127
  puts option_parser
109
128
  exit 1
110
129
  else
@@ -0,0 +1,64 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ class DeprecationTracker
5
+ class ShardMerger
6
+ attr_reader :base_path, :delete_shards
7
+
8
+ def initialize(base_path, delete_shards: false)
9
+ @base_path = base_path
10
+ @delete_shards = delete_shards
11
+ end
12
+
13
+ def merge
14
+ dirname = File.dirname(base_path)
15
+ unless File.directory?(dirname)
16
+ warn "Directory does not exist: #{dirname}"
17
+ return { shards: 0, result: {} }
18
+ end
19
+
20
+ shard_files = Dir.glob(shard_glob).sort
21
+
22
+ if shard_files.empty?
23
+ warn "No shards found at #{shard_glob}"
24
+ return { shards: 0, result: {} }
25
+ end
26
+
27
+ merged = {}
28
+ shard_files.each do |file|
29
+ parse_shard(file).each do |bucket, messages|
30
+ merged[bucket] = (merged[bucket] || []).concat(Array(messages))
31
+ end
32
+ end
33
+
34
+ result = {}
35
+ merged.sort.each do |k, v|
36
+ result[k] = v.sort
37
+ end
38
+
39
+ begin
40
+ File.write(base_path, JSON.pretty_generate(result))
41
+ rescue Errno::EACCES => e
42
+ raise "Cannot write to #{base_path}: #{e.message}"
43
+ end
44
+
45
+ shard_files.each { |f| File.delete(f) } if delete_shards
46
+
47
+ { shards: shard_files.size, result: result }
48
+ end
49
+
50
+ private
51
+
52
+ def shard_glob
53
+ "#{base_path.chomp('.json')}.node-*.json"
54
+ end
55
+
56
+ def parse_shard(file)
57
+ JSON.parse(File.read(file))
58
+ rescue Errno::ENOENT
59
+ raise "Shard file not found: #{file}"
60
+ rescue JSON::ParserError => e
61
+ raise "Invalid JSON in shard file #{file}: #{e.message}"
62
+ end
63
+ end
64
+ end
@@ -1,4 +1,4 @@
1
- require "rainbow"
1
+ require "next_rails/tint"
2
2
  require "json"
3
3
 
4
4
  # A shitlist for deprecation warnings during test runs. It has two modes: "save" and "compare"
@@ -73,11 +73,14 @@ class DeprecationTracker
73
73
  end
74
74
  end
75
75
 
76
+ DEFAULT_PATH = "spec/support/deprecation_warning.shitlist.json"
77
+
76
78
  def self.init_tracker(opts = {})
77
- shitlist_path = opts[:shitlist_path]
78
- mode = opts[:mode]
79
+ shitlist_path = opts[:shitlist_path] || DEFAULT_PATH
80
+ mode = opts[:mode] || ENV["DEPRECATION_TRACKER"] || :save
79
81
  transform_message = opts[:transform_message]
80
- deprecation_tracker = DeprecationTracker.new(shitlist_path, transform_message, mode)
82
+ node_index = opts[:node_index]
83
+ deprecation_tracker = DeprecationTracker.new(shitlist_path, transform_message, mode, node_index: node_index)
81
84
  # Since Rails 7.1 the preferred way to track deprecations is to use the deprecation trackers via
82
85
  # `Rails.application.deprecators`.
83
86
  # We fallback to tracking deprecations via the ActiveSupport singleton object if Rails.application.deprecators is
@@ -126,13 +129,35 @@ class DeprecationTracker
126
129
  ActiveSupport::TestCase.include(MinitestExtension.new(tracker))
127
130
  end
128
131
 
129
- attr_reader :deprecation_messages, :shitlist_path, :transform_message, :bucket, :mode
132
+ def self.merge_shards(base_path, delete_shards: false)
133
+ require_relative "deprecation_tracker/shard_merger"
134
+ ShardMerger.new(base_path, delete_shards: delete_shards).merge[:result]
135
+ end
136
+
137
+ attr_reader :deprecation_messages, :shitlist_path, :transform_message, :bucket, :mode, :node_index
130
138
 
131
- def initialize(shitlist_path, transform_message = nil, mode = :save)
139
+ def initialize(shitlist_path, transform_message = nil, mode = :save, node_index: nil)
132
140
  @shitlist_path = shitlist_path
133
141
  @transform_message = transform_message || -> (message) { message }
134
142
  @deprecation_messages = {}
135
- @mode = mode.to_sym
143
+ @mode = mode ? mode.to_sym : :save
144
+ if @mode == :compare && node_index
145
+ raise ArgumentError, "node_index cannot be used with compare mode"
146
+ end
147
+ @node_index = node_index
148
+ end
149
+
150
+ def parallel?
151
+ !@node_index.nil?
152
+ end
153
+
154
+ def shard_path
155
+ ext = File.extname(shitlist_path)
156
+ "#{shitlist_path.chomp(ext)}.node-#{node_index}#{ext}"
157
+ end
158
+
159
+ def target_path
160
+ parallel? ? shard_path : shitlist_path
136
161
  end
137
162
 
138
163
  def add(message)
@@ -155,16 +180,17 @@ class DeprecationTracker
155
180
  end
156
181
 
157
182
  def compare
158
- shitlist = read_shitlist
183
+ stored = read_json(shitlist_path)
159
184
 
160
185
  changed_buckets = []
186
+
161
187
  normalized_deprecation_messages.each do |bucket, messages|
162
- if shitlist[bucket] != messages
188
+ if stored[bucket] != messages
163
189
  changed_buckets << bucket
164
190
  end
165
191
  end
166
192
 
167
- if changed_buckets.length > 0
193
+ if changed_buckets.any?
168
194
  message = <<-MESSAGE
169
195
  ⚠️ Deprecation warnings have changed!
170
196
 
@@ -183,33 +209,33 @@ class DeprecationTracker
183
209
  See \e[4;37mdev-docs/testing/deprecation_tracker.md\e[0;31m for more information.
184
210
  MESSAGE
185
211
 
186
- raise UnexpectedDeprecations, Rainbow(message).red
212
+ raise UnexpectedDeprecations, NextRails::Tint(message).red
187
213
  end
188
214
  end
189
215
 
190
216
  def diff
191
- new_shitlist = create_temp_shitlist
192
- `git diff --no-index #{shitlist_path} #{new_shitlist.path}`
217
+ temp_file = create_temp_file
218
+ `git diff --no-index #{shitlist_path} #{temp_file.path}`
193
219
  ensure
194
- new_shitlist.delete
220
+ temp_file.delete
195
221
  end
196
222
 
197
223
  def save
198
- new_shitlist = create_temp_shitlist
199
- create_if_shitlist_path_does_not_exist
200
- FileUtils.cp(new_shitlist.path, shitlist_path)
224
+ temp_file = create_temp_file
225
+ create_if_path_does_not_exist(target_path)
226
+ FileUtils.cp(temp_file.path, target_path)
201
227
  ensure
202
- new_shitlist.delete if new_shitlist
228
+ temp_file.delete if temp_file
203
229
  end
204
230
 
205
- def create_if_shitlist_path_does_not_exist
206
- dirname = File.dirname(shitlist_path)
231
+ def create_if_path_does_not_exist(path)
232
+ dirname = File.dirname(path)
207
233
  unless File.directory?(dirname)
208
234
  FileUtils.mkdir_p(dirname)
209
235
  end
210
236
  end
211
237
 
212
- def create_temp_shitlist
238
+ def create_temp_file
213
239
  temp_file = Tempfile.new("temp-deprecation-tracker-shitlist")
214
240
  temp_file.write(JSON.pretty_generate(normalized_deprecation_messages))
215
241
  temp_file.flush
@@ -219,22 +245,24 @@ class DeprecationTracker
219
245
 
220
246
  # Normalize deprecation messages to reduce noise from file output and test files to be tracked with separate test runs
221
247
  def normalized_deprecation_messages
222
- normalized = read_shitlist.merge(deprecation_messages).each_with_object({}) do |(bucket, messages), hash|
223
- hash[bucket] = messages.sort
224
- end
248
+ @normalized_deprecation_messages ||= begin
249
+ normalized = read_json(target_path).merge(deprecation_messages).each_with_object({}) do |(bucket, messages), hash|
250
+ hash[bucket] = messages.sort
251
+ end
225
252
 
226
- # not using `to_h` here to support older ruby versions
227
- {}.tap do |h|
228
- normalized.reject {|_key, value| value.empty? }.sort_by {|key, _value| key }.each do |k ,v|
229
- h[k] = v
253
+ # not using `to_h` here to support older ruby versions
254
+ {}.tap do |h|
255
+ normalized.reject {|_key, value| value.empty? }.sort_by {|key, _value| key }.each do |k ,v|
256
+ h[k] = v
257
+ end
230
258
  end
231
259
  end
232
260
  end
233
261
 
234
- def read_shitlist
235
- return {} unless File.exist?(shitlist_path)
236
- JSON.parse(File.read(shitlist_path))
262
+ def read_json(path)
263
+ return {} unless File.exist?(path)
264
+ JSON.parse(File.read(path))
237
265
  rescue JSON::ParserError => e
238
- raise "#{shitlist_path} is not valid JSON: #{e.message}"
266
+ raise "#{path} is not valid JSON: #{e.message}"
239
267
  end
240
268
  end
@@ -3,6 +3,7 @@
3
3
  require "optparse"
4
4
  require "next_rails"
5
5
  require "next_rails/bundle_report"
6
+ require "next_rails/tint"
6
7
 
7
8
  class NextRails::BundleReport::CLI
8
9
  def initialize(argv)
@@ -89,7 +90,7 @@ class NextRails::BundleReport::CLI
89
90
  begin
90
91
  option_parser.parse!(@argv)
91
92
  rescue OptionParser::ParseError => e
92
- warn Rainbow(e.message).red
93
+ warn NextRails::Tint(e.message).red
93
94
  puts option_parser
94
95
  exit 1
95
96
  end
@@ -1,3 +1,5 @@
1
+ require "next_rails/tint"
2
+
1
3
  class NextRails::BundleReport::RailsVersionCompatibility
2
4
  def initialize(gems: NextRails::GemInfo.all, options: {})
3
5
  @gems = gems
@@ -20,8 +22,8 @@ class NextRails::BundleReport::RailsVersionCompatibility
20
22
  def erb_output
21
23
  template = <<-ERB
22
24
  <% if incompatible_gems_by_state[:found_compatible] -%>
23
- <%= Rainbow("=> Incompatible with Rails #{rails_version} (with new versions that are compatible):").white.bold %>
24
- <%= Rainbow("These gems will need to be upgraded before upgrading to Rails #{rails_version}.").italic %>
25
+ <%= NextRails::Tint("=> Incompatible with Rails #{rails_version} (with new versions that are compatible):").white.bold %>
26
+ <%= NextRails::Tint("These gems will need to be upgraded before upgrading to Rails #{rails_version}.").italic %>
25
27
 
26
28
  <% incompatible_gems_by_state[:found_compatible].each do |gem| -%>
27
29
  <%= gem_header(gem) %> - upgrade to <%= gem.latest_compatible_version.version %>
@@ -29,8 +31,8 @@ class NextRails::BundleReport::RailsVersionCompatibility
29
31
 
30
32
  <% end -%>
31
33
  <% if incompatible_gems_by_state[:incompatible] -%>
32
- <%= Rainbow("=> Incompatible with Rails #{rails_version} (with no new compatible versions):").white.bold %>
33
- <%= Rainbow("These gems will need to be removed or replaced before upgrading to Rails #{rails_version}.").italic %>
34
+ <%= NextRails::Tint("=> Incompatible with Rails #{rails_version} (with no new compatible versions):").white.bold %>
35
+ <%= NextRails::Tint("These gems will need to be removed or replaced before upgrading to Rails #{rails_version}.").italic %>
34
36
 
35
37
  <% incompatible_gems_by_state[:incompatible].each do |gem| -%>
36
38
  <%= gem_header(gem) %> - new version, <%= gem.latest_version.version %>, is not compatible with Rails #{rails_version}
@@ -38,16 +40,16 @@ class NextRails::BundleReport::RailsVersionCompatibility
38
40
 
39
41
  <% end -%>
40
42
  <% if incompatible_gems_by_state[:no_new_version] -%>
41
- <%= Rainbow("=> Incompatible with Rails #{rails_version} (with no new versions):").white.bold %>
42
- <%= Rainbow("These gems will need to be upgraded by us or removed before upgrading to Rails #{rails_version}.").italic %>
43
- <%= Rainbow("This list is likely to contain internal gems, like Cuddlefish.").italic %>
43
+ <%= NextRails::Tint("=> Incompatible with Rails #{rails_version} (with no new versions):").white.bold %>
44
+ <%= NextRails::Tint("These gems will need to be upgraded by us or removed before upgrading to Rails #{rails_version}.").italic %>
45
+ <%= NextRails::Tint("This list is likely to contain internal gems, like Cuddlefish.").italic %>
44
46
 
45
47
  <% incompatible_gems_by_state[:no_new_version].each do |gem| -%>
46
48
  <%= gem_header(gem) %> - new version not found
47
49
  <% end -%>
48
50
 
49
51
  <% end -%>
50
- <%= Rainbow(incompatible_gems.length.to_s).red %> gems incompatible with Rails <%= rails_version %>
52
+ <%= NextRails::Tint(incompatible_gems.length.to_s).red %> gems incompatible with Rails <%= rails_version %>
51
53
  ERB
52
54
 
53
55
  erb_version = ERB.version
@@ -63,9 +65,9 @@ class NextRails::BundleReport::RailsVersionCompatibility
63
65
  end
64
66
 
65
67
  def gem_header(_gem)
66
- header = Rainbow("#{_gem.name} #{_gem.version}").bold
67
- header << Rainbow(" (loaded from git)").magenta if _gem.sourced_from_git?
68
- header
68
+ parts = [NextRails::Tint("#{_gem.name} #{_gem.version}").bold]
69
+ parts << NextRails::Tint(" (loaded from git)").magenta if _gem.sourced_from_git?
70
+ parts.join
69
71
  end
70
72
 
71
73
  def incompatible_gems
@@ -1,4 +1,4 @@
1
- require "rainbow"
1
+ require "next_rails/tint"
2
2
 
3
3
  class NextRails::BundleReport::RubyVersionCompatibility
4
4
  MINIMAL_VERSION = 1.0
@@ -10,20 +10,20 @@ class NextRails::BundleReport::RubyVersionCompatibility
10
10
  end
11
11
 
12
12
  def generate
13
- return invalid_message unless valid?
14
-
15
- message
13
+ (valid? ? message : invalid_message).to_s
16
14
  end
17
15
 
18
16
  private
19
17
 
20
18
  def message
21
- output = Rainbow("=> Incompatible gems with Ruby #{ruby_version}:").white.bold
19
+ noun = incompatible.one? ? "gem" : "gems"
20
+ parts = [NextRails::Tint("=> Incompatible gems with Ruby #{ruby_version}:").white.bold]
22
21
  incompatible.each do |gem|
23
- output += Rainbow("\n#{gem.name} - required Ruby version: #{gem.gem_specification.required_ruby_version}").magenta
22
+ parts << NextRails::Tint("#{gem.name} - required Ruby version: #{gem.gem_specification.required_ruby_version}").magenta
24
23
  end
25
- output += Rainbow("\n\n#{incompatible.length} incompatible #{incompatible.one? ? 'gem' : 'gems' } with Ruby #{ruby_version}").red
26
- output
24
+ parts << ""
25
+ parts << NextRails::Tint("#{incompatible.length} incompatible #{noun} with Ruby #{ruby_version}").red
26
+ parts.join("\n")
27
27
  end
28
28
 
29
29
  def incompatible
@@ -35,7 +35,7 @@ class NextRails::BundleReport::RubyVersionCompatibility
35
35
  end
36
36
 
37
37
  def invalid_message
38
- Rainbow("=> Invalid Ruby version: #{options[:ruby_version]}.").red.bold
38
+ NextRails::Tint("=> Invalid Ruby version: #{options[:ruby_version]}.").red.bold
39
39
  end
40
40
 
41
41
  def valid?
@@ -1,4 +1,4 @@
1
- require "rainbow"
1
+ require "next_rails/tint"
2
2
  require "cgi"
3
3
  require "erb"
4
4
  require "json"
@@ -105,14 +105,14 @@ module NextRails
105
105
  header = "#{gem.name} #{gem.version}"
106
106
 
107
107
  puts <<-MESSAGE
108
- #{Rainbow(header).bold.white}: released #{gem.age} (latest version, #{gem.latest_version.version}, released #{gem.latest_version.age})
108
+ #{NextRails::Tint(header).bold.white}: released #{gem.age} (latest version, #{gem.latest_version.version}, released #{gem.latest_version.age})
109
109
  MESSAGE
110
110
  end
111
111
 
112
112
  percentage_out_of_date = ((out_of_date_gems.count / total_gem_count.to_f) * 100).round
113
113
  footer = <<-MESSAGE
114
- #{Rainbow(sourced_from_git_count.to_s).yellow} gems are sourced from git
115
- #{Rainbow(out_of_date_gems.count.to_s).red} of the #{total_gem_count} gems are out-of-date (#{percentage_out_of_date}%)
114
+ #{NextRails::Tint(sourced_from_git_count.to_s).yellow} gems are sourced from git
115
+ #{NextRails::Tint(out_of_date_gems.count.to_s).red} of the #{total_gem_count} gems are out-of-date (#{percentage_out_of_date}%)
116
116
  MESSAGE
117
117
 
118
118
  puts ''
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NextRails
4
+ # Lightweight ANSI color/style wrapper with chainable style methods.
5
+ # Wrap a string with `NextRails::Tint("text")` then chain styles:
6
+ #
7
+ # NextRails::Tint("hello").red.bold
8
+ #
9
+ # Instances are effectively immutable: each style method returns a new
10
+ # `Tint` rather than mutating the receiver, so a reference can be reused
11
+ # without styles accumulating across chains.
12
+ class Tint
13
+ CODES = {
14
+ bold: 1,
15
+ italic: 3,
16
+ red: 31,
17
+ green: 32,
18
+ yellow: 33,
19
+ blue: 34,
20
+ magenta: 35,
21
+ cyan: 36,
22
+ white: 37
23
+ }.freeze
24
+
25
+ def initialize(string, codes = [])
26
+ @string = string.to_s
27
+ @codes = codes
28
+ end
29
+
30
+ CODES.each_key do |style|
31
+ define_method(style) do
32
+ self.class.new(@string, @codes + [CODES[style]])
33
+ end
34
+ end
35
+
36
+ def to_s
37
+ return @string if @codes.empty?
38
+
39
+ "\e[#{@codes.join(";")}m#{@string}\e[0m"
40
+ end
41
+ alias_method :to_str, :to_s
42
+ end
43
+
44
+ def self.Tint(string)
45
+ Tint.new(string)
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NextRails
4
- VERSION = "1.5.0"
4
+ VERSION = "1.6.0"
5
5
  end
data/next_rails.gemspec CHANGED
@@ -22,8 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_dependency "rainbow", ">= 3"
26
- spec.add_development_dependency "bundler", ">= 1.16", "< 3.0"
25
+ spec.add_development_dependency "bundler", ">= 1.16"
27
26
  spec.add_development_dependency "rake"
28
27
  spec.add_development_dependency "rspec", "~> 3.0"
29
28
  spec.add_development_dependency "simplecov", "~> 0.17.1"
metadata CHANGED
@@ -1,29 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: next_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernesto Tagwerker
8
8
  - Luis Sagastume
9
+ autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2026-04-02 00:00:00.000000000 Z
12
+ date: 2026-05-07 00:00:00.000000000 Z
12
13
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: rainbow
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '3'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '3'
27
14
  - !ruby/object:Gem::Dependency
28
15
  name: bundler
29
16
  requirement: !ruby/object:Gem::Requirement
@@ -31,9 +18,6 @@ dependencies:
31
18
  - - ">="
32
19
  - !ruby/object:Gem::Version
33
20
  version: '1.16'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '3.0'
37
21
  type: :development
38
22
  prerelease: false
39
23
  version_requirements: !ruby/object:Gem::Requirement
@@ -41,9 +25,6 @@ dependencies:
41
25
  - - ">="
42
26
  - !ruby/object:Gem::Version
43
27
  version: '1.16'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '3.0'
47
28
  - !ruby/object:Gem::Dependency
48
29
  name: rake
49
30
  requirement: !ruby/object:Gem::Requirement
@@ -194,6 +175,7 @@ files:
194
175
  - exe/next.sh
195
176
  - exe/next_rails
196
177
  - lib/deprecation_tracker.rb
178
+ - lib/deprecation_tracker/shard_merger.rb
197
179
  - lib/next_rails.rb
198
180
  - lib/next_rails/bundle_report.rb
199
181
  - lib/next_rails/bundle_report/cli.rb
@@ -201,6 +183,7 @@ files:
201
183
  - lib/next_rails/bundle_report/ruby_version_compatibility.rb
202
184
  - lib/next_rails/gem_info.rb
203
185
  - lib/next_rails/init.rb
186
+ - lib/next_rails/tint.rb
204
187
  - lib/next_rails/version.rb
205
188
  - next_rails.gemspec
206
189
  - pull_request_template.md
@@ -208,6 +191,7 @@ homepage: https://github.com/fastruby/next_rails
208
191
  licenses:
209
192
  - MIT
210
193
  metadata: {}
194
+ post_install_message:
211
195
  rdoc_options: []
212
196
  require_paths:
213
197
  - lib
@@ -222,7 +206,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
206
  - !ruby/object:Gem::Version
223
207
  version: '0'
224
208
  requirements: []
225
- rubygems_version: 3.6.2
209
+ rubygems_version: 3.4.19
210
+ signing_key:
226
211
  specification_version: 4
227
212
  summary: A toolkit to upgrade your next Rails application
228
213
  test_files: []