rails_preflight 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 02fcbcfa3f7d98bbc550334fcf9bd65919fbb2eb5266088823b87c0fd7465ae3
4
+ data.tar.gz: 42c66fa2d33b67e47fed5a36a7127eba476c34b57705d0f3c2591dea9fa1f5eb
5
+ SHA512:
6
+ metadata.gz: ab95dfb109b6fee6cd50d1014792f85d4e941ba7871734766b834844bea2a2bfb15e526e426f7a19a97be7abd58339141bfea037365d9e0b34a3e6f5063b8226
7
+ data.tar.gz: 1606dfaa001672ed19d45469592f212979228c2efb6500b9e7f8b8017d9977817e1814b25045f79b3ada23a9d3d9fe32b09fbe71e74c3b606e5e69e761ea7731
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-09)
4
+
5
+ First release. Point it at a Rails 5.0 – 8.1 app and a target version: it writes a report into the app as HTML, Markdown or JSON, plus a short terminal summary. Read-only and static.
6
+
7
+ - Upgrade path with one step per Rails minor, the Ruby each step needs, and which step to upgrade Ruby on
8
+ - 118 deprecation rules from the release notes, 5.0 to 8.1, each cited, scanning `.rb`, `.erb`, `.haml`, `.slim` and `.rake` files, config YAML, and rake tasks in scripts and CI; APIs already removed from the current Rails are listed first as already broken
9
+ - Gem checks: private gems from `Gemfile.lock` and the Gemfile's sources; locked gems whose declared Rails requirement caps the upgrade; limits no gemspec declares, which Rails or the gem checks when it loads (database adapters, listen, capybara, selenium-webdriver, redis, google-cloud-storage, bullet); gems the rails gem stops pulling in; a curated list of retired gems; and, online, gems with no release since the target Rails shipped
10
+ - Ruby and Node end-of-life, Docker runtime risks, `config.load_defaults`, and schema charset and integer IDs from `db/schema.rb` (an app on `db/structure.sql` gets a note that these were skipped)
11
+ - HTML report: verdict, counts, route, plan table, one card per step with files and lines, and what the report can't see; dark mode, print styles, no network requests
12
+ - `--format markdown`: a checklist per step for issues, PRs and coding agents
13
+ - `--format json`: every finding with its parts (`"schema": 1`, may change before 1.0)
14
+ - `--stdout` prints the report instead of writing it, for pipes and coding agents
15
+ - `--fail-on blockers|broken` sets the exit code, so CI can gate on it
16
+ - `--offline` for a fully static run; private gem names are never sent
17
+ - Report snippets hide quoted values on lines that name a secret, token or password
18
+ - Checked against real Rails upgrades in Mastodon, Discourse, Forem, Redmine, Coursemology and Fat Free CRM
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Syed Aslam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # RailsPreFlight 🛡️
2
+
3
+ **Assess Rails upgrade risk before you touch a single line of code.**
4
+
5
+ `rails-preflight` is a static analysis tool that scans legacy Rails applications and produces a **human-readable upgrade risk report**.
6
+
7
+ It helps teams understand:
8
+
9
+ - what will block a Rails upgrade
10
+ - what is easy vs painful to fix
11
+ - where hidden dependency and infrastructure risks exist
12
+
13
+ This tool is designed for **planning and scoping**, not automated migration.
14
+
15
+ <picture>
16
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/report-overview-dark.png">
17
+ <img alt="The report for Mastodon v3.3.0, Rails 5.2.4.4 to 7.0: verdict, counts, the route through 6.0, 6.1 and 7.0 with blockers at each, and the plan table" src="docs/images/report-overview-light.png">
18
+ </picture>
19
+
20
+ <sub>Mastodon v3.3.0, an open-source app, checked for Rails 7.0. <a href="https://aslam.github.io/rails-preflight/sample-report.html">See the full report</a>.</sub>
21
+
22
+ ## Why this exists
23
+
24
+ Rails upgrades fail not because teams can’t write code, but because they underestimate risk:
25
+
26
+ - private gems with unknown compatibility
27
+ - subtle framework deprecations
28
+ - Ruby and OS lifecycle mismatches
29
+ - Docker runtime issues that surface late
30
+
31
+ `rails-preflight` makes these risks visible before you start upgrading.
32
+
33
+ Think of it as **upgrade reconnaissance**, not a fixer. Run it before the upgrade starts, whoever does it: your team, a consultant or a coding agent.
34
+
35
+ ## What this tool does
36
+
37
+ The audit analyzes your project for common Rails upgrade risk factors:
38
+
39
+ - Ruby & Rails compatibility, and which upgrade step to move Ruby on
40
+ - End-of-life Ruby and Node versions
41
+ - Private / internal gem dependencies
42
+ - Locked gems whose declared Rails requirement caps the upgrade, plus a curated list of gems with known limits
43
+ - Removed and deprecated Rails APIs in code, config YAML, rake tasks in scripts and CI, including ones already removed from your current Rails
44
+ - Docker runtime risks (EOL base image, locale, tzdata, OpenSSL mismatch)
45
+ - Database schema risks (charset, integer IDs)
46
+ - Missing or outdated `config.load_defaults`
47
+
48
+ The output is a **single HTML report** designed to be:
49
+
50
+ - readable by engineers
51
+ - understandable by EMs and tech leads
52
+ - usable in upgrade planning discussions
53
+
54
+ The same findings also come as **Markdown**, a checklist for issues, PRs and coding agents, and as **JSON** for CI and other tools. See [Output formats](#output-formats).
55
+
56
+ ## What this tool intentionally does NOT do
57
+
58
+ - ❌ It does not modify your code
59
+ - ❌ It does not auto-fix deprecations
60
+ - ❌ It does not guarantee upgrade success
61
+ - ❌ It is not a replacement for running tests or CI
62
+
63
+ If you’re looking for a “one-click upgrade,” this is not that tool.
64
+
65
+ ## Related tools
66
+
67
+ These cover what `rails-preflight` leaves out, and pair well with it:
68
+
69
+ - [next_rails](https://github.com/fastruby/next_rails) or [RailsBump](https://railsbump.org): which gem versions work with your target Rails
70
+ - [Brakeman](https://brakemanscanner.org): security issues
71
+ - [rubocop-rails](https://github.com/rubocop/rubocop-rails): autofixes for many deprecations
72
+
73
+ ## Who this is for
74
+
75
+ This tool is especially useful if you are:
76
+
77
+ - Upgrading a Rails 5.0 or newer application (Rails 3 and 4 aren't covered)
78
+ - Planning a security-driven upgrade
79
+ - Scoping an upgrade before committing resources
80
+ - Auditing multiple legacy Rails apps
81
+ - A consultant or staff engineer responsible for upgrade strategy
82
+
83
+ ## Installation
84
+
85
+ Install it globally and point it at your app. It only reads files, so the app can be on any Ruby version, including the old one your servers run. The machine running it needs Ruby 2.7 or newer.
86
+
87
+ ```bash
88
+ gem install rails_preflight
89
+ rails-preflight /path/to/your/app
90
+ ```
91
+
92
+ With no target, it reports on the next Rails minor after the app's (5.2 → 6.0, 7.1 → 7.2), the step Rails recommends taking next. Pass a target to plan a bigger jump: `rails-preflight 8.1 /path/to/your/app`. Leave out the path to audit the current directory.
93
+
94
+ Private gems come from `Gemfile.lock` and the Gemfile's `source` blocks, which are read, never run. Only when neither says where a gem comes from does the tool ask rubygems.org. It also asks rubygems.org when each public gem that depends on Rails last had a release, and lists the ones with nothing newer than the target Rails: nothing says they break, but nobody may have tried. Private gem names are never sent. Pass `--offline` for a fully static run; unplaced gems are then reported as unchecked, and release dates are skipped.
95
+
96
+ It knows Rails 5.0 to 8.1 (`database/compatibility.yml`); the default target is only as current as that list.
97
+
98
+ The tool will analyze:
99
+
100
+ - `Gemfile` and `Gemfile.lock`
101
+ - `.ruby-version`
102
+ - `Dockerfile` (if present)
103
+ - `db/schema.rb` (if present; an app on `db/structure.sql` gets a note that the schema checks were skipped)
104
+ - `config/application.rb` and `config/**/*.yml`
105
+ - `.rb`, `.erb`, `.haml`, `.slim` and `.rake` files under `app/`, `config/`, `db/`, `lib/`, `test/` and `spec/`
106
+ - Scripts and CI config (`bin/`, `script/`, Procfiles, Makefiles, CI workflows) for removed rake tasks
107
+
108
+ And generate:
109
+
110
+ ```
111
+ rails_preflight_report.html
112
+ ```
113
+
114
+ Or `rails_preflight_report.md` or `.json` with `--format markdown` or `--format json`: see [Output formats](#output-formats).
115
+
116
+ ## Output formats
117
+
118
+ Every format renders the same findings. Each finding carries its parts, not only a sentence: the gem, its locked version and the requirement it fails, or the rule with every file, line and matched line, plus the release notes, README or Rails source it rests on.
119
+
120
+ | Format | Flag | Written to, in the app | For |
121
+ |---|---|---|---|
122
+ | HTML | (default) | `rails_preflight_report.html` | reading, planning, sharing |
123
+ | Markdown | `--format markdown` | `rails_preflight_report.md` | issues, PRs, coding agents |
124
+ | JSON | `--format json` | `rails_preflight_report.json` | CI and other tools |
125
+
126
+ The report is the only file the tool writes. Add `--stdout` to print it instead, for pipes and coding agents: no file is written, and progress goes to stderr so stdout holds only the report. Every run also prints a short summary (counts, then each blocker) for SSH sessions and CI logs.
127
+
128
+ ### HTML
129
+
130
+ One self-contained file. It makes no network requests when opened, follows the OS dark mode, and prints on A4. [Sample: Mastodon v3.3.0 to Rails 7.0](https://aslam.github.io/rails-preflight/sample-report.html).
131
+
132
+ 1. **Title and verdict**: `app: Rails current to target`, then one sentence: how many steps, when to upgrade Ruby, how many blockers.
133
+
134
+ 2. **Counts**, each linked to where the findings are:
135
+ * **Already broken**: APIs removed before your current Rails, or gems past their last supported Rails. That code fails when it runs, or never runs.
136
+ * **Blockers**: must be fixed before the upgrade can work (e.g. Ruby too old, removed APIs still in use).
137
+ * **To fix**: will warn or break along the way (deprecations, lagging config), split into now and later versions.
138
+ * **Couldn't check**: what the tool could not verify (private gems, missing files), to review by hand.
139
+
140
+ 3. **Route and plan**: one stop per Rails minor, since Rails recommends upgrading one at a time, with the blockers at each. Later versions past the target are faded and count the code they remove, found in the same scan. A table gives each step's work, the Ruby it needs and a relative effort: the hardest single fix in it (low, medium or high, set per check), not the amount of work. The occurrence counts on each finding show that.
141
+
142
+ 4. **Steps**: "Before you start" for findings that don't belong to a step, then one card per step with its Ruby range and the removed APIs and gems to fix for it. Each finding links to the Rails release notes or the gem's source; its occurrence line expands to file, line and snippet, with app and test occurrences counted apart.
143
+
144
+ 5. **Ahead of the target**: code that later Rails versions remove, by version. Not needed now.
145
+
146
+ 6. **What this report can't see**: private gems and quiet gems by name, behavior changes, multi-line code and test coverage, with what to do instead.
147
+
148
+ 7. **Footer**: the confidence of each section (read from project files, pattern matches, or a key input missing) and where the findings come from.
149
+
150
+ <picture>
151
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/report-step-dark.png">
152
+ <img alt="The 6.1 to 7.0 step card: four gem blockers, then removed APIs with their files, line numbers and matched lines expanded" src="docs/images/report-step-light.png">
153
+ </picture>
154
+
155
+ ### Markdown
156
+
157
+ `--format markdown` writes `rails_preflight_report.md` into the app. A coding agent can read it from stdout instead:
158
+
159
+ ```bash
160
+ rails-preflight --format markdown --stdout 7.2 /path/to/your/app
161
+ ```
162
+
163
+ It's a checklist per step, with `file:line` and the matched line under each finding. It pastes into an issue or PR as is, and opens with a note telling a coding agent to take one step at a time, run the tests after each, and leave "Ahead" and "Couldn't check" alone. From the same Mastodon run ([full sample](docs/sample-report.md)):
164
+
165
+ ```markdown
166
+ ## Step 3: 6.1 to 7.0
167
+
168
+ Needs Ruby 2.7.0–3.2: 2.7.2 works.
169
+
170
+ - [ ] **Blocker:** annotate 3.1.1 requires activerecord >= 3.2, < 7.0, so it doesn't install on Rails 7.0. Upgrade it to a release that allows 7.0, or replace it.
171
+ - [ ] **Blocker:** `ActionMailer::DeliveryJob` and `ActionMailer::Parameterized::DeliveryJob` were removed in Rails 7.0. Mail is delivered by `ActionMailer::MailDeliveryJob`. ([source](https://guides.rubyonrails.org/7_0_release_notes.html#action-mailer-removals))
172
+ - `config/initializers/delivery_job.rb:1` `ActionMailer::DeliveryJob.class_eval do`
173
+ - `spec/models/user_spec.rb:178` `expect { user.send_confirmation_instructions }.to have_enqueued_job(ActionMailer::DeliveryJob)`
174
+ ```
175
+
176
+ ### JSON
177
+
178
+ `--format json` writes `rails_preflight_report.json` into the app, for CI and other tools. With `--stdout` it can be piped:
179
+
180
+ ```bash
181
+ rails-preflight --format json --stdout 7.2 /path/to/your/app | jq '.counts'
182
+ ```
183
+
184
+ The top level holds `schema`, `tool`, `app`, `current_rails`, `target_rails`, `ruby`, `offline`, `verdict` and `counts`, then the findings in `before` (before the first step), `steps` (each with `from`, `version`, its Ruby note and `findings`), `ahead` (keyed by the Rails version that removes them) and `cant_see`. There's no timestamp, so the same app gives the same output. Schema 1 may still change before 1.0; the number goes up when it does. A step from the same run, shortened ([full sample](docs/sample-report.json)):
185
+
186
+ ```json
187
+ {
188
+ "from": "6.1",
189
+ "version": "7.0",
190
+ "findings": [
191
+ {
192
+ "section": "Gem Compatibility",
193
+ "kind": "blocker",
194
+ "message": "annotate 3.1.1 requires activerecord >= 3.2, < 7.0, so it doesn't install on Rails 7.0. ...",
195
+ "removed_in": "7.0",
196
+ "fix_effort": "medium",
197
+ "gem": "annotate",
198
+ "version": "3.1.1",
199
+ "requires": "activerecord >= 3.2, < 7.0"
200
+ },
201
+ {
202
+ "section": "Deprecation Warnings",
203
+ "kind": "blocker",
204
+ "message": "'ActiveModel::Errors#keys', '#values', '#to_h', '#slice!' and '#to_xml' were removed in Rails 7.0. ...",
205
+ "rule": "errors_hash_methods",
206
+ "removed_in": "7.0",
207
+ "fix_effort": "low",
208
+ "confidence": "medium",
209
+ "source": "https://guides.rubyonrails.org/7_0_release_notes.html#active-model-removals",
210
+ "files": [
211
+ { "file": "lib/mastodon/accounts_cli.rb", "line": 106, "snippet": "user.errors.to_h.each do |key, error|", "test": false }
212
+ ]
213
+ }
214
+ ]
215
+ }
216
+ ```
217
+
218
+ `kind` is `broken`, `blocker`, `to_fix`, `tip` or `unknown`. A finding has only the parts that apply to it: gem findings have `gem`, `version` and `requires`; code findings have `rule`, `confidence` and `files`; what the report couldn't check has `names`.
219
+
220
+ ### Gating CI
221
+
222
+ `--fail-on blockers` exits 1 when anything blocks the upgrade or is already broken; `--fail-on broken` only when something is already broken. It works with any format. In CI, `--offline` keeps the run fully static:
223
+
224
+ ```bash
225
+ rails-preflight --offline --format json --fail-on blockers 7.2
226
+ ```
227
+
228
+ ## Roadmap
229
+
230
+ For a detailed list of current features and future plans, please see [ROADMAP.md](ROADMAP.md).
231
+
232
+ ## Philosophy
233
+
234
+ This tool favors:
235
+
236
+ - clarity over completeness
237
+ - honesty over false confidence
238
+ - planning support over automation
239
+
240
+ If it helps you avoid one failed upgrade attempt, it has done its job.
241
+
242
+ ## Development
243
+
244
+ ```bash
245
+ bundle install
246
+ bundle exec rake
247
+ ```
248
+
249
+ CI runs the suite on every Ruby from 2.7 to 4.0.
250
+
251
+ ## License
252
+
253
+ MIT
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # No bundler/setup on purpose: it would load the Gemfile in the current directory,
5
+ # which is usually the app being audited, often pinned to an old Ruby.
6
+ lib = File.expand_path("../lib", __dir__)
7
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
+
9
+ require 'rails_preflight'
10
+
11
+ if %w[-h --help].include?(ARGV[0])
12
+ puts "Usage: rails-preflight [--offline] [--format html|markdown|json] [--stdout] [--fail-on blockers|broken] [TARGET_RAILS_VERSION] [APP_PATH]"
13
+ puts "Example: rails-preflight 7.2 ~/code/my_app"
14
+ puts "TARGET defaults to the next Rails minor after the app's; APP_PATH to the current directory."
15
+ puts "--offline: don't call rubygems.org; gems the lockfile can't place are reported as unchecked, and gem release dates are skipped."
16
+ puts "--format markdown|json: write rails_preflight_report.md or .json into the app instead of the HTML report."
17
+ puts "--stdout: print the report instead of writing it (progress goes to stderr), for pipes and coding agents."
18
+ puts "--fail-on blockers: exit 1 when anything blocks the upgrade or is already broken. --fail-on broken: only when something is already broken."
19
+ exit
20
+ end
21
+
22
+ offline = !ARGV.delete("--offline").nil?
23
+ stdout = !ARGV.delete("--stdout").nil?
24
+ format = :html
25
+ if (i = ARGV.index("--format"))
26
+ format = ARGV.slice!(i, 2)[1].to_s.to_sym
27
+ abort "--format must be html, markdown or json (got #{format})." unless %i[html markdown json].include?(format)
28
+ end
29
+ fail_on = nil
30
+ if (i = ARGV.index("--fail-on"))
31
+ fail_on = ARGV.slice!(i, 2)[1].to_s.to_sym
32
+ abort "--fail-on must be blockers or broken (got #{fail_on})." unless %i[blockers broken].include?(fail_on)
33
+ end
34
+
35
+ # The target is optional, so a lone argument that isn't a version is the app path.
36
+ target = ARGV.shift if ARGV[0] && (Gem::Version.correct?(ARGV[0]) || !File.directory?(ARGV[0]))
37
+ abort "Target must be a Rails version such as 7.2 (got #{target.inspect})." if target && !Gem::Version.correct?(target)
38
+ project_path = ARGV[0] || Dir.pwd
39
+
40
+ begin
41
+ summary = RailsPreflight::UpgradeAnalyzer.new(target, project_path, offline: offline, format: format, stdout: stdout).run
42
+ rescue RailsPreflight::Error => e
43
+ abort e.message
44
+ end
45
+
46
+ # Already broken is worse than blocked, so --fail-on blockers counts it too.
47
+ failing = { blockers: %i[broken blockers], broken: %i[broken] }.fetch(fail_on, []).sum { |key| summary&.fetch(key)&.size.to_i }
48
+ exit 1 if failing.positive?
@@ -0,0 +1,59 @@
1
+ # database/compatibility.yml
2
+ # released: the X.Y.0 release date on rubygems.org.
3
+ rails_versions:
4
+ "5.0":
5
+ released: "2016-06-30"
6
+ required_ruby: ">= 2.2.2"
7
+ max_ruby: "2.4.99"
8
+ "5.1":
9
+ released: "2017-04-27"
10
+ required_ruby: ">= 2.2.2"
11
+ max_ruby: "2.5.99"
12
+ "5.2":
13
+ released: "2018-04-09"
14
+ required_ruby: ">= 2.2.2"
15
+ max_ruby: "2.6.99"
16
+ # Note: Rails 5.2.8.1 supports up to 2.7, but 3.0 is a hard break.
17
+ "6.0":
18
+ released: "2019-08-16"
19
+ required_ruby: ">= 2.5.0"
20
+ max_ruby: "2.7.99"
21
+ "6.1":
22
+ released: "2020-12-09"
23
+ required_ruby: ">= 2.5.0"
24
+ max_ruby: "3.0.99"
25
+ "7.0":
26
+ released: "2021-12-15"
27
+ required_ruby: ">= 2.7.0"
28
+ max_ruby: "3.2.99"
29
+ "7.1":
30
+ released: "2023-10-05"
31
+ required_ruby: ">= 2.7.0"
32
+ max_ruby: "3.4.99"
33
+ "7.2":
34
+ released: "2024-08-09"
35
+ required_ruby: ">= 3.1.0"
36
+ max_ruby: "3.4.99"
37
+ # Rails CI treats Ruby 3.4+ as allowed-to-fail on 7.x; 3.4 per FastRuby's compatibility table.
38
+ "8.0":
39
+ released: "2024-11-07"
40
+ required_ruby: ">= 3.2.0"
41
+ max_ruby: "4.0.99"
42
+ "8.1":
43
+ released: "2025-10-22"
44
+ required_ruby: ">= 3.2.0"
45
+ max_ruby: "4.0.99"
46
+ # Rails CI gates 8.x on Ruby 4.0 (rails/buildkite-config).
47
+
48
+ # Schema checks, applied when the target Rails is at or past `since`.
49
+ database_rules:
50
+ - name: "utf8_charset"
51
+ message: "Legacy utf8mb3 charset detected. Upgrade to utf8mb4 for Rails 6+."
52
+ pattern: "utf8mb3"
53
+ type: "warning"
54
+ since: "6.0"
55
+ - name: "integer_ids"
56
+ message: "Integer IDs detected. Rails 5.1+ defaults to bigint."
57
+ pattern: "id: :integer"
58
+ type: "warning"
59
+ since: "5.1"