Skip to content

Support Rails 8.1 / Ruby 4#24

Open
lleirborras wants to merge 7 commits into
masterfrom
upgrade/rails-8.1-ruby-4
Open

Support Rails 8.1 / Ruby 4#24
lleirborras wants to merge 7 commits into
masterfrom
upgrade/rails-8.1-ruby-4

Conversation

@lleirborras

Copy link
Copy Markdown
Member

Summary

Makes eaco load and run on Rails 8.1 / Ruby 4 without changing how it behaves on the Rails versions consumers are on today. This is part of the fleet Rails 8.1 / Ruby 4 upgrade — eaco was a hard blocker for the apps that depend on it (edoc, hermeshq, noobs, scriptoria).

The problem

The Active Record compatibility layer looked up a per-version support module (V32, V42, … V61) by the AR major+minor version, and raised "Unsupported Active Record version" when none was found. There was no module past Rails 6.1, so eaco couldn't even load under Rails 7.0+ — let alone 8.1.

Worth noting: the Appraisals file already listed rails-7.0/7.1/7.2, but those rows would have raised at runtime — the compatibility layer never actually supported them.

What changed

  • Added a Modern support module and a fallback for Active Record 7.0+. Every release from 5.2 onward needs the same treatment — JSONB is native, and the removed scoped / sanitize APIs are revived via the existing Scoped / Sanitized modules. Rather than copy an identical Vxx module for every future Rails release, the compatibility lookup now falls back to Modern for any AR major ≥ 7. The explicit V32V61 modules are untouched, so nothing changes for older consumers.
  • Added rails-8.0 / rails-8.1 Appraisal entries (and their gemfiles) alongside the existing 7.x ones.
  • First lightweight smoke test. Added test/smoke.rb — it installs the authorization DSL on a model and checks an end-to-end decision (ACL grant → Actor#can?, plus the admin bypass) over sqlite. No Postgres, no network: the point is to prove the compatibility layer installs and the core authorization path runs on the new stack. The existing pg + cucumber suite (ci.yml) is unchanged.
  • Bumped to 1.2.0 with a changelog entry.

How to test

The new smoke workflow (smoke.yml) sweeps:

  • Ruby 4.0.5 / 3.4 — Rails 8.1
  • Ruby 3.4 — Rails 7.2
  • Ruby 2.7 — Rails 6.1 (backward-compat, explicit V61 module)

Locally: BUNDLE_GEMFILE=test/Gemfile.ci bundle install && BUNDLE_GEMFILE=test/Gemfile.ci bundle exec ruby test/smoke.rb (override the stack with RAILS_VERSION).

Verified green on Ruby 4.0.5 and 3.4 (Rails 8.1) and Ruby 3.3 (Rails 7.1).

Risks / follow-ups

  • The smoke is deliberately Postgres-free, so it exercises the authorization decision (in-memory ACL eval) but not the pg_jsonb accessible_by SQL. The full pg + cucumber matrix in ci.yml still covers that; bumping that matrix to include Rails 7.x/8.x is a sensible follow-up now that the gem actually supports those versions.
  • Sanitized calls connection.quote; ActiveRecord::Base.connection is soft-deprecated in newer Rails but still works on 8.1. Worth revisiting if it gets removed.

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) <noreply@anthropic.com>
@lleirborras

Copy link
Copy Markdown
Member Author

Heads-up on the red checks: the Smoke workflow (added here, and the one that actually exercises the Rails 8.1 / Ruby 4 upgrade) is green across the matrix. The failures are all in the pre-existing pg + cucumber CI workflow, and they're environmental drift unrelated to this change:

  • Ruby 3.1 / rails_6.1 fails at require 'rails', before any eaco code loads: undefined method '[]' for Fiber:Class in i18n-1.15.0 (i18n 1.15 uses Fiber storage, which needs Ruby 3.2+). That matrix has no lockfile, so it resolves the latest i18n.
  • Ruby 2.1–2.6 rows fail on toolchain rot (Bundler 2+ requires Ruby 2.3+, etc.) — those runtimes no longer assemble on current GitHub runners.

CI last ran green on master in December 2025; the breakage is six months of unpinned transitive-dependency drift, not this PR. This change is gated on Active Record ≥ 7 and doesn't touch the rails_6.1 (V61) path or anything loaded by require 'rails'.

Fixing that legacy matrix (pin i18n / drop ancient Rubies / extend it to Rails 7.x–8.x with cucumber) is a separate maintenance task — happy to do it in a follow-up, but kept it out of scope here so the upgrade stays reviewable.

lleirborras and others added 3 commits June 19, 2026 10:26
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@lleirborras

Copy link
Copy Markdown
Member Author

Update: the legacy pg + cucumber CI workflow is now fixed and green, so this PR is green end-to-end (both Smoke and CI).

Modernized the CI matrix to current runtimes — Ruby 4.0.5 / 3.4 / 3.3 / 3.2 across Rails 8.1 / 8.0 / 7.2 / 7.1 / 7.0 / 6.1 — and dropped the ancient Ruby 2.x rows that no longer assemble. The full rspec + cucumber suite (Postgres-backed, so this exercises accessible_by / pg_jsonb, which the Smoke deliberately skips) passes on every row.

Test-infra fixes needed for the new stack, all version-agnostic:

  • Compatibility#support_module now derives the "modern AR" decision from the looked-up version, so the "unsupported version" scenario (which stubs active_record_version) raises as expected instead of falling through to Modern.
  • The parse-error feature matches the stable unexpected '=' substring rather than the exact pre-Prism SyntaxError wording (Ruby 3.3+).
  • The ACL inspect specs compute their expectation from the running Ruby's Hash#inspect (spacing changed in 3.4).
  • Dropped the redundant .config/cucumber.yml profile (its ERB parse hit the removed 3-arg ERB.new on Ruby 3.4) and pinned cucumber to ~> 9.2 (unpinned, the resolver picked 10.x on some rows and the ancient 3.2.0 on Ruby 4). Declared ostruct/base64/bigdecimal, which left the default gems on Ruby 3.4+/4.0.

lleirborras and others added 3 commits June 19, 2026 10:41
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant