Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 25 additions & 27 deletions .github/workflows/specs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,18 @@
- 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
strategy:
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' }
Expand All @@ -52,41 +41,50 @@
- 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
with:
ruby-version: '3.4'
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

sonarqube:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: SonarQube
runs-on: ubuntu-latest
steps:
Expand Down
43 changes: 42 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,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
Expand Down
80 changes: 39 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -83,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

Expand Down Expand Up @@ -146,26 +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 |
|---|---|
| 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 |

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

Expand Down
5 changes: 4 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion db/migrate/201506011700_create_patch.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 9 additions & 18 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -117,26 +116,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

Expand Down
12 changes: 9 additions & 3 deletions lib/patches.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -24,9 +31,8 @@ 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/engine"
require "patches/patch"
require "patches/pending"
require "patches/runner"
Expand Down
5 changes: 0 additions & 5 deletions lib/patches/capistrano.rb

This file was deleted.

12 changes: 0 additions & 12 deletions lib/patches/capistrano/tasks.rake

This file was deleted.

Loading
Loading