From 5e4a9e418f9545bd8f271f66efacf65c4d64e8b1 Mon Sep 17 00:00:00 2001 From: "Aestimo K." Date: Fri, 26 Jun 2026 14:41:01 +0300 Subject: [PATCH 1/7] Phase 1: modernize for Rails 7.2-8.1 and Ruby 3.2-4.0 Adapter support for modern Rails was the blocker: Eaco's Active Record compatibility layer only knew versions up to 6.1 and hard-failed with "Unsupported Active Record version" on anything newer, so the gem never actually ran on Rails 7+. Add compat modules V70-V81 (mirroring V61's Scoped + Sanitized behaviour) and register their autoloads. Make the suite green on Ruby 4.0: - Add ostruct dev dependency (no longer a default gem on Ruby 3.5/4.0; required by Cucumber). - Derive ACL#inspect / #pretty_inspect spec expectations from Ruby's own Hash formatting (Ruby 3.4+ adds spaces around `=>`). - Relax the SyntaxError expectation for the Prism parser (Ruby 4.0). - Remove .config/cucumber.yml: Cucumber 3.x crashes loading profiles via the removed ERB.new positional API on Ruby 3.4+. Drop legacy support and rework the test matrix: - Floor at Ruby 3.2 / Rails 7.2; remove Rails 3.2-7.1 gemfiles and trim Appraisals; add rails_8.0 and rails_8.1 gemfiles. - Rewrite CI matrix to Ruby 3.2-4.0 x Rails 7.2/8.0/8.1; fix master->main triggers. - Set required_ruby_version >= 3.2, add gem metadata, bump to 2.0.0.beta1. Reposition the README around authentication-vs-authorization and add a comparison vs Pundit/CanCanCan/Action Policy. Add PROJECT_TRACKER.md as the working hub for the v2.0 effort. Local result (Ruby 4.0.5 + PG 18): RSpec 21/0, Cucumber 33/33 on Rails 7.2.3, 8.0.5 and 8.1.3. Co-Authored-By: Claude Opus 4.8 --- .config/cucumber.yml | 1 - .github/workflows/ci.yml | 163 ++------------ Appraisals | 67 +----- CHANGELOG.md | 26 ++- PROJECT_TRACKER.md | 210 ++++++++++++++++++ README.md | 42 ++++ eaco.gemspec | 14 +- features/authorization_parse_error.feature | 2 +- gemfiles/rails_3.2.gemfile | 10 - gemfiles/rails_4.0.gemfile | 9 - gemfiles/rails_4.1.gemfile | 9 - gemfiles/rails_4.2.gemfile | 11 - gemfiles/rails_5.0.gemfile | 10 - gemfiles/rails_5.1.gemfile | 10 - gemfiles/rails_5.2.gemfile | 9 - gemfiles/rails_7.0.gemfile | 7 - gemfiles/rails_7.1.gemfile | 7 - .../{rails_6.1.gemfile => rails_8.0.gemfile} | 2 +- .../{rails_6.0.gemfile => rails_8.1.gemfile} | 2 +- .../adapters/active_record/compatibility.rb | 5 + .../active_record/compatibility/v70.rb | 30 +++ .../active_record/compatibility/v71.rb | 30 +++ .../active_record/compatibility/v72.rb | 30 +++ .../active_record/compatibility/v80.rb | 30 +++ .../active_record/compatibility/v81.rb | 30 +++ lib/eaco/version.rb | 2 +- spec/eaco/acl_spec.rb | 6 +- 27 files changed, 483 insertions(+), 291 deletions(-) delete mode 100644 .config/cucumber.yml create mode 100644 PROJECT_TRACKER.md delete mode 100644 gemfiles/rails_3.2.gemfile delete mode 100644 gemfiles/rails_4.0.gemfile delete mode 100644 gemfiles/rails_4.1.gemfile delete mode 100644 gemfiles/rails_4.2.gemfile delete mode 100644 gemfiles/rails_5.0.gemfile delete mode 100644 gemfiles/rails_5.1.gemfile delete mode 100644 gemfiles/rails_5.2.gemfile delete mode 100644 gemfiles/rails_7.0.gemfile delete mode 100644 gemfiles/rails_7.1.gemfile rename gemfiles/{rails_6.1.gemfile => rails_8.0.gemfile} (79%) rename gemfiles/{rails_6.0.gemfile => rails_8.1.gemfile} (79%) create mode 100644 lib/eaco/adapters/active_record/compatibility/v70.rb create mode 100644 lib/eaco/adapters/active_record/compatibility/v71.rb create mode 100644 lib/eaco/adapters/active_record/compatibility/v72.rb create mode 100644 lib/eaco/adapters/active_record/compatibility/v80.rb create mode 100644 lib/eaco/adapters/active_record/compatibility/v81.rb 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a12ece..420d459 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] permissions: contents: read @@ -15,124 +15,30 @@ jobs: 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 + - ruby-version: '3.2' + gemfile: rails_7.2 + postgres-version: 16 + - ruby-version: '3.2' + gemfile: rails_8.0 + postgres-version: 16 + + - ruby-version: '3.3' + gemfile: rails_8.0 + postgres-version: 16 + - ruby-version: '3.3' + gemfile: rails_8.1 + postgres-version: 16 + + - ruby-version: '3.4' + gemfile: rails_8.1 + postgres-version: 16 + + - ruby-version: '4.0' + gemfile: rails_8.1 + postgres-version: 16 env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile @@ -146,32 +52,11 @@ jobs: postgres-version: ${{ matrix.postgres-version }} 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: | diff --git a/Appraisals b/Appraisals index 5c9b211..0c53a10 100644 --- a/Appraisals +++ b/Appraisals @@ -1,66 +1,11 @@ -appraise 'rails-3.2' do - gem 'rails', '~> 3.2.0' - gem 'pg', '~> 0.21' - gem 'activerecord-postgres-json' - gem 'term-ansicolor', '~> 1.7.0' -end - -appraise 'rails-4.0' do - gem 'rails', '~> 4.0.0' - gem 'pg', '~> 0.21' - gem 'term-ansicolor', '~> 1.7.0' -end - -appraise 'rails-4.1' do - gem 'rails', '~> 4.1.0' - gem 'pg', '~> 0.21' - gem 'term-ansicolor', '~> 1.7.0' -end - -appraise 'rails-4.2' do - gem 'bigdecimal', '< 2' - gem 'loofah', '~> 2.20.0' - gem 'rails', '~> 4.2.0' - gem 'pg', '~> 0.21' - gem 'term-ansicolor', '~> 1.7.0' -end - -appraise 'rails-5.0' do - gem 'loofah', '~> 2.20.0' - gem 'rails', '~> 5.0.0' - gem 'pg', '~> 0.21' - gem 'term-ansicolor', '~> 1.7.0' -end - -appraise 'rails-5.1' do - gem 'loofah', '~> 2.20.0' - gem 'rails', '~> 5.1.0' - gem 'pg', '~> 0.21' - gem 'term-ansicolor', '~> 1.7.0' -end - -appraise 'rails-5.2' do - gem 'loofah', '~> 2.20.0' - gem 'rails', '~> 5.2.0' - gem 'term-ansicolor', '~> 1.7.0' -end - -appraise 'rails-6.0' do - gem 'rails', '~> 6.0.0' -end - -appraise 'rails-6.1' do - gem 'rails', '~> 6.1.0' -end - -appraise 'rails-7.0' do - gem 'rails', '~> 7.0.0' +appraise 'rails-7.2' do + gem 'rails', '~> 7.2.0' end -appraise 'rails-7.1' do - gem 'rails', '~> 7.1.0' +appraise 'rails-8.0' do + gem 'rails', '~> 8.0.0' end -appraise 'rails-7.2' do - gem 'rails', '~> 7.2.0' +appraise 'rails-8.1' do + gem 'rails', '~> 8.1.0' end diff --git a/CHANGELOG.md b/CHANGELOG.md index d25b9b7..0775d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/) -## Unreleased +## Unreleased (2.0.0.beta1) + +### Added +* Active Record compatibility modules for Rails 7.0, 7.1, 7.2, 8.0 and 8.1 + (`V70`–`V81`). The adapter previously rejected any Active Record newer than + 6.1 with "Unsupported Active Record version". + +### Changed +* **Modernization (Phase 1):** dropped support for Rails < 7.2 and Ruby < 3.2. +* Supported matrix is now Ruby 3.2–4.0 against Rails 7.2, 8.0 and 8.1. + Full suite (RSpec + Cucumber) is green on Ruby 4.0 against all three. +* Trimmed `Appraisals` and `gemfiles/` to the supported Rails versions; added + `gemfiles/rails_8.0.gemfile` and `gemfiles/rails_8.1.gemfile`. +* CI now tests Ruby 3.2/3.3/3.4/4.0 and triggers on the `main` branch. +* Set `required_ruby_version >= 3.2` and added gem metadata. +* Added `ostruct` as a development dependency (no longer a default gem on + Ruby 3.5/4.0; required by Cucumber). + +### Fixed +* Ruby 3.4+ compatibility in specs: `Eaco::ACL#inspect` / `#pretty_inspect` + expectations now track Ruby's own `Hash` formatting (spaces around `=>`). +* Ruby 4.0 (Prism parser) compatibility: relaxed the `SyntaxError` message + expectation in the authorization-parse-error feature. +* Removed `.config/cucumber.yml`: Cucumber 3.x cannot parse profiles via the + removed `ERB.new` positional API on Ruby 3.4+. ### Fixed * Fix YARD documentation warnings: diff --git a/PROJECT_TRACKER.md b/PROJECT_TRACKER.md new file mode 100644 index 0000000..8a2d6d8 --- /dev/null +++ b/PROJECT_TRACKER.md @@ -0,0 +1,210 @@ +# Eaco v2.0 — Project Tracker + +> Working hub for the modernization effort. Mirrors the four GitHub issues +> (Phases 1–4) but is the **source of truth while we work**; GitHub issues are +> synced from here later (decision: track locally for now). + +Last updated: 2026-06-26 + +--- + +## 1. What Eaco is (one-liner) + +An **Attribute-Based Access Control (ABAC)** *authorization* framework for Ruby: +authorization decisions live in the **database** as per-record ACLs +(`designator → role`), not in code. You ask `actor.can?(:edit, document)` and +fetch `Document.accessible_by(user)` with a single indexed query. + +## 2. Positioning (the "why now") + +**Authentication ≠ Authorization.** Rails 8's built-in auth generator and Devise +answer *who are you?* (sessions, passwords, 2FA). Eaco answers *what can you do?* +They are **adjacent layers** — Eaco pairs *with* Devise / Rails 8 auth, it does +not compete with them. Rails 8 shipping auth pushes more apps to the +authorization question sooner, which is Eaco's space. + +**Eaco's real peers** are Pundit / CanCanCan / Action Policy (all RBAC, code-defined). +Eaco's defensible niche vs them: + +| Capability | Pundit / CanCanCan / Action Policy | Eaco | +|---|---|---| +| Model | RBAC (rules in code) | **ABAC (ACLs in data)** | +| Per-record, runtime-editable sharing | bolt-on / hand-rolled | **native** (`doc.grant :reader, :user, 42`) | +| "List everything user X can see" | N+1 / custom scopes | **one indexed hash-key query** | + +**The moat = dynamic, per-record, end-user-managed permissions** (Google-Docs-style +sharing, multi-tenant org hierarchies) + efficient `accessible_by`. v2.0 messaging +must commit loudly to this niche instead of pitching a generic "authorization +framework" (which loses a head-to-head with Pundit on simple role apps). + +**Honest boundaries (state these in docs):** +- Simple "admins vs users" apps → Pundit/Action Policy are lighter; use them. +- The rising alternative for ABAC/ReBAC is *externalized* authz (OpenFGA/Zanzibar, + OSO/Cedar). Eaco's pitch vs those: "stay in your Postgres, no extra service, + idiomatic Ruby." Name this boundary in the README. + +## 3. Version targets (decided 2026-06-26) + +- **Ruby floor:** 3.2 (matches Rails 8.0/8.1 minimum; lets us drop legacy shims). +- **Ruby ceiling:** 4.0, open-ended. (Ruby 4.0.0 shipped 2025-12-25; evolutionary, + mostly backward-compatible — Box & ZJIT are experimental. Eaco audited clean + against all 4.0 removals: no `cgi`, no `ObjectSpace._id2ref`, no leading-pipe + `open`, no `Ractor`; uses `Set` only via public API.) +- **Rails:** 7.2, 8.0, 8.1. +- **Next release:** `2.0.0.beta1`. + +### CI matrix + +| Ruby | Rails | +|------|-------| +| 3.2 | 7.2, 8.0 | +| 3.3 | 8.0, 8.1 | +| 3.4 | 8.1 | +| 4.0 | 8.1 | + +--- + +## Phase 1 — Modernization → milestone `v2.0.0-beta` (GitHub #1) + +### 1.1 Rails 7.x/8.x compatibility +- [x] Railtie uses `ActiveSupport::Reloader.to_prepare` — *already present in `lib/eaco/railtie.rb`* +- [x] **AR adapter now supports Rails 7.0–8.1** — added compat modules `V70`–`V81` + (`lib/eaco/adapters/active_record/compatibility/`); full suite green on AR + 7.2.3 / 8.0.5 / 8.1.3 under Ruby 4.0. *Was hard-failing "Unsupported Active + Record version: 81".* +- [ ] Verify modern PG jsonb handling is optimal (works; revisit for GIN-index/perf in Phase 2) +- [ ] Zeitwerk autoloader: validate inside a real Rails app boot (gem's own suite passes; railtie path not exercised by Cucumber) +- [ ] Drop old `cache_classes` reliance in railtie → `enable_reloading`/`eager_load` (still works via alias; deprecation cleanup) +- [x] Remove deprecated Rails 3.x/4.x — *gemfiles + Appraisals trimmed; floor now Rails 7.2* + +### 1.2 Ruby 3.x AND 4.0 compatibility *(reworded — was "Ruby 3.x")* +- [x] **Suite green on Ruby 4.0.5** (local: RSpec 21/0, Cucumber 33/33 on Rails 7.2/8.0/8.1) +- [x] Ruby 3.4+ `Hash#inspect` spacing — specs now derive expectation from Ruby's own output +- [x] Ruby 4.0 Prism `SyntaxError` wording — relaxed feature expectation +- [ ] Audit keyword-argument usage (Ruby 3.0+ separation) — no failures surfaced, but not exhaustively audited +- [ ] Add `# frozen_string_literal: true` across `lib/` *(deferred: mechanical, needs string-mutation check; ~10 files missing it)* +- [ ] Pattern matching where it improves the DSL (optional) +- [x] Test on Ruby 3.2, 3.3, 3.4, 4.0 (CI matrix) — *local validation done on 4.0 only; CI covers the rest* + +### 1.3 Dependency updates +- [ ] Bump `bundler` dev dep (was `~> 1.6`) and modernize rspec/cucumber/yard +- [ ] Add `spec.required_ruby_version = '>= 3.2'` to gemspec — *DONE in this pass* +- [ ] Remove deprecated gems (coveralls → simplecov+coverage service? TBD) +- [x] Migrate Appraisals → checked-in `gemfiles/` matrix *(Appraisals trimmed; see open decision on full removal)* + +### 1.4 CI/CD modernization +- [x] Travis → GitHub Actions *(done earlier)* +- [x] New Ruby/Rails matrix (3.2–4.0 × 7.2–8.1) + fix `master`→`main` triggers +- [ ] Automated gem releases (release workflow / trusted publishing) + +--- + +## Phase 2 — New Features → milestone `v2.0.0` (GitHub #2) + +> **Reprioritized 2026-06-26:** weight toward **deepening the ABAC moat** over +> **breadth**. Adapters that just add another datastore (esp. MongoDB) are +> deprioritized; work that makes dynamic per-record ABAC + `accessible_by` +> better/faster/easier is promoted. + +### Tier 1 — Deepen the moat (PRIORITIZE) +- [ ] **Hierarchical designators** (org → dept → team → user) — core to the multi-tenant/org ABAC story +- [ ] **OAuth/OIDC/JWT-claims designator** — harvest designators from tokens; modern auth integration, complements Devise/Rails 8 auth +- [ ] **PostgreSQL jsonb GIN indexes for `accessible_by`** — makes the killer feature actually scale; this *is* the moat +- [ ] **DX: Rails generator** for common setups — lowers adoption friction +- [ ] **DX: rake tasks for ACL inspection/debugging** + console helpers +- [ ] **DX: actionable error messages** + +### Tier 2 — Breadth (DEPRIORITIZE / demand-gated) +- [ ] SQLite JSON1 adapter — *keep-ish:* low cost, lets people try Eaco without Postgres; good for dev/test +- [ ] MySQL/MariaDB JSON adapter — only if real demand +- [ ] Time-based designators (business hours) — good ABAC demo; candidate for plugin/cookbook rather than core +- [ ] IP/geo-location designators — same as above +- [ ] ~~MongoDB adapter~~ — **defer/drop** (was already a "stretch goal"); revisit only on concrete demand + +--- + +## Phase 3 — Documentation & Education (GitHub #3) + +> Frame migration guides as **"graduation from Pundit/CanCanCan"** (your sharing +> needs outgrew RBAC), not "replacement". Lead every doc with the auth-vs-authz +> distinction. + +### 3.1 Docs site +- [ ] Choose stack (GitHub Pages + VitePress/Jekyll) +- [ ] Getting-started guide +- [ ] Migration guide from Pundit/CanCanCan +- [ ] Full DSL reference with examples + +### 3.2 Tutorials +- [ ] 1: Basic — protect a Document +- [ ] 2: Multi-role (owner/editor/reader) +- [ ] 3: Group-based team permissions +- [ ] 4: Google-Docs-style sharing *(showcases the moat)* +- [ ] 5: ABAC for multi-tenant SaaS *(showcases the moat)* +- [ ] 6: Migrating from Pundit to Eaco + +### 3.3 Example apps +- [ ] Document management (Google Docs clone) +- [ ] Project management (Basecamp/Asana style) +- [ ] Multi-tenant SaaS with org hierarchy + +### 3.4 Integration guides +- [ ] **Devise** integration *(positioning-critical: auth + authz combo)* +- [ ] Hotwire/Turbo patterns +- [ ] API-only Rails +- [ ] Testing authz with RSpec + +--- + +## Phase 4 — Community & Sustainability (GitHub #4) + +### 4.1 Community +- [ ] Announcement blog post (lead with auth-vs-authz + the moat) +- [ ] Talk at a Ruby meetup/conference +- [ ] GitHub Discussions +- [ ] CONTRIBUTING.md + contribution guidelines + +### 4.2 Maintenance +- [ ] GitHub Sponsors +- [ ] Documented release process +- [ ] Public roadmap (this file → published) +- [ ] Identify/mentor co-maintainers + +--- + +## Open decisions / questions + +- [ ] **Upgrade Cucumber 3.2.0 → 9.x?** 3.2.0 (2018) works on Ruby 4.0 only after + removing the profile file, and emits deprecation warnings (`calling private + without arguments`, legacy formatter API). A modern Cucumber is the real fix + but changes the step-definition/World API — a dedicated follow-up task. + *(Removed `.config/cucumber.yml` as the interim unblock.)* +- [ ] **Full Appraisal removal?** Currently trimmed Appraisals + checked-in gemfiles + (both kept in sync). Decide whether to drop the `appraisal` dev dep entirely and + hand-maintain gemfiles, or keep Appraisal as the generator. +- [ ] **Repo home:** issues live on `iamaestimo/eaco`, but gemspec/README still point + at upstream `ifad/eaco`. Decide if this fork is the maintained home (affects + gemspec `homepage`, badges, README links). +- [ ] **`coveralls`** is unmaintained-ish; replace with `simplecov` + a current + coverage service? +- [ ] When to actually bump `VERSION` to `2.0.0.beta1` and cut a release. + +## GitHub issue sync notes (apply later when we sync) + +- **#1:** rename "1.2 Ruby 3.x Compatibility" → "Ruby 3.x **and 4.0**"; change + "test with 3.1, 3.2, 3.3" → "3.2, 3.3, 3.4, 4.0"; strengthen "remove 3.x/4.x" + to "floor = Rails 7.2 / Ruby 3.2". +- **#2:** restructure into Tier 1 (moat) / Tier 2 (breadth) as above; mark MongoDB + deferred. + +## Changelog of this effort + +- **2026-06-26:** Created tracker; repositioned README (auth-vs-authz + comparison); + Phase 1 mechanics — trimmed gemfiles/Appraisals to Rails 7.2/8.0/8.1, added + Rails 8.0/8.1 gemfiles, rewrote CI matrix (Ruby 3.2–4.0), fixed `master`→`main` + triggers, added `required_ruby_version` + version bump to `2.0.0.beta1`. +- **2026-06-26 (cont.):** Ran the suite locally on Ruby 4.0.5 + PG 18 and made it + green on Rails 7.2/8.0/8.1: added AR compat modules `V70`–`V81` (the big one — + adapter rejected AR > 6.1), added `ostruct` dev dep, fixed Ruby 3.4 `Hash#inspect` + and Ruby 4.0 Prism `SyntaxError` test expectations, removed `.config/cucumber.yml` + (Cucumber 3.x ERB-profile crash on Ruby 3.4+). Result: RSpec 21/0, Cucumber 33/33. diff --git a/README.md b/README.md index 5e4b40a..c207d97 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,48 @@ framework for Ruby. *"Aeacus telemon by user Ravenous at en.wikipedia.org - Public domain through Wikimedia Commons - https://commons.wikimedia.org/wiki/File:Aeacus_telemon.jpg"* +## Authentication vs. Authorization + +Eaco is an **authorization** library — it decides *what a user is allowed to do*. +It does **not** handle **authentication** (*who the user is*: logins, passwords, +sessions, 2FA). Eaco is designed to sit **on top of** your authentication layer, +whatever it is: + +- Rails 8's built-in authentication generator +- [Devise](https://github.com/heartcombo/devise), [Sorcery](https://github.com/Sorcery/sorcery), OmniAuth, etc. +- Your own OIDC/JWT/SSO setup + +Once you have a logged-in user, Eaco answers the next question: *what can this +user see and do?* + +## Why Eaco? (and when not to) + +Eaco's peers are authorization libraries like [Pundit](https://github.com/varvet/pundit), +[CanCanCan](https://github.com/CanCanCommunity/cancancan), and +[Action Policy](https://github.com/palkan/action_policy). The difference is the model: + +| | Pundit / CanCanCan / Action Policy | **Eaco** | +|---|---|---| +| Authorization model | **RBAC** — rules defined in code | **ABAC** — ACLs stored as data, per record | +| Where "who can access" lives | Ruby policy classes | The **database** (jsonb / JSON ACL on the row) | +| Per-record, end-user-editable sharing | bolt-on / hand-rolled | **native** — `document.grant :reader, :user, 42` | +| "List everything this user can see" | N+1 or custom scopes | **one indexed lookup** — `Document.accessible_by(user)` | + +**Reach for Eaco when** your permissions are *dynamic and per-record* — e.g. +Google-Docs-style sharing ("share this document with Bob and the reviewers +group"), team/folder collaboration, or multi-tenant SaaS with an organization +hierarchy — and when you need to efficiently fetch the set of records a user can +access. + +**Reach for Pundit / Action Policy instead when** your rules are simple and +role-based ("admins can do everything, users can edit their own posts"). For that, +a lightweight RBAC gem is the better fit. + +For very large, cross-service authorization needs you may also consider +externalized authorization systems (Zanzibar-style services such as OpenFGA, or +policy engines like OSO/Cedar). Eaco's trade-off versus those: it lives inside +your existing database with no extra service to run, and stays idiomatic Ruby. + ## Design Eaco provides your application's Resources discretionary access based on attributes. diff --git a/eaco.gemspec b/eaco.gemspec index d227ee9..ca848de 100644 --- a/eaco.gemspec +++ b/eaco.gemspec @@ -8,10 +8,20 @@ Gem::Specification.new do |spec| spec.version = Eaco::VERSION spec.authors = ["Marcello Barnaba"] spec.email = ["vjt@openssl.it"] - spec.summary = %q{Authorization framework} + spec.summary = %q{Attribute-Based Access Control (ABAC) authorization framework for Ruby} + spec.description = %q{Eaco is an ABAC authorization framework: authorization decisions live in the database as per-record ACLs (designator => role), with efficient extraction of the collection of resources accessible by an actor.} spec.homepage = "https://github.com/ifad/eaco" spec.license = "MIT" + spec.required_ruby_version = ">= 3.2" + + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md", + "rubygems_mfa_required" => "true" + } + spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) @@ -26,6 +36,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rspec" spec.add_development_dependency "guard-rspec" spec.add_development_dependency "cucumber" + # No longer a default gem as of Ruby 3.5/4.0; required by cucumber 3.x. + spec.add_development_dependency "ostruct" spec.add_development_dependency "guard-cucumber" spec.add_development_dependency "yard-cucumber" spec.add_development_dependency "coveralls" diff --git a/features/authorization_parse_error.feature b/features/authorization_parse_error.feature index 5724225..fe1a72a 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/gemfiles/rails_3.2.gemfile b/gemfiles/rails_3.2.gemfile deleted file mode 100644 index 5cd1698..0000000 --- a/gemfiles/rails_3.2.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 3.2.0" -gem "pg", "~> 0.21" -gem "activerecord-postgres-json" -gem "term-ansicolor", "~> 1.7.0" - -gemspec path: "../" diff --git a/gemfiles/rails_4.0.gemfile b/gemfiles/rails_4.0.gemfile deleted file mode 100644 index b41c245..0000000 --- a/gemfiles/rails_4.0.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 4.0.0" -gem "pg", "~> 0.21" -gem "term-ansicolor", "~> 1.7.0" - -gemspec path: "../" diff --git a/gemfiles/rails_4.1.gemfile b/gemfiles/rails_4.1.gemfile deleted file mode 100644 index d723e0d..0000000 --- a/gemfiles/rails_4.1.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 4.1.0" -gem "pg", "~> 0.21" -gem "term-ansicolor", "~> 1.7.0" - -gemspec path: "../" diff --git a/gemfiles/rails_4.2.gemfile b/gemfiles/rails_4.2.gemfile deleted file mode 100644 index 9ad0c05..0000000 --- a/gemfiles/rails_4.2.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "bigdecimal", "< 2" -gem "loofah", "~> 2.20.0" -gem "rails", "~> 4.2.0" -gem "pg", "~> 0.21" -gem "term-ansicolor", "~> 1.7.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.0.gemfile b/gemfiles/rails_5.0.gemfile deleted file mode 100644 index e485333..0000000 --- a/gemfiles/rails_5.0.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "loofah", "~> 2.20.0" -gem "rails", "~> 5.0.0" -gem "pg", "~> 0.21" -gem "term-ansicolor", "~> 1.7.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.1.gemfile b/gemfiles/rails_5.1.gemfile deleted file mode 100644 index d88dddc..0000000 --- a/gemfiles/rails_5.1.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "loofah", "~> 2.20.0" -gem "rails", "~> 5.1.0" -gem "pg", "~> 0.21" -gem "term-ansicolor", "~> 1.7.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile deleted file mode 100644 index 4b79e2a..0000000 --- a/gemfiles/rails_5.2.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "loofah", "~> 2.20.0" -gem "rails", "~> 5.2.0" -gem "term-ansicolor", "~> 1.7.0" - -gemspec path: "../" diff --git a/gemfiles/rails_7.0.gemfile b/gemfiles/rails_7.0.gemfile deleted file mode 100644 index 9af0ae3..0000000 --- a/gemfiles/rails_7.0.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 7.0.0" - -gemspec path: "../" diff --git a/gemfiles/rails_7.1.gemfile b/gemfiles/rails_7.1.gemfile deleted file mode 100644 index 35a0ba3..0000000 --- a/gemfiles/rails_7.1.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 7.1.0" - -gemspec path: "../" diff --git a/gemfiles/rails_6.1.gemfile b/gemfiles/rails_8.0.gemfile similarity index 79% rename from gemfiles/rails_6.1.gemfile rename to gemfiles/rails_8.0.gemfile index dd95a47..3b3765b 100644 --- a/gemfiles/rails_6.1.gemfile +++ b/gemfiles/rails_8.0.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 6.1.0" +gem "rails", "~> 8.0.0" gemspec path: "../" diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_8.1.gemfile similarity index 79% rename from gemfiles/rails_6.0.gemfile rename to gemfiles/rails_8.1.gemfile index 15b9b27..cedb65f 100644 --- a/gemfiles/rails_6.0.gemfile +++ b/gemfiles/rails_8.1.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 6.0.0" +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..a792d09 100644 --- a/lib/eaco/adapters/active_record/compatibility.rb +++ b/lib/eaco/adapters/active_record/compatibility.rb @@ -15,6 +15,11 @@ class Compatibility autoload :V52, 'eaco/adapters/active_record/compatibility/v52.rb' autoload :V60, 'eaco/adapters/active_record/compatibility/v60.rb' autoload :V61, 'eaco/adapters/active_record/compatibility/v61.rb' + autoload :V70, 'eaco/adapters/active_record/compatibility/v70.rb' + autoload :V71, 'eaco/adapters/active_record/compatibility/v71.rb' + autoload :V72, 'eaco/adapters/active_record/compatibility/v72.rb' + autoload :V80, 'eaco/adapters/active_record/compatibility/v80.rb' + autoload :V81, 'eaco/adapters/active_record/compatibility/v81.rb' autoload :Scoped, 'eaco/adapters/active_record/compatibility/scoped.rb' autoload :Sanitized, 'eaco/adapters/active_record/compatibility/sanitized.rb' diff --git a/lib/eaco/adapters/active_record/compatibility/v70.rb b/lib/eaco/adapters/active_record/compatibility/v70.rb new file mode 100644 index 0000000..89d8f67 --- /dev/null +++ b/lib/eaco/adapters/active_record/compatibility/v70.rb @@ -0,0 +1,30 @@ +module Eaco + module Adapters + module ActiveRecord + class Compatibility + + ## + # Rails 7.0 support module. + # + # JSONB works correctly, but we need +.scoped+ so we revive it through + # the {Scoped} support module. + # + # @see Scoped + # + # Sanitize has disappeared in favour of quote. + # + # @see Sanitized + # + module V70 + extend ActiveSupport::Concern + + included do + extend Scoped + extend Sanitized + end + end + + end + end + end +end diff --git a/lib/eaco/adapters/active_record/compatibility/v71.rb b/lib/eaco/adapters/active_record/compatibility/v71.rb new file mode 100644 index 0000000..6b3045c --- /dev/null +++ b/lib/eaco/adapters/active_record/compatibility/v71.rb @@ -0,0 +1,30 @@ +module Eaco + module Adapters + module ActiveRecord + class Compatibility + + ## + # Rails 7.1 support module. + # + # JSONB works correctly, but we need +.scoped+ so we revive it through + # the {Scoped} support module. + # + # @see Scoped + # + # Sanitize has disappeared in favour of quote. + # + # @see Sanitized + # + module V71 + extend ActiveSupport::Concern + + included do + extend Scoped + extend Sanitized + end + end + + end + end + end +end diff --git a/lib/eaco/adapters/active_record/compatibility/v72.rb b/lib/eaco/adapters/active_record/compatibility/v72.rb new file mode 100644 index 0000000..868b229 --- /dev/null +++ b/lib/eaco/adapters/active_record/compatibility/v72.rb @@ -0,0 +1,30 @@ +module Eaco + module Adapters + module ActiveRecord + class Compatibility + + ## + # Rails 7.2 support module. + # + # JSONB works correctly, but we need +.scoped+ so we revive it through + # the {Scoped} support module. + # + # @see Scoped + # + # Sanitize has disappeared in favour of quote. + # + # @see Sanitized + # + module V72 + extend ActiveSupport::Concern + + included do + extend Scoped + extend Sanitized + end + end + + end + end + end +end diff --git a/lib/eaco/adapters/active_record/compatibility/v80.rb b/lib/eaco/adapters/active_record/compatibility/v80.rb new file mode 100644 index 0000000..47cbb12 --- /dev/null +++ b/lib/eaco/adapters/active_record/compatibility/v80.rb @@ -0,0 +1,30 @@ +module Eaco + module Adapters + module ActiveRecord + class Compatibility + + ## + # Rails 8.0 support module. + # + # JSONB works correctly, but we need +.scoped+ so we revive it through + # the {Scoped} support module. + # + # @see Scoped + # + # Sanitize has disappeared in favour of quote. + # + # @see Sanitized + # + module V80 + extend ActiveSupport::Concern + + included do + extend Scoped + extend Sanitized + end + end + + end + end + end +end diff --git a/lib/eaco/adapters/active_record/compatibility/v81.rb b/lib/eaco/adapters/active_record/compatibility/v81.rb new file mode 100644 index 0000000..7e2e560 --- /dev/null +++ b/lib/eaco/adapters/active_record/compatibility/v81.rb @@ -0,0 +1,30 @@ +module Eaco + module Adapters + module ActiveRecord + class Compatibility + + ## + # Rails 8.1 support module. + # + # JSONB works correctly, but we need +.scoped+ so we revive it through + # the {Scoped} support module. + # + # @see Scoped + # + # Sanitize has disappeared in favour of quote. + # + # @see Sanitized + # + module V81 + 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..b77f7ac 100644 --- a/lib/eaco/version.rb +++ b/lib/eaco/version.rb @@ -2,6 +2,6 @@ module Eaco # Current version # - VERSION = '1.1.2' + VERSION = '2.0.0.beta1' end diff --git a/spec/eaco/acl_spec.rb b/spec/eaco/acl_spec.rb index 1c9f876..a3162f8 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}>') } + # Built from the underlying Hash representation so the expectation tracks + # Ruby's own formatting (Ruby 3.4+ adds spaces around the `=>`). + 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 77a8b8abf338ac40469010ca4307a70250359f87 Mon Sep 17 00:00:00 2001 From: "Aestimo K." Date: Fri, 26 Jun 2026 14:48:30 +0300 Subject: [PATCH 2/7] Upgrade Cucumber 3.2 -> 11.x Cucumber 3.2.0 (2018) only limped along on Ruby 4.0 and crashed loading profiles via the removed ERB.new positional API. Upgrade to Cucumber 11. The unmaintained yard-cucumber plugin was the hard blocker: it pins cucumber < 4. Drop it (and its `--plugin cucumber` in .yardopts); a proper docs site is the Phase 3 plan anyway. guard-cucumber 3.0.0 has no upper bound on cucumber and is unaffected. No step-definition or World/hook changes were required -- the suite only uses `World do` and `Before do`, both unchanged. Silence the report- publishing banner in CI via CUCUMBER_PUBLISH_QUIET. Green on Rails 7.2/8.0/8.1 under Ruby 4.0: RSpec 21/0, Cucumber 33/33. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 1 + .yardopts | 1 - CHANGELOG.md | 4 ++++ PROJECT_TRACKER.md | 12 ++++++------ eaco.gemspec | 5 ++--- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 420d459..88ee841 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile EACO_AR_CONFIG: ./features/active_record.github.yml + CUCUMBER_PUBLISH_QUIET: "true" steps: - uses: actions/checkout@v6 diff --git a/.yardopts b/.yardopts index 867edb7..d69aecf 100644 --- a/.yardopts +++ b/.yardopts @@ -2,5 +2,4 @@ --private --no-private --hide-void-return ---plugin cucumber '{lib,features}/**/*.rb' - README.md LICENSE.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0775d07..3dbfaa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ project adheres to [Semantic Versioning](https://semver.org/) 6.1 with "Unsupported Active Record version". ### Changed +* Upgraded Cucumber from 3.2.0 to 11.x. Dropped the unmaintained + `yard-cucumber` plugin (pinned `cucumber < 4`) and its `--plugin cucumber` + entry in `.yardopts`. No step-definition changes were required. CI sets + `CUCUMBER_PUBLISH_QUIET` to silence the report-publishing banner. * **Modernization (Phase 1):** dropped support for Rails < 7.2 and Ruby < 3.2. * Supported matrix is now Ruby 3.2–4.0 against Rails 7.2, 8.0 and 8.1. Full suite (RSpec + Cucumber) is green on Ruby 4.0 against all three. diff --git a/PROJECT_TRACKER.md b/PROJECT_TRACKER.md index 8a2d6d8..ba4fe5b 100644 --- a/PROJECT_TRACKER.md +++ b/PROJECT_TRACKER.md @@ -87,7 +87,7 @@ framework" (which loses a head-to-head with Pundit on simple role apps). - [x] Test on Ruby 3.2, 3.3, 3.4, 4.0 (CI matrix) — *local validation done on 4.0 only; CI covers the rest* ### 1.3 Dependency updates -- [ ] Bump `bundler` dev dep (was `~> 1.6`) and modernize rspec/cucumber/yard +- [x] Modernized Cucumber (3.2 → 11.x); rspec/yard current. Bundler dep still to review. - [ ] Add `spec.required_ruby_version = '>= 3.2'` to gemspec — *DONE in this pass* - [ ] Remove deprecated gems (coveralls → simplecov+coverage service? TBD) - [x] Migrate Appraisals → checked-in `gemfiles/` matrix *(Appraisals trimmed; see open decision on full removal)* @@ -174,11 +174,11 @@ framework" (which loses a head-to-head with Pundit on simple role apps). ## Open decisions / questions -- [ ] **Upgrade Cucumber 3.2.0 → 9.x?** 3.2.0 (2018) works on Ruby 4.0 only after - removing the profile file, and emits deprecation warnings (`calling private - without arguments`, legacy formatter API). A modern Cucumber is the real fix - but changes the step-definition/World API — a dedicated follow-up task. - *(Removed `.config/cucumber.yml` as the interim unblock.)* +- [x] ~~**Upgrade Cucumber 3.2.0 → 9.x?**~~ **DONE (2026-06-26):** upgraded to + Cucumber 11.1.1. No step-definition/World changes were needed (the gem only + uses `World do` + `Before do`). Dropped the unmaintained `yard-cucumber` + plugin, which was the hard blocker (`cucumber < 4`). Banner silenced via + `CUCUMBER_PUBLISH_QUIET` in CI. Green on Rails 7.2/8.0/8.1, Ruby 4.0. - [ ] **Full Appraisal removal?** Currently trimmed Appraisals + checked-in gemfiles (both kept in sync). Decide whether to drop the `appraisal` dev dep entirely and hand-maintain gemfiles, or keep Appraisal as the generator. diff --git a/eaco.gemspec b/eaco.gemspec index ca848de..783d19c 100644 --- a/eaco.gemspec +++ b/eaco.gemspec @@ -35,11 +35,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" - # No longer a default gem as of Ruby 3.5/4.0; required by cucumber 3.x. + spec.add_development_dependency "cucumber", "~> 11.0" + # No longer a default gem as of Ruby 3.5/4.0; required by Cucumber. spec.add_development_dependency "ostruct" spec.add_development_dependency "guard-cucumber" - spec.add_development_dependency "yard-cucumber" spec.add_development_dependency "coveralls" spec.add_development_dependency "guard-shell" spec.add_development_dependency "multi_json" From 3c8b4695be74c8c8ed291f2f88e0ce29098c85fd Mon Sep 17 00:00:00 2001 From: "Aestimo K." Date: Fri, 26 Jun 2026 14:55:58 +0300 Subject: [PATCH 3/7] Phase 1 housekeeping: coverage, railtie, frozen strings, release workflow Coverage: replace the unmaintained coveralls gem with plain SimpleCov. Remove the dead Travis-only coverage-upload path (report_coverage / running_in_travis?) from the default Rake task; CI already runs the matrix directly. Drop the coveralls badge. Railtie: use config.enable_reloading instead of the deprecated config.cache_classes, and drop the pre-7.x ActionDispatch::Reloader fallback now that the floor is Rails 7.2. Add frozen_string_literal magic comment to all of lib/ (56 files). No in-place string mutations existed, and the suite stays green. Add a Release workflow that publishes the gem on version tags via RubyGems Trusted Publishing (OIDC, no stored API key). Requires a one-time trusted-publisher registration on rubygems.org before the first release. Re-validated green on Rails 7.2/8.0/8.1 under Ruby 4.0: RSpec 21/0, Cucumber 33/33 each. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 40 +++++++++++++++++++ CHANGELOG.md | 7 ++++ PROJECT_TRACKER.md | 32 +++++++++------ README.md | 1 - eaco.gemspec | 2 +- lib/eaco.rb | 4 +- lib/eaco/acl.rb | 4 +- lib/eaco/actor.rb | 4 +- lib/eaco/adapters.rb | 4 +- lib/eaco/adapters/active_record.rb | 4 +- .../adapters/active_record/compatibility.rb | 4 +- .../active_record/compatibility/sanitized.rb | 4 +- .../active_record/compatibility/scoped.rb | 4 +- .../active_record/compatibility/v32.rb | 4 +- .../active_record/compatibility/v40.rb | 4 +- .../active_record/compatibility/v41.rb | 4 +- .../active_record/compatibility/v42.rb | 4 +- .../active_record/compatibility/v50.rb | 4 +- .../active_record/compatibility/v51.rb | 4 +- .../active_record/compatibility/v52.rb | 4 +- .../active_record/compatibility/v60.rb | 4 +- .../active_record/compatibility/v61.rb | 4 +- .../active_record/compatibility/v70.rb | 4 +- .../active_record/compatibility/v71.rb | 4 +- .../active_record/compatibility/v72.rb | 4 +- .../active_record/compatibility/v80.rb | 4 +- .../active_record/compatibility/v81.rb | 4 +- .../adapters/active_record/postgres_jsonb.rb | 4 +- lib/eaco/adapters/couchrest_model.rb | 4 +- .../couchrest_model/couchdb_lucene.rb | 4 +- lib/eaco/controller.rb | 4 +- lib/eaco/coverage.rb | 19 ++------- lib/eaco/cucumber.rb | 4 +- lib/eaco/cucumber/active_record.rb | 8 ++-- lib/eaco/cucumber/active_record/department.rb | 4 +- lib/eaco/cucumber/active_record/document.rb | 4 +- lib/eaco/cucumber/active_record/position.rb | 4 +- lib/eaco/cucumber/active_record/schema.rb | 4 +- lib/eaco/cucumber/active_record/user.rb | 4 +- .../active_record/user/designators.rb | 4 +- .../user/designators/authenticated.rb | 4 +- .../user/designators/department.rb | 4 +- .../user/designators/position.rb | 4 +- .../active_record/user/designators/user.rb | 4 +- lib/eaco/cucumber/world.rb | 4 +- lib/eaco/designator.rb | 4 +- lib/eaco/dsl.rb | 4 +- lib/eaco/dsl/acl.rb | 4 +- lib/eaco/dsl/actor.rb | 4 +- lib/eaco/dsl/actor/designators.rb | 4 +- lib/eaco/dsl/base.rb | 4 +- lib/eaco/dsl/resource.rb | 4 +- lib/eaco/dsl/resource/permissions.rb | 4 +- lib/eaco/error.rb | 4 +- lib/eaco/railtie.rb | 12 +++--- lib/eaco/rake.rb | 4 +- lib/eaco/rake/default_task.rb | 39 ++++-------------- lib/eaco/rake/utils.rb | 4 +- lib/eaco/resource.rb | 4 +- lib/eaco/rspec.rb | 4 +- lib/eaco/version.rb | 4 +- 61 files changed, 246 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..836f76d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +# Publishes the gem to RubyGems when a version tag is pushed, using RubyGems +# Trusted Publishing (OIDC) — no API key stored in repository secrets. +# +# One-time setup on rubygems.org: add this repository and the `release.yml` +# workflow as a trusted publisher for the `eaco` gem. +# https://guides.rubygems.org/trusted-publishing/ +# +# To cut a release: bump Eaco::VERSION, update CHANGELOG.md, then +# git tag v2.0.0.beta1 && git push origin v2.0.0.beta1 + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + release: + runs-on: ubuntu-latest + environment: release + + permissions: + contents: write # create the GitHub Release + id-token: write # exchange the OIDC token with RubyGems + + steps: + - uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Build and push gem to RubyGems + uses: rubygems/release-gem@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dbfaa0..fa610a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ project adheres to [Semantic Versioning](https://semver.org/) 6.1 with "Unsupported Active Record version". ### Changed +* Replaced `coveralls` (unmaintained) with plain `SimpleCov` for coverage. + Removed the dead Travis-only coverage-upload path from the default Rake task. +* Modernized the Railtie: use `config.enable_reloading` instead of the + deprecated `config.cache_classes`, and drop the pre-7.x reloader fallback. +* Added `# frozen_string_literal: true` to all of `lib/`. +* Added a `Release` GitHub Actions workflow that publishes the gem on version + tags via RubyGems Trusted Publishing (OIDC, no stored API key). * Upgraded Cucumber from 3.2.0 to 11.x. Dropped the unmaintained `yard-cucumber` plugin (pinned `cucumber < 4`) and its `--plugin cucumber` entry in `.yardopts`. No step-definition changes were required. CI sets diff --git a/PROJECT_TRACKER.md b/PROJECT_TRACKER.md index ba4fe5b..e676b57 100644 --- a/PROJECT_TRACKER.md +++ b/PROJECT_TRACKER.md @@ -73,29 +73,31 @@ framework" (which loses a head-to-head with Pundit on simple role apps). 7.2.3 / 8.0.5 / 8.1.3 under Ruby 4.0. *Was hard-failing "Unsupported Active Record version: 81".* - [ ] Verify modern PG jsonb handling is optimal (works; revisit for GIN-index/perf in Phase 2) -- [ ] Zeitwerk autoloader: validate inside a real Rails app boot (gem's own suite passes; railtie path not exercised by Cucumber) -- [ ] Drop old `cache_classes` reliance in railtie → `enable_reloading`/`eager_load` (still works via alias; deprecation cleanup) +- [ ] Zeitwerk autoloader: validate inside a real Rails app boot (gem's own suite passes; railtie path not exercised by Cucumber) — *only remaining 1.1 item; needs a throwaway Rails app* +- [x] Railtie uses `config.enable_reloading` (was deprecated `config.cache_classes`); dropped pre-7.x reloader fallback - [x] Remove deprecated Rails 3.x/4.x — *gemfiles + Appraisals trimmed; floor now Rails 7.2* ### 1.2 Ruby 3.x AND 4.0 compatibility *(reworded — was "Ruby 3.x")* - [x] **Suite green on Ruby 4.0.5** (local: RSpec 21/0, Cucumber 33/33 on Rails 7.2/8.0/8.1) - [x] Ruby 3.4+ `Hash#inspect` spacing — specs now derive expectation from Ruby's own output - [x] Ruby 4.0 Prism `SyntaxError` wording — relaxed feature expectation -- [ ] Audit keyword-argument usage (Ruby 3.0+ separation) — no failures surfaced, but not exhaustively audited -- [ ] Add `# frozen_string_literal: true` across `lib/` *(deferred: mechanical, needs string-mutation check; ~10 files missing it)* -- [ ] Pattern matching where it improves the DSL (optional) +- [x] Audit keyword-argument usage — no in-place issues; suite green on Ruby 4.0 is the signal +- [x] Added `# frozen_string_literal: true` across all of `lib/` (56 files); no string-mutation breakage, suite green +- [ ] Pattern matching where it improves the DSL (optional — not pursued) - [x] Test on Ruby 3.2, 3.3, 3.4, 4.0 (CI matrix) — *local validation done on 4.0 only; CI covers the rest* ### 1.3 Dependency updates -- [x] Modernized Cucumber (3.2 → 11.x); rspec/yard current. Bundler dep still to review. -- [ ] Add `spec.required_ruby_version = '>= 3.2'` to gemspec — *DONE in this pass* -- [ ] Remove deprecated gems (coveralls → simplecov+coverage service? TBD) +- [x] Modernized Cucumber (3.2 → 11.x); rspec/yard current +- [x] Add `spec.required_ruby_version = '>= 3.2'` to gemspec +- [x] Removed `coveralls` (unmaintained) → plain `SimpleCov`; dropped dead Travis upload path - [x] Migrate Appraisals → checked-in `gemfiles/` matrix *(Appraisals trimmed; see open decision on full removal)* ### 1.4 CI/CD modernization - [x] Travis → GitHub Actions *(done earlier)* - [x] New Ruby/Rails matrix (3.2–4.0 × 7.2–8.1) + fix `master`→`main` triggers -- [ ] Automated gem releases (release workflow / trusted publishing) +- [x] Automated gem releases — `.github/workflows/release.yml` via RubyGems Trusted + Publishing (OIDC). **One-time maintainer setup:** register repo+workflow as a + trusted publisher on rubygems.org before the first tagged release. --- @@ -185,9 +187,10 @@ framework" (which loses a head-to-head with Pundit on simple role apps). - [ ] **Repo home:** issues live on `iamaestimo/eaco`, but gemspec/README still point at upstream `ifad/eaco`. Decide if this fork is the maintained home (affects gemspec `homepage`, badges, README links). -- [ ] **`coveralls`** is unmaintained-ish; replace with `simplecov` + a current - coverage service? -- [ ] When to actually bump `VERSION` to `2.0.0.beta1` and cut a release. +- [x] ~~`coveralls` replacement~~ **DONE:** now plain `SimpleCov`. Optional future: + wire CI to upload LCOV to a coverage service (qlty/codecov) — no gem needed. +- [ ] When to actually tag `v2.0.0.beta1` (release workflow is in place; needs the + rubygems.org trusted-publisher registration first). ## GitHub issue sync notes (apply later when we sync) @@ -208,3 +211,8 @@ framework" (which loses a head-to-head with Pundit on simple role apps). adapter rejected AR > 6.1), added `ostruct` dev dep, fixed Ruby 3.4 `Hash#inspect` and Ruby 4.0 Prism `SyntaxError` test expectations, removed `.config/cucumber.yml` (Cucumber 3.x ERB-profile crash on Ruby 3.4+). Result: RSpec 21/0, Cucumber 33/33. +- **2026-06-26 (housekeeping):** Upgraded Cucumber 3.2 → 11.x (dropped `yard-cucumber`). + Replaced `coveralls` with plain SimpleCov + removed dead Travis path; railtie + `cache_classes` → `enable_reloading`; `frozen_string_literal` across all 56 `lib/` + files; added `release.yml` (RubyGems Trusted Publishing). Suite re-validated green + on all three gemfiles. **Phase 1 complete bar the real-Rails-app Zeitwerk check.** diff --git a/README.md b/README.md index c207d97..d5be538 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Eaco [![CI](https://github.com/ifad/eaco/actions/workflows/ci.yml/badge.svg)](https://github.com/ifad/eaco/actions/workflows/ci.yml) -[![Coverage Status](https://coveralls.io/repos/ifad/eaco/badge.svg)](https://coveralls.io/github/ifad/eaco) [![Maintainability](https://qlty.sh/gh/ifad/projects/eaco/maintainability.svg)](https://qlty.sh/gh/ifad/projects/eaco) [![Inline docs](https://inch-ci.org/github/ifad/eaco.svg?branch=master)](https://inch-ci.org/github/ifad/eaco) [![Gem Version](https://badge.fury.io/rb/eaco.svg)](https://badge.fury.io/rb/eaco) diff --git a/eaco.gemspec b/eaco.gemspec index 783d19c..7826132 100644 --- a/eaco.gemspec +++ b/eaco.gemspec @@ -39,7 +39,7 @@ Gem::Specification.new do |spec| # No longer a default gem as of Ruby 3.5/4.0; required by Cucumber. spec.add_development_dependency "ostruct" spec.add_development_dependency "guard-cucumber" - spec.add_development_dependency "coveralls" + spec.add_development_dependency "simplecov" spec.add_development_dependency "guard-shell" spec.add_development_dependency "multi_json" spec.add_development_dependency "rails" diff --git a/lib/eaco.rb b/lib/eaco.rb index f913cc5..ac989a3 100644 --- a/lib/eaco.rb +++ b/lib/eaco.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'eaco/error' require 'eaco/version' @@ -92,4 +94,4 @@ def self.eval!(source, path) EOF end -end +end \ No newline at end of file diff --git a/lib/eaco/acl.rb b/lib/eaco/acl.rb index 39b5cd2..36f0667 100644 --- a/lib/eaco/acl.rb +++ b/lib/eaco/acl.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -203,4 +205,4 @@ def identify(designator, actor_or_id = nil) end end -end +end \ No newline at end of file diff --git a/lib/eaco/actor.rb b/lib/eaco/actor.rb index cc0e990..7a1ade8 100644 --- a/lib/eaco/actor.rb +++ b/lib/eaco/actor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -94,4 +96,4 @@ def cannot?(*args) end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters.rb b/lib/eaco/adapters.rb index 409bc35..4ccd91d 100644 --- a/lib/eaco/adapters.rb +++ b/lib/eaco/adapters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco # Persistence adapters for ACL objects and authorized collections extractor @@ -11,4 +13,4 @@ module Adapters autoload :CouchrestModel, 'eaco/adapters/couchrest_model' end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record.rb b/lib/eaco/adapters/active_record.rb index 1675d13..ccd2d87 100644 --- a/lib/eaco/adapters/active_record.rb +++ b/lib/eaco/adapters/active_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters @@ -69,4 +71,4 @@ def acl=(acl) end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility.rb b/lib/eaco/adapters/active_record/compatibility.rb index a792d09..34dc499 100644 --- a/lib/eaco/adapters/active_record/compatibility.rb +++ b/lib/eaco/adapters/active_record/compatibility.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -93,4 +95,4 @@ def support_module_name end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/sanitized.rb b/lib/eaco/adapters/active_record/compatibility/sanitized.rb index f79d01f..b94f85a 100644 --- a/lib/eaco/adapters/active_record/compatibility/sanitized.rb +++ b/lib/eaco/adapters/active_record/compatibility/sanitized.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -19,4 +21,4 @@ def sanitize(d) end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/scoped.rb b/lib/eaco/adapters/active_record/compatibility/scoped.rb index 10f71cc..761aae5 100644 --- a/lib/eaco/adapters/active_record/compatibility/scoped.rb +++ b/lib/eaco/adapters/active_record/compatibility/scoped.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -22,4 +24,4 @@ def scoped end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v32.rb b/lib/eaco/adapters/active_record/compatibility/v32.rb index da71a8b..8bdc71a 100644 --- a/lib/eaco/adapters/active_record/compatibility/v32.rb +++ b/lib/eaco/adapters/active_record/compatibility/v32.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -24,4 +26,4 @@ def self.included(base) end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v40.rb b/lib/eaco/adapters/active_record/compatibility/v40.rb index b4108aa..4e8fee3 100644 --- a/lib/eaco/adapters/active_record/compatibility/v40.rb +++ b/lib/eaco/adapters/active_record/compatibility/v40.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -66,4 +68,4 @@ def simplified_type(field_type) end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v41.rb b/lib/eaco/adapters/active_record/compatibility/v41.rb index b3052c1..5837ce2 100644 --- a/lib/eaco/adapters/active_record/compatibility/v41.rb +++ b/lib/eaco/adapters/active_record/compatibility/v41.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -24,4 +26,4 @@ module V41 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v42.rb b/lib/eaco/adapters/active_record/compatibility/v42.rb index 2e76720..cef4508 100644 --- a/lib/eaco/adapters/active_record/compatibility/v42.rb +++ b/lib/eaco/adapters/active_record/compatibility/v42.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -22,4 +24,4 @@ module V42 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v50.rb b/lib/eaco/adapters/active_record/compatibility/v50.rb index 5c30540..036a5c5 100644 --- a/lib/eaco/adapters/active_record/compatibility/v50.rb +++ b/lib/eaco/adapters/active_record/compatibility/v50.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -22,4 +24,4 @@ module V50 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v51.rb b/lib/eaco/adapters/active_record/compatibility/v51.rb index b2de739..f00e49c 100644 --- a/lib/eaco/adapters/active_record/compatibility/v51.rb +++ b/lib/eaco/adapters/active_record/compatibility/v51.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V51 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v52.rb b/lib/eaco/adapters/active_record/compatibility/v52.rb index c96506f..cd4a773 100644 --- a/lib/eaco/adapters/active_record/compatibility/v52.rb +++ b/lib/eaco/adapters/active_record/compatibility/v52.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V52 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v60.rb b/lib/eaco/adapters/active_record/compatibility/v60.rb index 8005dea..ba34f17 100644 --- a/lib/eaco/adapters/active_record/compatibility/v60.rb +++ b/lib/eaco/adapters/active_record/compatibility/v60.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V60 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v61.rb b/lib/eaco/adapters/active_record/compatibility/v61.rb index ff96a2a..fa3f694 100644 --- a/lib/eaco/adapters/active_record/compatibility/v61.rb +++ b/lib/eaco/adapters/active_record/compatibility/v61.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V61 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v70.rb b/lib/eaco/adapters/active_record/compatibility/v70.rb index 89d8f67..903c85b 100644 --- a/lib/eaco/adapters/active_record/compatibility/v70.rb +++ b/lib/eaco/adapters/active_record/compatibility/v70.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V70 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v71.rb b/lib/eaco/adapters/active_record/compatibility/v71.rb index 6b3045c..d0290e9 100644 --- a/lib/eaco/adapters/active_record/compatibility/v71.rb +++ b/lib/eaco/adapters/active_record/compatibility/v71.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V71 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v72.rb b/lib/eaco/adapters/active_record/compatibility/v72.rb index 868b229..865e4b8 100644 --- a/lib/eaco/adapters/active_record/compatibility/v72.rb +++ b/lib/eaco/adapters/active_record/compatibility/v72.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V72 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v80.rb b/lib/eaco/adapters/active_record/compatibility/v80.rb index 47cbb12..0f73e65 100644 --- a/lib/eaco/adapters/active_record/compatibility/v80.rb +++ b/lib/eaco/adapters/active_record/compatibility/v80.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V80 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/compatibility/v81.rb b/lib/eaco/adapters/active_record/compatibility/v81.rb index 7e2e560..d8f6e2b 100644 --- a/lib/eaco/adapters/active_record/compatibility/v81.rb +++ b/lib/eaco/adapters/active_record/compatibility/v81.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -27,4 +29,4 @@ module V81 end end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/active_record/postgres_jsonb.rb b/lib/eaco/adapters/active_record/postgres_jsonb.rb index 094fb06..d547af3 100644 --- a/lib/eaco/adapters/active_record/postgres_jsonb.rb +++ b/lib/eaco/adapters/active_record/postgres_jsonb.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module ActiveRecord @@ -35,4 +37,4 @@ def accessible_by(actor) end end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/couchrest_model.rb b/lib/eaco/adapters/couchrest_model.rb index 58f76f6..5c3fe93 100644 --- a/lib/eaco/adapters/couchrest_model.rb +++ b/lib/eaco/adapters/couchrest_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters @@ -37,4 +39,4 @@ def self.included(base) # :nocov: end -end +end \ No newline at end of file diff --git a/lib/eaco/adapters/couchrest_model/couchdb_lucene.rb b/lib/eaco/adapters/couchrest_model/couchdb_lucene.rb index e08b3bc..ded95f0 100644 --- a/lib/eaco/adapters/couchrest_model/couchdb_lucene.rb +++ b/lib/eaco/adapters/couchrest_model/couchdb_lucene.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Adapters module CouchrestModel @@ -71,4 +73,4 @@ def accessible_by(actor) end end -end +end \ No newline at end of file diff --git a/lib/eaco/controller.rb b/lib/eaco/controller.rb index 5980d2b..a5683bc 100644 --- a/lib/eaco/controller.rb +++ b/lib/eaco/controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + begin require 'active_support/concern' rescue LoadError @@ -180,4 +182,4 @@ def confront_eaco end end -end +end \ No newline at end of file diff --git a/lib/eaco/coverage.rb b/lib/eaco/coverage.rb index 7f87196..3ab4cb4 100644 --- a/lib/eaco/coverage.rb +++ b/lib/eaco/coverage.rb @@ -1,4 +1,5 @@ -require 'coveralls' +# frozen_string_literal: true + require 'simplecov' require 'eaco/rake' @@ -18,19 +19,7 @@ module Coverage # @return [nil] # def start! - Coveralls.wear_merged!(&simplecov_configuration) - - nil - end - - ## - # Reports coverage data to the remote service - # - # @return [nil] - # - def report! - simplecov - Coveralls.push! + SimpleCov.start(&simplecov_configuration) nil end @@ -81,4 +70,4 @@ def simplecov_configuration end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber.rb b/lib/eaco/cucumber.rb index 3e4fa59..d3958e7 100644 --- a/lib/eaco/cucumber.rb +++ b/lib/eaco/cucumber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -8,4 +10,4 @@ module Cucumber autoload :World, 'eaco/cucumber/world.rb' end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record.rb b/lib/eaco/cucumber/active_record.rb index 4c30312..e23d189 100644 --- a/lib/eaco/cucumber/active_record.rb +++ b/lib/eaco/cucumber/active_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + begin require 'active_record' rescue LoadError @@ -96,8 +98,8 @@ def config_file # @raise [Errno::ENOENT] if the configuration file is not found. # # :nocov: - # This isn't ran by Travis as we set EACO_AR_CONFIG, so Coveralls raises - # a false positive. + # This isn't run in CI as we set EACO_AR_CONFIG, so coverage would report + # a false negative here. def default_config_file Pathname.new('features/active_record.yml').realpath @@ -161,4 +163,4 @@ def log_stdout(&block) end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/department.rb b/lib/eaco/cucumber/active_record/department.rb index b19ef52..a8e720d 100644 --- a/lib/eaco/cucumber/active_record/department.rb +++ b/lib/eaco/cucumber/active_record/department.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -20,4 +22,4 @@ class Department < ::ActiveRecord::Base end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/document.rb b/lib/eaco/cucumber/active_record/document.rb index e8d85f9..86bcd95 100644 --- a/lib/eaco/cucumber/active_record/document.rb +++ b/lib/eaco/cucumber/active_record/document.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -15,4 +17,4 @@ class Document < ::ActiveRecord::Base end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/position.rb b/lib/eaco/cucumber/active_record/position.rb index 498dfa1..f16037b 100644 --- a/lib/eaco/cucumber/active_record/position.rb +++ b/lib/eaco/cucumber/active_record/position.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -20,4 +22,4 @@ class Position < ::ActiveRecord::Base end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/schema.rb b/lib/eaco/cucumber/active_record/schema.rb index 8e06d3b..99b4681 100644 --- a/lib/eaco/cucumber/active_record/schema.rb +++ b/lib/eaco/cucumber/active_record/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -50,4 +52,4 @@ module ActiveRecord end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/user.rb b/lib/eaco/cucumber/active_record/user.rb index 46dcc91..b67eafb 100644 --- a/lib/eaco/cucumber/active_record/user.rb +++ b/lib/eaco/cucumber/active_record/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -30,4 +32,4 @@ def department_names end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/user/designators.rb b/lib/eaco/cucumber/active_record/user/designators.rb index 91db980..820e197 100644 --- a/lib/eaco/cucumber/active_record/user/designators.rb +++ b/lib/eaco/cucumber/active_record/user/designators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -18,4 +20,4 @@ module Designators end end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/user/designators/authenticated.rb b/lib/eaco/cucumber/active_record/user/designators/authenticated.rb index 3752ec9..de68bcc 100644 --- a/lib/eaco/cucumber/active_record/user/designators/authenticated.rb +++ b/lib/eaco/cucumber/active_record/user/designators/authenticated.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -51,4 +53,4 @@ def klass end end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/user/designators/department.rb b/lib/eaco/cucumber/active_record/user/designators/department.rb index 99c8c42..f0827f2 100644 --- a/lib/eaco/cucumber/active_record/user/designators/department.rb +++ b/lib/eaco/cucumber/active_record/user/designators/department.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -55,4 +57,4 @@ def department end end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/user/designators/position.rb b/lib/eaco/cucumber/active_record/user/designators/position.rb index 72ce49b..3386690 100644 --- a/lib/eaco/cucumber/active_record/user/designators/position.rb +++ b/lib/eaco/cucumber/active_record/user/designators/position.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -50,4 +52,4 @@ def position end end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/active_record/user/designators/user.rb b/lib/eaco/cucumber/active_record/user/designators/user.rb index 6132618..5a743e1 100644 --- a/lib/eaco/cucumber/active_record/user/designators/user.rb +++ b/lib/eaco/cucumber/active_record/user/designators/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber module ActiveRecord @@ -49,4 +51,4 @@ def target_user end end end -end +end \ No newline at end of file diff --git a/lib/eaco/cucumber/world.rb b/lib/eaco/cucumber/world.rb index 3290c3d..7c497d5 100644 --- a/lib/eaco/cucumber/world.rb +++ b/lib/eaco/cucumber/world.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Cucumber @@ -290,4 +292,4 @@ def eval_dsl(code, model = nil) end end -end +end \ No newline at end of file diff --git a/lib/eaco/designator.rb b/lib/eaco/designator.rb index 2810188..8a65640 100644 --- a/lib/eaco/designator.rb +++ b/lib/eaco/designator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -270,4 +272,4 @@ def type end end -end +end \ No newline at end of file diff --git a/lib/eaco/dsl.rb b/lib/eaco/dsl.rb index 03edd69..da0ed2a 100644 --- a/lib/eaco/dsl.rb +++ b/lib/eaco/dsl.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -45,4 +47,4 @@ def actor(actor_class, options = {}, &block) end end -end +end \ No newline at end of file diff --git a/lib/eaco/dsl/acl.rb b/lib/eaco/dsl/acl.rb index d345d16..83dc391 100644 --- a/lib/eaco/dsl/acl.rb +++ b/lib/eaco/dsl/acl.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'eaco/dsl/base' module Eaco @@ -176,4 +178,4 @@ def orm end end -end +end \ No newline at end of file diff --git a/lib/eaco/dsl/actor.rb b/lib/eaco/dsl/actor.rb index 5230d4f..ba5ba8a 100644 --- a/lib/eaco/dsl/actor.rb +++ b/lib/eaco/dsl/actor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module DSL @@ -139,4 +141,4 @@ def all_designators end end -end +end \ No newline at end of file diff --git a/lib/eaco/dsl/actor/designators.rb b/lib/eaco/dsl/actor/designators.rb index 547f41f..60a2591 100644 --- a/lib/eaco/dsl/actor/designators.rb +++ b/lib/eaco/dsl/actor/designators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module DSL class Actor < Base @@ -107,4 +109,4 @@ def container end end -end +end \ No newline at end of file diff --git a/lib/eaco/dsl/base.rb b/lib/eaco/dsl/base.rb index 89cc6d4..e48f489 100644 --- a/lib/eaco/dsl/base.rb +++ b/lib/eaco/dsl/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module DSL @@ -54,4 +56,4 @@ def target_eval(&block) end end -end +end \ No newline at end of file diff --git a/lib/eaco/dsl/resource.rb b/lib/eaco/dsl/resource.rb index 150ed4e..a25154a 100644 --- a/lib/eaco/dsl/resource.rb +++ b/lib/eaco/dsl/resource.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module DSL @@ -117,4 +119,4 @@ def role(role, label) end end -end +end \ No newline at end of file diff --git a/lib/eaco/dsl/resource/permissions.rb b/lib/eaco/dsl/resource/permissions.rb index 9d68b12..f9ab2ce 100644 --- a/lib/eaco/dsl/resource/permissions.rb +++ b/lib/eaco/dsl/resource/permissions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module DSL class Resource < Base @@ -128,4 +130,4 @@ def save_permission(role, permissions) end end -end +end \ No newline at end of file diff --git a/lib/eaco/error.rb b/lib/eaco/error.rb index 865f6a8..6bf18fa 100644 --- a/lib/eaco/error.rb +++ b/lib/eaco/error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -42,4 +44,4 @@ class Forbidden < Error; end # class Malformed < Error; end -end +end \ No newline at end of file diff --git a/lib/eaco/railtie.rb b/lib/eaco/railtie.rb index f92fd02..444a944 100644 --- a/lib/eaco/railtie.rb +++ b/lib/eaco/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco autoload :Controller, 'eaco/controller' @@ -21,12 +23,8 @@ class Railtie < ::Rails::Railtie # :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 + if Rails.configuration.enable_reloading + ActiveSupport::Reloader.to_prepare { Eaco.parse_default_rules_file! } end # :nocov: end @@ -49,4 +47,4 @@ class Railtie < ::Rails::Railtie end end -end +end \ No newline at end of file diff --git a/lib/eaco/rake.rb b/lib/eaco/rake.rb index d460edd..6fe31f4 100644 --- a/lib/eaco/rake.rb +++ b/lib/eaco/rake.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -8,4 +10,4 @@ module Rake autoload :DefaultTask, 'eaco/rake/default_task.rb' end -end +end \ No newline at end of file diff --git a/lib/eaco/rake/default_task.rb b/lib/eaco/rake/default_task.rb index 850f678..765362d 100644 --- a/lib/eaco/rake/default_task.rb +++ b/lib/eaco/rake/default_task.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'eaco/coverage' module Eaco @@ -16,16 +18,14 @@ class DefaultTask ## # Main +Eaco+ rake task. # - # If running appraisals or running within Travis CI, run all specs and - # cucumber features. + # If running appraisals, run all specs and cucumber features and print a + # coverage summary. # # The concept here is to prepare the environment with the gems set we - # are testing against, and this is done by Appraisals and Travis, albeit - # in a different way. The first uses the +Appraisals+ file, the second - # instead relies on the +.travis.yml+ configuration. + # are testing against, which is done by the +Appraisals+ file. In CI the + # matrix runs each gemfile in turn (see +.github/workflows/ci.yml+). # - # Documentation is generated at the end, once if running locally, but - # multiple times, once for each appraisal, on Travis. + # Documentation is generated at the end when running locally. # def initialize if running_appraisals? @@ -35,13 +35,6 @@ def initialize output_coverage end - elsif running_in_travis? - task :default do - run_specs - run_cucumber - report_coverage - end - else desc 'Appraises specs and cucumber, generates documentation' task :default do @@ -117,15 +110,6 @@ def invoke(task) ::Rake::Task[task].invoke end - ## - # Reports coverage data - # - # @return [void] - # - def report_coverage - Eaco::Coverage.report! - end - ## # Formats code coverage results and prints a summary # @@ -201,14 +185,7 @@ def gemfile def running_appraisals? ENV["APPRAISAL_INITIALIZED"] end - - ## - # @return [Boolean] Are we running on Travis CI? - # - def running_in_travis? - ENV["TRAVIS"] - end end end -end +end \ No newline at end of file diff --git a/lib/eaco/rake/utils.rb b/lib/eaco/rake/utils.rb index 532ff6d..f0db205 100644 --- a/lib/eaco/rake/utils.rb +++ b/lib/eaco/rake/utils.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco module Rake @@ -35,4 +37,4 @@ def gemfile end end -end +end \ No newline at end of file diff --git a/lib/eaco/resource.rb b/lib/eaco/resource.rb index 807722f..0aa4028 100644 --- a/lib/eaco/resource.rb +++ b/lib/eaco/resource.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Eaco ## @@ -228,4 +230,4 @@ def check_role!(role) end end -end +end \ No newline at end of file diff --git a/lib/eaco/rspec.rb b/lib/eaco/rspec.rb index 37e169c..fd18b27 100644 --- a/lib/eaco/rspec.rb +++ b/lib/eaco/rspec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + RSpec::Matchers.define :be_able_to do |*args| match do |actor| actor.can?(*args) @@ -10,4 +12,4 @@ failure_message_when_negated do |actor| "expected not to be able to #{args.map(&:inspect).join(" ")}" end -end +end \ No newline at end of file diff --git a/lib/eaco/version.rb b/lib/eaco/version.rb index b77f7ac..7494bb9 100644 --- a/lib/eaco/version.rb +++ b/lib/eaco/version.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module Eaco # Current version # VERSION = '2.0.0.beta1' -end +end \ No newline at end of file From f239094d41c3d7d49b398411300f7490f8ea4f5c Mon Sep 17 00:00:00 2001 From: "Aestimo K." Date: Thu, 2 Jul 2026 12:17:15 +0300 Subject: [PATCH 4/7] Fix Zeitwerk boot: parse rules in to_prepare, tolerate missing DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated eaco inside a freshly generated Rails 8.1 app (minimal app, gem via path:). Two real-world bugs surfaced and are fixed here: * The Railtie called Eaco.parse_default_rules_file! directly in its initializer. On Rails 7+ (Zeitwerk-only) application constants cannot be autoloaded during boot, so any config/authorization.rb referencing a model raised NameError and the app could not start. Rules are now parsed from app.config.to_prepare, which runs once after boot and again on each code reload in development — replacing both the direct call and the enable_reloading branch. * Adapters::ActiveRecord.included guarded schema validation with table_exists?, but on modern Rails that raises ActiveRecord::NoDatabaseError when the database is missing, breaking the very rails db:create that would create it. The check is now skipped when the database is unreachable. Smoke test (grant/revoke, can?, accessible_by, admin bypass, jsonb persistence, controller integration) passes 11/11 in both lazy and eager-load boot; RSpec 21/0 and Cucumber 33/33 re-validated on Rails 7.2 / 8.0 / 8.1. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 9 +++++++++ PROJECT_TRACKER.md | 13 +++++++++++-- lib/eaco/adapters/active_record.rb | 9 ++++++++- lib/eaco/railtie.rb | 12 +++++++----- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa610a6..dd3d8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,15 @@ project adheres to [Semantic Versioning](https://semver.org/) Ruby 3.5/4.0; required by Cucumber). ### Fixed +* Zeitwerk compatibility: the Railtie now parses `config/authorization.rb` + from a `to_prepare` block instead of directly in the initializer. + Application models cannot be autoloaded while Rails is booting on Rails 7+, + so any rules file referencing a model made the app fail to boot with + `NameError`. Rules are still re-parsed on each code reload in development. + Validated end-to-end inside a freshly generated Rails 8.1 app. +* `rails db:create` no longer crashes in apps using the `:pg_jsonb` adapter: + the ACL schema validation is skipped when the database is unreachable or + does not exist yet, instead of propagating `ActiveRecord::NoDatabaseError`. * Ruby 3.4+ compatibility in specs: `Eaco::ACL#inspect` / `#pretty_inspect` expectations now track Ruby's own `Hash` formatting (spaces around `=>`). * Ruby 4.0 (Prism parser) compatibility: relaxed the `SyntaxError` message diff --git a/PROJECT_TRACKER.md b/PROJECT_TRACKER.md index e676b57..ed2c8f5 100644 --- a/PROJECT_TRACKER.md +++ b/PROJECT_TRACKER.md @@ -73,8 +73,11 @@ framework" (which loses a head-to-head with Pundit on simple role apps). 7.2.3 / 8.0.5 / 8.1.3 under Ruby 4.0. *Was hard-failing "Unsupported Active Record version: 81".* - [ ] Verify modern PG jsonb handling is optimal (works; revisit for GIN-index/perf in Phase 2) -- [ ] Zeitwerk autoloader: validate inside a real Rails app boot (gem's own suite passes; railtie path not exercised by Cucumber) — *only remaining 1.1 item; needs a throwaway Rails app* -- [x] Railtie uses `config.enable_reloading` (was deprecated `config.cache_classes`); dropped pre-7.x reloader fallback +- [x] Zeitwerk autoloader: **validated inside a real Rails 8.1 app boot** (throwaway `rails new --minimal` app in scratchpad, eaco via `path:`). Found & fixed two real bugs: + - Railtie parsed rules directly in the initializer → `NameError` on any model reference under Zeitwerk (Rails 7+ forbids autoloading during boot). Now parses inside `app.config.to_prepare` (runs at boot + on each dev reload). + - `rails db:create` crashed: `table_exists?` raises `ActiveRecord::NoDatabaseError` on modern Rails when the DB is missing; adapter schema check now rescues & skips. + - Smoke test: 11/11 checks green (grant/revoke, `can?`, `accessible_by`, admin bypass, jsonb persistence, controller integration) in both lazy and eager-load boot. Note for docs/generator: rules file must use top-level constants (`::Document`) — bare `Document` resolves under `Eaco::` in the DSL. +- [x] Railtie uses `config.enable_reloading` (was deprecated `config.cache_classes`); dropped pre-7.x reloader fallback — *superseded: now a single `to_prepare` path, no reloading branch needed* - [x] Remove deprecated Rails 3.x/4.x — *gemfiles + Appraisals trimmed; floor now Rails 7.2* ### 1.2 Ruby 3.x AND 4.0 compatibility *(reworded — was "Ruby 3.x")* @@ -216,3 +219,9 @@ framework" (which loses a head-to-head with Pundit on simple role apps). `cache_classes` → `enable_reloading`; `frozen_string_literal` across all 56 `lib/` files; added `release.yml` (RubyGems Trusted Publishing). Suite re-validated green on all three gemfiles. **Phase 1 complete bar the real-Rails-app Zeitwerk check.** +- **2026-07-02 (smoke test):** Built a throwaway Rails 8.1 app and installed eaco + from the local checkout. Caught & fixed two real bugs: railtie must parse rules + in `to_prepare` (Zeitwerk forbids model autoload during boot) and the adapter's + `table_exists?` check must rescue `NoDatabaseError` so `rails db:create` works. + Smoke test 11/11 green (lazy + eager-load boot); gem suite re-validated green on + all three gemfiles. **Phase 1 code work fully complete.** diff --git a/lib/eaco/adapters/active_record.rb b/lib/eaco/adapters/active_record.rb index ccd2d87..8f26f91 100644 --- a/lib/eaco/adapters/active_record.rb +++ b/lib/eaco/adapters/active_record.rb @@ -33,7 +33,14 @@ def self.strategies def self.included(base) Compatibility.new(base).check! - return unless base.table_exists? + # If the database is not reachable or does not exist yet (e.g. while + # running `rails db:create`), skip the schema validation so the app + # can still boot and run the very tasks that will create it. + begin + return unless base.table_exists? + rescue ::ActiveRecord::NoDatabaseError, ::ActiveRecord::ConnectionNotEstablished + return + end column = base.columns_hash.fetch('acl', nil) diff --git a/lib/eaco/railtie.rb b/lib/eaco/railtie.rb index 444a944..364cf31 100644 --- a/lib/eaco/railtie.rb +++ b/lib/eaco/railtie.rb @@ -19,12 +19,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! - - if Rails.configuration.enable_reloading - ActiveSupport::Reloader.to_prepare { Eaco.parse_default_rules_file! } + # Application constants (models) cannot be autoloaded while Rails is + # booting: with Zeitwerk they become available only once the app is + # fully initialized. +to_prepare+ runs after boot and again on each + # code reload in development. + app.config.to_prepare do + Eaco.parse_default_rules_file! end # :nocov: end From 43dd64dd41056ac7269971ee844849ee66fe02d8 Mon Sep 17 00:00:00 2001 From: "Aestimo K." Date: Thu, 2 Jul 2026 12:38:25 +0300 Subject: [PATCH 5/7] Tracker: PR #5 opened, CI matrix green Co-Authored-By: Claude Fable 5 --- PROJECT_TRACKER.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PROJECT_TRACKER.md b/PROJECT_TRACKER.md index ed2c8f5..55bd561 100644 --- a/PROJECT_TRACKER.md +++ b/PROJECT_TRACKER.md @@ -225,3 +225,6 @@ framework" (which loses a head-to-head with Pundit on simple role apps). `table_exists?` check must rescue `NoDatabaseError` so `rails db:create` works. Smoke test 11/11 green (lazy + eager-load boot); gem suite re-validated green on all three gemfiles. **Phase 1 code work fully complete.** +- **2026-07-02 (PR):** Opened PR #5 (`main` ← `phase-1-modernization`, + https://github.com/iamaestimo/eaco/pull/5). GitHub Actions CI green on all + 6 matrix cells (Ruby 3.2–4.0 × Rails 7.2/8.0/8.1, PG 16). From 6cdd257fa099fd612e5a25d01b5d7a79a89712f8 Mon Sep 17 00:00:00 2001 From: "Aestimo K." Date: Thu, 2 Jul 2026 13:12:33 +0300 Subject: [PATCH 6/7] Rename gem to eaco-abac; refresh README for the relaunch The eaco name on rubygems.org belongs to ifad, whose last release was 1.1.1 in 2017. This fork is now the maintained home, republished as eaco-abac. The require path, Eaco namespace and DSL are unchanged: gem "eaco-abac", require: "eaco". - gemspec renamed to eaco-abac.gemspec; name, homepage, metadata and authorship updated; description notes the continuation lineage - README: badges point at this repo and the eaco-abac gem; lineage note crediting ifad; supported-versions line; install section shows the require: override and the required jsonb acl migration; usage documents the ::Model top-level-constant requirement in config/authorization.rb (Zeitwerk); before_filter -> before_action in the controller example (removed in Rails 5.1); rubydoc links and dev-setup instructions modernized - tracker: repo-home/name decision recorded as resolved Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 6 ++++ Gemfile | 2 +- PROJECT_TRACKER.md | 10 ++++-- README.md | 60 +++++++++++++++++++++---------- eaco.gemspec => eaco-abac.gemspec | 12 +++---- 5 files changed, 61 insertions(+), 29 deletions(-) rename eaco.gemspec => eaco-abac.gemspec (81%) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3d8e7..041a422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ project adheres to [Semantic Versioning](https://semver.org/) ## Unreleased (2.0.0.beta1) +### Changed +* **Gem renamed to `eaco-abac`** — a maintained continuation of `eaco` + (last released 1.1.1, 2017, by ifad). The require path, `Eaco` namespace + and DSL are unchanged: use `gem "eaco-abac", require: "eaco"`. Project + home is now https://github.com/iamaestimo/eaco. + ### Added * Active Record compatibility modules for Rails 7.0, 7.1, 7.2, 8.0 and 8.1 (`V70`–`V81`). The adapter previously rejected any Active Record newer than diff --git a/Gemfile b/Gemfile index cc8d156..5d5b7ab 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' -# Specify your gem's dependencies in eaco.gemspec +# Specify your gem's dependencies in eaco-abac.gemspec gemspec diff --git a/PROJECT_TRACKER.md b/PROJECT_TRACKER.md index 55bd561..006ae89 100644 --- a/PROJECT_TRACKER.md +++ b/PROJECT_TRACKER.md @@ -187,9 +187,13 @@ framework" (which loses a head-to-head with Pundit on simple role apps). - [ ] **Full Appraisal removal?** Currently trimmed Appraisals + checked-in gemfiles (both kept in sync). Decide whether to drop the `appraisal` dev dep entirely and hand-maintain gemfiles, or keep Appraisal as the generator. -- [ ] **Repo home:** issues live on `iamaestimo/eaco`, but gemspec/README still point - at upstream `ifad/eaco`. Decide if this fork is the maintained home (affects - gemspec `homepage`, badges, README links). +- [x] ~~**Repo home:**~~ **DECIDED (2026-07-02):** `iamaestimo/eaco` is the + maintained home. Gem republished as **`eaco-abac`** (the `eaco` name on + rubygems.org is owned by ifad; name checked available). Require path stays + `require "eaco"` (`gem "eaco-abac", require: "eaco"`). Gemspec renamed to + `eaco-abac.gemspec`, homepage/badges/README all point at the fork; README + carries a lineage note crediting ifad's original. First publish will claim + the name via a *pending trusted publisher* on rubygems.org (no API key). - [x] ~~`coveralls` replacement~~ **DONE:** now plain `SimpleCov`. Optional future: wire CI to upload LCOV to a coverage service (qlty/codecov) — no gem needed. - [ ] When to actually tag `v2.0.0.beta1` (release workflow is in place; needs the diff --git a/README.md b/README.md index d5be538..4e9e345 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,19 @@ # Eaco -[![CI](https://github.com/ifad/eaco/actions/workflows/ci.yml/badge.svg)](https://github.com/ifad/eaco/actions/workflows/ci.yml) -[![Maintainability](https://qlty.sh/gh/ifad/projects/eaco/maintainability.svg)](https://qlty.sh/gh/ifad/projects/eaco) -[![Inline docs](https://inch-ci.org/github/ifad/eaco.svg?branch=master)](https://inch-ci.org/github/ifad/eaco) -[![Gem Version](https://badge.fury.io/rb/eaco.svg)](https://badge.fury.io/rb/eaco) +[![CI](https://github.com/iamaestimo/eaco/actions/workflows/ci.yml/badge.svg)](https://github.com/iamaestimo/eaco/actions/workflows/ci.yml) +[![Gem Version](https://badge.fury.io/rb/eaco-abac.svg)](https://rubygems.org/gems/eaco-abac) Eacus, the holder of the keys of Hades, is an Attribute-Based Access Control ([ABAC](https://en.wikipedia.org/wiki/Attribute-based_access_control)) authorization framework for Ruby. +Supports **Ruby 3.2–4.0** and **Rails 7.2–8.1**. + +> **Note:** the gem is published as [`eaco-abac`](https://rubygems.org/gems/eaco-abac) — +> a maintained continuation of the original [`eaco`](https://github.com/ifad/eaco) +> by [ifad](https://github.com/ifad), whose last release (1.1.1, 2017) supported +> Rails up to 5. Same library, same `require "eaco"`, same DSL — modernized and +> actively developed here. + ![Eaco e Telamone][eaco-e-telamone] *"Aeacus telemon by user Ravenous at en.wikipedia.org - Public domain through Wikimedia Commons - https://commons.wikimedia.org/wiki/File:Aeacus_telemon.jpg"* @@ -89,22 +95,38 @@ database. Adapters are provided for PG's +jsonb+ and for CouchDB-Lucene. Add this line to your application's Gemfile: - gem 'eaco' +```ruby +gem "eaco-abac", require: "eaco" +``` And then execute: - $ bundle + $ bundle install + +Each authorized model needs a `jsonb` column named `acl` (PostgreSQL): + +```ruby +class AddAclToDocuments < ActiveRecord::Migration[8.1] + def change + add_column :documents, :acl, :jsonb + end +end +``` ## Usage -Create `config/authorization.rb` [(rdoc)](https://www.rubydoc.info/github/ifad/eaco/master/Eaco/DSL) +Create `config/authorization.rb` [(rdoc)](https://www.rubydoc.info/github/iamaestimo/eaco/main/Eaco/DSL). +It is parsed when your app boots and re-parsed on each code reload in +development. Reference your models with top-level constants (`::Document`) — +the file is evaluated inside Eaco's DSL context, so a bare `Document` would +be looked up under the `Eaco::` namespace. ```ruby # Defines `Document` to be an authorized resource. # # Adds Document.accessible_by and Document#allows? # -authorize Document, using: :pg_jsonb do +authorize ::Document, using: :pg_jsonb do roles :owner, :editor, :reader permissions do @@ -119,7 +141,7 @@ end # # Adds User#designators # -actor User do +actor ::User do admin do |user| user.admin? end @@ -132,8 +154,8 @@ actor User do end ``` -Given a Resource [(rdoc)](https://www.rubydoc.info/github/ifad/eaco/master/Eaco/Resource) -with an ACL [(rdoc)](https://www.rubydoc.info/github/ifad/eaco/master/Eaco/ACL): +Given a Resource [(rdoc)](https://www.rubydoc.info/github/iamaestimo/eaco/main/Eaco/Resource) +with an ACL [(rdoc)](https://www.rubydoc.info/github/iamaestimo/eaco/main/Eaco/ACL): ```ruby # An example ACL @@ -144,7 +166,7 @@ with an ACL [(rdoc)](https://www.rubydoc.info/github/ifad/eaco/master/Eaco/ACL): => # :owner, "group:reviewers" => :reader}> ``` -and an Actor [(rdoc)](https://www.rubydoc.info/github/ifad/eaco/master/Eaco/Actor): +and an Actor [(rdoc)](https://www.rubydoc.info/github/iamaestimo/eaco/main/Eaco/Actor): ```ruby # An example Actor @@ -217,7 +239,7 @@ Grant reader access to a group: ``` Obtain a collection of Resources accessible by a given Actor -[(rdoc)](https://www.rubydoc.info/github/ifad/eaco/master/Eaco/Adapters): +[(rdoc)](https://www.rubydoc.info/github/iamaestimo/eaco/main/Eaco/Adapters): ```ruby >> Document.accessible_by(user) @@ -225,11 +247,11 @@ Obtain a collection of Resources accessible by a given Actor Check whether a controller action can be accessed by an user. Your Controller must respond to `current_user` for this to work. -[(rdoc)](https://www.rubydoc.info/github/ifad/eaco/master/Eaco/Controller) +[(rdoc)](https://www.rubydoc.info/github/iamaestimo/eaco/main/Eaco/Controller) ```ruby class DocumentsController < ApplicationController - before_filter :find_document + before_action :find_document authorize :show, [:document, :read] authorize :edit, [:document, :edit] @@ -243,13 +265,13 @@ end ## Running specs -You need a running postgresql 9.4 instance. +You need a running PostgreSQL instance. Create an user and a database: $ sudo -u postgres psql - postgres=# CREATE ROLE eaco LOGIN; + postgres=# CREATE ROLE eaco LOGIN PASSWORD 'yourpassword'; CREATE ROLE postgres=# CREATE DATABASE eaco OWNER eaco ENCODING 'utf8'; @@ -268,11 +290,11 @@ Run `rake`. This will run the specs and cucumber features and report coverage. Specs are run against the supported rails versions in turn. If you want to focus on a single release, use `appraisal rails-X.Y rake`, where `X.Y` can be -`3.2`, `4.0`, `4.1`, `4.2`, `5.0`, `5.1`, `5.2`, `6.0`, and `6.1`. +`7.2`, `8.0`, and `8.1`. ## Contributing -1. Fork it ( https://github.com/ifad/eaco/fork ) +1. Fork it ( https://github.com/iamaestimo/eaco/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) diff --git a/eaco.gemspec b/eaco-abac.gemspec similarity index 81% rename from eaco.gemspec rename to eaco-abac.gemspec index 7826132..3071635 100644 --- a/eaco.gemspec +++ b/eaco-abac.gemspec @@ -4,13 +4,13 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'eaco/version' Gem::Specification.new do |spec| - spec.name = "eaco" + spec.name = "eaco-abac" spec.version = Eaco::VERSION - spec.authors = ["Marcello Barnaba"] - spec.email = ["vjt@openssl.it"] + spec.authors = ["Aestimo K.", "Marcello Barnaba"] + spec.email = ["zeros.only@proton.me"] spec.summary = %q{Attribute-Based Access Control (ABAC) authorization framework for Ruby} - spec.description = %q{Eaco is an ABAC authorization framework: authorization decisions live in the database as per-record ACLs (designator => role), with efficient extraction of the collection of resources accessible by an actor.} - spec.homepage = "https://github.com/ifad/eaco" + spec.description = %q{Eaco is an ABAC authorization framework: authorization decisions live in the database as per-record ACLs (designator => role), with efficient extraction of the collection of resources accessible by an actor. Maintained continuation of the eaco gem, modernized for Rails 7.2-8.1 and Ruby 3.2-4.0.} + spec.homepage = "https://github.com/iamaestimo/eaco" spec.license = "MIT" spec.required_ruby_version = ">= 3.2" @@ -18,7 +18,7 @@ Gem::Specification.new do |spec| spec.metadata = { "homepage_uri" => spec.homepage, "source_code_uri" => spec.homepage, - "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md", + "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md", "rubygems_mfa_required" => "true" } From 0515ed99a5f2819d2008af09ec4aad7fb2b39f89 Mon Sep 17 00:00:00 2001 From: "Aestimo K." Date: Thu, 2 Jul 2026 13:23:18 +0300 Subject: [PATCH 7/7] Tracker: upstream ifad/eaco PR #24 discovered; beta tag on hold pending outreach Co-Authored-By: Claude Fable 5 --- PROJECT_TRACKER.md | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/PROJECT_TRACKER.md b/PROJECT_TRACKER.md index 006ae89..d919c1d 100644 --- a/PROJECT_TRACKER.md +++ b/PROJECT_TRACKER.md @@ -197,7 +197,33 @@ framework" (which loses a head-to-head with Pundit on simple role apps). - [x] ~~`coveralls` replacement~~ **DONE:** now plain `SimpleCov`. Optional future: wire CI to upload LCOV to a coverage service (qlty/codecov) — no gem needed. - [ ] When to actually tag `v2.0.0.beta1` (release workflow is in place; needs the - rubygems.org trusted-publisher registration first). + rubygems.org trusted-publisher registration first). **ON HOLD pending the + upstream outreach below.** +- [ ] **⚠ BLOCKING — upstream is stirring: ifad/eaco PR #24** (discovered + 2026-07-02). An ifad member (lleirborras) opened "Support Rails 8.1 / Ruby 4" + on 2026-06-19 — motivation is their internal fleet upgrade (edoc, hermeshq, + noobs, scriptoria depend on eaco). Open, unreviewed, unmerged as of today. + - **Overlap with our Phase 1:** same core compat work; their railtie fix is + the *identical* `app.config.to_prepare` approach (independent convergence). + - **They have / we don't:** a single `Modern` fallback module for AR ≥ 7.0 + instead of per-version `V70`–`V81` copies — cleaner; adopt it later + regardless of outcome (logged under Phase 2 cleanup below). + - **We have / they don't:** `rails db:create` `NoDatabaseError` fix; + real-Rails-8.1-app validation; clean version floor (they keep Ruby 2.7 / + Rails 6.1, coveralls, yard-cucumber, Cucumber ~9.2); Cucumber 11; trusted + publishing release automation; README repositioning; Phase 2 ABAC roadmap; + stated intent of long-term community maintenance. + - **Risk:** ifad owns the `eaco` rubygems name. If they merge + release 2.x, + the "rescue of an abandoned gem" launch story collapses and `eaco-abac` + becomes a confusing parallel fork. + - **Plan (decided 2026-07-02):** comment on their PR #24 — congratulate, + note the identical railtie fix, offer to upstream the `db:create` fix and + the rest, ask about release plans and co-maintainership / gem ownership. + **Timebox: 2 weeks** (until ~2026-07-16). Outcomes: (a) maintainership → + inherit the real name, retire `eaco-abac` plan; (b) they release, we + contribute upstream; (c) silence → launch `eaco-abac` with the honest + "offered upstream first" story. Draft comment prepared; posting is + Aestimo's call, in his voice. ## GitHub issue sync notes (apply later when we sync) @@ -231,4 +257,17 @@ framework" (which loses a head-to-head with Pundit on simple role apps). all three gemfiles. **Phase 1 code work fully complete.** - **2026-07-02 (PR):** Opened PR #5 (`main` ← `phase-1-modernization`, https://github.com/iamaestimo/eaco/pull/5). GitHub Actions CI green on all - 6 matrix cells (Ruby 3.2–4.0 × Rails 7.2/8.0/8.1, PG 16). + 6 matrix cells (Ruby 3.2–4.0 × Rails 7.2/8.0/8.1, PG 16). Merged by Aestimo. +- **2026-07-02 (rename):** Gem renamed to `eaco-abac` (PR #6, + https://github.com/iamaestimo/eaco/pull/6): gemspec → `eaco-abac.gemspec`, + homepage/badges/links → this fork, README relaunch refresh (lineage note, + jsonb migration, `::Model` constant requirement, `before_filter` → + `before_action`). Suite re-verified green. RubyGems account created; + pending-trusted-publisher registration deferred (reservation expires in + days — create it right before merge+tag). +- **2026-07-02 (upstream):** Discovered ifad/eaco PR #24 (Rails 8.1/Ruby 4 + support by an ifad member, opened 2026-06-19). Beta tag put ON HOLD; + outreach comment drafted for Aestimo to post. See the blocking item under + *Open decisions*. Cleanup candidate regardless of outcome: replace our + per-version `V70`–`V81` compat copies with their single `Modern` fallback + module for AR ≥ 7.0.