From b4b398aca38b50593ccd889308c4959d50f6457d Mon Sep 17 00:00:00 2001 From: Stephen Nelson Date: Mon, 22 Jun 2026 12:19:35 +0930 Subject: [PATCH 1/2] Testing and CI --- .github/dependabot.yml | 10 +++++ .github/workflows/release.yml | 28 ++++++++++++ .github/workflows/test.yml | 19 ++++++++ .rspec | 2 + .rubocop.yml | 5 +++ .ruby-version | 1 + Gemfile | 1 - Gemfile.lock | 3 -- Rakefile | 10 +++++ bin/ci | 8 ++++ bin/setup | 6 +++ config/ci.rb | 9 ++++ lib/katalyst-thermite.rb | 2 +- lib/katalyst/thermite.rb | 9 ---- lib/thermite.rb | 17 +++++++ lib/thermite/engine.rb | 11 +++++ lib/thermite/tasks.rb | 6 +++ .../app/config/environments/production.rb | 5 +++ spec/rails_helper.rb | 15 +++++++ spec/spec_helper.rb | 15 +++++++ spec/support/generator_helpers.rb | 45 +++++++++++++++++++ 21 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .rspec create mode 100644 .ruby-version create mode 100644 Rakefile create mode 100755 bin/ci create mode 100755 bin/setup create mode 100644 config/ci.rb delete mode 100644 lib/katalyst/thermite.rb create mode 100644 lib/thermite.rb create mode 100644 lib/thermite/engine.rb create mode 100644 lib/thermite/tasks.rb create mode 100644 spec/fixtures/app/config/environments/production.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/generator_helpers.rb diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..45966bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "monthly" + groups: + ruby: + patterns: + - "*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..77458bd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release + +on: + push: + tags: [ v* ] + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - uses: rubygems/release-gem@v1 + - name: Create GitHub release + run: | + tag_name="$(git describe --tags --abbrev=0)" + gh release create "${tag_name}" --verify-tag --generate-notes --latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ffe0454 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Test + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + # Single source of truth: the same steps run locally via bin/ci (== rake). + - name: Test + run: bin/ci diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..5be63fc --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--format documentation diff --git a/.rubocop.yml b/.rubocop.yml index 7ee256f..35b7052 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,3 +4,8 @@ plugins: inherit_mode: merge: - Exclude + +# This is a gem, not a Rails app: there's no :environment task to depend on. +Rails/RakeEnvironment: + Exclude: + - Rakefile diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..7636e75 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +4.0.5 diff --git a/Gemfile b/Gemfile index 4588900..e848871 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,6 @@ source "https://rubygems.org" gemspec -gem "brakeman", require: false gem "rake" gem "rspec" gem "rubocop-katalyst" diff --git a/Gemfile.lock b/Gemfile.lock index dfa4dcd..d0bb2d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -40,8 +40,6 @@ GEM ast (2.4.3) base64 (0.3.0) bigdecimal (4.1.2) - brakeman (8.0.5) - racc builder (3.3.0) concurrent-ruby (1.3.7) connection_pool (3.0.2) @@ -203,7 +201,6 @@ PLATFORMS x86_64-linux DEPENDENCIES - brakeman katalyst-thermite! rake rspec diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..a70f3ce --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" + +desc "Run the full CI suite (style, security, tests) via bin/ci" +task :ci do + sh "bin/ci" +end + +task default: :ci diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000..d1ccf77 --- /dev/null +++ b/bin/ci @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci" diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..254c558 --- /dev/null +++ b/bin/setup @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +Dir.chdir(File.expand_path("..", __dir__)) do + system("bundle check") || system("bundle install", exception: true) +end diff --git a/config/ci.rb b/config/ci.rb new file mode 100644 index 0000000..cd6c7c9 --- /dev/null +++ b/config/ci.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Run using bin/ci + +CI.run do + step "Setup", "bin/setup" + step "Style: Ruby", "bundle exec rubocop" + step "Tests: RSpec", "bundle exec rspec" +end diff --git a/lib/katalyst-thermite.rb b/lib/katalyst-thermite.rb index 82bb76a..1f76c53 100644 --- a/lib/katalyst-thermite.rb +++ b/lib/katalyst-thermite.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -require "katalyst/thermite" +require "thermite" diff --git a/lib/katalyst/thermite.rb b/lib/katalyst/thermite.rb deleted file mode 100644 index 5d9e7ad..0000000 --- a/lib/katalyst/thermite.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "active_support" - -module Katalyst - module Thermite - extend ActiveSupport::Autoload - end -end diff --git a/lib/thermite.rb b/lib/thermite.rb new file mode 100644 index 0000000..f540e09 --- /dev/null +++ b/lib/thermite.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "thermite/engine" + +require "active_support" + +require "zeitwerk" + +loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false) +loader.ignore("#{__dir__}/generators") +loader.ignore("#{__dir__}/katalyst-thermite.rb") +loader.ignore("#{__dir__}/thermite/tasks.rb") +loader.setup + +module Thermite + extend self +end diff --git a/lib/thermite/engine.rb b/lib/thermite/engine.rb new file mode 100644 index 0000000..e34a358 --- /dev/null +++ b/lib/thermite/engine.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Thermite + class Engine < ::Rails::Engine + isolate_namespace Thermite + + rake_tasks do + load "thermite/tasks.rb" + end + end +end diff --git a/lib/thermite/tasks.rb b/lib/thermite/tasks.rb new file mode 100644 index 0000000..1749e05 --- /dev/null +++ b/lib/thermite/tasks.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +namespace :thermite do + namespace :install do + end +end diff --git a/spec/fixtures/app/config/environments/production.rb b/spec/fixtures/app/config/environments/production.rb new file mode 100644 index 0000000..4f9eed9 --- /dev/null +++ b/spec/fixtures/app/config/environments/production.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Minimal stand-in for a real app's production environment. +Rails.application.configure do +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..1019e5b --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "spec_helper" + +ENV["RAILS_ENV"] ||= "test" + +require "rails/generators" + +RSpec.configure do |config| + config.define_derived_metadata(file_path: %r{/spec/generators/}) do |metadata| + metadata[:type] ||= :generator + end +end + +Dir[File.expand_path("support/**/*.rb", __dir__)].each { |f| require f } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..1946fe0 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.disable_monkey_patching! + config.order = :random + Kernel.srand config.seed +end diff --git a/spec/support/generator_helpers.rb b/spec/support/generator_helpers.rb new file mode 100644 index 0000000..448ef63 --- /dev/null +++ b/spec/support/generator_helpers.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "rails/generators/testing/behavior" +require "rails/generators/testing/assertions" +require "minitest/assertions" +require "fileutils" + +module GeneratorExampleGroup + extend ActiveSupport::Concern + + include Rails::Generators::Testing::Behavior + include Rails::Generators::Testing::Assertions + include Minitest::Assertions # assert_file & friends are Minitest assertions + include FileUtils + + # Generated files land here; wiped and reseeded before every example. + DESTINATION = File.expand_path("../../tmp/generated", __dir__) + + included do + destination DESTINATION + + before do + self.class.tests(described_class) + prepare_destination # rm_rf + mkdir the destination + prepare_app # seed a minimal app skeleton to run against + end + end + + # Minitest::Assertions needs a mutable assertion counter on the instance. + attr_writer :assertions + + def assertions = @assertions ||= 0 + + # Copy a minimal app skeleton into the destination so the generator has env + # files to rewrite, a place to drop migrations, etc. + def prepare_app(fixture = "app") + source = File.expand_path("../fixtures/#{fixture}", __dir__) + cp_r("#{source}/.", destination_root) + destination_root + end +end + +RSpec.configure do |config| + config.include GeneratorExampleGroup, type: :generator +end From 5a7f3cb35c5e5f9487b8c3893b26f43ff5250463 Mon Sep 17 00:00:00 2001 From: Stephen Nelson Date: Mon, 22 Jun 2026 20:27:01 +0930 Subject: [PATCH 2/2] Install solid queue using the primary database --- Gemfile.lock | 26 +++++ katalyst-thermite.gemspec | 1 + .../thermite/install/solid_queue/USAGE | 18 ++++ .../solid_queue/solid_queue_generator.rb | 94 +++++++++++++++++++ .../templates/add_solid_queue.rb.tt | 7 ++ .../solid_queue/templates/config/queue.yml.tt | 14 +++ .../templates/config/recurring.yml.tt | 9 ++ lib/thermite/tasks.rb | 4 + .../app/config/environments/production.rb | 3 + .../app/config/environments/staging.rb | 8 ++ .../install/solid_queue_generator_spec.rb | 34 +++++++ 11 files changed, 218 insertions(+) create mode 100644 lib/generators/thermite/install/solid_queue/USAGE create mode 100644 lib/generators/thermite/install/solid_queue/solid_queue_generator.rb create mode 100644 lib/generators/thermite/install/solid_queue/templates/add_solid_queue.rb.tt create mode 100644 lib/generators/thermite/install/solid_queue/templates/config/queue.yml.tt create mode 100644 lib/generators/thermite/install/solid_queue/templates/config/recurring.yml.tt create mode 100644 spec/fixtures/app/config/environments/staging.rb create mode 100644 spec/generators/thermite/install/solid_queue_generator_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index d0bb2d6..caa60ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ PATH katalyst-thermite (1.0.0) activesupport railties + solid_queue GEM remote: https://rubygems.org/ @@ -24,6 +25,15 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) activesupport (8.1.3) base64 bigdecimal @@ -49,6 +59,13 @@ GEM drb (2.2.3) erb (6.0.4) erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + fugit (1.12.2) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) i18n (1.15.2) concurrent-ruby (~> 1.0) io-console (0.8.2) @@ -82,6 +99,7 @@ GEM psych (5.4.0) date stringio + raabro (1.4.0) racc (1.8.1) rack (3.2.6) rack-session (2.1.2) @@ -182,8 +200,16 @@ GEM rubocop-rspec (~> 3.5) ruby-progressbar (1.13.0) securerandom (0.4.1) + solid_queue (1.4.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) stringio (3.2.0) thor (1.5.0) + timeout (0.6.1) tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) diff --git a/katalyst-thermite.gemspec b/katalyst-thermite.gemspec index d823b85..1b1b1af 100644 --- a/katalyst-thermite.gemspec +++ b/katalyst-thermite.gemspec @@ -17,4 +17,5 @@ Gem::Specification.new do |spec| spec.add_dependency "activesupport" spec.add_dependency "railties" + spec.add_dependency "solid_queue" end diff --git a/lib/generators/thermite/install/solid_queue/USAGE b/lib/generators/thermite/install/solid_queue/USAGE new file mode 100644 index 0000000..bf46931 --- /dev/null +++ b/lib/generators/thermite/install/solid_queue/USAGE @@ -0,0 +1,18 @@ +Description: + Installs Solid Queue using a single, shared database (Katalyst's preferred setup). + + Unlike `solid_queue:install`, this does not provision a separate queue + database. The solid_queue tables are added to the primary database via a + normal migration in db/migrate, and `config.active_job.queue_adapter` is set + to :solid_queue without a `connects_to` configuration. + + Requires the solid_queue gem to be present in the host application. + +Example: + bin/rails thermite:install:solid_queue + + This will: + Create config/queue.yml and config/recurring.yml + Create a db/migrate migration for the solid_queue tables + Install the bin/jobs binstub + Set the Active Job queue adapter to :solid_queue (non dev/test environments) diff --git a/lib/generators/thermite/install/solid_queue/solid_queue_generator.rb b/lib/generators/thermite/install/solid_queue/solid_queue_generator.rb new file mode 100644 index 0000000..f805af1 --- /dev/null +++ b/lib/generators/thermite/install/solid_queue/solid_queue_generator.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require "rails" +require "rails/generators" +require "rails/generators/active_record/migration" + +require "active_record" + +# For a single-database install, solid queue's documented setup is to copy the contents +# of db/queue_schema.rb into a normal migration, delete db/queue_schema.rb, +# remove config.solid_queue.connects_to, then run db:migrate. +module Thermite + module Install + class SolidQueueGenerator < Rails::Generators::Base + include ActiveRecord::Generators::Migration + + # Reuse solid_queue's own templates. Resolved lazily so the class loads + # without solid_queue present; populated once #verify_solid_queue! requires it. + def self.source_root + ::SolidQueue::InstallGenerator.source_root if defined?(::SolidQueue::InstallGenerator) + end + + # Search our own templates (the migration) first, then solid_queue's + # (queue.yml, recurring.yml, bin/jobs). + def self.source_paths + [File.expand_path("templates", __dir__), source_root].compact + end + + # Our source_root points into solid_queue, so the inherited USAGE lookup + # would resolve to solid_queue's USAGE. Point it at ours instead. + def self.usage_path + File.expand_path("USAGE", __dir__) + end + + # Fail early with a helpful message when solid_queue isn't available. + def verify_solid_queue! + require "solid_queue/version" + require "generators/solid_queue/install/install_generator" + rescue LoadError + raise Thor::Error, <<~MSG.strip + thermite:install:solid_queue requires the solid_queue gem, which is not available. + Add it to your Gemfile and run `bundle install`: + + gem "solid_queue" + MSG + end + + # Based on SolidQueue::InstallGenerator#copy_files but writing schema to a migration instead + # @see https://github.com/rails/solid_queue#single-database-configuration + def copy_files + template "config/queue.yml" + template "config/recurring.yml" + migration_template "add_solid_queue.rb", "db/migrate/add_solid_queue.rb", skip: true + template "bin/jobs" + chmod "bin/jobs", 0o755 & ~File.umask, verbose: false + end + + # Based on SolidQueue::InstallGenerator#configure_adapter but without connects_to configuration + # @see https://github.com/rails/solid_queue#single-database-configuration + def configure_adapter + Pathname(destination_root).join("config/environments").glob("*.rb").each do |pathname| + next if %w[development.rb test.rb].include?(pathname.basename.to_s) + + gsub_file pathname, /\n\s*config\.solid_queue\.connects_to\s+=.*\n/, "\n", verbose: false + gsub_file pathname, /(# )?config\.active_job\.queue_adapter\s+=.*\n/, + "config.active_job.queue_adapter = :solid_queue\n" + end + end + + private + + # The application's environments, derived from config/environments/*.rb so + # we don't hard-code names like "staging" / "uat". + def application_environments + Pathname(destination_root).join("config/environments") + .glob("*.rb") + .map { |path| path.basename(".rb").to_s } + .sort + end + + def solid_queue_schema_body + schema = File.read(File.join(self.class.source_root, "db/queue_schema.rb")) + + schema + # Keep only the schema block body. + .sub(%r{\A.*ActiveRecord::Schema[^\n]+do\n}, "") + .sub(/\nend\n?\z/, "") + .lines + .map { |line| line == "\n" ? line : " #{line}" } + .join + end + end + end +end diff --git a/lib/generators/thermite/install/solid_queue/templates/add_solid_queue.rb.tt b/lib/generators/thermite/install/solid_queue/templates/add_solid_queue.rb.tt new file mode 100644 index 0000000..99b1f1e --- /dev/null +++ b/lib/generators/thermite/install/solid_queue/templates/add_solid_queue.rb.tt @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] + def change + <%= solid_queue_schema_body.chomp %> + end +end diff --git a/lib/generators/thermite/install/solid_queue/templates/config/queue.yml.tt b/lib/generators/thermite/install/solid_queue/templates/config/queue.yml.tt new file mode 100644 index 0000000..bb7edf6 --- /dev/null +++ b/lib/generators/thermite/install/solid_queue/templates/config/queue.yml.tt @@ -0,0 +1,14 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 +<% application_environments.each do |environment| -%> + +<%= environment %>: + <<: *default +<% end -%> diff --git a/lib/generators/thermite/install/solid_queue/templates/config/recurring.yml.tt b/lib/generators/thermite/install/solid_queue/templates/config/recurring.yml.tt new file mode 100644 index 0000000..cd5e5b6 --- /dev/null +++ b/lib/generators/thermite/install/solid_queue/templates/config/recurring.yml.tt @@ -0,0 +1,9 @@ +production: &production + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 +<% (application_environments - %w[development test production]).each do |environment| -%> + +<%= environment %>: + <<: *production +<% end -%> diff --git a/lib/thermite/tasks.rb b/lib/thermite/tasks.rb index 1749e05..9f7128e 100644 --- a/lib/thermite/tasks.rb +++ b/lib/thermite/tasks.rb @@ -2,5 +2,9 @@ namespace :thermite do namespace :install do + desc "Install Solid Queue" + task :solid_queue do + Rails::Command.invoke :generate, ["thermite:install:solid_queue"] + end end end diff --git a/spec/fixtures/app/config/environments/production.rb b/spec/fixtures/app/config/environments/production.rb index 4f9eed9..3eb8294 100644 --- a/spec/fixtures/app/config/environments/production.rb +++ b/spec/fixtures/app/config/environments/production.rb @@ -2,4 +2,7 @@ # Minimal stand-in for a real app's production environment. Rails.application.configure do + # Active Job (for thermite:install:solid_queue) + # config.active_job.queue_adapter = :async + # config.solid_queue.connects_to = { database: { writing: :queue } } end diff --git a/spec/fixtures/app/config/environments/staging.rb b/spec/fixtures/app/config/environments/staging.rb new file mode 100644 index 0000000..a7ebfe4 --- /dev/null +++ b/spec/fixtures/app/config/environments/staging.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Smoke test for generators that should identify and configure non-standard environments +Rails.application.configure do + # Active Job (for thermite:install:solid_queue) + # config.active_job.queue_adapter = :async + # config.solid_queue.connects_to = { database: { writing: :queue } } +end diff --git a/spec/generators/thermite/install/solid_queue_generator_spec.rb b/spec/generators/thermite/install/solid_queue_generator_spec.rb new file mode 100644 index 0000000..f5997de --- /dev/null +++ b/spec/generators/thermite/install/solid_queue_generator_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "generators/thermite/install/solid_queue/solid_queue_generator" + +RSpec.describe Thermite::Install::SolidQueueGenerator do + it "completes successfully" do + expect { run_generator }.not_to raise_error + end + + it "writes config files, migration, and binstub" do + run_generator + + assert_file "config/queue.yml" + assert_file "config/recurring.yml" + assert_migration "db/migrate/add_solid_queue.rb" + assert_file "bin/jobs" + end + + it "includes non-standard environments like staging in the queue and recurring config" do + run_generator + + assert_file "config/queue.yml", /^staging:/ + assert_file "config/recurring.yml", /^staging:/ + end + + it "raises a helpful error when solid_queue is unavailable" do + allow(generator).to receive(:require).and_raise(LoadError) + + expect { generator.verify_solid_queue! } + .to raise_error(Thor::Error, /requires the solid_queue gem/) + end +end