diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c535fe7 Binary files /dev/null and b/.DS_Store differ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a52d08a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..90409e4 --- /dev/null +++ b/.github/copilot-instructions.md @@ -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 +``` \ No newline at end of file diff --git a/.github/workflows/specs.yml b/.github/workflows/ruby.yml similarity index 72% rename from .github/workflows/specs.yml rename to .github/workflows/ruby.yml index 1dfedc9..b6d0360 100644 --- a/.github/workflows/specs.yml +++ b/.github/workflows/ruby.yml @@ -1,4 +1,4 @@ -name: Run specs +name: Ruby on: pull_request: branches: @@ -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: > diff --git a/.gitignore b/.gitignore index 21fb6e5..9570997 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /test.db /*.gem .byebug_history +/vendor/ diff --git a/Gemfile b/Gemfile index 84505c1..a02d575 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,4 @@ source 'https://rubygems.org' gemspec -gem "rails", "~> 7.0" +gem "rails", ">= 7.0" diff --git a/README.md b/README.md index 9431325..4301d9a 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/lib/patches/tenant_worker.rb b/lib/patches/tenant_worker.rb index 87c8d4d..b43d0ab 100644 --- a/lib/patches/tenant_worker.rb +++ b/lib/patches/tenant_worker.rb @@ -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 diff --git a/lib/patches/worker.rb b/lib/patches/worker.rb index 5c5ef5d..c049d46 100644 --- a/lib/patches/worker.rb +++ b/lib/patches/worker.rb @@ -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 diff --git a/patches.gemspec b/patches.gemspec index ae90691..0799c19 100644 --- a/patches.gemspec +++ b/patches.gemspec @@ -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" @@ -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 diff --git a/spec/tenant_worker_spec.rb b/spec/tenant_worker_spec.rb index 8219cf8..4ea82e0 100644 --- a/spec/tenant_worker_spec.rb +++ b/spec/tenant_worker_spec.rb @@ -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 diff --git a/spec/worker_spec.rb b/spec/worker_spec.rb index 349b9c8..ebf5ba1 100644 --- a/spec/worker_spec.rb +++ b/spec/worker_spec.rb @@ -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