diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e645d0a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,13 @@ +{ + "name": "patches", + "dockerComposeFile": "../docker-compose.yml", + "service": "app", + "workspaceFolder": "/app", + "overrideCommand": true, + "postCreateCommand": "bundle install", + "customizations": { + "vscode": { + "extensions": ["Shopify.ruby-lsp"] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d1e5f73 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +# Gemfile.lock is not committed, so copying a locally-resolved one would pin the +# image to whatever the host last used. .git is deliberately NOT excluded: the +# gemspec builds spec.files from `git ls-files`. +Gemfile.lock +.bundle +vendor +coverage +test.db +*.gem diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 0ec8514..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Publish - -on: - release: - types: [published] - -jobs: - build: - name: Build + Publish - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v3 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: '2.7' - - - name: Publish to RubyGems - run: | - gem build *.gemspec - gem push *.gem - env: - GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6dcbac..52e75ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,6 @@ on: permissions: contents: write # rake release (via rubygems/release-gem) pushes the tag; no-op here since it's already pushed id-token: write # RubyGems.org OIDC trusted publishing - packages: write # GitHub Packages push jobs: release: diff --git a/.github/workflows/specs.yml b/.github/workflows/specs.yml index 2f7763a..7181eda 100644 --- a/.github/workflows/specs.yml +++ b/.github/workflows/specs.yml @@ -98,6 +98,10 @@ jobs: - name: Run specs run: bundle exec rspec + # lib/patches/version.rb is excluded from the coverage requirement, not + # from analysis: patches.gemspec requires it, so Bundler loads it under + # `bundle exec` before spec_helper starts SimpleCov, and its lines can + # never be recorded as covered. - name: SonarQube Scan uses: sonarsource/sonarqube-scan-action@master with: @@ -108,6 +112,7 @@ jobs: -Dsonar.inclusions=**/*.rb -Dsonar.tests=spec/ -Dsonar.ruby.coverage.reportPaths=coverage/.resultset.json + -Dsonar.coverage.exclusions=lib/patches/version.rb -Dsonar.plugins.downloadOnlyRequired=true env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore index 21fb6e5..9570997 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /test.db /*.gem .byebug_history +/vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b833e4..42fc509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,55 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -Converted to GitHub Actions for CI +## [3.7.0] - 2026-09-10 + +Declares dependencies the gem always relied on, and announces what 4.0 is +expected to require. No public API changes and no version constraint moves, so +anything that installed 3.6.x installs this. + +### Added +- Gem metadata (`source_code_uri`, `changelog_uri`, `bug_tracker_uri`, + `documentation_uri`). None were declared, so rubygems.org kept showing values + carried over from older releases - 3.6.3 still lists `source_code_uri` as + `http://github.com/jobready/patches`, from before the org was renamed +- `activerecord` as an explicit runtime dependency, at the same `>= 3.2` floor + as `railties` so nothing currently installable is excluded. `Patches::Patch` + subclasses `ActiveRecord::Base` and `Patches::Base#execute` calls + `ActiveRecord::Base.connection`, but only `railties` had been declared +- `Patches.deprecator`, a gem-owned `ActiveSupport::Deprecation` instance rather + than the singleton Rails 7.1 deprecated and 8.0 removed. Host applications can + silence or redirect it with `Patches.deprecator.behavior = :silence` +- A deprecation warning when `config.use_slack` is set: from 4.0 Patches will + not depend on `slack-notifier`, so only applications using Slack carry it. + Add `gem 'slack-notifier'` to your Gemfile to keep notifications working +- A deprecation warning on `require 'patches/capistrano'`. The Capistrano task + is removed in 4.0; invoke `rake patches:run` from your deployment process + instead. It remains opt-in and loads nothing unless a Capfile requires it +- Deprecation warnings for the two other things 4.0 is expected to require: + Sidekiq 6.3 or newer, which is where `Sidekiq::Job` arrives, and Ruby 3.2 / + Rails 7.2. Those floors are higher than the range CI verifies - Ruby 3.0 and + Rails 7.1 still pass - so anyone below them hears about it a release ahead. + The Ruby and Rails notice runs from a Rails initializer, after the + application's own, so `Patches.deprecator.behavior` can silence it + +### Changed +- The published gem carries only what a consumer needs - 27 files rather than + 42. `Dockerfile`, `docker-compose.yml`, `.devcontainer/`, `.github/`, `bin/`, + `Rakefile`, `RELEASING.md` and a decorative image were being packaged. `lib`, + the install migration, `docs/usage.md` and the top-level docs remain +- The development container works again. It was pinned to `ruby:2.3`, which + cannot satisfy the current Gemfile, and had not been touched since 2018. The + Ruby version is now a build argument and the gem versions are passed through + from the shell, so any cell of the matrix in README.md is reproducible in it. + A `.devcontainer/` referencing the same compose service comes with it, for + VS Code and Codespaces + +### Removed +- `.github/workflows/publish.yml`. It published on `release: published` using a + `GEM_HOST_API_KEY`, had never run in the four years since it was added, and + would now attempt a duplicate push if a GitHub Release were created after a + tag. Releases come from `.github/workflows/release.yml` - see + [RELEASING.md](RELEASING.md) ## [3.6.3] - 2026-09-09 diff --git a/Dockerfile b/Dockerfile index 07ba0e7..061b52f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,12 @@ -FROM ruby:2.3 - -RUN touch ~/.gemrc && echo "gem: --no-ri --no-rdoc" >> ~/.gemrc +# Development container for working on the gem. The Ruby version can be +# overridden to reproduce any cell of the matrix in README.md: +# +# docker compose build --build-arg RUBY_VERSION=3.1 +ARG RUBY_VERSION=3.4 +FROM ruby:${RUBY_VERSION} WORKDIR /app/ -ADD . /app/ +COPY . /app/ RUN bundle install -CMD bundle exec rake -T +CMD ["bundle", "exec", "rspec"] diff --git a/README.md b/README.md index 6282ef4..7189a1f 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,58 @@ # Patches [![Run specs](https://github.com/rdytech/patches/actions/workflows/specs.yml/badge.svg)](https://github.com/rdytech/patches/actions/workflows/specs.yml) -[![Maintainability](https://api.codeclimate.com/v1/badges/39d142050017ffeb2564/maintainability)](https://codeclimate.com/repos/557f93b76956807f81000001/maintainability) -[![Test Coverage](https://api.codeclimate.com/v1/badges/39d142050017ffeb2564/test_coverage)](https://codeclimate.com/repos/557f93b76956807f81000001/test_coverage) [![Gem Version](https://badge.fury.io/rb/patches.svg)](https://badge.fury.io/rb/patches) ![patches](docs/patches.jpg) +A simple gem for one off tasks - the things that do not belong in a schema +migration. Back-filling a column, correcting bad data, kicking off a one-time +import: write it as a class, deploy it, and Patches runs it once and records +that it ran. -A simple gem for one off tasks +## Installation + +Add the gem to your application's Gemfile: + +```ruby +gem 'patches' +``` + +Then install the migration that records which patches have run: + +``` +bundle exec rake patches:install:migrations +bundle exec rake db:migrate +``` + +## Usage + +Generate a patch: + +``` +bundle exec rails g patches:patch PreferenceUpdate +``` + +Fill in `run`: + +```ruby +class PreferenceUpdate < Patches::Base + def run + User.where(preference: nil).update_all(preference: 'default') + end +end +``` -## Version 2.0 +Run the pending ones - each patch runs once, and is recorded in +`patches_patches`: -Please note the breaking change release around deployment. See [docs/usage.md](docs/usage.md) for the full change. +``` +bundle exec rake patches:run +``` -TL;DR You need to manually declare the patches task to run in your deploy.rb +`rake patches:pending` lists what has not run yet. Patches can run inline or in +the background via Sidekiq, across all tenants when you use Apartment, and +report to Slack. See [docs/usage.md](docs/usage.md) for configuration, tenants, +deployment and the Capistrano task. ## Compatibility @@ -107,30 +146,58 @@ combination again, and delete `test.db` when switching between them: the suite creates `patches_patches` only when it is missing, so a database left by an earlier run can mask ordering problems. -## Installation +## Deprecations -Add this line to your application's Gemfile: +3.7.0 introduces no breaking changes. It announces what 4.0 is expected to +require, so the upgrade is uneventful. Each warning names its replacement and +goes through the gem's own deprecator, which a host application can silence or +redirect: ```ruby -gem 'patches' +Patches.deprecator.behavior = :silence # or :raise, :log, a lambda... ``` -And then execute: - $ bundle +| Deprecated | Replacement | +|---|---| +| Relying on Patches to install `slack-notifier` | Add `gem 'slack-notifier'` to your Gemfile if you use `config.use_slack`. From 4.0 only applications that want it carry it | +| `require 'patches/capistrano'` | Invoke `rake patches:run` from your deployment process. The Capistrano task is removed in 4.0 | +| Sidekiq older than 6.3 | Any supported Sidekiq (7.x or 8.x). 4.0 is expected to include `Sidekiq::Job` directly instead of falling back to `Sidekiq::Worker` | +| Ruby older than 3.2, Rails older than 7.2 | Ruby 3.2+ and Rails 7.2+. 4.0 is expected to raise the gemspec floors to these, which are higher than the range CI verifies today | -Or install it yourself as: +Nothing is removed in this release, and the gemspec floors are unchanged, so +anything that installed 3.6.x installs 3.7.0. - $ gem install patches +## Development -## Usage +In VS Code or Codespaces, open the repository in the dev container - it reuses +the `app` service from `docker-compose.yml`, so there is one definition of the +environment rather than two. Otherwise, from a shell: -see [docs/usage.md](docs/usage.md) +``` +docker compose build +docker compose run --rm app +``` -## Development +The container's default command runs the specs. Any cell of the matrix above is +reproducible in it: the Ruby version is a build argument, and the gem versions +are passed through from your shell. The image is built with the default +versions, so a non-default cell has to install them first: ``` -docker-compose build -docker-compose run app bundle exec rspec +docker compose build --build-arg RUBY_VERSION=3.1 + +RAILS_VERSION="~> 7.1.0" docker compose run --rm app \ + sh -c 'bundle install && bundle exec rspec' + +SIDEKIQ_VERSION=none docker compose run --rm app \ + sh -c 'bundle install && bundle exec rspec --exclude-pattern "sidekiq/**/*_spec.rb"' +``` + +Or without Docker, if you have the Ruby you want on your path: + +``` +bundle install +bundle exec rspec ``` To install this gem onto your local machine, run `bundle exec rake install`. @@ -146,4 +213,4 @@ Releases are published by GitHub Actions from a `vX.Y.Z` tag — see 2. Create your feature branch (`git checkout -b feature/my-feature-name`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin feature/my-new-feature`) -5. Create a new Pull Request +5. Create a new Pull Request against `develop` diff --git a/RELEASING.md b/RELEASING.md index 639efdf..88f2dd3 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -35,16 +35,3 @@ version already exists. [Trusted Publishing: adding a publisher](https://guides.rubygems.org/trusted-publishing/adding-a-publisher/). Until this is done, the release will fail while configuring credentials. The gem currently lists a single owner, handle `jr`. -- **GitHub Packages**: the workflow carries a `packages: write` permission, inherited from the - shared workflow, but no step publishes there and nothing is pushed to it. - -## Why this exists - -Six tagged versions never reached RubyGems.org — `2.0.1`, `2.1.0`, `2.2.0`, `2.3.0`, `3.0.0` and -`3.6.1` — and nothing reported the gap. The `3.6.2` entry in `CHANGELOG.md` ("Fixes incorrect -release - tag and published gem back in sync") is the visible trace of the most recent one: -`3.6.1` was tagged and released on GitHub but never published, because -`.github/workflows/publish.yml` was added the same day and has never run. - -`rubygems/release-gem` waits for the new version to appear on RubyGems.org before the job -succeeds, so a release that does not land now fails loudly instead of passing unnoticed. diff --git a/docker-compose.yml b/docker-compose.yml index eb5bdee..9749ff4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,20 @@ -version: '3.2' services: app: - build: . + build: + context: . + args: + RUBY_VERSION: ${RUBY_VERSION:-3.4} volumes: - .:/app - app-gems:/usr/local/bundle + # Passed through from the host so a container run can reproduce a matrix + # cell, e.g. RAILS_VERSION="~> 7.2.0" docker compose run --rm app + environment: + RAILS_VERSION: + SQLITE3_VERSION: + CONCURRENT_RUBY_VERSION: + SIDEKIQ_VERSION: + SIDEKIQ_STRICT_ARGS: volumes: app-gems: diff --git a/docs/usage.md b/docs/usage.md index 7ccc040..dab6e70 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -22,6 +22,10 @@ bundle exec rake db:migrate ## Configuration +> **Note.** From 4.0 Patches will not depend on `slack-notifier`, so only +> applications using Slack carry it. If you set `config.use_slack`, add +> `gem 'slack-notifier'` to your Gemfile. + If you would like to run the patches asynchronously, or would like them to notify your Slack channel when they fail or succeed, you need to set up an initializer to set those options. @@ -113,6 +117,11 @@ bundle exec rake patches:run Patches will only ever run once, patches will run in order of creation date. +### Capistrano + +> **Deprecated.** `patches/capistrano` is removed in 4.0. Invoke +> `rake patches:run` from your deployment process instead. + To run patches on deployment using Capistrano, edit your Capfile and add ```ruby diff --git a/lib/patches.rb b/lib/patches.rb index 1698365..7cf3cc6 100644 --- a/lib/patches.rb +++ b/lib/patches.rb @@ -19,6 +19,7 @@ def self.logger=(log) end end +require "patches/deprecation" require "patches/base" require "patches/config" require "patches/tenant_run_concern" diff --git a/lib/patches/capistrano.rb b/lib/patches/capistrano.rb index f819ff0..480a0b2 100644 --- a/lib/patches/capistrano.rb +++ b/lib/patches/capistrano.rb @@ -1 +1,5 @@ +require 'patches/deprecation' + +Patches.warn_capistrano_support_removed_in_4_0 + load File.expand_path("capistrano/tasks.rake", File.dirname(__FILE__)) diff --git a/lib/patches/config.rb b/lib/patches/config.rb index 4a08776..525f4c0 100644 --- a/lib/patches/config.rb +++ b/lib/patches/config.rb @@ -1,3 +1,5 @@ +require 'patches/deprecation' + module Patches class Config class << self @@ -30,6 +32,18 @@ def initialize @sidekiq_queue = 'default' end + # Warns only for applications that enable Slack, and only the first time + # - an initializer that reloads, or configures twice, should not repeat + # the notice. + def use_slack=(enabled) + if enabled && !@warned_about_slack_notifier + @warned_about_slack_notifier = true + Patches.warn_slack_notifier_dependency_removed_in_4_0 + end + + @use_slack = enabled + end + def sidekiq_options @sidekiq_options ||= { retry: false, queue: sidekiq_queue } end diff --git a/lib/patches/deprecation.rb b/lib/patches/deprecation.rb new file mode 100644 index 0000000..1048f26 --- /dev/null +++ b/lib/patches/deprecation.rb @@ -0,0 +1,48 @@ +require 'active_support/deprecation' + +module Patches + # A gem-owned deprecator rather than ActiveSupport::Deprecation's singleton, + # which Rails 7.1 deprecated and 8.0 removed. Host applications can silence or + # redirect it: Patches.deprecator.behavior = :silence. + def self.deprecator + @deprecator ||= ActiveSupport::Deprecation.new('4.0', 'patches') + end + + # The floors 4.0 is expected to set. Deliberately higher than what CI verifies + # today - Ruby 3.0 and Rails 7.1 still pass - so consumers on those versions + # hear about it a release ahead rather than at upgrade time. + MINIMUM_RUBY_IN_4_0 = '3.2'.freeze + MINIMUM_RAILS_IN_4_0 = '7.2'.freeze + + def self.warn_slack_notifier_dependency_removed_in_4_0 + deprecator.warn( + 'patches will stop depending on slack-notifier in 4.0, so that only ' \ + "applications using Slack carry it. Add gem 'slack-notifier' to your " \ + 'Gemfile to keep notifications working.' + ) + end + + def self.warn_capistrano_support_removed_in_4_0 + deprecator.warn( + 'patches/capistrano is deprecated and will be removed in 4.0. Invoke ' \ + '`rake patches:run` from your deployment process instead.' + ) + end + + def self.warn_about_versions_dropped_in_4_0 + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new(MINIMUM_RUBY_IN_4_0) + deprecator.warn( + "Ruby #{RUBY_VERSION} will not be supported by patches 4.0, which is " \ + "expected to require Ruby >= #{MINIMUM_RUBY_IN_4_0}." + ) + end + + return unless defined?(::Rails::VERSION::STRING) + return if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new(MINIMUM_RAILS_IN_4_0) + + deprecator.warn( + "Rails #{::Rails::VERSION::STRING} will not be supported by patches 4.0, " \ + "which is expected to require Rails >= #{MINIMUM_RAILS_IN_4_0}." + ) + end +end diff --git a/lib/patches/engine.rb b/lib/patches/engine.rb index b74e2df..96cdcd9 100644 --- a/lib/patches/engine.rb +++ b/lib/patches/engine.rb @@ -3,5 +3,14 @@ module Patches class Engine < Rails::Engine isolate_namespace Patches + + # After the application's own initializers, so that a host which sets + # Patches.deprecator.behavior in config/initializers can actually silence + # this. Warning at require time - which happens during Bundler.require, + # before any initializer - meant the documented mechanism could not work. + # It also means Rails::VERSION is always loaded by the time we look. + initializer 'patches.version_deprecations', after: :load_config_initializers do + Patches.warn_about_versions_dropped_in_4_0 + end end end diff --git a/lib/patches/sidekiq_job.rb b/lib/patches/sidekiq_job.rb index 3d6ee00..4ee42ed 100644 --- a/lib/patches/sidekiq_job.rb +++ b/lib/patches/sidekiq_job.rb @@ -1,4 +1,5 @@ require 'sidekiq' +require 'patches/deprecation' module Patches # Sidekiq renamed Sidekiq::Worker to Sidekiq::Job in 6.3 and still ships the @@ -6,6 +7,12 @@ module Patches # workers working on every Sidekiq this gem has ever supported, so the change # needs no version bump from consumers. def self.sidekiq_job_module - defined?(::Sidekiq::Job) ? ::Sidekiq::Job : ::Sidekiq::Worker + return ::Sidekiq::Job if defined?(::Sidekiq::Job) + + deprecator.warn( + 'Sidekiq older than 6.3 does not provide Sidekiq::Job; patches 4.0 is ' \ + 'expected to require it. Upgrade Sidekiq, which supports 7.x and 8.x.' + ) + ::Sidekiq::Worker end end diff --git a/lib/patches/version.rb b/lib/patches/version.rb index 114311d..f47ac48 100644 --- a/lib/patches/version.rb +++ b/lib/patches/version.rb @@ -1,6 +1,6 @@ module Patches MAJOR = 3 - MINOR = 6 - PATCH = 3 + MINOR = 7 + PATCH = 0 VERSION = [MAJOR, MINOR, PATCH].compact.join(".").freeze end diff --git a/patches.gemspec b/patches.gemspec index ca19044..52021fa 100644 --- a/patches.gemspec +++ b/patches.gemspec @@ -14,17 +14,32 @@ Gem::Specification.new do |spec| spec.description = %q{A simple gem for one off tasks for example database patches} spec.homepage = "https://github.com/rdytech/patches" - spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.metadata = { + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{spec.homepage}/blob/v#{Patches::VERSION}/CHANGELOG.md", + "bug_tracker_uri" => "#{spec.homepage}/issues", + "documentation_uri" => "#{spec.homepage}/blob/v#{Patches::VERSION}/docs/usage.md" + } + + # Only what a consumer needs: the library (including the generator templates, + # rake tasks and Capistrano integration), the install migration, and the + # documentation. Development files - Dockerfile, .devcontainer, .github, bin, + # Rakefile, RELEASING.md, specs - stay out of the package. + spec.files = `git ls-files -z lib db/migrate docs/usage.md README.md CHANGELOG.md LICENSE.md`.split("\x0") spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - # Runtime constraints are deliberately unchanged here: raising a minimum can - # block an install that works today, which is a major-version change. - # README.md records the range CI actually verifies. Declaring activerecord - # (Patches::Patch subclasses ActiveRecord::Base) and bounding slack-notifier - # are queued for the next minor. + # Floors stay where they are: raising a minimum can block an install that works + # today, which is a major-version change. README.md records the range CI + # verifies, and 4.0 will raise these to match. spec.add_dependency "railties", ">= 3.2" + + # Declared at the same floor as railties so nothing currently installable is + # excluded. Patches::Patch subclasses ActiveRecord::Base and lib/patches/base.rb + # calls ActiveRecord::Base.connection, but only railties had been declared. + spec.add_dependency "activerecord", ">= 3.2" + spec.add_dependency "slack-notifier" spec.add_development_dependency "bundler", "> 1.8" diff --git a/spec/capistrano_spec.rb b/spec/capistrano_spec.rb new file mode 100644 index 0000000..8472384 --- /dev/null +++ b/spec/capistrano_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'rake' + +# Loaded the way a Capfile loads it - nothing else in the suite requires this +# file, so neither the deprecation nor the task definition was exercised. +describe 'patches/capistrano' do + let(:capistrano_path) { File.expand_path('../lib/patches/capistrano.rb', __dir__) } + + around do |example| + original_application = Rake.application + original_behavior = Patches.deprecator.behavior + Rake.application = Rake::Application.new + @messages = [] + Patches.deprecator.behavior = ->(message, *) { @messages << message } + example.run + ensure + Patches.deprecator.behavior = original_behavior + Rake.application = original_application + end + + it 'warns that the integration is removed in 4.0' do + load capistrano_path + + expect(@messages.join).to include('patches/capistrano is deprecated') + expect(@messages.join).to include('rake patches:run') + end + + it 'defines the deploy task' do + load capistrano_path + + expect(Rake::Task.task_defined?('patches:run')).to be(true) + end +end diff --git a/spec/deprecation_spec.rb b/spec/deprecation_spec.rb new file mode 100644 index 0000000..ebaa359 --- /dev/null +++ b/spec/deprecation_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe 'deprecations announced for 4.0' do + # Collect messages rather than printing them, and restore the behaviour after + # each example so one spec cannot silence another. + around do |example| + original = Patches.deprecator.behavior + @messages = [] + Patches.deprecator.behavior = ->(message, *) { @messages << message } + example.run + ensure + Patches.deprecator.behavior = original + end + + it 'owns its deprecator rather than using the removed ActiveSupport singleton' do + expect(Patches.deprecator).to be_a(ActiveSupport::Deprecation) + expect(Patches.deprecator.deprecation_horizon).to eql('4.0') + end + + describe 'Slack notifications' do + # A fresh configuration each time: the notice fires once per instance, and + # other spec files set use_slack on the shared one, so without this the + # examples pass or fail depending on the random order. + before { Patches::Config.configuration = nil } + after { Patches::Config.configuration = nil } + + it 'warns when they are enabled that slack-notifier becomes the host\'s' do + Patches::Config.configuration.use_slack = true + expect(@messages.join).to include('slack-notifier') + expect(@messages.join).to include("gem 'slack-notifier'") + end + + it 'stays quiet when they are not enabled' do + Patches::Config.configuration.use_slack = false + expect(@messages).to be_empty + end + end + + describe 'Capistrano support' do + it 'warns that it is removed in 4.0, naming the replacement' do + Patches.warn_capistrano_support_removed_in_4_0 + expect(@messages.join).to include('patches/capistrano is deprecated') + expect(@messages.join).to include('rake patches:run') + end + end + + describe 'when the notice runs' do + # It used to fire at require time, during Bundler.require, which is before + # config/initializers - so the documented silencing could never apply. + let(:initializer) do + Patches::Engine.initializers.find { |i| i.name == 'patches.version_deprecations' } + end + + it 'is deferred to a Rails initializer, after the application ones' do + expect(initializer).not_to be_nil + expect(initializer.after).to eql(:load_config_initializers) + end + + it 'warns when that initializer runs' do + stub_const('RUBY_VERSION', '3.1.4') + stub_const('Rails::VERSION::STRING', '8.1.0') + + initializer.block.call + + expect(@messages.join).to include('Ruby 3.1.4 will not be supported by patches 4.0') + end + + it 'does not warn merely by requiring the gem' do + messages = [] + Patches.deprecator.behavior = ->(message, *) { messages << message } + + load File.expand_path('../lib/patches.rb', __dir__) + + expect(messages).to be_empty + end + end + + describe 'version support' do + # Both dimensions are stubbed in every example: the suite itself runs on + # Rubies either side of the announced floor, so leaving one to the ambient + # version makes these pass or fail by cell. + it 'warns on a Ruby that CI still verifies but 4.0 will drop' do + stub_const('RUBY_VERSION', '3.1.4') + stub_const('Rails::VERSION::STRING', '8.1.0') + + Patches.warn_about_versions_dropped_in_4_0 + + expect(@messages.join).to include('Ruby 3.1.4 will not be supported by patches 4.0') + expect(@messages.join).to include('Ruby >= 3.2') + end + + it 'warns on a Rails that CI still verifies but 4.0 will drop' do + stub_const('RUBY_VERSION', '3.4.0') + stub_const('Rails::VERSION::STRING', '7.1.6') + + Patches.warn_about_versions_dropped_in_4_0 + + expect(@messages.join).to include('Rails 7.1.6 will not be supported by patches 4.0') + expect(@messages.join).to include('Rails >= 7.2') + end + + it 'stays quiet at the announced floors' do + stub_const('RUBY_VERSION', '3.2.0') + stub_const('Rails::VERSION::STRING', '7.2.0') + + Patches.warn_about_versions_dropped_in_4_0 + + expect(@messages).to be_empty + end + + it 'stays quiet above them' do + stub_const('RUBY_VERSION', '4.0.3') + stub_const('Rails::VERSION::STRING', '8.1.0') + + Patches.warn_about_versions_dropped_in_4_0 + + expect(@messages).to be_empty + end + end +end diff --git a/spec/sidekiq/sidekiq_job_spec.rb b/spec/sidekiq/sidekiq_job_spec.rb index e205b2c..edc9a90 100644 --- a/spec/sidekiq/sidekiq_job_spec.rb +++ b/spec/sidekiq/sidekiq_job_spec.rb @@ -8,6 +8,25 @@ it 'falls back to Sidekiq::Worker when Sidekiq::Job is unavailable' do hide_const('Sidekiq::Job') + allow(Patches.deprecator).to receive(:warn) expect(Patches.sidekiq_job_module).to eql(Sidekiq::Worker) end + + it 'announces that 4.0 is expected to require Sidekiq::Job' do + hide_const('Sidekiq::Job') + messages = [] + original = Patches.deprecator.behavior + Patches.deprecator.behavior = ->(message, *) { messages << message } + + Patches.sidekiq_job_module + + expect(messages.join).to include('Sidekiq older than 6.3') + ensure + Patches.deprecator.behavior = original + end + + it 'stays quiet on a Sidekiq that provides Sidekiq::Job' do + expect(Patches.deprecator).not_to receive(:warn) + Patches.sidekiq_job_module + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f5c5ad9..a779c47 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,13 @@ require 'rails/all' require 'active_model' require 'active_record' +# Deprecation notices are asserted in spec/deprecation_spec.rb, which installs +# its own behaviour; silence them elsewhere so the suite output stays readable. +# Set before requiring patches, which emits the Ruby/Rails notice as it loads - +# the matrix includes cells below the floors 4.0 will set. +require 'patches/deprecation' +Patches.deprecator.behavior = :silence + require 'patches' require 'pry' require 'webmock/rspec'