From bafdb07934bae62716932984cf037c5383277abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Fri, 19 Jun 2026 00:12:18 +0200 Subject: [PATCH 1/7] Support Rails 8.1 / Ruby 4.0.5 The Active Record compatibility layer raised "Unsupported Active Record version" on anything past 6.1: it looked up a per-version Vxx support module and had none for 7.0+. Every release from 5.2 onward needs the same treatment (native JSONB, plus the removed `scoped` and `sanitize` APIs revived), so add a single `Modern` support module and fall back to it for any Active Record major >= 7 instead of shipping an identical Vxx module per Rails release. The explicit V32..V61 modules are untouched, so older consumers behave exactly as before. Add rails-8.0 and rails-8.1 Appraisal entries (and their gemfiles) next to the existing 7.x ones. Add a self-contained, Postgres-free smoke harness (test/smoke.rb) that installs the authorization DSL and checks an end-to-end ACL grant -> Actor#can? decision over sqlite, plus a CI matrix sweeping Ruby 4.0.5/3.4/2.7 across Rails 8.1/7.2/6.1. Verified green on Ruby 4.0.5 and 3.4 (Rails 8.1) and Ruby 3.3 (Rails 7.1). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/smoke.yml | 42 +++++++ .gitignore | 4 + Appraisals | 8 ++ CHANGELOG.md | 11 ++ gemfiles/rails_8.0.gemfile | 7 ++ gemfiles/rails_8.1.gemfile | 7 ++ .../adapters/active_record/compatibility.rb | 18 ++- .../active_record/compatibility/modern.rb | 31 ++++++ lib/eaco/version.rb | 2 +- test/Gemfile.ci | 13 +++ test/smoke.rb | 103 ++++++++++++++++++ 11 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/smoke.yml create mode 100644 gemfiles/rails_8.0.gemfile create mode 100644 gemfiles/rails_8.1.gemfile create mode 100644 lib/eaco/adapters/active_record/compatibility/modern.rb create mode 100644 test/Gemfile.ci create mode 100644 test/smoke.rb diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..c09a38e --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,42 @@ +name: Smoke + +# Fast, Postgres-free authorization smoke for the Ruby 4 / Rails 8.1 upgrade. +# The full pg + cucumber matrix lives in ci.yml; this proves the compatibility +# layer installs and Actor#can? resolves on the new stack (and still on the old +# one) using an in-memory ACL over sqlite. + +on: + push: + branches: [ master, "upgrade/**" ] + pull_request: + +permissions: + contents: read + +jobs: + smoke: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # upgrade target + - { ruby: "4.0.5", rails: "~> 8.1.0" } + - { ruby: "3.4", rails: "~> 8.1.0" } + - { ruby: "3.4", rails: "~> 7.2.0" } + # backward compat — consumers still on the old stack (explicit V61 + # support module, sqlite3 1.4 for Ruby 2.7) + - { ruby: "2.7", rails: "~> 6.1.0", sqlite3: "~> 1.4.0" } + name: "ruby ${{ matrix.ruby }} / rails ${{ matrix.rails }}" + env: + BUNDLE_GEMFILE: test/Gemfile.ci + RAILS_VERSION: ${{ matrix.rails }} + SQLITE3_VERSION: ${{ matrix.sqlite3 }} + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Smoke (authorize on the version stack) + run: bundle exec ruby test/smoke.rb diff --git a/.gitignore b/.gitignore index dfdea9c..2c019bf 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ build-iPhoneSimulator/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: Gemfile.lock +test/*.lock .ruby-version .ruby-gemset @@ -64,3 +65,6 @@ spec/debug.log features/active_record.yml features/active_record.log + +# Local-only Ruby 4.0.5 smoke runner (AI scratch) — not committed +Dockerfile.ai diff --git a/Appraisals b/Appraisals index 5c9b211..e0c80ec 100644 --- a/Appraisals +++ b/Appraisals @@ -64,3 +64,11 @@ end appraise 'rails-7.2' do gem 'rails', '~> 7.2.0' end + +appraise 'rails-8.0' do + gem 'rails', '~> 8.0.0' +end + +appraise 'rails-8.1' do + gem 'rails', '~> 8.1.0' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index d25b9b7..e5171d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,17 @@ project adheres to [Semantic Versioning](https://semver.org/) - Remove curly braces from `@see` tags (causes rendering issues) - Use fully-qualified constant names for `ACL#find_by_role` references (`Eaco::ACL#find_by_role`) +## 1.2.0 - 2026-06-18 + +### Added +* Rails 8.1 / Ruby 4.0.5 support. +* `Adapters::ActiveRecord::Compatibility::Modern` support module, used for + every Active Record major >= 7. Previously the compatibility layer raised + "Unsupported Active Record version" on anything past 6.1; new Rails majors + no longer require an identical per-version `Vxx` module. +* Self-contained smoke harness (`test/smoke.rb`) plus a CI matrix sweeping + Ruby 4.0.5/3.4/2.7 across Rails 8.1/7.2/6.1. + ## 1.1.1 - 2017-03-08 ### Fixed diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile new file mode 100644 index 0000000..3b3765b --- /dev/null +++ b/gemfiles/rails_8.0.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 8.0.0" + +gemspec path: "../" diff --git a/gemfiles/rails_8.1.gemfile b/gemfiles/rails_8.1.gemfile new file mode 100644 index 0000000..cedb65f --- /dev/null +++ b/gemfiles/rails_8.1.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 8.1.0" + +gemspec path: "../" diff --git a/lib/eaco/adapters/active_record/compatibility.rb b/lib/eaco/adapters/active_record/compatibility.rb index ae1ee94..1ba130e 100644 --- a/lib/eaco/adapters/active_record/compatibility.rb +++ b/lib/eaco/adapters/active_record/compatibility.rb @@ -16,6 +16,8 @@ class Compatibility autoload :V60, 'eaco/adapters/active_record/compatibility/v60.rb' autoload :V61, 'eaco/adapters/active_record/compatibility/v61.rb' + autoload :Modern, 'eaco/adapters/active_record/compatibility/modern.rb' + autoload :Scoped, 'eaco/adapters/active_record/compatibility/scoped.rb' autoload :Sanitized, 'eaco/adapters/active_record/compatibility/sanitized.rb' @@ -66,13 +68,17 @@ def active_record_version # @see check! # def support_module - unless self.class.const_defined?(support_module_name) - raise Eaco::Error, <<-EOF - Unsupported Active Record version: #{active_record_version} - EOF - end + return self.class.const_get(support_module_name) if + self.class.const_defined?(support_module_name) + + # No exact Vxx module: Active Record 7.0+ all share the same + # requirements, so fall back to {Modern} rather than raising on every + # new Rails release. Genuinely unknown/old versions still raise. + return Modern if ::ActiveRecord::VERSION::MAJOR >= 7 - self.class.const_get support_module_name + raise Eaco::Error, <<-EOF + Unsupported Active Record version: #{active_record_version} + EOF end ## diff --git a/lib/eaco/adapters/active_record/compatibility/modern.rb b/lib/eaco/adapters/active_record/compatibility/modern.rb new file mode 100644 index 0000000..d764103 --- /dev/null +++ b/lib/eaco/adapters/active_record/compatibility/modern.rb @@ -0,0 +1,31 @@ +module Eaco + module Adapters + module ActiveRecord + class Compatibility + + ## + # Active Record 7.0 and up support module. + # + # From 5.2 through the current releases the story is unchanged: JSONB + # works natively, while +.scoped+ and +sanitize+ stay removed and are + # revived through the {Scoped} and {Sanitized} support modules. + # + # This module is the fallback for every Active Record major >= 7, so a + # new identical +Vxx+ module is no longer needed on each Rails release. + # + # @see Scoped + # @see Sanitized + # + module Modern + extend ActiveSupport::Concern + + included do + extend Scoped + extend Sanitized + end + end + + end + end + end +end diff --git a/lib/eaco/version.rb b/lib/eaco/version.rb index 2e25f50..0cb5346 100644 --- a/lib/eaco/version.rb +++ b/lib/eaco/version.rb @@ -2,6 +2,6 @@ module Eaco # Current version # - VERSION = '1.1.2' + VERSION = '1.2.0' end diff --git a/test/Gemfile.ci b/test/Gemfile.ci new file mode 100644 index 0000000..c1bb496 --- /dev/null +++ b/test/Gemfile.ci @@ -0,0 +1,13 @@ +# CI / smoke Gemfile. eaco declares no runtime deps (the host app brings Rails), +# so we pin the stack here. RAILS_VERSION / SQLITE3_VERSION swept by the matrix. +source 'https://rubygems.org' + +# GitHub Actions sets an unspecified matrix var to "" (not unset), so a bare +# ENV.fetch(...) returns "" and `gem 'x', ""` raises Illformed requirement. +# Treat blank as absent. +ci = ->(name, default = nil) { v = ENV[name].to_s.strip; v.empty? ? default : v } + +gem 'eaco', path: '..' # runtime deps only (eaco declares none); skips pg/guard/etc. + +gem 'rails', ci.('RAILS_VERSION', '~> 8.1.0') +gem 'sqlite3', ci.('SQLITE3_VERSION', '>= 0') diff --git a/test/smoke.rb b/test/smoke.rb new file mode 100644 index 0000000..701158c --- /dev/null +++ b/test/smoke.rb @@ -0,0 +1,103 @@ +# Boot Active Record on the upgrade target (Ruby 4 / Rails 8.1) and exercise +# eaco's load-bearing path: the AR compatibility layer that USED to raise +# "Unsupported Active Record version" on anything past 6.1, plus an end-to-end +# in-memory authorization check (ACL grant -> Actor#can?). +# +# No PostgreSQL: the pg_jsonb adapter only needs a json/jsonb `acl` column to +# install, which sqlite's :json type satisfies. We assert the authorization +# decision (in-memory ACL eval), not the pg-specific accessible_by SQL. +$stdout.sync = true +require 'logger' +require 'active_record' +require 'eaco' + +# Reuse the gem's own example Actor/Resource/Designator fixtures. +require 'eaco/cucumber/active_record/document' +require 'eaco/cucumber/active_record/user' + +AR = Eaco::Cucumber::ActiveRecord +User = AR::User +Doc = AR::Document + +$failed = false +def check(label) + yield + puts "[ok] #{label}" +rescue => e + puts "[FAIL] #{label}: #{e.class}: #{e.message}" + puts e.backtrace.first(6).map { |l| " #{l}" } + $failed = true +end + +ActiveRecord::Base.logger = Logger.new(IO::NULL) +ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') +ActiveRecord::Schema.verbose = false +ActiveRecord::Schema.define do + create_table :documents do |t| + t.string :name + t.json :acl # sqlite :json satisfies the adapter's column check + end + create_table :users do |t| + t.string :name + t.boolean :admin, default: false + end +end + +puts "stack: ruby #{RUBY_VERSION} / Active Record #{ActiveRecord::VERSION::STRING}" + +# 1. THE blocker: installing the pg_jsonb adapter runs Compatibility#check!, +# which raised on AR > 6.1 before this upgrade. Configure via the real DSL. +check "eaco authorization DSL installs on Active Record #{ActiveRecord::VERSION::MAJOR}" do + Eaco.eval! <<~DSL, '(smoke)' + actor #{User} do + admin do |user| + user.admin? + end + + designators do + authenticated from: :class + user from: :id + end + end + + authorize #{Doc}, using: :pg_jsonb do + roles :writer, :reader + + permissions do + reader :read + writer reader, :write + end + end + DSL + + raise "Document not a Resource" unless Doc.include?(Eaco::Resource) + raise "User not an Actor" unless User.include?(Eaco::Actor) +end + +# 2. End-to-end authorization decision (in-memory ACL eval, no pg query). +check "ACL grant + Actor#can? resolves the right permissions" do + granted = User.new(id: 1, name: 'Granted') + other = User.new(id: 2, name: 'Other') + + doc = Doc.new(name: 'Spec') + doc.acl = Doc.acl.new.tap { |acl| acl.add(:reader, :user, 1) } + + raise "granted user cannot read" unless granted.can?(:read, doc) + raise "reader unexpectedly can write" if granted.can?(:write, doc) + raise "ungranted user can read" if other.can?(:read, doc) +end + +# 3. Admin bypass (admin_logic short-circuits to allow). +check "admin actor is allowed regardless of ACL" do + admin = User.new(id: 3, name: 'Boss', admin: true) + doc = Doc.new(name: 'Locked') # empty ACL + raise "admin denied" unless admin.can?(:write, doc) +end + +if $failed + puts "\nSMOKE FAILED" + exit 1 +else + puts "\nSMOKE OK — eaco authorizes on Ruby #{RUBY_VERSION} / " \ + "Active Record #{ActiveRecord::VERSION::STRING}" +end From 3eab5258a4550b912cad9f5be3b9b0ff8465e183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Fri, 19 Jun 2026 10:26:11 +0200 Subject: [PATCH 2/7] =?UTF-8?q?CI:=20modernize=20the=20pg=20+=20cucumber?= =?UTF-8?q?=20matrix=20for=20Rails=207=E2=80=938=20/=20Ruby=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy CI matrix tested Ruby 2.1–3.1 against Rails 3.2–6.1 and had rotted: old Rubies no longer assemble on current runners, and the rails_6.1/Ruby 3.1 row died at `require 'rails'` on i18n 1.15's Fiber storage (needs Ruby 3.2+). Replace it with a current matrix — Ruby 4.0.5/3.4/3.3/3.2 across Rails 8.1/8.0/7.2/7.1/7.0/6.1 — keeping Postgres + the full rspec and cucumber runs. Three test-only fixes for the new Ruby/Rails, all version-agnostic: - Compatibility#support_module derived the "modern AR" decision from the live `ActiveRecord::VERSION::MAJOR`, which broke the "unsupported version" cucumber scenario (it stubs `active_record_version` to 3.0, but the process runs AR 8, so the fallback returned Modern instead of raising). Derive the major from the looked-up version instead, so a stubbed value is honoured. - The parse-error feature asserted the exact pre-Prism SyntaxError wording; match the stable `unexpected '='` substring so it holds on Ruby 3.3+ and older. - The ACL#inspect/#pretty_inspect specs hard-coded the pre-3.4 Hash#inspect spacing; compute the expectation from the running Ruby's Hash#inspect. Full suite green locally on Ruby 3.4 / Rails 8.1: 21 examples, 33 scenarios / 253 steps, 0 failures. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 163 ++---------------- features/authorization_parse_error.feature | 2 +- .../adapters/active_record/compatibility.rb | 13 +- spec/eaco/acl_spec.rb | 6 +- 4 files changed, 34 insertions(+), 150 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a12ece..a59f596 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,181 +2,52 @@ name: CI on: push: - branches: [ master ] + branches: [ master, "upgrade/**" ] pull_request: - branches: [ master ] permissions: contents: read jobs: test: - name: Ruby ${{ matrix.ruby-version }}, ${{ matrix.gemfile }}, PG ${{ matrix.postgres-version }} + name: Ruby ${{ matrix.ruby-version }}, ${{ matrix.gemfile }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - ruby-version: ['3.1'] - gemfile: [rails_6.1] - postgres-version: ['12'] - include: - - ruby-version: '2.1' - gemfile: rails_3.2 - postgres-version: 10 - - ruby-version: '2.1' - gemfile: rails_4.0 - postgres-version: 10 - - ruby-version: '2.1' - gemfile: rails_4.1 - postgres-version: 10 - - ruby-version: '2.1' - gemfile: rails_4.2 - postgres-version: 12 - - - ruby-version: '2.2' - gemfile: rails_3.2 - postgres-version: 10 - - ruby-version: '2.2' - gemfile: rails_4.0 - postgres-version: 10 - - ruby-version: '2.2' - gemfile: rails_4.1 - postgres-version: 10 - - ruby-version: '2.2' - gemfile: rails_4.2 - postgres-version: 12 - - ruby-version: '2.2' - gemfile: rails_5.0 - postgres-version: 12 - - ruby-version: '2.2' - gemfile: rails_5.1 - postgres-version: 12 - - ruby-version: '2.2' - gemfile: rails_5.2 - postgres-version: 12 - - - ruby-version: '2.3' - gemfile: rails_3.2 - postgres-version: 10 - - ruby-version: '2.3' - gemfile: rails_4.2 - postgres-version: 12 - - ruby-version: '2.3' - gemfile: rails_5.0 - postgres-version: 12 - - ruby-version: '2.3' - gemfile: rails_5.1 - postgres-version: 12 - - ruby-version: '2.3' - gemfile: rails_5.2 - postgres-version: 12 - - - ruby-version: '2.4' - gemfile: rails_4.2 - postgres-version: 12 - - ruby-version: '2.4' - gemfile: rails_5.0 - postgres-version: 12 - - ruby-version: '2.4' - gemfile: rails_5.1 - postgres-version: 12 - - ruby-version: '2.4' - gemfile: rails_5.2 - postgres-version: 12 - - - ruby-version: '2.5' - gemfile: rails_5.0 - postgres-version: 12 - - ruby-version: '2.5' - gemfile: rails_5.1 - postgres-version: 12 - - ruby-version: '2.5' - gemfile: rails_5.2 - postgres-version: 12 - - ruby-version: '2.5' - gemfile: rails_6.0 - postgres-version: 12 - - ruby-version: '2.5' - gemfile: rails_6.1 - postgres-version: 12 - - - ruby-version: '2.6' - gemfile: rails_5.0 - postgres-version: 12 - - ruby-version: '2.6' - gemfile: rails_5.1 - postgres-version: 12 - - ruby-version: '2.6' - gemfile: rails_5.2 - postgres-version: 12 - - ruby-version: '2.6' - gemfile: rails_6.0 - postgres-version: 12 - - ruby-version: '2.6' - gemfile: rails_6.1 - postgres-version: 12 - - - ruby-version: '2.7' - gemfile: rails_5.2 - postgres-version: 12 - - ruby-version: '2.7' - gemfile: rails_6.0 - postgres-version: 12 - - ruby-version: '2.7' - gemfile: rails_6.1 - postgres-version: 12 - - - ruby-version: '3.0' - gemfile: rails_6.0 - postgres-version: 12 - - ruby-version: '3.0' - gemfile: rails_6.1 - postgres-version: 12 + # upgrade target + - { ruby-version: '4.0.5', gemfile: rails_8.1 } + - { ruby-version: '3.4', gemfile: rails_8.1 } + - { ruby-version: '3.4', gemfile: rails_8.0 } + - { ruby-version: '3.4', gemfile: rails_7.2 } + - { ruby-version: '3.3', gemfile: rails_7.1 } + - { ruby-version: '3.3', gemfile: rails_7.0 } + # backward compat — explicit V61 module (Ruby 3.2 so i18n's Fiber + # storage is available) + - { ruby-version: '3.2', gemfile: rails_6.1 } env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile EACO_AR_CONFIG: ./features/active_record.github.yml steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - uses: ankane/setup-postgres@v1 with: - postgres-version: ${{ matrix.postgres-version }} + postgres-version: 16 database: eaco - - name: Decide RubyGems - id: setup-params - run: | - rv="${{ matrix.ruby-version }}" - gf="${{ matrix.gemfile }}" - - # Default RubyGems - rubygems=default - - # Only for: - # - Ruby 2.3 + rails_3.2 - # - Ruby 2.3 + rails_4.2 - # - Ruby 2.4 + rails_4.2 - if { [ "$rv" = "2.3" ] && { [ "$gf" = "rails_3.2" ] || [ "$gf" = "rails_4.2" ]; }; } || - { [ "$rv" = "2.4" ] && [ "$gf" = "rails_4.2" ]; }; then - rubygems=2.7.11 - fi - - echo "version=$rubygems" >> "$GITHUB_OUTPUT" - - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - rubygems: ${{ steps.setup-params.outputs.version }} - name: Run Specs - run: | - bundle exec rspec + run: bundle exec rspec - name: Run Cucumber features - run: | - bundle exec cucumber -f pretty + run: bundle exec cucumber -f progress diff --git a/features/authorization_parse_error.feature b/features/authorization_parse_error.feature index 5724225..ea150a3 100644 --- a/features/authorization_parse_error.feature +++ b/features/authorization_parse_error.feature @@ -10,7 +10,7 @@ Feature: Authorization rules error handling """ Then I should receive a DSL error SyntaxError saying """ - \(feature\):1: syntax error, unexpected '=', expecting end-of-input + \(feature\):1:.+unexpected '=' """ Scenario: Referencing a non-existing model diff --git a/lib/eaco/adapters/active_record/compatibility.rb b/lib/eaco/adapters/active_record/compatibility.rb index 1ba130e..02a6706 100644 --- a/lib/eaco/adapters/active_record/compatibility.rb +++ b/lib/eaco/adapters/active_record/compatibility.rb @@ -74,13 +74,24 @@ def support_module # No exact Vxx module: Active Record 7.0+ all share the same # requirements, so fall back to {Modern} rather than raising on every # new Rails release. Genuinely unknown/old versions still raise. - return Modern if ::ActiveRecord::VERSION::MAJOR >= 7 + # The major is taken from the looked-up version (so a stubbed + # active_record_version is honoured), not the live constant. + return Modern if active_record_major >= 7 raise Eaco::Error, <<-EOF Unsupported Active Record version: #{active_record_version} EOF end + ## + # @return [Integer] the major of the {#active_record_version} being + # looked up. The minor is always a single digit in Rails, so the + # major is everything but the last character of the joined version. + # + def active_record_major + active_record_version.to_s[0..-2].to_i + end + ## # @return [String] "V" with {.active_record_version} appended. # diff --git a/spec/eaco/acl_spec.rb b/spec/eaco/acl_spec.rb index 1c9f876..d25bf6a 100644 --- a/spec/eaco/acl_spec.rb +++ b/spec/eaco/acl_spec.rb @@ -167,7 +167,9 @@ subject { acl.inspect } - it { expect(subject).to eq('#:bar}>') } + # Hash#inspect spacing changed in Ruby 3.4 ("a"=>1 -> "a" => 1); derive the + # expectation from the same Ruby so it holds on every version. + it { expect(subject).to eq("# :bar }.inspect }>") } end describe '#pretty_inspect' do @@ -177,7 +179,7 @@ subject { acl.pretty_inspect } - it { expect(subject).to eq("Eaco::ACL\n{\"foo\"=>:bar}\n") } + it { expect(subject).to eq("Eaco::ACL\n#{ { 'foo' => :bar }.pretty_inspect }") } end end From 8b78f0775b32a4150df1f2f1dbf90a610d19338f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Fri, 19 Jun 2026 10:31:19 +0200 Subject: [PATCH 3/7] CI: drop the redundant cucumber profile cucumber's ProfileLoader runs .config/cucumber.yml through ERB at startup, and on Ruby 3.4 the cucumber version CI resolves calls the removed 3-argument ERB.new, so every cucumber run aborted with "could not be parsed with ERB: wrong number of arguments (given 3, expected 1)" before any feature ran. The profile only set `--format progress`, which the CI command passes explicitly, so the file is redundant. Removing it sidesteps the ERB parse on every cucumber version. Suite still green locally (33 scenarios / 253 steps). Co-Authored-By: Claude Opus 4.8 (1M context) --- .config/cucumber.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .config/cucumber.yml diff --git a/.config/cucumber.yml b/.config/cucumber.yml deleted file mode 100644 index ecb6857..0000000 --- a/.config/cucumber.yml +++ /dev/null @@ -1 +0,0 @@ -default: --format progress From 286f18d2be98d2cebac8f21dedfafc9b08e7b30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Fri, 19 Jun 2026 10:33:21 +0200 Subject: [PATCH 4/7] Pin cucumber and declare ostruct/base64/bigdecimal for the test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unpinned, the resolver chose a different cucumber per matrix row — 10.x on some rows (profile ERB breaks on Ruby 3.4) and the ancient 3.2.0 on Ruby 4.0.5, which needs ostruct and failed to load. Pin cucumber to ~> 9.2 for a consistent, Ruby-4-clean version across the matrix, and declare ostruct/base64/bigdecimal, which left the default gems on Ruby 3.4+/4.0 and are pulled in by the test toolchain. Co-Authored-By: Claude Opus 4.8 (1M context) --- eaco.gemspec | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/eaco.gemspec b/eaco.gemspec index d227ee9..142e508 100644 --- a/eaco.gemspec +++ b/eaco.gemspec @@ -25,7 +25,10 @@ Gem::Specification.new do |spec| spec.add_development_dependency "appraisal" spec.add_development_dependency "rspec" spec.add_development_dependency "guard-rspec" - spec.add_development_dependency "cucumber" + # Pin cucumber: unpinned, the resolver picks wildly different versions per row + # (10.x on some, the ancient 3.2.0 on Ruby 4) — 3.2.0 needs ostruct and the + # profile ERB path breaks on Ruby 3.4. ~> 9.2 is consistent and Ruby-4 clean. + spec.add_development_dependency "cucumber", "~> 9.2" spec.add_development_dependency "guard-cucumber" spec.add_development_dependency "yard-cucumber" spec.add_development_dependency "coveralls" @@ -33,4 +36,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency "multi_json" spec.add_development_dependency "rails" spec.add_development_dependency "pg" + # No longer default gems on Ruby 3.4+/4.0 — needed by the test toolchain. + spec.add_development_dependency "ostruct" + spec.add_development_dependency "base64" + spec.add_development_dependency "bigdecimal" end From 1e3595abd1388671b7926ee5391d3bde57661747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Fri, 19 Jun 2026 10:41:06 +0200 Subject: [PATCH 5/7] CI: keep actions/checkout@v6 (was wrongly downgraded to v4) The legacy ci.yml already used checkout@v6; the rewrite dragged it back to v4. Restore v6 and bump the smoke workflow to match (v4 also trips the Node 20 deprecation warning). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 2 +- .github/workflows/smoke.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a59f596..f0c9a90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: EACO_AR_CONFIG: ./features/active_record.github.yml steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ankane/setup-postgres@v1 with: diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index c09a38e..4c41601 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -33,7 +33,7 @@ jobs: RAILS_VERSION: ${{ matrix.rails }} SQLITE3_VERSION: ${{ matrix.sqlite3 }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} From 7bd1527d2119f0153d02aadfea71a39cac1a13c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Fri, 19 Jun 2026 10:44:56 +0200 Subject: [PATCH 6/7] CI: restore Ruby 2.7 / 3.0 / 3.1 backward-compat rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring back old-Ruby coverage (the rewrite had narrowed to a 3.2 floor). The modern test toolchain fights old Ruby, so the gemspec now pins per-RUBY_VERSION: cucumber ~> 8.0 on Ruby < 3.0 (9.x needs 3.0+) and i18n < 1.15 on Ruby < 3.2 (1.15's Fiber storage needs 3.2+, which is what broke Rails 6.1 there). The ostruct/base64/bigdecimal dev deps are scoped to Ruby >= 3.4 so older rows don't pull a Ruby-3+ build. Ruby 2.1–2.6 stay out: Bundler 2 requires Ruby >= 2.3, and 2.3–2.6 no longer assemble on current GitHub runners. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 10 ++++++++-- eaco.gemspec | 23 +++++++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0c9a90..5aeb7b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,15 @@ jobs: - { ruby-version: '3.4', gemfile: rails_7.2 } - { ruby-version: '3.3', gemfile: rails_7.1 } - { ruby-version: '3.3', gemfile: rails_7.0 } - # backward compat — explicit V61 module (Ruby 3.2 so i18n's Fiber - # storage is available) + # backward compat — explicit Vxx modules. i18n < 1.15 and cucumber 8.x + # are held back (gemspec) for Ruby < 3.2 / < 3.0. Ruby 2.1–2.6 are not + # tested: Bundler 2 needs Ruby >= 2.3 and 2.3–2.6 no longer assemble on + # current runners. - { ruby-version: '3.2', gemfile: rails_6.1 } + - { ruby-version: '3.1', gemfile: rails_6.1 } + - { ruby-version: '3.0', gemfile: rails_6.1 } + - { ruby-version: '2.7', gemfile: rails_6.1 } + - { ruby-version: '2.7', gemfile: rails_5.2 } env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/eaco.gemspec b/eaco.gemspec index 142e508..1170335 100644 --- a/eaco.gemspec +++ b/eaco.gemspec @@ -27,8 +27,13 @@ Gem::Specification.new do |spec| spec.add_development_dependency "guard-rspec" # Pin cucumber: unpinned, the resolver picks wildly different versions per row # (10.x on some, the ancient 3.2.0 on Ruby 4) — 3.2.0 needs ostruct and the - # profile ERB path breaks on Ruby 3.4. ~> 9.2 is consistent and Ruby-4 clean. - spec.add_development_dependency "cucumber", "~> 9.2" + # profile ERB path breaks on Ruby 3.4. ~> 9.2 is consistent and Ruby-4 clean, + # but cucumber 9 needs Ruby >= 3.0; the 2.7 row uses the last 8.x line. + if RUBY_VERSION >= '3.0' + spec.add_development_dependency "cucumber", "~> 9.2" + else + spec.add_development_dependency "cucumber", "~> 8.0" + end spec.add_development_dependency "guard-cucumber" spec.add_development_dependency "yard-cucumber" spec.add_development_dependency "coveralls" @@ -36,8 +41,14 @@ Gem::Specification.new do |spec| spec.add_development_dependency "multi_json" spec.add_development_dependency "rails" spec.add_development_dependency "pg" - # No longer default gems on Ruby 3.4+/4.0 — needed by the test toolchain. - spec.add_development_dependency "ostruct" - spec.add_development_dependency "base64" - spec.add_development_dependency "bigdecimal" + # Left the default gems on Ruby 3.4+ — declare them there (and only there, so + # older rows don't pull a Ruby-3+ build of them). + if RUBY_VERSION >= '3.4' + spec.add_development_dependency "ostruct" + spec.add_development_dependency "base64" + spec.add_development_dependency "bigdecimal" + end + # i18n 1.15 uses Fiber storage (needs Ruby 3.2+); hold it back on the older + # backward-compat rows so Rails 6.1 boots there. + spec.add_development_dependency "i18n", "< 1.15" if RUBY_VERSION < '3.2' end From 8cc09dace05f09d1beb4eb82203a98996bf2d480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Fri, 19 Jun 2026 18:21:06 +0200 Subject: [PATCH 7/7] Parse authorization rules in to_prepare, not at initializer time The rules reference application models (e.g. ::Dossier). Under Zeitwerk (Rails 7+) those can't be autoloaded during initialization, so parsing inline in the 'eaco.parse_rules' initializer raised 'uninitialized constant Dossier' on boot. Move the parse into config.to_prepare, which runs after the app is initialized (once at boot in every env, again on each dev reload) with the autoloaders ready. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/eaco/railtie.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/eaco/railtie.rb b/lib/eaco/railtie.rb index f92fd02..99bd813 100644 --- a/lib/eaco/railtie.rb +++ b/lib/eaco/railtie.rb @@ -17,17 +17,14 @@ class Railtie < ::Rails::Railtie # # @!method parse_rules # - initializer 'eaco.parse_rules' do + initializer 'eaco.parse_rules' do |app| # :nocov: - Eaco.parse_default_rules_file! - - unless Rails.configuration.cache_classes - if defined? ActiveSupport::Reloader - ActiveSupport::Reloader.to_prepare { Eaco.parse_default_rules_file! } - else - ActionDispatch::Reloader.to_prepare { Eaco.parse_default_rules_file! } - end - end + # Parse in a to_prepare hook, not inline: the rules reference application + # models (e.g. ::Dossier), which under Zeitwerk (Rails 7+) cannot be + # autoloaded during initialization. to_prepare runs after the app is + # initialized (once at boot in every env, and again on each dev reload), + # by which point the autoloaders are ready. + app.config.to_prepare { Eaco.parse_default_rules_file! } # :nocov: end