Skip to content

Commit 3ed8972

Browse files
committed
Swap ParameterFilter example for ignorableignored_columns
Review found `ActionDispatch::Http::ParameterFilter` → `ActiveSupport::ParameterFilter` at Rails 6.0 → 6.1 was not a genuine two-sided break: `ActiveSupport::ParameterFilter` shipped in Rails 6.0 (verified against the `activesupport/CHANGELOG.md` at the v6.0.0 tag), so the replacement already exists on the current side and the correct fix is an unconditional migration — the exact anti-pattern this PR is arguing against. Replace with the `ignorable` gem's `ignore_columns` → native `ignored_columns=` at Rails 4.2 → 5.0. This is genuinely two-sided: dropping the `ignorable` gem on the 5.0 side makes `ignore_columns` raise `NoMethodError`, and `ignored_columns=` raises `NoMethodError` on 4.2 where the setter doesn't exist yet. `NextRails.next?` is required. Updated `SKILL.md` Pattern, `examples/basic-setup.md` (scenario shifted to 4.2 → 5.0), `references/code-patterns.md`, `README.md` Key Principle, `workflows/cleanup-workflow.md` Step 2, and the `CHANGELOG.md` v1.1 entry. Also added a "When NOT to Branch: Deprecations" section in `references/code-patterns.md` using `fixture_path` → `fixture_paths` as the counter-example, so readers learn that deprecations should be migrated unconditionally — not wrapped in `NextRails.next?`.
1 parent dea7c6d commit 3ed8972

6 files changed

Lines changed: 81 additions & 68 deletions

File tree

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,24 @@ In Claude Code, navigate to your Rails application directory and use natural lan
6666
"Clean up dual-boot code after upgrade"
6767
```
6868

69-
## Key Principle: Use `NextRails.next?` (not `respond_to?`!)
69+
## Key Principle: Use `NextRails.next?` (not feature detection)
7070

7171
When writing code that must work with two versions, always use `NextRails.next?`:
7272

7373
```ruby
74-
# CORRECT
75-
if NextRails.next?
76-
config.fixture_paths = ["#{::Rails.root}/spec/fixtures"]
77-
else
78-
config.fixture_path = "#{::Rails.root}/spec/fixtures"
74+
# app/models/project.rb
75+
class Project < ActiveRecord::Base
76+
if NextRails.next?
77+
self.ignored_columns += [:category]
78+
else
79+
ignore_columns :category
80+
end
7981
end
8082
```
8183

82-
Never use `respond_to?` for version branching. It's hard to understand, hard to maintain, and obscures intent.
84+
Never use `respond_to?`, `defined?`, or other feature-detection patterns. They are fragile, hard to clean up, and obscure intent.
85+
86+
Only branch when the old API genuinely breaks on the next version. A plain deprecation warning is not a reason for a conditional — if the new API works on both versions, just use it directly.
8387

8488
## Related Skills
8589

dual-boot/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Changelog
22

33
## v1.1 — 22 April 2026
4-
- Replaced `fixture_path`/`fixture_paths` and `serialize coder:` examples (both are backwards-compat deprecations) with genuine breaking changes (`ActionDispatch::Http::ParameterFilter` removal in Rails 6.1). Added explicit guidance: deprecations should be fixed by direct replacement, not wrapped in `NextRails.next?`. (#2)
4+
- Replaced the `fixture_path`/`fixture_paths` and `serialize coder:` examples (both backwards-compat deprecations that don't require a conditional) with a genuine breaking change: `ignorable` gem's `ignore_columns` → native `ignored_columns=` at Rails 4.2 → 5.0, where each side's API raises `NoMethodError` on the other. Added explicit guidance that deprecations should be fixed by direct replacement, not wrapped in `NextRails.next?` (SKILL.md "Pattern", `references/code-patterns.md` "When NOT to Branch: Deprecations"). Broadened the "no feature detection" rule to call out `defined?` alongside `respond_to?`. Standardized on `NextRails.next?` polarity across the skill (removed the "`.current?` is also fine" clause from `CLAUDE.md`). Added invariant #7 codifying "only branch for genuine breaking changes". (#2)
55

66
## v1.0 — 04 March 2025
77
- Initial release as standalone skill (extracted from `rails-upgrade` skill)

dual-boot/SKILL.md

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,38 +46,43 @@ When writing code that must work with both the current and target versions, **al
4646

4747
### Pattern
4848

49-
The right time for a conditional is when the old code actually **breaks** on the next version (removed constant, removed method, incompatible signature) — not when it merely emits a deprecation warning. Deprecations should be fixed by replacing the code outright, since the new form works on both versions.
49+
Reach for `NextRails.next?` only when the old and new APIs are genuinely **two-sided** the old one raises on the next version and the new one doesn't exist on the current version. A plain deprecation warning is *not* a reason to branch: if the new API works on both sides, migrate the call site directly.
5050

51-
Example: `ActionDispatch::Http::ParameterFilter` was removed in Rails 6.1 (replaced by `ActiveSupport::ParameterFilter`). Referencing the old constant under 6.1 raises `NameError`, so a conditional is required during 6.0 → 6.1 dual-boot.
52-
53-
**WRONG — Do NOT use feature detection (`defined?`, `respond_to?`, etc.):**
51+
**WRONG — Do NOT use feature detection (`respond_to?`, `defined?`, etc.):**
5452
```ruby
55-
if defined?(ActiveSupport::ParameterFilter)
56-
filter = ActiveSupport::ParameterFilter.new([:password])
53+
if Project.respond_to?(:ignored_columns=)
54+
self.ignored_columns += [:category]
5755
else
58-
filter = ActionDispatch::Http::ParameterFilter.new([:password])
56+
ignore_columns :category
5957
end
6058
```
6159

6260
**CORRECT — Use `NextRails.next?`:**
6361
```ruby
64-
if NextRails.next?
65-
filter = ActiveSupport::ParameterFilter.new([:password])
66-
else
67-
filter = ActionDispatch::Http::ParameterFilter.new([:password])
62+
# app/models/project.rb
63+
class Project < ActiveRecord::Base
64+
if NextRails.next?
65+
self.ignored_columns += [:category]
66+
else
67+
ignore_columns :category
68+
end
6869
end
6970
```
7071

72+
This is a genuine two-sided case (Rails 4.2 → 5.0): on 4.2 the app uses the [`ignorable`](https://github.com/nthj/ignorable) gem's `ignore_columns` class method. Rails 5.0 introduced native `ignored_columns=` with different syntax, and once the gem is dropped on the 5.0 side, `ignore_columns` raises `NoMethodError`. `ignored_columns=` raises `NoMethodError` on 4.2 where it doesn't yet exist.
73+
74+
Put the next-version branch on top so cleanup is mechanical: after the upgrade, keep the `if` body and drop the `else`. See `references/code-patterns.md` for more examples.
75+
7176
### When to Apply
7277

7378
Use `NextRails.next?` branching for:
74-
- **Removed constants or methods** (e.g., `ActionDispatch::Http::ParameterFilter` removed in Rails 6.1, `update_attributes` removed in Rails 6.1)
75-
- **Incompatible signature or return-type changes** (e.g., a method that now returns a different object type)
76-
- **Gem version differences where the old API is gone in the new version** (not just renamed-with-alias)
77-
- **Initializer changes that break on one side** (e.g., middleware removed from defaults, config key that raises on the other version)
79+
- **Removed methods or constants** where the replacement only exists on the new side (e.g., a gem-provided method replaced by a native Rails method with different syntax)
80+
- **Incompatible signature or return-type changes** where the old and new forms genuinely fail on opposite sides
81+
- **Gem version differences** where the gem's API is different across versions pinned to each Rails version
82+
- **Initializer changes** where a config or middleware raises on one side
7883
- **Ruby version differences** (e.g., syntax changes, stdlib removals)
7984

80-
**Not a reason to branch:** deprecation warnings. If the new API works on both versions, migrate the call site directly — do not wrap it in `NextRails.next?`. See `references/code-patterns.md` "When NOT to Branch: Deprecations".
85+
**Not a reason to branch:** plain deprecation warnings. If the new API works on both versions (e.g. `config.fixture_path=``config.fixture_paths=`, `update_attributes``update` before its removal), migrate the call site directly — do not wrap it in `NextRails.next?`. See `references/code-patterns.md` "When NOT to Branch: Deprecations".
8186

8287
---
8388

dual-boot/examples/basic-setup.md

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This example walks through a Rails version upgrade. The same approach applies wh
44

55
## Scenario
66

7-
You have a Rails 6.0 application and want to upgrade to Rails 6.1. You want to set up dual-boot to test both versions during the transition.
7+
You have a Rails 4.2 application and want to upgrade to Rails 5.0. You want to set up dual-boot to test both versions during the transition.
88

99
---
1010

@@ -36,9 +36,10 @@ def next?
3636
end
3737

3838
if next?
39-
gem 'rails', '~> 6.1.0'
39+
gem 'rails', '~> 5.0.0'
4040
else
41-
gem 'rails', '~> 6.0.0'
41+
gem 'rails', '~> 4.2.0'
42+
gem 'ignorable' # used for `ignore_columns` on 4.2; dropped on 5.0
4243
end
4344

4445
gem 'next_rails'
@@ -58,32 +59,30 @@ BUNDLE_GEMFILE=Gemfile.next bundle install
5859
### 5. Run Tests Against Both
5960

6061
```bash
61-
# Current version (6.0)
62+
# Current version (4.2)
6263
bundle exec rspec
6364
# => All green
6465

65-
# Next version (6.1)
66+
# Next version (5.0)
6667
BUNDLE_GEMFILE=Gemfile.next bundle exec rspec
6768
# => Some failures — fix using NextRails.next? branching
6869
```
6970

7071
### 6. Fix a Breaking Change
7172

72-
Rails 6.1 removed `ActionDispatch::Http::ParameterFilter` — referencing it under 6.1 raises `NameError: uninitialized constant`. The replacement `ActiveSupport::ParameterFilter` does not exist on 6.0, so a conditional is required:
73+
Rails 5.0 introduced a native `ignored_columns=` setter, replacing the `ignorable` gem's `ignore_columns` class method. With the gem dropped on the 5.0 side, `ignore_columns` raises `NoMethodError` there; `ignored_columns=` raises `NoMethodError` on 4.2 where it doesn't yet exist. A conditional is required:
7374

7475
```ruby
75-
# app/services/log_sanitizer.rb
76-
if NextRails.next?
77-
filter = ActiveSupport::ParameterFilter.new([:password, :token])
78-
else
79-
filter = ActionDispatch::Http::ParameterFilter.new([:password, :token])
76+
# app/models/project.rb
77+
class Project < ActiveRecord::Base
78+
if NextRails.next?
79+
self.ignored_columns += [:category]
80+
else
81+
ignore_columns :category
82+
end
8083
end
81-
82-
filter.filter(params)
8384
```
8485

85-
Note: a pure deprecation warning (old API still works under next) is **not** a reason to branch — replace the call site directly. Reserve `NextRails.next?` for code that would otherwise fail to load or run.
86-
8786
### 7. Verify Both Pass
8887

8988
```bash
@@ -94,29 +93,30 @@ BUNDLE_GEMFILE=Gemfile.next bundle exec rspec # Next: green
9493
### 8. Commit
9594

9695
```bash
97-
git add Gemfile Gemfile.next Gemfile.next.lock app/services/log_sanitizer.rb
98-
git commit -m "Set up dual-boot for Rails 6.06.1 upgrade"
96+
git add Gemfile Gemfile.next Gemfile.next.lock app/models/project.rb
97+
git commit -m "Set up dual-boot for Rails 4.25.0 upgrade"
9998
```
10099

101100
---
102101

103102
## After Upgrade Is Complete
104103

105-
Once you're fully on Rails 6.1, clean up:
104+
Once you're fully on Rails 5.0, clean up:
106105

107106
```ruby
108-
# app/services/log_sanitizer.rb — BEFORE cleanup
109-
if NextRails.next?
110-
filter = ActiveSupport::ParameterFilter.new([:password, :token])
111-
else
112-
filter = ActionDispatch::Http::ParameterFilter.new([:password, :token])
107+
# app/models/project.rb — BEFORE cleanup
108+
class Project < ActiveRecord::Base
109+
if NextRails.next?
110+
self.ignored_columns += [:category]
111+
else
112+
ignore_columns :category
113+
end
113114
end
114115

115-
filter.filter(params)
116-
117-
# app/services/log_sanitizer.rb — AFTER cleanup
118-
filter = ActiveSupport::ParameterFilter.new([:password, :token])
119-
filter.filter(params)
116+
# app/models/project.rb — AFTER cleanup
117+
class Project < ActiveRecord::Base
118+
self.ignored_columns += [:category]
119+
end
120120
```
121121

122122
See `workflows/cleanup-workflow.md` for the full cleanup process.

dual-boot/references/code-patterns.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ Use `NextRails.next?` anywhere your application code must behave differently bet
88

99
---
1010

11-
## Rails API Removals
11+
## Rails API Changes Requiring a Conditional
1212

13-
### `ActionDispatch::Http::ParameterFilter` removed in Rails 6.1
13+
### `ignorable` gem → native `ignored_columns=` (Rails 4.2 → 5.0)
1414

15-
Under Rails 6.1 the old constant raises `NameError`; under Rails 6.0 the new one does not yet exist. A conditional is required during dual-boot.
15+
An app using the [`ignorable`](https://github.com/nthj/ignorable) gem to ignore columns on Rails 4.2 hits a genuine two-sided case when upgrading to 5.0, which introduced a native `ignored_columns=` with different syntax. If the gem is dropped on the 5.0 side (since Rails now has the feature), `ignore_columns :category` raises `NoMethodError` there; `self.ignored_columns += [:category]` raises `NoMethodError` on 4.2 where the setter doesn't exist yet.
1616

1717
```ruby
18-
# app/services/log_sanitizer.rb
19-
if NextRails.next?
20-
filter = ActiveSupport::ParameterFilter.new([:password, :token])
21-
filter.filter(params)
22-
else
23-
filter = ActionDispatch::Http::ParameterFilter.new([:password, :token])
24-
filter.filter(params)
18+
# app/models/project.rb
19+
class Project < ActiveRecord::Base
20+
if NextRails.next?
21+
self.ignored_columns += [:category]
22+
else
23+
ignore_columns :category
24+
end
2525
end
2626
```
2727

@@ -49,7 +49,7 @@ end
4949
config.fixture_paths = ["#{::Rails.root}/spec/fixtures"]
5050
```
5151

52-
Same rule applies to `serialize :preferences, coder: JSON` vs. the older positional form, `update` vs. `update_attributes` (prior to 6.1 removal), and most Rails API evolution: if both versions accept the new form, migrate call sites directly and skip the conditional.
52+
Same rule applies to `serialize :preferences, coder: JSON` vs. the older positional form, `update` vs. `update_attributes` (prior to the 6.1 removal), and most Rails API evolution: if both versions accept the new form, migrate call sites directly and skip the conditional.
5353

5454
---
5555

dual-boot/workflows/cleanup-workflow.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,20 @@ For each file found in Step 1, keep only the `NextRails.next?` (true) branch and
2828

2929
### Before:
3030
```ruby
31-
if NextRails.next?
32-
config.fixture_paths = ["#{::Rails.root}/spec/fixtures"]
33-
else
34-
config.fixture_path = "#{::Rails.root}/spec/fixtures"
31+
class Project < ActiveRecord::Base
32+
if NextRails.next?
33+
self.ignored_columns += [:category]
34+
else
35+
ignore_columns :category
36+
end
3537
end
3638
```
3739

3840
### After:
3941
```ruby
40-
config.fixture_paths = ["#{::Rails.root}/spec/fixtures"]
42+
class Project < ActiveRecord::Base
43+
self.ignored_columns += [:category]
44+
end
4145
```
4246

4347
---

0 commit comments

Comments
 (0)