Add deprecations boot to capture load-time deprecations#194
Draft
JuanVqz wants to merge 21 commits into
Draft
Conversation
Capture deprecation warnings emitted while the app loads and eager-loads, the surface the per-example test tracker structurally misses (association / scope / callback declaration warnings that fire when a class body is evaluated). It does not reimplement capture: a gem-shipped runner script drives the existing DeprecationTracker (init_tracker installs the version-correct hooks across Rails 2-8.1 -- Rails.application.deprecators on 7.1+, the ActiveSupport::Deprecation singleton before that, plus KernelWarnTracker), sets a bucket, eager-loads so declaration-time warnings fire while it listens, then after_run writes an ordinary shitlist. BootCapture only builds the boot command and resolves the shitlist path (spec/support convention, ".boot" marker, ".next" for the next bundle).
`deprecations boot` (and `--next boot`) boots the app, runs the BootCapture runner, and reuses print_info to summarize the resulting shitlist. Adds the --output option and help text. A boot failure is reported honestly: the result file is cleared first, and a non-zero exit or missing output prints "Boot did not complete ... NOT a clean result" and exits 1, so a crashing boot is never mistaken for "no deprecations." Selecting the next bundle uses BUNDLE_GEMFILE=Gemfile.next rather than bin/next, which need not exist in every project.
Covers the boot command shape (RAILS_ENV=test, output env, BUNDLE_GEMFILE for --next, no bin/next), the spec/support default paths, and that the shipped runner is valid Ruby that reuses DeprecationTracker and always eager-loads.
Adds a Boot-time deprecations section (what it captures, the two commands, output path) plus a note on when --next applies, and lists the mode in the Features and command sections.
References PR #194 (the predicted next number). Update the link if the PR is assigned a different number when opened.
rails runner fully boots the app before this script loads, so an env with config.eager_load = true has already eager-loaded and the declaration-time warnings fired before the tracker could attach. Re-running eager_load! is blocked by Zeitwerk's @eager_loaded guard, so the capture silently reported "clean". Pass CI= so the stock Rails 7.1+ test env (eager_load = ENV["CI"].present?) does not eager-load, and refuse with a distinct exit code for apps that hardcode it, which the CLI surfaces instead of its generic boot-failure guess.
rails runner finishes initialize! before the runner script loads, and init_tracker appends to the deprecators registered at that moment, so warnings from gem requires and initializers are already gone. The docs and the success message claimed "load-time" coverage the command does not have; the strongest case was "no deprecation warnings at load time" printed on an empty result. Narrow the wording to eager-load and state the out-of-scope surface where each claim is made. Those warnings still print to stderr during this boot, so they are visible, just not collected.
A silenced deprecator (config.active_support.report_deprecations = false, or ActiveSupport::Deprecation.silenced = true) returns early in Reporting#warn before behavior is consulted, so capture recorded nothing and reported a false "clean". A :raise-configured env aborts on the first eager-load warning before after_run. Both defeat capture. Before attaching the collector, un-silence and force :stderr / disallowed :stderr via the collection-level setters, which update the collection's stored options so deprecators a gem/engine registers during eager-load inherit the non-fatal setup instead of the app's :raise. Mirrors init_tracker's deprecators-vs-singleton version fork.
boot_command interpolated the operator-supplied --output path and the gem's RUNNER_PATH into one string passed to system, so a path containing spaces broke the run and --output 'x.json; rm -rf foo' executed. Both surfaced as the misleading "app failed to load / check database.yml". Return [env_hash, *argv] and call system(*command), which bypasses the shell entirely. Add command_display for the logged line, which is rendered but never executed. CI is now unset rather than set to an empty string, so apps testing ENV["CI"] for truthiness also see it absent.
run_boot deleted the output file before running, so a boot that failed or refused left the user with no capture at all, including the good one from last time. Write to a sibling .partial file and rename it into place only after the runner succeeds; clear the partial on both failure paths. Same-directory rename, so the replacement is atomic.
The boot capture writes to spec/support/deprecation_warning.boot.shitlist.json, but info/run/merge hardcoded the test-run path, so the README's claim that you could read it with info was false. --path selects the file to read (overriding --next); --output still selects where boot writes.
Pass the Rails.root-stripping transform_message so the boot shitlist stores project-relative, committable paths (matching the RSpec/Minitest setups), and require tempfile/fileutils in deprecation_tracker so save/diff work under `rails runner` in a slim app where those stdlib files aren't already loaded — previously a NameError that run_boot misreported as a boot failure. The stdlib requirement is covered by a subprocess spec (bare Ruby, no RSpec preloading) that reproduces and guards the crash.
boot no longer silently ignores --pattern (rejects it, pointing to `info --path`). print_info derives the bucket label from the data, so a boot shitlist shows "Source" instead of mislabeling the synthetic "boot" bucket a test file — on both the post-capture summary and a later `info --path`. `run --path` is rejected (its buckets may not be spec files) and a missing shitlist aborts with a message instead of a raw Errno::ENOENT. boot_command declares output_path as an optional kwarg + guard rather than a required kwarg, so the file parses on Ruby 2.0 — the gemspec's floor, which the rest of the lib keeps to (it was the only required kwarg present).
boot_command set only BUNDLE_GEMFILE=Gemfile.next for --next, so the next boot resolved against the current bundle's vendored cache. `next` and gem-next-diff always pair Gemfile.next with BUNDLE_CACHE_PATH=vendor/cache.next; do the same. The current bundle keeps Bundler's defaults (Gemfile, vendor/cache).
run_boot's branching lived inline in exe/deprecations, which no spec loads, so the partial-promotion and cleanup logic was unverified. Move the partial path and the success/failure/refusal classification onto BootCapture and cover them directly. Also switch the runner syntax check to `ruby -c`, since RubyVM::InstructionSequence is MRI-only.
exe/deprecations required rainbow, which the gemspec never declares as a runtime dependency, so the shipped executable raised LoadError on a clean install and only worked where rainbow happened to be present for another reason. The gem already has a dependency-free ANSI wrapper used throughout lib/. Add the underline style it was missing and switch the CLI to it.
`deprecations run` called DeprecationTracker.sanitize_mode, which is defined in deprecation_tracker.rb, but the CLI only required valid_modes/shard_merger/boot_capture. Every invocation of run mode raised NoMethodError before reaching any of its own logic.
No spec loaded the CLI, which is how an undeclared rainbow require and a NoMethodError in run mode both shipped. Execute the real script in a subprocess and cover every mode: info's summary, filtering and labelling, merge, the flag guards, and unknown/missing modes. Drive boot's three outcomes with a stub `bundle` on PATH, so the partial promotion, the preserved previous capture on failure, and the refusal exit code are verified end to end rather than only through boot_result.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
We got a request on our upgrade skill (claude-code_rails-upgrade-skill #113) about apps that don't have a solid test suite. The per-example
DeprecationTrackeronly catches what the tests actually exercise, so on a thin suite a lot of deprecations never show up. I wanted a way to find them regardless of coverage.This is that attempt, built on the same pattern
next_railsalready uses but moved to boot time: it keeps the app from eager-loading on its own, attaches the tracker, and then eager-loads, so every declaration-time warning gets recorded into a shitlist. Living innext_railsmeans it's a shared solution instead of keeping it in the skill only.deprecations bootcaptures the warnings that fire at eager-load time (association, scope, and callback declarations evaluated as the class bodies load), which the per-example tracker never sees.It writes an ordinary shitlist to
spec/support/deprecation_warning.boot.shitlist.jsonand summarizes it the same wayinfodoes.Notes
DeprecationTrackerrather than adding new capture logic (init_tracker, eager-load,after_run). That also means it talks to whichever deprecator the Rails version exposes:Rails.application.deprecatorson 7.1+, and theActiveSupport::Deprecationsingleton before that. So it works from Rails 2 through 8.1.--nextonly gives you the full set once the next bundle already boots (dual boot is green). Before that, eager-load stops at the first breaking change, one at a time, which is the boot-smoke problem rather than this one. When a boot fails or is refused, the command exits non-zero and says why, instead of pretending the app is clean.rainbow, which is no longer a dependency, so it wouldn't even load on a clean install; anddeprecations runblew up with aNoMethodErrorbefore doing any work.