Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
}
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR also adds /vendor/ to .gitignore (for a locally vendored bundle path), but .dockerignore isn't updated to match. If a dev vendors gems locally (bundle config set --local path vendor/bundle) and then runs docker compose build, the full vendor/bundle tree gets sent as build context and copied into the image — unnecessary bloat given gems are already stored via the compose volume. Consider adding vendor here too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AI] Fixed in 0eec058 — vendor added to .dockerignore, so the two files agree.

You are right about the mechanism: the build context is what gets sent, and COPY . /app/ would then bake a vendored tree into the image, on top of the gems the compose volume already holds at /usr/local/bundle.

For what it is worth, the /vendor/ entry in .gitignore is itself belt-and-braces — nothing in the committed config vendors gems locally, and ruby/setup-ruby only does it inside an ephemeral runner workspace. But if someone does adopt the habit, both files now cover it.

26 changes: 0 additions & 26 deletions .github/workflows/publish.yml

This file was deleted.

1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/specs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
/test.db
/*.gem
.byebug_history
/vendor/
50 changes: 49 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 8 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
105 changes: 86 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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`.
Expand All @@ -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`
13 changes: 0 additions & 13 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 12 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
9 changes: 9 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/patches.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def self.logger=(log)
end
end

require "patches/deprecation"
require "patches/base"
require "patches/config"
require "patches/tenant_run_concern"
Expand Down
4 changes: 4 additions & 0 deletions lib/patches/capistrano.rb
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
require 'patches/deprecation'

Patches.warn_capistrano_support_removed_in_4_0

load File.expand_path("capistrano/tasks.rake", File.dirname(__FILE__))
Loading
Loading