From 410cefef950896ba660c6c6bac053868c910ccdb Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 08:48:05 +1000 Subject: [PATCH 01/10] Raise the floors to Ruby 3.2 and Rails 7.2 The versions 3.7.0 announced. CI now covers every combination the gemspec allows rather than a wider range, so the two are the same set. --- .github/workflows/specs.yml | 15 ++------------- Gemfile | 6 ++---- README.md | 35 ++++++++++++++++------------------- patches.gemspec | 15 ++++++--------- 4 files changed, 26 insertions(+), 45 deletions(-) diff --git a/.github/workflows/specs.yml b/.github/workflows/specs.yml index 7181eda..782b368 100644 --- a/.github/workflows/specs.yml +++ b/.github/workflows/specs.yml @@ -9,14 +9,8 @@ on: - develop jobs: - # This patch release changes no version constraint, so the matrix records the - # range the gem is verified against rather than narrowing what it allows - - # older Ruby and Rails remain permitted, just untested. Rails 6.1/7.0 and - # Ruby 2.7 are left out deliberately: they are long past upstream support, - # and they were the cells that needed harness pins (Rails <= 7.0 pins its - # sqlite3 adapter to `~> 1.4`, Rails < 7.1 predates concurrent-ruby 1.3.5 - # dropping a Logger constant) or a pinned runner image. The Gemfile still - # accepts those overrides for reproducing an old combination locally. + # Every combination the gemspec allows: Ruby 3.2+ against Rails 7.2+. The + # floors and the matrix are the same set now, which they were not before 4.0. specs: name: Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} runs-on: ubuntu-latest @@ -24,14 +18,9 @@ jobs: fail-fast: false matrix: include: - - { ruby: '3.0', rails: '~> 7.1.0' } - - { ruby: '3.1', rails: '~> 7.1.0' } - - { ruby: '3.1', rails: '~> 7.2.0' } - - { ruby: '3.2', rails: '~> 7.1.0' } - { ruby: '3.2', rails: '~> 7.2.0' } - { ruby: '3.2', rails: '~> 8.0.0' } - { ruby: '3.2', rails: '~> 8.1.0' } - - { ruby: '3.3', rails: '~> 7.1.0' } - { ruby: '3.3', rails: '~> 7.2.0' } - { ruby: '3.3', rails: '~> 8.0.0' } - { ruby: '3.3', rails: '~> 8.1.0' } diff --git a/Gemfile b/Gemfile index 207680e..bda2192 100644 --- a/Gemfile +++ b/Gemfile @@ -16,10 +16,8 @@ def version_requirement(name, default = nil) value.empty? ? default : value end -# Unset, this resolves to the newest Rails the running Ruby allows. The gemspec -# floor stays at railties >= 3.2; see Compatibility in README.md for the range -# CI verifies. -gem 'rails', version_requirement('RAILS_VERSION', '>= 7.1') +# Unset, this resolves to the newest Rails the running Ruby allows. +gem 'rails', version_requirement('RAILS_VERSION', '>= 7.2') # Rails pins its sqlite3 adapter: <= 7.0 needs `~> 1.4`, 7.1 accepts either, # 8.0+ needs `>= 2.1`. Unset, bundler picks the newest 2.x. diff --git a/README.md b/README.md index 7189a1f..cb18f80 100644 --- a/README.md +++ b/README.md @@ -56,25 +56,22 @@ deployment and the Capistrano task. ## Compatibility -This release changes no version constraint: the gemspec still requires only -`railties >= 3.2` and sets no Ruby floor, so anything that installed 3.6.2 -installs 3.6.3. The table is what CI verifies, which is narrower than what the -gem permits - older Ruby and Rails are untested here, not blocked. - -| | Rails 7.1 | Rails 7.2 | Rails 8.0 | Rails 8.1 | -|---|---|---|---|---| -| **Ruby 3.0** | ✅ | | | | -| **Ruby 3.1** | ✅ | ✅ | | | -| **Ruby 3.2** | ✅ | ✅ | ✅ | ✅ | -| **Ruby 3.3** | ✅ | ✅ | ✅ | ✅ | -| **Ruby 3.4** | | ✅ | ✅ | ✅ | -| **Ruby 4.0** | | ✅ | ✅ | ✅ | - -A blank cell is a pairing Rails itself does not support. Ruby 2.7 and Rails -6.1/7.0 are deliberately not exercised - they are well past upstream support - -though the gem still installs and, as of this release, passes on them. - -Ruby 4.0 is newer than all of these Rails releases, so those cells record that +| | Minimum | +|---|---| +| Ruby | 3.2 | +| Rails | 7.2 | + +Every combination the gemspec allows is verified on each push and pull request - +the floors and the matrix are the same set, which they were not before 4.0: + +| | Rails 7.2 | Rails 8.0 | Rails 8.1 | +|---|---|---|---| +| **Ruby 3.2** | ✅ | ✅ | ✅ | +| **Ruby 3.3** | ✅ | ✅ | ✅ | +| **Ruby 3.4** | ✅ | ✅ | ✅ | +| **Ruby 4.0** | ✅ | ✅ | ✅ | + +Ruby 4.0 is newer than all three Rails releases, so those cells record that Patches is verified on it, not that Rails claims support for it. ### Sidekiq diff --git a/patches.gemspec b/patches.gemspec index 52021fa..2eca5ce 100644 --- a/patches.gemspec +++ b/patches.gemspec @@ -30,15 +30,12 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - # 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" + # The floors 3.7.0 announced. Every combination CI verifies is at or above + # them; see the table in README.md. + spec.required_ruby_version = ">= 3.2" + + spec.add_dependency "railties", ">= 7.2" + spec.add_dependency "activerecord", ">= 7.2" spec.add_dependency "slack-notifier" From 305b27a24832a9146b71938ba13d7a858df88d02 Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 08:48:43 +1000 Subject: [PATCH 02/10] Remove Capistrano support Deprecated in 3.7.0. docs/usage.md now documents invoking rake patches:run from the deployment process, keeping the Sidekiq restart caveat. --- README.md | 1 - docs/usage.md | 22 +++++++-------------- lib/patches/capistrano.rb | 5 ----- lib/patches/capistrano/tasks.rake | 12 ----------- lib/patches/deprecation.rb | 7 ------- spec/capistrano_spec.rb | 33 ------------------------------- spec/deprecation_spec.rb | 8 -------- 7 files changed, 7 insertions(+), 81 deletions(-) delete mode 100644 lib/patches/capistrano.rb delete mode 100644 lib/patches/capistrano/tasks.rake delete mode 100644 spec/capistrano_spec.rb diff --git a/README.md b/README.md index cb18f80..819d083 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,6 @@ Patches.deprecator.behavior = :silence # or :raise, :log, a lambda... | 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 | diff --git a/docs/usage.md b/docs/usage.md index dab6e70..6b51d83 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -117,26 +117,18 @@ bundle exec rake patches:run Patches will only ever run once, patches will run in order of creation date. -### Capistrano +### Running on deploy -> **Deprecated.** `patches/capistrano` is removed in 4.0. Invoke -> `rake patches:run` from your deployment process instead. +Invoke the rake task from your deployment process, after the application code is +in place: -To run patches on deployment using Capistrano, edit your Capfile and add - -```ruby -require 'patches/capistrano' ``` - -And then in your deploy.rb - -```ruby -after 'last_task_you_want_to_run' 'patches:run' +bundle exec rake patches:run ``` -If you are using sidekiq and restarting the sidekiq process on the box -as a part of the deploy process, please make sure that the patches run task runs -after sidekiq restarts, otherwise there is no guarentee the tasks will run. +If you use Sidekiq and restart it as part of the deploy, make sure the patches +task runs after Sidekiq restarts - otherwise there is no guarantee the jobs will +be picked up. ## File Download diff --git a/lib/patches/capistrano.rb b/lib/patches/capistrano.rb deleted file mode 100644 index 480a0b2..0000000 --- a/lib/patches/capistrano.rb +++ /dev/null @@ -1,5 +0,0 @@ -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/capistrano/tasks.rake b/lib/patches/capistrano/tasks.rake deleted file mode 100644 index 7940795..0000000 --- a/lib/patches/capistrano/tasks.rake +++ /dev/null @@ -1,12 +0,0 @@ -namespace :patches do - desc 'Runs rake patches:run' - task :run do - on primary fetch(:migration_role) do - within release_path do - with rails_env: fetch(:rails_env) do - execute :rake, "patches:run" - end - end - end - end -end diff --git a/lib/patches/deprecation.rb b/lib/patches/deprecation.rb index 1048f26..f8b0f2d 100644 --- a/lib/patches/deprecation.rb +++ b/lib/patches/deprecation.rb @@ -22,13 +22,6 @@ def self.warn_slack_notifier_dependency_removed_in_4_0 ) 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( diff --git a/spec/capistrano_spec.rb b/spec/capistrano_spec.rb deleted file mode 100644 index 8472384..0000000 --- a/spec/capistrano_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -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 index ebaa359..3f4c5b2 100644 --- a/spec/deprecation_spec.rb +++ b/spec/deprecation_spec.rb @@ -36,14 +36,6 @@ 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. From e35c2b2b35bc916417c3d2aee9e770f78fe5684b Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 08:51:30 +1000 Subject: [PATCH 03/10] Make slack-notifier the host application's dependency Deprecated in 3.7.0. Patches::Notifier requires it inside a begin/rescue, so the defined?(Slack) guard it always had now does something. Slack-dependent specs move under spec/slack/ so the suite runs without the gem, mirroring Sidekiq. --- Gemfile | 5 ++++ README.md | 1 - docs/usage.md | 5 ++-- lib/patches/config.rb | 14 ----------- lib/patches/deprecation.rb | 8 ------ lib/patches/notifier.rb | 10 +++++++- patches.gemspec | 4 ++- spec/deprecation_spec.rb | 19 -------------- spec/notifier_spec.rb | 29 ++------------------- spec/slack/notifier_spec.rb | 50 +++++++++++++++++++++++++++++++++++++ spec/slack_optional_spec.rb | 41 ++++++++++++++++++++++++++++++ 11 files changed, 112 insertions(+), 74 deletions(-) create mode 100644 spec/slack/notifier_spec.rb create mode 100644 spec/slack_optional_spec.rb diff --git a/Gemfile b/Gemfile index bda2192..0fcd4ef 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,11 @@ gem 'sqlite3', version_requirement('SQLITE3_VERSION', '>= 1.4') concurrent_ruby = version_requirement('CONCURRENT_RUBY_VERSION') gem 'concurrent-ruby', concurrent_ruby if concurrent_ruby +# Slack notifications are optional too. SLACK_NOTIFIER=none leaves the gem out, +# which exercises the `defined?(Slack)` guard in Patches::Notifier. +slack_notifier = version_requirement('SLACK_NOTIFIER', '') +gem 'slack-notifier' unless slack_notifier == 'none' + # Sidekiq is an optional integration, not a runtime dependency. SIDEKIQ_VERSION # =none leaves it out entirely so the `defined?(Sidekiq)` guards get exercised. # The library itself still supports older Sidekiq through diff --git a/README.md b/README.md index 819d083..76e4249 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,6 @@ Patches.deprecator.behavior = :silence # or :raise, :log, a lambda... | 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 | | 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 | diff --git a/docs/usage.md b/docs/usage.md index 6b51d83..2af6c6d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -22,9 +22,8 @@ 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. +> **Note.** Patches does not depend on `slack-notifier`. If you set +> `config.use_slack`, add `gem 'slack-notifier'` to your own 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 diff --git a/lib/patches/config.rb b/lib/patches/config.rb index 525f4c0..4a08776 100644 --- a/lib/patches/config.rb +++ b/lib/patches/config.rb @@ -1,5 +1,3 @@ -require 'patches/deprecation' - module Patches class Config class << self @@ -32,18 +30,6 @@ 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 index f8b0f2d..d12290d 100644 --- a/lib/patches/deprecation.rb +++ b/lib/patches/deprecation.rb @@ -14,14 +14,6 @@ def self.deprecator 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_about_versions_dropped_in_4_0 if Gem::Version.new(RUBY_VERSION) < Gem::Version.new(MINIMUM_RUBY_IN_4_0) deprecator.warn( diff --git a/lib/patches/notifier.rb b/lib/patches/notifier.rb index f2ec0ef..2cc2739 100644 --- a/lib/patches/notifier.rb +++ b/lib/patches/notifier.rb @@ -1,4 +1,12 @@ -require 'slack-notifier' +# Optional: an application that sets config.use_slack adds slack-notifier to its +# own Gemfile. Patches runs without it, and send_slack_message's `defined?(Slack)` +# guard - which could never be false while this require was unconditional - now +# does its job. +begin + require 'slack-notifier' +rescue LoadError + nil +end class Patches::Notifier class << self diff --git a/patches.gemspec b/patches.gemspec index 2eca5ce..e4f9006 100644 --- a/patches.gemspec +++ b/patches.gemspec @@ -37,7 +37,9 @@ Gem::Specification.new do |spec| spec.add_dependency "railties", ">= 7.2" spec.add_dependency "activerecord", ">= 7.2" - spec.add_dependency "slack-notifier" + # Slack notifications are optional, so the gem no longer installs + # slack-notifier - an application that sets config.use_slack adds it itself. + # Declared in the Gemfile for the suite, the way sidekiq is. spec.add_development_dependency "bundler", "> 1.8" spec.add_development_dependency "rake", "> 10.0" diff --git a/spec/deprecation_spec.rb b/spec/deprecation_spec.rb index 3f4c5b2..32026c9 100644 --- a/spec/deprecation_spec.rb +++ b/spec/deprecation_spec.rb @@ -17,25 +17,6 @@ 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 '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. diff --git a/spec/notifier_spec.rb b/spec/notifier_spec.rb index 5a1f8a5..6297755 100644 --- a/spec/notifier_spec.rb +++ b/spec/notifier_spec.rb @@ -168,11 +168,11 @@ def self.current describe '.send_slack_message' do subject { notifier.send_slack_message(message, options) } - let(:options) { 'good' } + let(:options) { 'good' } before do Patches::Config.configure do |config| - config.use_slack = use_slack + config.use_slack = true config.slack_options = { webhook_url: 'https://example.com', channel: 'deploy-notifications', @@ -181,31 +181,6 @@ def self.current end end - context 'where Slack is defined and use_slack config is true' do - let(:use_slack) { true } - - it 'sends a message to Slack channel' do - expect(Slack::Notifier).to receive(:new) - .with('https://example.com', channel: 'deploy-notifications', username: 'slack-bot') - .and_call_original - - expect_any_instance_of(Slack::Notifier).to receive(:post) - .with({ icon_emoji: ":dog:", attachments: [{ text: 'This is a message', color: 'good' }] }) - - subject - end - end - - context 'use_slack is false' do - let(:use_slack) { false } - - it "doesn't send a message to Slack channel" do - expect(Slack::Notifier).not_to receive(:new) - expect_any_instance_of(Slack::Notifier).not_to receive(:post) - subject - end - end - context 'where Slack is not defined' do let(:use_slack) { true } diff --git a/spec/slack/notifier_spec.rb b/spec/slack/notifier_spec.rb new file mode 100644 index 0000000..2129a3b --- /dev/null +++ b/spec/slack/notifier_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +# Requires slack-notifier, which 4.0 no longer installs - an application that +# sets config.use_slack adds it itself. Kept apart from spec/notifier_spec.rb so +# the suite still runs when the gem is absent. +describe Patches::Notifier do + let(:notifier) { described_class } + let(:message) { 'This is a message' } + let(:options) { 'good' } + + describe '.send_slack_message' do + subject { notifier.send_slack_message(message, options) } + + before do + Patches::Config.configure do |config| + config.use_slack = use_slack + config.slack_options = { + webhook_url: 'https://example.com', + channel: 'deploy-notifications', + username: 'slack-bot' + } + end + end + + context 'where Slack is defined and use_slack config is true' do + let(:use_slack) { true } + + it 'sends a message to Slack channel' do + expect(Slack::Notifier).to receive(:new) + .with('https://example.com', channel: 'deploy-notifications', username: 'slack-bot') + .and_call_original + + expect_any_instance_of(Slack::Notifier).to receive(:post) + .with({ icon_emoji: ":dog:", attachments: [{ text: 'This is a message', color: 'good' }] }) + + subject + end + end + + context 'use_slack is false' do + let(:use_slack) { false } + + it "doesn't send a message to Slack channel" do + expect(Slack::Notifier).not_to receive(:new) + expect_any_instance_of(Slack::Notifier).not_to receive(:post) + subject + end + end + end +end diff --git a/spec/slack_optional_spec.rb b/spec/slack_optional_spec.rb new file mode 100644 index 0000000..53480ca --- /dev/null +++ b/spec/slack_optional_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +# slack-notifier is an optional integration rather than a runtime dependency: +# Patches::Notifier requires it inside a begin/rescue and guards sends with +# `defined?(Slack)`. This spec runs in both CI legs and asserts the contract +# from whichever side applies. +slack_notifier_installed = begin + require 'slack-notifier' + true +rescue LoadError + false +end + +describe 'optional Slack integration' do + before do + Patches::Config.configure do |config| + config.use_slack = true + config.slack_options = { webhook_url: 'https://example.com/hook' } + # Set so notification_suffix does not fall through to Apartment, which + # other spec files define without stubbing Apartment::Tenant.current. + config.notification_suffix = 'spec' + end + end + + after { Patches::Config.configuration = nil } + + if slack_notifier_installed + it 'posts to the webhook when the gem is installed' do + request = stub_request(:post, 'https://example.com/hook') + + Patches::Notifier.notify_success([]) + + expect(request).to have_been_requested + end + else + it 'notifies without raising when the gem is absent' do + expect(defined?(Slack)).to be_nil + expect { Patches::Notifier.notify_success([]) }.not_to raise_error + end + end +end From 9510f42808f5da31ec22755e34709cedb4aa39b8 Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 08:52:03 +1000 Subject: [PATCH 04/10] Include Sidekiq::Job directly Deprecated in 3.7.0. Sidekiq has provided the constant since 6.3, so the Patches.sidekiq_job_module shim and its Sidekiq::Worker fallback go. --- README.md | 15 +++++++++------ lib/patches.rb | 1 - lib/patches/sidekiq_job.rb | 18 ------------------ lib/patches/tenant_worker.rb | 4 ++-- lib/patches/worker.rb | 4 ++-- spec/sidekiq/sidekiq_job_spec.rb | 32 -------------------------------- 6 files changed, 13 insertions(+), 61 deletions(-) delete mode 100644 lib/patches/sidekiq_job.rb delete mode 100644 spec/sidekiq/sidekiq_job_spec.rb diff --git a/README.md b/README.md index 76e4249..46fc2ee 100644 --- a/README.md +++ b/README.md @@ -80,17 +80,21 @@ Sidekiq is an **optional** integration, not a runtime dependency: Patches runs each patch inline when Sidekiq is absent. Set `config.use_sidekiq = true` to run them in the background - see [docs/usage.md](docs/usage.md). -The workers resolve their mixin at load time via `Patches.sidekiq_job_module`, -preferring `Sidekiq::Job` (the name Sidekiq has used since 6.3) and falling back -to `Sidekiq::Worker`, so older Sidekiq keeps working. CI covers each state a -consumer can be in: +The workers include `Sidekiq::Job`, so Sidekiq 6.3 or newer is required when the +integration is used - 6.2 and earlier predate that constant. CI covers each +state a consumer can be in: | State | Covered by | |---|---| | Sidekiq not installed | A leg omitting the gem entirely, exercising the `defined?(Sidekiq)` guards | | Installed, strict arguments on | Sidekiq 7.x and 8.x at their default (`:raise`) | | Installed, strict arguments off | Sidekiq 7.x and 8.x with `Sidekiq.strict_args!(false)` | -| Older Sidekiq | A 6.5 leg, plus a spec covering the `Sidekiq::Worker` fallback | + +### Slack + +Slack notifications are optional and Patches does not install +`slack-notifier` - add `gem 'slack-notifier'` to your own Gemfile if you set +`config.use_slack`. CI runs a leg without it to keep that path honest. ### Running one combination locally @@ -156,7 +160,6 @@ Patches.deprecator.behavior = :silence # or :raise, :log, a lambda... | Deprecated | Replacement | |---|---| -| 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 | Nothing is removed in this release, and the gemspec floors are unchanged, so diff --git a/lib/patches.rb b/lib/patches.rb index 7cf3cc6..f4ade62 100644 --- a/lib/patches.rb +++ b/lib/patches.rb @@ -24,7 +24,6 @@ def self.logger=(log) require "patches/config" require "patches/tenant_run_concern" require "patches/application_version_validation" -require "patches/sidekiq_job" if defined?(Sidekiq) require "patches/tenant_worker" if defined?(Sidekiq) require "patches/engine" if defined?(Rails) require "patches/patch" diff --git a/lib/patches/sidekiq_job.rb b/lib/patches/sidekiq_job.rb deleted file mode 100644 index 4ee42ed..0000000 --- a/lib/patches/sidekiq_job.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'sidekiq' -require 'patches/deprecation' - -module Patches - # Sidekiq renamed Sidekiq::Worker to Sidekiq::Job in 6.3 and still ships the - # old name as a deprecated alias. Resolving it at include time keeps the - # workers working on every Sidekiq this gem has ever supported, so the change - # needs no version bump from consumers. - def self.sidekiq_job_module - 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/tenant_worker.rb b/lib/patches/tenant_worker.rb index f213bed..cd9b95b 100644 --- a/lib/patches/tenant_worker.rb +++ b/lib/patches/tenant_worker.rb @@ -1,8 +1,8 @@ require 'active_support/core_ext/hash/keys' -require 'patches/sidekiq_job' +require 'sidekiq' class Patches::TenantWorker - include Patches.sidekiq_job_module + include Sidekiq::Job include Patches::TenantRunConcern include Patches::ApplicationVersionValidation diff --git a/lib/patches/worker.rb b/lib/patches/worker.rb index 18a6ac1..066152a 100644 --- a/lib/patches/worker.rb +++ b/lib/patches/worker.rb @@ -1,8 +1,8 @@ require 'active_support/core_ext/hash/keys' -require 'patches/sidekiq_job' +require 'sidekiq' class Patches::Worker - include Patches.sidekiq_job_module + include Sidekiq::Job include Patches::ApplicationVersionValidation sidekiq_options Patches::Config.configuration.sidekiq_options diff --git a/spec/sidekiq/sidekiq_job_spec.rb b/spec/sidekiq/sidekiq_job_spec.rb deleted file mode 100644 index edc9a90..0000000 --- a/spec/sidekiq/sidekiq_job_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' -require 'patches/sidekiq_job' - -describe 'Patches.sidekiq_job_module' do - it 'prefers Sidekiq::Job, the name Sidekiq has used since 6.3' do - expect(Patches.sidekiq_job_module).to eql(Sidekiq::Job) - end - - 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 From 3035dc33bdd9ec5a659763658a06f680ef5e10cf Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 08:52:50 +1000 Subject: [PATCH 05/10] Drop the version deprecation warning, keep the deprecator The gemspec floors enforce what the warning announced. Patches.deprecator stays - applications were told to configure it to silence 3.7.0's warnings - now pointing at the next major. --- lib/patches/deprecation.rb | 31 +++------------ lib/patches/engine.rb | 9 ----- spec/deprecation_spec.rb | 81 +++----------------------------------- 3 files changed, 11 insertions(+), 110 deletions(-) diff --git a/lib/patches/deprecation.rb b/lib/patches/deprecation.rb index d12290d..dcb6b3b 100644 --- a/lib/patches/deprecation.rb +++ b/lib/patches/deprecation.rb @@ -2,32 +2,11 @@ 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. + # which Rails 7.1 deprecated and 8.0 removed. Nothing is deprecated in 4.0 - + # everything 3.7.0 announced has been removed - but this stays: applications + # were told to configure it to silence those warnings, so removing it would + # break their initializers. The horizon is the next major. 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_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}." - ) + @deprecator ||= ActiveSupport::Deprecation.new('5.0', 'patches') end end diff --git a/lib/patches/engine.rb b/lib/patches/engine.rb index 96cdcd9..b74e2df 100644 --- a/lib/patches/engine.rb +++ b/lib/patches/engine.rb @@ -3,14 +3,5 @@ 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/spec/deprecation_spec.rb b/spec/deprecation_spec.rb index 32026c9..5959c98 100644 --- a/spec/deprecation_spec.rb +++ b/spec/deprecation_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'deprecations announced for 4.0' do +describe Patches, '.deprecator' do # Collect messages rather than printing them, and restore the behaviour after # each example so one spec cannot silence another. around do |example| @@ -12,82 +12,13 @@ Patches.deprecator.behavior = original end - it 'owns its deprecator rather than using the removed ActiveSupport singleton' do + it 'is gem-owned rather than the ActiveSupport singleton Rails 8 removed' do expect(Patches.deprecator).to be_a(ActiveSupport::Deprecation) - expect(Patches.deprecator.deprecation_horizon).to eql('4.0') + expect(Patches.deprecator.deprecation_horizon).to eql('5.0') 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 + it 'can be silenced by a host application' do + Patches.deprecator.behavior = :silence + expect { Patches.deprecator.warn('anything') }.not_to raise_error end end From eaf874bddab595156405d63ea91e2e636c9260e9 Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 08:54:34 +1000 Subject: [PATCH 06/10] Cover a consumer using neither optional integration The optional axis tested each gem absent but never both, which is the common case for a plain Rails app. Also fixes the exclude logic: --exclude-pattern does not accumulate, so the patterns must be one comma-separated value. --- .github/workflows/specs.yml | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/.github/workflows/specs.yml b/.github/workflows/specs.yml index 782b368..6bb15a4 100644 --- a/.github/workflows/specs.yml +++ b/.github/workflows/specs.yml @@ -41,26 +41,28 @@ jobs: - name: Run specs run: bundle exec rspec - # Sidekiq is optional, so it gets its own axis rather than multiplying the - # matrix above. All three states a consumer can be in are covered: absent, - # present with strict argument checking (the Sidekiq 7+ default), and present - # with it disabled. The 6.5 leg covers the Sidekiq::Worker-era fallback path. - sidekiq: - name: Sidekiq ${{ matrix.sidekiq }} (strict args ${{ matrix.strict_args || 'absent' }}) + # Sidekiq and Slack are optional integrations, so they get their own axis + # rather than multiplying the matrix above. Every state a consumer can be in + # is covered: each gem absent, and Sidekiq present with strict argument + # checking both at its default and disabled. + optional: + name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - - { sidekiq: '~> 6.5' } - - { sidekiq: '~> 7.0', strict_args: 'raise' } - - { sidekiq: '~> 7.0', strict_args: 'false' } - - { sidekiq: '~> 8.0', strict_args: 'raise' } - - { sidekiq: '~> 8.0', strict_args: 'false' } - - { sidekiq: 'none' } + - { name: 'Sidekiq 7 (strict args raise)', sidekiq: '~> 7.0', strict_args: 'raise' } + - { name: 'Sidekiq 7 (strict args false)', sidekiq: '~> 7.0', strict_args: 'false' } + - { name: 'Sidekiq 8 (strict args raise)', sidekiq: '~> 8.0', strict_args: 'raise' } + - { name: 'Sidekiq 8 (strict args false)', sidekiq: '~> 8.0', strict_args: 'false' } + - { name: 'Sidekiq absent', sidekiq: 'none' } + - { name: 'slack-notifier absent', slack_notifier: 'none' } + - { name: 'Neither installed', sidekiq: 'none', slack_notifier: 'none' } env: SIDEKIQ_VERSION: ${{ matrix.sidekiq }} SIDEKIQ_STRICT_ARGS: ${{ matrix.strict_args }} + SLACK_NOTIFIER: ${{ matrix.slack_notifier }} steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 @@ -69,8 +71,15 @@ jobs: bundler-cache: true - name: Run specs run: | - if [ "$SIDEKIQ_VERSION" = "none" ]; then - bundle exec rspec --exclude-pattern "sidekiq/**/*_spec.rb" + # --exclude-pattern does not accumulate; the patterns go in one + # comma-separated value. + patterns="" + [ "$SIDEKIQ_VERSION" = "none" ] && patterns="sidekiq/**/*_spec.rb" + if [ "$SLACK_NOTIFIER" = "none" ]; then + patterns="${patterns:+$patterns,}slack/**/*_spec.rb" + fi + if [ -n "$patterns" ]; then + bundle exec rspec --exclude-pattern "$patterns" else bundle exec rspec fi From 810014c0fd5b849c3e155c54413d68013a932dc7 Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 09:32:59 +1000 Subject: [PATCH 07/10] Require Rails rather than guarding for its absence railties and activerecord are hard dependencies, and the engine, the install migration and default_path all assume a Rails application, so the defined?(Rails) guards described a mode nobody used. One of them read defined?(:Rails) - always truthy - and went unnoticed until 3.6.3, which is proof enough that the path was never exercised. lib/patches.rb now requires rails explicitly: requiring the engine without it fails, because railties expects ActiveSupport's core extensions to be loaded first. Sidekiq and slack-notifier stay conditional - those are genuinely optional. --- lib/patches.rb | 11 +++++++++-- lib/patches/notifier.rb | 2 +- spec/patches_spec.rb | 7 ------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/patches.rb b/lib/patches.rb index f4ade62..e6440c4 100644 --- a/lib/patches.rb +++ b/lib/patches.rb @@ -1,8 +1,15 @@ +# railties and activerecord are hard dependencies, and the engine, the install +# migration and Patches.default_path all assume a Rails application. The gem +# used to guard on `defined?(Rails)` as though Rails were optional; one of those +# guards read `defined?(:Rails)`, which is always truthy, and went unnoticed for +# years - nobody was running this outside Rails. +require 'rails' + require "patches/version" module Patches def self.default_path - Rails.root.join('db/patches/') if defined?(Rails) + Rails.root.join('db/patches/') end def self.class_name(path) @@ -25,7 +32,7 @@ def self.logger=(log) require "patches/tenant_run_concern" require "patches/application_version_validation" require "patches/tenant_worker" if defined?(Sidekiq) -require "patches/engine" if defined?(Rails) +require "patches/engine" require "patches/patch" require "patches/pending" require "patches/runner" diff --git a/lib/patches/notifier.rb b/lib/patches/notifier.rb index 2cc2739..b9bbc57 100644 --- a/lib/patches/notifier.rb +++ b/lib/patches/notifier.rb @@ -36,7 +36,7 @@ def notification_prefix end def environment_prefix - Rails.env.upcase if defined?(Rails) + Rails.env.upcase end def notification_suffix diff --git a/spec/patches_spec.rb b/spec/patches_spec.rb index 8ad6942..488492f 100644 --- a/spec/patches_spec.rb +++ b/spec/patches_spec.rb @@ -10,13 +10,6 @@ expect(Patches.default_path).to eql(Pathname.new('/tmp/db/patches/')) end - it 'has no default path outside Rails' do - # The guard used to read `defined?(:Rails)`, which tests a symbol literal - # and is therefore always true, so this raised NameError instead. - hide_const('Rails') - expect(Patches.default_path).to be_nil - end - context 'class name' do specify { expect(Patches.class_name('201203031234_class_name.rb')).to eql('ClassName') } specify { expect(Patches.class_name('201203031234_.rb')).to be_nil } From a816dad76524e5d148d0e9f9be28ce073ec4bc7d Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 10:41:09 +1000 Subject: [PATCH 08/10] Declare the install migration for Rails 7.2 It said [5.0], which the Rails floor now makes inconsistent. Not only cosmetic: under [5.0] compatibility a fresh table gets datetime columns without precision, where [7.2] gets datetime(6), matching what a modern Rails app's own schema uses. Existing installs are untouched - they ran their own copy of this file long ago. A spec asserts the declared release is no older than the railties floor, so the two cannot drift apart again. --- db/migrate/201506011700_create_patch.rb | 2 +- spec/migration_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/db/migrate/201506011700_create_patch.rb b/db/migrate/201506011700_create_patch.rb index 3a268be..925d192 100644 --- a/db/migrate/201506011700_create_patch.rb +++ b/db/migrate/201506011700_create_patch.rb @@ -1,4 +1,4 @@ -class CreatePatch < ActiveRecord::Migration[5.0] +class CreatePatch < ActiveRecord::Migration[7.2] def change create_table :patches_patches do |t| t.string :path, null: false diff --git a/spec/migration_spec.rb b/spec/migration_spec.rb index 2c0f1a7..1e8ba9d 100644 --- a/spec/migration_spec.rb +++ b/spec/migration_spec.rb @@ -12,6 +12,15 @@ expect(File.read(migration_path)).to match(/ActiveRecord::Migration\[\d+\.\d+\]/) end + it 'declares a release no older than the gemspec floor' do + declared = File.read(migration_path)[/ActiveRecord::Migration\[(\d+\.\d+)\]/, 1] + floor = Gem::Specification.load(File.expand_path('../patches.gemspec', __dir__)) + .dependencies.find { |d| d.name == 'railties' } + .requirement.requirements.first.last.to_s + + expect(Gem::Version.new(declared)).to be >= Gem::Version.new(floor[/\d+\.\d+/]) + end + it 'loads on the installed Rails version' do load migration_path expect(CreatePatch.superclass).to be < ActiveRecord::Migration From c22327da06493d1a6ac761227effd5fc7da3fcf3 Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Thu, 10 Sep 2026 10:21:32 +1000 Subject: [PATCH 09/10] Release 4.0.0 The heading carries `unreleased` until the day it ships; RELEASING.md step 2 now says to replace it with the date then. 3.7.0's entry is corrected to 2026-09-17, the day it merged and published, rather than 2026-09-10 when it was drafted. --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++- README.md | 28 +++++++++++++-------------- RELEASING.md | 5 ++++- lib/patches/version.rb | 4 ++-- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42fc509..17806a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,48 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -## [3.7.0] - 2026-09-10 +## [4.0.0] - unreleased + +Removes everything 3.7.0 deprecated. Nothing here is a surprise if you upgraded +to 3.7.0 and cleared its warnings; see Upgrading from 3.x in README.md. + +### Breaking +- Ruby 3.2 and Rails 7.2 are the minimum, enforced by `required_ruby_version` + and `railties`/`activerecord >= 7.2`. Every combination CI verifies is now at + or above the floors, which was not true before +- `slack-notifier` is no longer a runtime dependency. `Patches::Notifier` + requires it inside a `begin`/`rescue`, so the `defined?(Slack)` guard it always + carried finally does something. Applications that set `config.use_slack` add + `gem 'slack-notifier'` themselves +- The Capistrano integration is removed - `require 'patches/capistrano'` and its + `patches:run` task. Invoke `bundle exec rake patches:run` from your deployment + process instead; `docs/usage.md` shows where +- The workers include `Sidekiq::Job` directly, so Sidekiq 6.3 or newer is + required when the integration is used. `Patches.sidekiq_job_module` and its + `Sidekiq::Worker` fallback are gone +- The install migration declares `ActiveRecord::Migration[7.2]` rather than + `[5.0]`. A fresh `patches_patches` therefore gets `datetime(6)` timestamp + columns, matching what a modern Rails schema uses, where `[5.0]` + compatibility produced `datetime` without precision. Existing installs are + untouched - they ran their own copy of the file long ago +- Rails is required rather than guarded for. `railties` and `activerecord` were + always hard dependencies and the gem ships an engine, so the `defined?(Rails)` + checks described a mode nobody used - one of them read `defined?(:Rails)`, + always truthy, unnoticed until 3.6.3. `lib/patches.rb` now requires `rails`, + because loading the engine without it fails + +### Removed +- The deprecation warnings 3.7.0 added, now that what they announced has + happened. `Patches.deprecator` itself stays - applications were told to + configure it - and its horizon points at the next major + +### Changed +- CI covers every combination the gemspec allows, 12 cells rather than a wider + range, plus a leg for each optional-integration state: Sidekiq 7.x and 8.x + with strict arguments on and off, Sidekiq absent, `slack-notifier` absent, and + neither installed - the last of which nothing covered before + +## [3.7.0] - 2026-09-17 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 diff --git a/README.md b/README.md index 46fc2ee..bcdbf64 100644 --- a/README.md +++ b/README.md @@ -147,23 +147,23 @@ 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. -## Deprecations +## Upgrading from 3.x -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: +Everything 4.0 removes was deprecated in 3.7.0, so upgrade to that first and +clear its warnings - `Patches.deprecator.behavior = :raise` in an initializer +turns them into failures if you would rather find them that way. -```ruby -Patches.deprecator.behavior = :silence # or :raise, :log, a lambda... -``` - -| Deprecated | Replacement | +| Change | What to do | |---|---| -| 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 | - -Nothing is removed in this release, and the gemspec floors are unchanged, so -anything that installed 3.6.x installs 3.7.0. +| Ruby 3.2 and Rails 7.2 are now the minimum | Upgrade, or stay on 3.7.x | +| `slack-notifier` is no longer a dependency | Add `gem 'slack-notifier'` to your Gemfile if you set `config.use_slack` | +| The Capistrano task is gone | Invoke `bundle exec rake patches:run` from your deployment process | +| The workers include `Sidekiq::Job` | Use Sidekiq 6.3 or newer - 7.x or 8.x are the supported lines | + +Nothing else changed: patch classes, `Patches::Base`, the generator, the rake +tasks, the configuration and the `patches_patches` table are all as they were. +`Patches.deprecator` remains, so an initializer that configures it keeps +working. ## Development diff --git a/RELEASING.md b/RELEASING.md index 88f2dd3..c7f773d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,7 +12,10 @@ versions listed on rubygems.org never carry the prefix. ## Cutting a release 1. Bump `MAJOR` / `MINOR` / `PATCH` in `lib/patches/version.rb`. -2. Add a new `## [X.Y.Z] - YYYY-MM-DD` entry to the top of `CHANGELOG.md` describing the changes. +2. Replace `unreleased` in the `## [X.Y.Z] - unreleased` heading at the top of `CHANGELOG.md` with + today's date. A release branch carries `unreleased` while it waits for review, so the date is + always the day it ships - 3.7.0 shipped a week after its entry was drafted and claimed the + drafting date until someone noticed. 3. Commit both changes and get them onto `develop` (PR + merge, as normal). 4. From an up-to-date `develop`, tag the commit and push the tag: diff --git a/lib/patches/version.rb b/lib/patches/version.rb index f47ac48..cdef74c 100644 --- a/lib/patches/version.rb +++ b/lib/patches/version.rb @@ -1,6 +1,6 @@ module Patches - MAJOR = 3 - MINOR = 7 + MAJOR = 4 + MINOR = 0 PATCH = 0 VERSION = [MAJOR, MINOR, PATCH].compact.join(".").freeze end From 6da7bd825c7bc3e4d30dd4889b91e10c51cfd6ae Mon Sep 17 00:00:00 2001 From: Michael MacDonald Date: Mon, 21 Sep 2026 21:44:06 +1000 Subject: [PATCH 10/10] Set the 4.0.0 release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17806a1..5e10531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -## [4.0.0] - unreleased +## [4.0.0] - 2026-09-21 Removes everything 3.7.0 deprecated. Nothing here is a surprise if you upgraded to 3.7.0 and cleared its warnings; see Upgrading from 3.x in README.md.