next_rails 1.5.0 → 1.7.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: 683bb3cbd05a568e913cdbecabc84aa9046f534ebfee134eae53b5ae98d3690f
4
+ data.tar.gz: 83e65946157ab5de2ee05b9d73f735e6d638df7c14bda1ddaff6bd489dec1bd6
5
5
  SHA512:
6
- metadata.gz: c879317b36a0afe079c194700fc2007b0fb7a3d4e5a68fa22c75c12f04dbc457dd0883e2641543e8a474fec3a795176d2cba0e4f39afbc9d728fb459096d911f
7
- data.tar.gz: 17bb529e77fc546e39916c9264d7b02a6d51f21586aefd2aee26e2d6815ae5bb5ebb7ad79a33653add34e32158a79887e747949853271e2af0cc4541b818b9bf
6
+ metadata.gz: 38021c508b777bbef26be6b897b43f60150ad1658b06d07795b370173c4e1b0f75307e78d95cc147737313d9f263966e0cf7b94370dbd998ba50ed94b31fcd72
7
+ data.tar.gz: 751730c5320f1fa4dcbdba5b26cd1d2deac911fa5de9abd42ced0224ebc9f811827b3dd4de9f2c63181187c32a6beb3a6956ca0b176f655b1a2ad6a382d125ec
@@ -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,26 @@
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.7.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.7.0 / 2026-08-05 [(commits)](https://github.com/fastruby/next_rails/compare/v1.6.0...v1.7.0)
8
+
9
+ - [FEATURE: Add support for SimpleCov 1.0](https://github.com/fastruby/next_rails/pull/196)
10
+ - [FEATURE: Validate the DeprecationTracker mode at initialization, treating a blank mode as the default `save`](https://github.com/fastruby/next_rails/pull/186)
11
+ - [BUGFIX: `bundle_report outdated` no longer confuses a locally-sourced (`path:`) gem with a same-named public gem on rubygems; local gems are excluded from the out-of-date check and counted separately](https://github.com/fastruby/next_rails/pull/189)
12
+ - [BUGFIX: The `deprecations` executable no longer requires the undeclared `rainbow` gem, which made it fail to load on a clean install](https://github.com/fastruby/next_rails/pull/197)
13
+ - [BUGFIX: `deprecations run` no longer raises `NoMethodError` before doing any work](https://github.com/fastruby/next_rails/pull/198)
14
+ - [DOC: Explain `BUNDLE_GEMFILE` usage for the next Gemfile](https://github.com/fastruby/next_rails/pull/187)
15
+
16
+ # v1.6.0 / 2026-05-07 [(commits)](https://github.com/fastruby/next_rails/compare/v1.5.0...v1.6.0)
17
+
18
+ - [CHORE: Drop `rainbow` gem dependency in favor of a native `NextRails::Tint` ANSI wrapper](https://github.com/fastruby/next_rails/pull/183)
19
+ - [BUGFIX: Compare mode now checks only buckets the current process ran, fixing parallel test support](https://github.com/fastruby/next_rails/pull/179)
20
+ - [FEATURE: Add `deprecations merge` command to combine parallel CI shards](https://github.com/fastruby/next_rails/pull/177)
21
+ - [FEATURE: Add parallel CI support for DeprecationTracker](https://github.com/fastruby/next_rails/pull/176)
22
+ - [CHORE: Add Ruby 4.0 to the test matrix](https://github.com/fastruby/next_rails/pull/178)
23
+
7
24
  # v1.5.0 / 2026-04-01 [(commits)](https://github.com/fastruby/next_rails/compare/v1.4.8...v1.5.0)
8
25
 
9
26
  - [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,157 @@
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
+ ```
33
+
34
+ ## Dual Boot
35
+
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.
28
37
 
29
- Learn about your Gemfile and see what needs updating.
38
+ ### Setup
39
+
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.
30
42
 
31
43
  ```bash
32
- # Show all out-of-date gems
33
- bundle_report outdated
44
+ # Initialize dual boot (creates Gemfile.next and Gemfile.next.lock)
45
+ next_rails --init
34
46
 
35
- # Show five oldest, out-of-date gems
36
- bundle_report outdated | head -n 5
47
+ # Edit your Gemfile to conditionally set gem versions using `next?`
48
+ vim Gemfile
37
49
 
38
- # Show all out-of-date gems in machine readable JSON format
39
- bundle_report outdated --json
50
+ # Install dependencies for the next version
51
+ next bundle install
40
52
 
41
- # Show gems that don't work with Rails 5.2.0
42
- bundle_report compatibility --rails-version=5.2.0
53
+ # Start your server using the next Gemfile
54
+ next rails server
55
+ ```
43
56
 
44
- # Show gems that don't work with Ruby 3.0
45
- bundle_report compatibility --ruby-version=3.0
57
+ ### How `next` works (and using `BUNDLE_GEMFILE` directly)
46
58
 
47
- # Find minimum compatible ruby version with Rails 7.0.0
48
- bundle_report ruby_check --rails-version=7.0.0
59
+ The `next` command is a thin wrapper. It runs your command with `BUNDLE_GEMFILE`
60
+ set so Bundler reads the next dependency set instead of the default one. So these
61
+ two are equivalent:
49
62
 
50
- # Show the usual help message
51
- bundle_report --help
63
+ ```bash
64
+ next rails server
65
+
66
+ BUNDLE_GEMFILE=Gemfile.next rails server
67
+ ```
68
+
69
+ Set `BUNDLE_GEMFILE=Gemfile.next` directly when you can't use the `next` wrapper,
70
+ for example in CI jobs, IDE run configurations, Docker, or other tooling that
71
+ calls Bundler on its own:
72
+
73
+ ```bash
74
+ # Run the test suite against the next Gemfile in CI
75
+ BUNDLE_GEMFILE=Gemfile.next bundle exec rspec
76
+
77
+ # Export it for a whole shell session
78
+ export BUNDLE_GEMFILE=Gemfile.next
79
+ bundle install
80
+ rails server
81
+ ```
82
+
83
+ When updating `Gemfile.next.lock`, prefer a conservative update so you only bump
84
+ what you intend to and keep the diff small:
85
+
86
+ ```bash
87
+ # Update a single gem against the next Gemfile, leaving everything else pinned
88
+ BUNDLE_GEMFILE=Gemfile.next bundle update rails --conservative
52
89
  ```
53
90
 
54
- ### Application usage
91
+ > [!IMPORTANT]
92
+ > Whenever you update `Gemfile.lock`, update `Gemfile.next.lock` at the same time
93
+ > (run the equivalent command with `BUNDLE_GEMFILE=Gemfile.next`). Keeping the two
94
+ > lockfiles in sync avoids surprising differences between the current and next
95
+ > dependency sets.
96
+
97
+ ### Conditional code
55
98
 
56
- Every now and then it will be necessary to add code like this to your
57
- application:
99
+ When your Gemfile targets two versions, you may need to branch application code as well:
58
100
 
59
101
  ```ruby
60
102
  if NextRails.next?
61
- # Do things "the Rails 7 way"
103
+ # Do things "the Rails 7.2 way"
62
104
  else
63
- # Do things "the Rails 6.1 way"
105
+ # Do things "the Rails 7.1 way"
64
106
  end
65
107
  ```
66
108
 
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.
109
+ Or use `NextRails.current?` for the inverse check:
110
+
111
+ ```ruby
112
+ if NextRails.current?
113
+ # Do things "the Rails 7.1 way"
114
+ else
115
+ # Do things "the Rails 7.2 way"
116
+ end
117
+ ```
118
+
119
+ 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).
120
+
121
+ ## Bundle Report
122
+
123
+ Inspect your Gemfile and check compatibility with a target Rails or Ruby version.
124
+
125
+ ```bash
126
+ # Show all out-of-date gems
127
+ bundle_report outdated
128
+
129
+ # Show all out-of-date gems in JSON format
130
+ bundle_report outdated --json
131
+
132
+ # Show gems incompatible with Rails 7.2
133
+ bundle_report compatibility --rails-version=7.2
134
+
135
+ # Show gems incompatible with Ruby 3.3
136
+ bundle_report compatibility --ruby-version=3.3
137
+
138
+ # Find minimum Ruby version compatible with Rails 7.2
139
+ bundle_report ruby_check --rails-version=7.2
140
+
141
+ # Help
142
+ bundle_report --help
143
+ ```
144
+
145
+ ## Deprecation Tracking
70
146
 
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).
147
+ Track deprecation warnings in your test suite so you can monitor and fix them incrementally.
73
148
 
74
- ### Deprecation tracking
149
+ ### RSpec
75
150
 
76
- If you're using RSpec, add this snippet to `rails_helper.rb` or `spec_helper.rb` (whichever loads Rails).
151
+ Add to `rails_helper.rb` or `spec_helper.rb`:
77
152
 
78
153
  ```ruby
79
154
  RSpec.configure do |config|
80
- # Tracker deprecation messages in each file
81
155
  if ENV["DEPRECATION_TRACKER"]
82
156
  DeprecationTracker.track_rspec(
83
157
  config,
@@ -89,10 +163,11 @@ RSpec.configure do |config|
89
163
  end
90
164
  ```
91
165
 
92
- If using minitest, add this somewhere close to the top of your `test_helper.rb`:
166
+ ### Minitest
167
+
168
+ Add near the top of `test_helper.rb`:
93
169
 
94
170
  ```ruby
95
- # Tracker deprecation messages in each file
96
171
  if ENV["DEPRECATION_TRACKER"]
97
172
  DeprecationTracker.track_minitest(
98
173
  shitlist_path: "test/support/deprecation_warning.shitlist.json",
@@ -102,105 +177,125 @@ if ENV["DEPRECATION_TRACKER"]
102
177
  end
103
178
  ```
104
179
 
105
- > Keep in mind this is currently not compatible with the `minitest/parallel_fork` gem!
180
+ > [!NOTE]
181
+ > This is currently not compatible with the `minitest/parallel_fork` gem.
106
182
 
107
- Once you have that, you can start using deprecation tracking in your tests:
183
+ ### Running deprecation tracking
108
184
 
109
185
  ```bash
110
- # Run your tests and save the deprecations to the shitlist
186
+ # Save current deprecations to the shitlist
111
187
  DEPRECATION_TRACKER=save rspec
112
- # Run your tests and raise an error when the deprecations change
188
+
189
+ # Fail if deprecations have changed since the last save
113
190
  DEPRECATION_TRACKER=compare rspec
114
191
  ```
115
192
 
116
- #### `deprecations` command
193
+ ### Parallel CI support
117
194
 
118
- Once you have stored your deprecations, you can use `deprecations` to display common warnings, run specs, or update the shitlist file.
195
+ 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
196
 
120
- ```bash
121
- deprecations info
122
- deprecations info --pattern "ActiveRecord::Base"
123
- deprecations run
124
- deprecations --help # For more options and examples
197
+ #### RSpec
198
+
199
+ ```ruby
200
+ RSpec.configure do |config|
201
+ if ENV["DEPRECATION_TRACKER"]
202
+ DeprecationTracker.track_rspec(
203
+ config,
204
+ node_index: ENV["CI_NODE_INDEX"]
205
+ )
206
+ end
207
+ end
125
208
  ```
126
209
 
127
- Right now, the path to the shitlist is hardcoded so make sure you store yours at `spec/support/deprecation_warning.shitlist.json`.
210
+ 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
211
 
129
- #### `next_rails` command
212
+ #### Merging shards
130
213
 
131
- You can use `next_rails` to fetch the version of the gem installed.
214
+ After all parallel nodes finish saving, merge shards into the canonical file:
132
215
 
133
216
  ```bash
134
- next_rails --version
135
- next_rails --help # For more options and examples
136
- ```
217
+ # Merge all shard files and remove them afterwards
218
+ deprecations merge --delete-shards
137
219
 
138
- ### Dual-boot Rails next
220
+ # Or use --next to merge shards for the next Rails version
221
+ deprecations merge --next --delete-shards
222
+ ```
139
223
 
140
- This command helps you dual-boot your application.
224
+ You can also merge shards programmatically:
141
225
 
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
226
+ ```ruby
227
+ DeprecationTracker.merge_shards(
228
+ "spec/support/deprecation_warning.shitlist.json",
229
+ delete_shards: true
230
+ )
147
231
  ```
148
232
 
149
- ## Installation
233
+ #### Example CI workflow
150
234
 
151
- Add this line to your application's Gemfile
235
+ ```yaml
236
+ # 1. Save phase — each parallel node writes its own shard
237
+ # (runs on every node)
238
+ DEPRECATION_TRACKER=save CI_NODE_INDEX=$NODE bundle exec rspec <subset>
152
239
 
153
- > NOTE: If you add this gem to a group, make sure it is the test env group
240
+ # 2. Merge phase fan-in step, runs once after all nodes finish
241
+ deprecations merge --delete-shards
154
242
 
155
- ```ruby
156
- gem 'next_rails'
243
+ # 3. Compare phase — each parallel node checks only its own buckets
244
+ # against the merged canonical file (no CI_NODE_INDEX needed)
245
+ DEPRECATION_TRACKER=compare bundle exec rspec <subset>
157
246
  ```
158
247
 
159
- And then execute:
248
+ ### `deprecations` command
160
249
 
161
- $ bundle
250
+ View, filter, and manage stored deprecation warnings:
162
251
 
163
- Or install it yourself as:
252
+ ```bash
253
+ deprecations info
254
+ deprecations info --pattern "ActiveRecord::Base"
255
+ deprecations merge --delete-shards
256
+ deprecations run
257
+ deprecations --help
258
+ ```
164
259
 
165
- $ gem install next_rails
260
+ ## CLI Reference
166
261
 
167
- ## Setup
262
+ ```bash
263
+ bundle exec next_rails --init # Set up dual boot
264
+ bundle exec next_rails --version # Show gem version
265
+ bundle exec next_rails --help # Show help
266
+ ```
168
267
 
169
- Execute:
268
+ ## Contributing
170
269
 
171
- $ next_rails --init
270
+ Bug reports and pull requests are welcome! See the [Contributing guide](CONTRIBUTING.md) for setup instructions and guidelines.
172
271
 
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.
272
+ ## Releases
177
273
 
178
- ## Contributing
274
+ `next_rails` follows [Semantic Versioning](https://semver.org). Given a version number `MAJOR.MINOR.PATCH`, we increment the:
179
275
 
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`?
276
+ - **MAJOR** version for incompatible API changes
277
+ - **MINOR** version for backwards-compatible new functionality
278
+ - **PATCH** version for backwards-compatible bug fixes
181
279
 
182
- Take a look at the [Contributing document](CONTRIBUTING.md) for instructions to set up the repo on your machine!
280
+ ### Steps to release a new version
183
281
 
184
- ## Releases
282
+ 1. Update the version number in `lib/next_rails/version.rb`
283
+ 2. Update `CHANGELOG.md` with the appropriate headers and entries
284
+ 3. Commit your changes to a `release/v1.x.x` branch
285
+ 4. Push your changes and submit a pull request `Release v1.x.x`
286
+ 5. Merge your pull request to the `main` branch
287
+ 6. Tag the latest version on `main`: `git tag v1.x.x`
288
+ 7. Push the tag to GitHub: `git push --tags`
289
+ 8. Build the gem: `gem build next_rails.gemspec`
290
+ 9. Push to RubyGems: `gem push next_rails-1.x.x.gem`
185
291
 
186
- `next_rails` adheres to [semver](https://semver.org). So given a version number MAJOR.MINOR.PATCH, we will increment the:
292
+ ## Maintainers
187
293
 
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.
294
+ Maintained by [OmbuLabs / FastRuby.io](https://www.fastruby.io).
191
295
 
192
- Here are the steps to release a new version:
296
+ ## History
193
297
 
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!
298
+ 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
299
 
205
300
  ## License
206
301
 
data/exe/deprecations CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
  require "json"
3
- require "rainbow"
4
3
  require "optparse"
5
4
  require "set"
5
+ require_relative "../lib/next_rails/tint"
6
+ require_relative "../lib/deprecation_tracker/valid_modes"
7
+ require_relative "../lib/deprecation_tracker/shard_merger"
6
8
 
7
9
  def run_tests(deprecation_warnings, opts = {})
8
- tracker_mode = opts[:tracker_mode]
10
+ tracker_mode = DeprecationTracker.sanitize_mode(opts[:tracker_mode])
11
+ if tracker_mode && !DeprecationTracker.valid_mode?(tracker_mode)
12
+ abort "Invalid --tracker-mode #{tracker_mode.inspect}. Must be one of: #{DeprecationTracker.valid_modes_display}."
13
+ end
9
14
  next_mode = opts[:next_mode]
10
15
  rspec_command = if next_mode
11
16
  "bin/next rspec"
@@ -28,11 +33,11 @@ def print_info(deprecation_warnings, opts = {})
28
33
  end
29
34
  end.sort_by {|message, data| data[:occurrences] }.reverse.to_h
30
35
 
31
- puts Rainbow("Ten most common deprecation warnings:").underline
36
+ puts NextRails::Tint("Ten most common deprecation warnings:").underline
32
37
  frequency_by_message.take(10).each do |message, data|
33
- puts Rainbow("Occurrences: #{data.fetch(:occurrences)}").bold
38
+ puts NextRails::Tint("Occurrences: #{data.fetch(:occurrences)}").bold
34
39
  puts "Test files: #{data.fetch(:test_files).to_a.join(" ")}" if verbose
35
- puts Rainbow(message).red
40
+ puts NextRails::Tint(message).red
36
41
  puts "----------"
37
42
  end
38
43
  end
@@ -49,6 +54,7 @@ option_parser = OptionParser.new do |opts|
49
54
  bin/deprecations --next info # Show top ten deprecations for Rails 5
50
55
  bin/deprecations --pattern "ActiveRecord::Base" --verbose info # Show full details on deprecations matching pattern
51
56
  bin/deprecations --tracker-mode save --pattern "pass" run # Run tests that output deprecations matching pattern and update shitlist
57
+ bin/deprecations merge --delete-shards # Merge parallel CI shards and remove shard files
52
58
 
53
59
  Modes:
54
60
  info
@@ -57,6 +63,9 @@ option_parser = OptionParser.new do |opts|
57
63
  run
58
64
  Run tests that are known to cause deprecation warnings. Use --pattern to filter what tests are run.
59
65
 
66
+ merge
67
+ Merge parallel CI shard files into the canonical shitlist. Use with --delete-shards to remove shard files after merging.
68
+
60
69
  Options:
61
70
  MESSAGE
62
71
 
@@ -64,7 +73,7 @@ option_parser = OptionParser.new do |opts|
64
73
  options[:next] = next_mode
65
74
  end
66
75
 
67
- opts.on("--tracker-mode MODE", "Set DEPRECATION_TRACKER in test mode. Options: save or compare") do |tracker_mode|
76
+ opts.on("--tracker-mode MODE", "Set DEPRECATION_TRACKER in test mode. Options: #{DeprecationTracker.valid_modes_display}") do |tracker_mode|
68
77
  options[:tracker_mode] = tracker_mode
69
78
  end
70
79
 
@@ -76,6 +85,10 @@ option_parser = OptionParser.new do |opts|
76
85
  options[:verbose] = true
77
86
  end
78
87
 
88
+ opts.on("--delete-shards", "Delete shard files after merging") do
89
+ options[:delete_shards] = true
90
+ end
91
+
79
92
  opts.on_tail("-h", "--help", "Prints this help") do
80
93
  puts opts
81
94
  exit
@@ -87,27 +100,37 @@ option_parser.parse!
87
100
  options[:mode] = ARGV.last
88
101
  path = options[:next] ? "spec/support/deprecation_warning.next.shitlist.json" : "spec/support/deprecation_warning.shitlist.json"
89
102
 
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
103
+ case options[:mode]
104
+ when "merge"
105
+ output = DeprecationTracker::ShardMerger.new(path, delete_shards: !!options[:delete_shards]).merge
106
+ shards = output[:shards]
107
+ result = output[:result]
108
+ total_messages = result.values.map(&:size).reduce(0, :+)
109
+ puts "Merged #{shards} shard files into #{path} (#{result.size} buckets, #{total_messages} deprecation messages)"
110
+ when "run", "info"
111
+ pattern_string = options.fetch(:pattern, ".+")
112
+ pattern = /#{pattern_string}/
113
+
114
+ deprecation_warnings = JSON.parse(File.read(path)).each_with_object({}) do |(test_file, messages), hash|
115
+ filtered_messages = messages.select {|message| message.match(pattern) }
116
+ hash[test_file] = filtered_messages if !filtered_messages.empty?
117
+ end
97
118
 
98
- if deprecation_warnings.empty?
99
- abort "No test files with deprecations matching #{pattern.inspect}."
100
- exit 2
101
- end
119
+ if deprecation_warnings.empty?
120
+ abort "No test files with deprecations matching #{pattern.inspect}."
121
+ exit 2
122
+ end
102
123
 
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])
124
+ if options[:mode] == "run"
125
+ run_tests(deprecation_warnings, next_mode: options[:next], tracker_mode: options[:tracker_mode])
126
+ else
127
+ print_info(deprecation_warnings, verbose: options[:verbose])
128
+ end
106
129
  when nil
107
- STDERR.puts Rainbow("Must pass a mode: run or info").red
130
+ STDERR.puts NextRails::Tint("Must pass a mode: run, info, or merge").red
108
131
  puts option_parser
109
132
  exit 1
110
133
  else
111
- STDERR.puts Rainbow("Unknown mode: #{options[:mode]}").red
134
+ STDERR.puts NextRails::Tint("Unknown mode: #{options[:mode]}").red
112
135
  exit 1
113
136
  end
@@ -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
@@ -0,0 +1,22 @@
1
+ class DeprecationTracker
2
+ VALID_MODES = %i[save compare].freeze
3
+
4
+ # Human-readable list of valid modes for error messages and CLI help text.
5
+ def self.valid_modes_display
6
+ @valid_modes_display ||= VALID_MODES.map(&:to_s).join(", ")
7
+ end
8
+
9
+ def self.valid_mode?(mode)
10
+ mode && VALID_MODES.include?(mode.to_sym)
11
+ end
12
+
13
+ # Returns the mode as-is, or nil when it is blank. A blank DEPRECATION_TRACKER
14
+ # (e.g. `DEPRECATION_TRACKER= rspec`) is truthy in Ruby, so callers can use this
15
+ # to treat an empty value as unset and fall back to the default mode.
16
+ def self.sanitize_mode(mode)
17
+ return if mode.nil?
18
+
19
+ stripped = mode.to_s.strip
20
+ stripped.empty? ? nil : stripped
21
+ end
22
+ end
@@ -1,4 +1,4 @@
1
- require "rainbow"
1
+ require_relative "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"
@@ -10,6 +10,8 @@ require "json"
10
10
  # Tracks deprecation warnings, grouped by spec file. After the test run, compare against shitlist of expected
11
11
  # deprecation warnings. If anything is added or removed, raise an error with a diff of the changes.
12
12
  #
13
+ require_relative "deprecation_tracker/valid_modes"
14
+
13
15
  class DeprecationTracker
14
16
  UnexpectedDeprecations = Class.new(StandardError)
15
17
 
@@ -73,11 +75,14 @@ class DeprecationTracker
73
75
  end
74
76
  end
75
77
 
78
+ DEFAULT_PATH = "spec/support/deprecation_warning.shitlist.json"
79
+
76
80
  def self.init_tracker(opts = {})
77
- shitlist_path = opts[:shitlist_path]
78
- mode = opts[:mode]
81
+ shitlist_path = opts[:shitlist_path] || DEFAULT_PATH
82
+ mode = sanitize_mode(opts[:mode] || ENV["DEPRECATION_TRACKER"]) || :save
79
83
  transform_message = opts[:transform_message]
80
- deprecation_tracker = DeprecationTracker.new(shitlist_path, transform_message, mode)
84
+ node_index = opts[:node_index]
85
+ deprecation_tracker = DeprecationTracker.new(shitlist_path, transform_message, mode, node_index: node_index)
81
86
  # Since Rails 7.1 the preferred way to track deprecations is to use the deprecation trackers via
82
87
  # `Rails.application.deprecators`.
83
88
  # We fallback to tracking deprecations via the ActiveSupport singleton object if Rails.application.deprecators is
@@ -126,13 +131,38 @@ class DeprecationTracker
126
131
  ActiveSupport::TestCase.include(MinitestExtension.new(tracker))
127
132
  end
128
133
 
129
- attr_reader :deprecation_messages, :shitlist_path, :transform_message, :bucket, :mode
134
+ def self.merge_shards(base_path, delete_shards: false)
135
+ require_relative "deprecation_tracker/shard_merger"
136
+ ShardMerger.new(base_path, delete_shards: delete_shards).merge[:result]
137
+ end
138
+
139
+ attr_reader :deprecation_messages, :shitlist_path, :transform_message, :bucket, :mode, :node_index
130
140
 
131
- def initialize(shitlist_path, transform_message = nil, mode = :save)
141
+ def initialize(shitlist_path, transform_message = nil, mode = :save, node_index: nil)
132
142
  @shitlist_path = shitlist_path
133
143
  @transform_message = transform_message || -> (message) { message }
134
144
  @deprecation_messages = {}
135
- @mode = mode.to_sym
145
+ @mode = mode ? mode.to_sym : :save
146
+ unless self.class.valid_mode?(@mode)
147
+ raise ArgumentError, "mode must be one of: #{self.class.valid_modes_display}. Got: #{mode.inspect}"
148
+ end
149
+ if @mode == :compare && node_index
150
+ raise ArgumentError, "node_index cannot be used with compare mode"
151
+ end
152
+ @node_index = node_index
153
+ end
154
+
155
+ def parallel?
156
+ !@node_index.nil?
157
+ end
158
+
159
+ def shard_path
160
+ ext = File.extname(shitlist_path)
161
+ "#{shitlist_path.chomp(ext)}.node-#{node_index}#{ext}"
162
+ end
163
+
164
+ def target_path
165
+ parallel? ? shard_path : shitlist_path
136
166
  end
137
167
 
138
168
  def add(message)
@@ -155,16 +185,17 @@ class DeprecationTracker
155
185
  end
156
186
 
157
187
  def compare
158
- shitlist = read_shitlist
188
+ stored = read_json(shitlist_path)
159
189
 
160
190
  changed_buckets = []
191
+
161
192
  normalized_deprecation_messages.each do |bucket, messages|
162
- if shitlist[bucket] != messages
193
+ if stored[bucket] != messages
163
194
  changed_buckets << bucket
164
195
  end
165
196
  end
166
197
 
167
- if changed_buckets.length > 0
198
+ if changed_buckets.any?
168
199
  message = <<-MESSAGE
169
200
  ⚠️ Deprecation warnings have changed!
170
201
 
@@ -183,33 +214,33 @@ class DeprecationTracker
183
214
  See \e[4;37mdev-docs/testing/deprecation_tracker.md\e[0;31m for more information.
184
215
  MESSAGE
185
216
 
186
- raise UnexpectedDeprecations, Rainbow(message).red
217
+ raise UnexpectedDeprecations, NextRails::Tint(message).red
187
218
  end
188
219
  end
189
220
 
190
221
  def diff
191
- new_shitlist = create_temp_shitlist
192
- `git diff --no-index #{shitlist_path} #{new_shitlist.path}`
222
+ temp_file = create_temp_file
223
+ `git diff --no-index #{shitlist_path} #{temp_file.path}`
193
224
  ensure
194
- new_shitlist.delete
225
+ temp_file.delete
195
226
  end
196
227
 
197
228
  def save
198
- new_shitlist = create_temp_shitlist
199
- create_if_shitlist_path_does_not_exist
200
- FileUtils.cp(new_shitlist.path, shitlist_path)
229
+ temp_file = create_temp_file
230
+ create_if_path_does_not_exist(target_path)
231
+ FileUtils.cp(temp_file.path, target_path)
201
232
  ensure
202
- new_shitlist.delete if new_shitlist
233
+ temp_file.delete if temp_file
203
234
  end
204
235
 
205
- def create_if_shitlist_path_does_not_exist
206
- dirname = File.dirname(shitlist_path)
236
+ def create_if_path_does_not_exist(path)
237
+ dirname = File.dirname(path)
207
238
  unless File.directory?(dirname)
208
239
  FileUtils.mkdir_p(dirname)
209
240
  end
210
241
  end
211
242
 
212
- def create_temp_shitlist
243
+ def create_temp_file
213
244
  temp_file = Tempfile.new("temp-deprecation-tracker-shitlist")
214
245
  temp_file.write(JSON.pretty_generate(normalized_deprecation_messages))
215
246
  temp_file.flush
@@ -219,22 +250,24 @@ class DeprecationTracker
219
250
 
220
251
  # Normalize deprecation messages to reduce noise from file output and test files to be tracked with separate test runs
221
252
  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
253
+ @normalized_deprecation_messages ||= begin
254
+ normalized = read_json(target_path).merge(deprecation_messages).each_with_object({}) do |(bucket, messages), hash|
255
+ hash[bucket] = messages.sort
256
+ end
225
257
 
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
258
+ # not using `to_h` here to support older ruby versions
259
+ {}.tap do |h|
260
+ normalized.reject {|_key, value| value.empty? }.sort_by {|key, _value| key }.each do |k ,v|
261
+ h[k] = v
262
+ end
230
263
  end
231
264
  end
232
265
  end
233
266
 
234
- def read_shitlist
235
- return {} unless File.exist?(shitlist_path)
236
- JSON.parse(File.read(shitlist_path))
267
+ def read_json(path)
268
+ return {} unless File.exist?(path)
269
+ JSON.parse(File.read(path))
237
270
  rescue JSON::ParserError => e
238
- raise "#{shitlist_path} is not valid JSON: #{e.message}"
271
+ raise "#{path} is not valid JSON: #{e.message}"
239
272
  end
240
273
  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"
@@ -63,22 +63,27 @@ module NextRails
63
63
 
64
64
  def outdated(format = nil)
65
65
  gems = NextRails::GemInfo.all
66
- out_of_date_gems = gems.reject(&:up_to_date?).sort_by(&:created_at)
66
+ sourced_locally = gems.select(&:sourced_locally?)
67
67
  sourced_from_git = gems.select(&:sourced_from_git?)
68
68
 
69
+ # Locally-sourced gems (e.g. `path:` engines) are excluded from the
70
+ # out-of-date check: looking them up by name on rubygems can match an
71
+ # unrelated public gem with the same name and report a bogus upgrade.
72
+ out_of_date_gems = (gems - sourced_locally).reject(&:up_to_date?).sort_by(&:created_at)
73
+
69
74
  if format == 'json'
70
- output_to_json(out_of_date_gems, gems.count, sourced_from_git.count)
75
+ output_to_json(out_of_date_gems, gems.count, sourced_from_git.count, sourced_locally.count)
71
76
  else
72
- output_to_stdout(out_of_date_gems, gems.count, sourced_from_git.count)
77
+ output_to_stdout(out_of_date_gems, gems.count, sourced_from_git.count, sourced_locally.count)
73
78
  end
74
79
  end
75
80
 
76
- def output_to_json(out_of_date_gems, total_gem_count, sourced_from_git_count)
77
- obj = build_json(out_of_date_gems, total_gem_count, sourced_from_git_count)
81
+ def output_to_json(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
82
+ obj = build_json(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
78
83
  puts JSON.pretty_generate(obj)
79
84
  end
80
85
 
81
- def build_json(out_of_date_gems, total_gem_count, sourced_from_git_count)
86
+ def build_json(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
82
87
  output = Hash.new { [] }
83
88
  out_of_date_gems.each do |gem|
84
89
  output[:outdated_gems] += [
@@ -95,24 +100,26 @@ module NextRails
95
100
  output.merge(
96
101
  {
97
102
  sourced_from_git_count: sourced_from_git_count,
103
+ sourced_locally_count: sourced_locally_count,
98
104
  total_gem_count: total_gem_count
99
105
  }
100
106
  )
101
107
  end
102
108
 
103
- def output_to_stdout(out_of_date_gems, total_gem_count, sourced_from_git_count)
109
+ def output_to_stdout(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
104
110
  out_of_date_gems.each do |gem|
105
111
  header = "#{gem.name} #{gem.version}"
106
112
 
107
113
  puts <<-MESSAGE
108
- #{Rainbow(header).bold.white}: released #{gem.age} (latest version, #{gem.latest_version.version}, released #{gem.latest_version.age})
114
+ #{NextRails::Tint(header).bold.white}: released #{gem.age} (latest version, #{gem.latest_version.version}, released #{gem.latest_version.age})
109
115
  MESSAGE
110
116
  end
111
117
 
112
118
  percentage_out_of_date = ((out_of_date_gems.count / total_gem_count.to_f) * 100).round
113
119
  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}%)
120
+ #{NextRails::Tint(sourced_from_git_count.to_s).yellow} gems are sourced from git
121
+ #{NextRails::Tint(sourced_locally_count.to_s).yellow} gems are sourced from a local path
122
+ #{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
123
  MESSAGE
117
124
 
118
125
  puts ''
@@ -66,6 +66,17 @@ module NextRails
66
66
  !!gem_specification.git_version
67
67
  end
68
68
 
69
+ def sourced_locally?
70
+ # Bundler defines Source::Path and patches Gem::Specification#source to return the
71
+ # gem's real source. Without Bundler loaded, #source is RubyGems' own and returns a
72
+ # Gem::Source::Installed, so no path source can exist; treat the gem as not local.
73
+ return false unless defined?(Bundler::Source::Path)
74
+
75
+ source = gem_specification.source
76
+ # Git sources subclass Path, so exclude them; they are reported via #sourced_from_git?.
77
+ source.is_a?(Bundler::Source::Path) && !source.is_a?(Bundler::Source::Git)
78
+ end
79
+
69
80
  def created_at
70
81
  @created_at ||= gem_specification.date
71
82
  end
@@ -0,0 +1,48 @@
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
+ underline: 4,
17
+ red: 31,
18
+ green: 32,
19
+ yellow: 33,
20
+ blue: 34,
21
+ magenta: 35,
22
+ cyan: 36,
23
+ white: 37
24
+ }.freeze
25
+
26
+ def initialize(string, codes = [])
27
+ @string = string.to_s
28
+ @codes = codes
29
+ end
30
+
31
+ CODES.each_key do |style|
32
+ define_method(style) do
33
+ self.class.new(@string, @codes + [CODES[style]])
34
+ end
35
+ end
36
+
37
+ def to_s
38
+ return @string if @codes.empty?
39
+
40
+ "\e[#{@codes.join(";")}m#{@string}\e[0m"
41
+ end
42
+ alias_method :to_str, :to_s
43
+ end
44
+
45
+ def self.Tint(string)
46
+ Tint.new(string)
47
+ end
48
+ 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.7.0"
5
5
  end
data/next_rails.gemspec CHANGED
@@ -22,11 +22,10 @@ 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
- spec.add_development_dependency "simplecov", "~> 0.17.1"
28
+ spec.add_development_dependency "simplecov", ">= 0.17.1"
30
29
  spec.add_development_dependency "timecop", "~> 0.9.1"
31
30
  spec.add_development_dependency "byebug"
32
31
  spec.add_development_dependency "rexml", "3.2.5" # limited on purpose, new versions don't work with old rubies
metadata CHANGED
@@ -1,29 +1,15 @@
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.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernesto Tagwerker
8
8
  - Luis Sagastume
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-02 00:00:00.000000000 Z
11
+ date: 2026-08-06 00:00:00.000000000 Z
12
12
  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
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -31,9 +17,6 @@ dependencies:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
19
  version: '1.16'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '3.0'
37
20
  type: :development
38
21
  prerelease: false
39
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -41,9 +24,6 @@ dependencies:
41
24
  - - ">="
42
25
  - !ruby/object:Gem::Version
43
26
  version: '1.16'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '3.0'
47
27
  - !ruby/object:Gem::Dependency
48
28
  name: rake
49
29
  requirement: !ruby/object:Gem::Requirement
@@ -76,14 +56,14 @@ dependencies:
76
56
  name: simplecov
77
57
  requirement: !ruby/object:Gem::Requirement
78
58
  requirements:
79
- - - "~>"
59
+ - - ">="
80
60
  - !ruby/object:Gem::Version
81
61
  version: 0.17.1
82
62
  type: :development
83
63
  prerelease: false
84
64
  version_requirements: !ruby/object:Gem::Requirement
85
65
  requirements:
86
- - - "~>"
66
+ - - ">="
87
67
  - !ruby/object:Gem::Version
88
68
  version: 0.17.1
89
69
  - !ruby/object:Gem::Dependency
@@ -194,6 +174,8 @@ files:
194
174
  - exe/next.sh
195
175
  - exe/next_rails
196
176
  - lib/deprecation_tracker.rb
177
+ - lib/deprecation_tracker/shard_merger.rb
178
+ - lib/deprecation_tracker/valid_modes.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