Skip to content
Open
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
Binary file added .DS_Store
Binary file not shown.
17 changes: 17 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "patches-dev",
"image": "mcr.microsoft.com/devcontainers/ruby:1-3.3-bullseye",

"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},

"postCreateCommand": "bundle install",

"mounts": [
"source=patches-bundle,target=/usr/local/bundle,type=volume"
],

"remoteUser": "vscode"
}
97 changes: 97 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# AI Coding Guidelines for Patches

## Overview

Patches is a Rails engine gem that provides a framework for running one-off database migration tasks (patches) in Rails applications. It supports both synchronous and asynchronous execution via Sidekiq, multi-tenant environments using the Apartment gem, Slack notifications, and application version validation.

## Architecture

### Core Components

1. **Patches Engine** (`lib/patches/engine.rb`)
- Rails engine that integrates Patches into host applications
- Provides database migrations and generators

2. **Base Patch Class** (`lib/patches/base.rb`)
- Abstract base class that all patches inherit from
- Defines the contract for patch execution

3. **Configuration System** (`lib/patches/config.rb`)
- Centralized configuration management
- Supports Sidekiq, Slack, and application version settings

4. **Execution Framework**
- **Runner** (`lib/patches/runner.rb`) - Main execution engine for patches
- **TenantRunner** (`lib/patches/tenant_runner.rb`) - Multi-tenant patch execution
- **Worker** (`lib/patches/worker.rb`) - Sidekiq background job wrapper
- **TenantWorker** (`lib/patches/tenant_worker.rb`) - Multi-tenant Sidekiq wrapper

5. **Notification System** (`lib/patches/notifier.rb`)
- Slack integration for patch execution notifications
- Configurable success/failure messaging

6. **Utilities**
- **Patch** (`lib/patches/patch.rb`) - Patch metadata and validation
- **Pending** (`lib/patches/pending.rb`) - Tracks unrun patches
- **ApplicationVersionValidation** - Ensures patches run on correct app version

## Key Design Patterns

### Execution Flow
1. Patches are discovered from `db/patches/` directory
2. Only unrun patches are executed (tracked in `patches_patches` table)
3. Patches run in chronological order based on filename timestamp
4. Each patch inherits from `Patches::Base` and implements a `run` method

### Multi-Tenant Support
- Automatically detects Apartment gem presence
- Runs patches across all tenants when `sidekiq_parallel` is enabled
- Uses `TenantRunConcern` for shared tenant iteration logic

### Asynchronous Execution
- Optional Sidekiq integration via `use_sidekiq` configuration
- Application version validation prevents version mismatches during deployments
- Configurable retry logic for version mismatches

## Host Application Requirements

### Database Setup
```ruby
# Run migration installer
bundle exec rake patches:install:migrations
bundle exec rake db:migrate
```

### Configuration (Initializer)
```ruby
Patches::Config.configure do |config|
# Optional: Asynchronous execution
config.use_sidekiq = true
config.sidekiq_parallel = true # For multi-tenant parallel execution

# Optional: Slack notifications
config.use_slack = true
config.slack_options = {
webhook_url: ENV['SLACK_WEBHOOK_URL'],
channel: ENV['SLACK_CHANNEL'],
username: ENV['SLACK_USER']
}

# Optional: Application version validation
config.application_version = File.read(Rails.root.join('REVISION'))
config.retry_after_version_mismatch_in = 1.minute
end
```

### Patch Creation
```ruby
# Generate patch
bundle exec rails g patches:patch PatchName

# Generated patch structure
class PatchName < Patches::Base
def run
# Implementation goes here
end
end
```
18 changes: 11 additions & 7 deletions .github/workflows/specs.yml → .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Run specs
name: Ruby
on:
pull_request:
branches:
Expand All @@ -8,21 +8,25 @@ on:
- develop

jobs:
build:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4']

steps:
- uses: actions/checkout@v3
- name: Set up Ruby 2.7
- uses: actions/checkout@v4
- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true
- name: Run specs
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
run: bundle exec rspec

- name: SonarQube Scan
if: matrix.ruby-version == '3.2'
uses: sonarsource/sonarqube-scan-action@master
with:
args: >
Expand Down
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/
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ source 'https://rubygems.org'

gemspec

gem "rails", "~> 7.0"
gem "rails", ">= 7.0"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Patches
[![Run specs](https://github.com/rdytech/patches/actions/workflows/specs.yml/badge.svg)](https://github.com/rdytech/patches/actions/workflows/specs.yml)
[![Ruby](https://github.com/rdytech/patches/actions/workflows/ruby.yml/badge.svg)](https://github.com/rdytech/patches/actions/workflows/ruby.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)
Expand Down
2 changes: 1 addition & 1 deletion lib/patches/tenant_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def perform(tenant_name, path, params = {})
if valid_application_version?(params['application_version'])
run(tenant_name, path)
else
self.class.perform_in(Patches::Config.configuration.retry_after_version_mismatch_in, tenant_name, path, params)
self.class.perform_in(Patches::Config.configuration.retry_after_version_mismatch_in, tenant_name, path, **params)
end
end
end
2 changes: 1 addition & 1 deletion lib/patches/worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def perform(runner, params = {})
if valid_application_version?(params['application_version'])
runner.constantize.new.perform
else
self.class.perform_in(Patches::Config.configuration.retry_after_version_mismatch_in, runner, params)
self.class.perform_in(Patches::Config.configuration.retry_after_version_mismatch_in, runner, **params)
end
end
end
6 changes: 4 additions & 2 deletions patches.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.required_ruby_version = ">= 3.0"

spec.add_dependency "railties", ">= 3.2"
spec.add_dependency "slack-notifier"

Expand All @@ -29,11 +31,11 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "capybara", "~> 2.3.0"
spec.add_development_dependency "generator_spec", "~> 0.9.0"
spec.add_development_dependency "simplecov", "~> 0.17", '< 0.18' # sonarscanner requires < 0.18
spec.add_development_dependency "factory_girl", "~> 4.5.0"
spec.add_development_dependency "factory_bot_rails", "~> 6.0"
spec.add_development_dependency "timecop", "~> 0.7.0"
spec.add_development_dependency "database_cleaner", "~> 1.3.0"
spec.add_development_dependency "pry"
spec.add_development_dependency "sidekiq", "~> 3.4.1"
spec.add_development_dependency "sidekiq", "~> 5.2.1"
spec.add_development_dependency "webmock"
spec.add_development_dependency "byebug"
end
1 change: 1 addition & 0 deletions spec/tenant_worker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
context 'when application_version does not match' do
it 'does not run patches' do
expect(subject).not_to receive(:run)
expect(Patches::TenantWorker).to receive(:perform_in)
subject.perform('test', 'path', 'application_version' => 'd8f190c')
end

Expand Down
1 change: 1 addition & 0 deletions spec/worker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
context 'when application_version does not match' do
it 'does not run patches' do
expect(runner).not_to receive(:perform)
expect(Patches::Worker).to receive(:perform_in)
subject.perform('Patches::Runner', 'application_version' => 'd8f190c')
end

Expand Down