studio-engine 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31ed2941026e5bf0114872ba16dd7a9b94b1a472d5407ff82ec604ba2e659962
4
- data.tar.gz: 1fda4b1328315323b8afcee644a68acc8678e756762a88b289ea10e0fc46a6cf
3
+ metadata.gz: 4d4a633071c04010dadfb92d3beef1a7e075a90c94e55ab5761a03a114f48f40
4
+ data.tar.gz: fc17d48b30cfcbde79e4c3bdc5bf618789a7765dbc359a134b28800cc772429e
5
5
  SHA512:
6
- metadata.gz: 6431e1dafbd0a614e1c0a6abe25004ab3888a3d1e7c263ab97d7883e10bcedde60da03f16c98b262d9926173749343b00c33d6a5580db9fc299c8b1b355a9665
7
- data.tar.gz: 7513d2735c2535ed520530eab4adc0b1ce60de617488e4fa934e43beb31ef330e7e49c8837e3310aa92467559dbabf714fab43b81d86a0d52cebce8f85674e35
6
+ metadata.gz: e32e14a996202c61dd230a4084927539618341c0343bbafad1066280394da6d84976222a5621000157d6e84df83452c6a5f234f36ca11e18c408e340757a0b49
7
+ data.tar.gz: 8592453e2efdbad7c3df75bfb13274a6abfa080f9de9e6188a70d90e935d2dfa1446c788f15d55eab467207bcfa9cef129bf9b0abf3fbb001e846a11a6c31f3f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.11.0 — 2026-06-24
8
+
9
+ ### Changed
10
+ - **Rails 8.1 support** — widened the gemspec `rails` bound from
11
+ `">= 7.0", "< 8.0"` to `">= 7.2", "< 8.2"`, so consuming apps can migrate to
12
+ Rails 8.1 independently (the engine no longer pins the ecosystem to Rails 7).
13
+ This release is the gate for the McRitchie Rails 8.1 train — `mcritchie-studio`
14
+ and `turf-monster` can bundle Rails 8.1 only once this version is published.
15
+ The lower bound moves to 7.2 (the oldest Rails the apps actually run) since
16
+ Rails 8 requires Ruby >= 3.2 and the ecosystem is already on Ruby 3.3.11.
17
+
18
+ ### Added
19
+ - **`test/dummy` Rails app + `test/integration/engine_rails_8_1_boot_test.rb`** —
20
+ boots the engine inside a real Rails app and exercises its Rails-version-
21
+ sensitive surfaces (the Railtie registration, Zeitwerk autoload wiring, the
22
+ `Studio.routes` DSL, and the `ErrorLog` / `ThemeSetting` / `Sluggable`
23
+ ActiveRecord models). Re-run this against any new Rails line before widening
24
+ the bound again. The engine's full unit suite + this boot test are green
25
+ against Rails 8.1.3 (ActiveSupport/ActiveRecord 8.1, Zeitwerk 2.8, minitest 6).
26
+
27
+ ### Fixed
28
+ - `bin/release-check` now prefers the `ruby@3.3` toolchain (was pinned to the
29
+ retired `ruby@3.1`, which cannot resolve Rails 8.1) and runs the full test
30
+ list, including the new Rails 8.1 boot test and the previously-omitted
31
+ `link_token` / sticky-header / admin-models-table tests.
32
+
33
+ ## 0.10.0 — 2026-06-24
34
+
35
+ ### Added
36
+ - **Shared websocket / Redis primitive** (`docs/CABLE.md`) — one place for the
37
+ setup every host app's realtime needs, extracted after a SEV-1 (a host app
38
+ shipped an ActionCable channel with no `redis` gem and no TLS cable config; the
39
+ broadcast raised `Gem::LoadError` — a `ScriptError`, not a `StandardError` — which
40
+ escaped a `rescue StandardError` and 500'd every task write).
41
+ - **`redis` is now an engine dependency** (plus `turbo-rails`), so a consuming app
42
+ can never hit that `Gem::LoadError` again.
43
+ - **`Studio::Redis`** — the single source of Redis connection truth for `cable.yml`,
44
+ the `cache_store`, and Sidekiq: `.url`, `.tls?`, and `.options(**extra)`, which
45
+ auto-applies `ssl_params: { verify_mode: VERIFY_NONE }` for Heroku's `rediss://`
46
+ self-signed TLS (the gotcha that silently drops every broadcast).
47
+ - **`Studio::Cable.safe_broadcast { … }`** — best-effort broadcast guard that
48
+ catches `StandardError` **and** `ScriptError`, so a cable failure never breaks
49
+ the caller (the exact hole that caused the SEV-1).
50
+ - **`Studio::Broadcastable`** — model concern with safe Turbo-Streams wrappers
51
+ (`safe_broadcast_replace_to`, `_append_to`, `_prepend_to`, `_update_to`,
52
+ `_remove_to`) every app should broadcast through.
53
+
7
54
  ## 0.9.0 — 2026-06-23
8
55
 
9
56
  ### Added
data/Gemfile CHANGED
@@ -2,17 +2,19 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- # nokogiri 1.18.10 has 3 CVEs (1 HIGH regex-backtracking + 2 MEDIUM
6
- # in XSLT / xmlC14NExecute). Fix is `>= 1.19.3` but nokogiri 1.19.x
7
- # requires Ruby 3.2+. The ecosystem currently runs on Ruby 3.1.7;
8
- # bumping Ruby is a separate effort (across both Rails apps).
9
- #
10
- # **Important**: this gem does NOT declare nokogiri as a runtime
11
- # dependency in its gemspec — nokogiri comes into the consuming
12
- # Rails app via Rails itself. Consumers running Rails on a Ruby
13
- # version that supports nokogiri 1.19.x will resolve it. The
14
- # studio-engine gem's own Gemfile.lock (this repo's dev env) is the
15
- # only place still on 1.18.10.
16
- #
17
- # Tracked in SECURITY-AUDIT-2026-05-17.md. When the ecosystem bumps
18
- # to Ruby 3.2+, add: gem "nokogiri", ">= 1.19.3", group: :development
5
+ # nokogiri: historically pinned to 1.18.10 here while the ecosystem was on
6
+ # Ruby 3.1.7 (nokogiri >= 1.19.x requires Ruby 3.2+). The ecosystem is now on
7
+ # Ruby 3.3.11, so a plain `bundle update` resolves the patched nokogiri
8
+ # (>= 1.19.3, the line that fixed 3 CVEs: 1 HIGH regex-backtracking + 2 MEDIUM
9
+ # XSLT / xmlC14NExecute). This gem still does NOT declare nokogiri as a runtime
10
+ # dependency it arrives via Rails in the consuming app. Tracked in
11
+ # SECURITY-AUDIT-2026-05-17.md.
12
+
13
+ group :development, :test do
14
+ # SQLite drives the test/dummy Rails app's in-memory database (see
15
+ # test/integration/engine_rails_8_1_boot_test.rb), which boots the engine
16
+ # against a real Rails app so we catch Rails-version incompatibilities in the
17
+ # engine's ActiveRecord/Railtie surfaces. Not a runtime dependency — the
18
+ # consuming apps bring their own database adapter. Rails 8.1 needs sqlite3 ~> 2.1.
19
+ gem "sqlite3", ">= 2.1"
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Studio
4
+ # Mixin for ActiveRecord models that broadcast Turbo Streams. It wraps
5
+ # turbo-rails' broadcast_*_to methods in Studio::Cable.safe_broadcast, so a cable
6
+ # failure (a Redis hiccup, a missing/misconfigured adapter) can NEVER break the
7
+ # model save / after_commit that triggered the broadcast — the SEV-1 guard, in
8
+ # one place. The host model already has the raw broadcast_*_to methods (turbo-rails
9
+ # includes Turbo::Broadcastable into ActiveRecord::Base); these are the SAFE
10
+ # variants every app should broadcast through.
11
+ #
12
+ # class Task < ApplicationRecord
13
+ # include Studio::Broadcastable
14
+ # after_create_commit { safe_broadcast_replace_to [:board], target: "card_#{id}",
15
+ # partial: "tasks/card", locals: { task: self } }
16
+ # end
17
+ module Broadcastable
18
+ extend ActiveSupport::Concern
19
+
20
+ def safe_broadcast_replace_to(*args, **kwargs, &block)
21
+ Studio::Cable.safe_broadcast { broadcast_replace_to(*args, **kwargs, &block) }
22
+ end
23
+
24
+ def safe_broadcast_update_to(*args, **kwargs, &block)
25
+ Studio::Cable.safe_broadcast { broadcast_update_to(*args, **kwargs, &block) }
26
+ end
27
+
28
+ def safe_broadcast_append_to(*args, **kwargs, &block)
29
+ Studio::Cable.safe_broadcast { broadcast_append_to(*args, **kwargs, &block) }
30
+ end
31
+
32
+ def safe_broadcast_prepend_to(*args, **kwargs, &block)
33
+ Studio::Cable.safe_broadcast { broadcast_prepend_to(*args, **kwargs, &block) }
34
+ end
35
+
36
+ def safe_broadcast_remove_to(*args, **kwargs, &block)
37
+ Studio::Cable.safe_broadcast { broadcast_remove_to(*args, **kwargs, &block) }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "redis"
4
+
5
+ module Studio
6
+ # Shared ActionCable / Turbo-Streams helpers for host apps.
7
+ module Cable
8
+ # Run a broadcast best-effort: a cable failure must NEVER break the caller (the
9
+ # model save / task write that triggered it). ScriptError is caught ON PURPOSE —
10
+ # a missing or misconfigured cable adapter raises Gem::LoadError, which is a
11
+ # ScriptError, NOT a StandardError. A plain `rescue StandardError` let exactly
12
+ # that escape an after_commit and 500 every task write in production (the SEV-1
13
+ # this primitive exists to prevent). Failures are captured to ErrorLog when the
14
+ # host defines it, else logged. Returns nil on failure.
15
+ #
16
+ # The never-raise guarantee is ABSOLUTE: even logging the failure must not break
17
+ # the caller. ErrorLog.capture! writes to the DB and can itself raise (e.g.
18
+ # ActiveRecord::NoDatabaseError when the DB is down), so the logging is wrapped
19
+ # in its own rescue — the guard cannot be defeated by its own error path.
20
+ def self.safe_broadcast
21
+ yield
22
+ rescue StandardError, ScriptError => e
23
+ begin
24
+ if defined?(ErrorLog) && ErrorLog.respond_to?(:capture!)
25
+ ErrorLog.capture!(e)
26
+ elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
27
+ Rails.logger.warn("[studio-cable] broadcast failed (non-fatal): #{e.class}: #{e.message}")
28
+ end
29
+ rescue StandardError, ScriptError
30
+ nil # logging is best-effort too — the never-raise guarantee is absolute
31
+ end
32
+ nil
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Studio
6
+ # Single source of Redis connection truth for EVERY Redis client in a host app —
7
+ # ActionCable (config/cable.yml), the Rails cache_store, and Sidekiq. It bakes in
8
+ # the Heroku gotcha so no app re-hits it:
9
+ #
10
+ # Heroku Redis serves rediss:// (TLS) with a SELF-SIGNED cert. redis-client
11
+ # verifies peer certs by default, so the connection is silently REJECTED — and
12
+ # because ActionCable's pubsub failure is silent (the /cable socket still
13
+ # upgrades to 101), broadcasts simply never reach subscribers; the cache no-ops.
14
+ # The fix is ssl_params verify_mode VERIFY_NONE, applied automatically here for
15
+ # any rediss:// URL.
16
+ #
17
+ # Usage:
18
+ # cable.yml -> Studio::Redis.options (url + ssl_params)
19
+ # cache_store -> Studio::Redis.options(namespace: "x", expires_in: 90.minutes)
20
+ # sidekiq -> Studio::Redis.options
21
+ module Redis
22
+ # The Redis URL from the environment, with a local-dev fallback.
23
+ def self.url(default = "redis://localhost:6379/1")
24
+ ENV.fetch("REDIS_URL", default)
25
+ end
26
+
27
+ # True when the endpoint is TLS (Heroku Redis), i.e. a rediss:// URL.
28
+ def self.tls?(redis_url = url)
29
+ redis_url.to_s.start_with?("rediss://")
30
+ end
31
+
32
+ # Connection options for a Redis client: the url, plus — for a TLS endpoint —
33
+ # the self-signed-cert handling Heroku requires. Caller extras (namespace,
34
+ # reconnect_attempts, error_handler, …) merge through untouched.
35
+ def self.options(redis_url: url, **extra)
36
+ opts = { url: redis_url }.merge(extra)
37
+ opts[:ssl_params] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } if tls?(redis_url)
38
+ opts
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.9.0"
2
+ VERSION = "0.11.0"
3
3
  end
data/lib/studio.rb CHANGED
@@ -10,6 +10,8 @@ require "studio/link_token"
10
10
  require "studio/email"
11
11
  require "studio/email_smoke"
12
12
  require "studio/mail_transport"
13
+ require "studio/redis"
14
+ require "studio/cable"
13
15
 
14
16
  module Studio
15
17
  mattr_accessor :app_name, default: "Studio"
@@ -21,11 +21,16 @@ Gem::Specification.new do |spec|
21
21
  spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "db/**/*", "tailwind/**/*", "Gemfile", "studio-engine.gemspec", "README.md", "CHANGELOG.md", "LICENSE"]
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.add_dependency "rails", ">= 7.0", "< 8.0"
24
+ spec.add_dependency "rails", ">= 7.2", "< 8.2"
25
25
  spec.add_dependency "tailwindcss-rails", "~> 4.5"
26
26
  spec.add_dependency "faker", ">= 2.0", "< 4.0"
27
27
  spec.add_dependency "solid_queue", ">= 1.0", "< 2.0"
28
28
  spec.add_dependency "aws-sdk-s3", "~> 1.218"
29
29
  spec.add_dependency "mini_magick", "~> 5.0"
30
30
  spec.add_dependency "resend", "~> 1.1"
31
+ # Realtime: the redis cable/cache/Sidekiq adapter (Studio::Redis) + Turbo Streams
32
+ # broadcasting (Studio::Broadcastable). `redis` is the dependency whose ABSENCE
33
+ # 500'd a host app's task board — declaring it here makes that impossible to repeat.
34
+ spec.add_dependency "redis", ">= 4.0.1"
35
+ spec.add_dependency "turbo-rails", ">= 1.0"
31
36
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-06-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -16,20 +15,20 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '7.0'
18
+ version: '7.2'
20
19
  - - "<"
21
20
  - !ruby/object:Gem::Version
22
- version: '8.0'
21
+ version: '8.2'
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
26
25
  requirements:
27
26
  - - ">="
28
27
  - !ruby/object:Gem::Version
29
- version: '7.0'
28
+ version: '7.2'
30
29
  - - "<"
31
30
  - !ruby/object:Gem::Version
32
- version: '8.0'
31
+ version: '8.2'
33
32
  - !ruby/object:Gem::Dependency
34
33
  name: tailwindcss-rails
35
34
  requirement: !ruby/object:Gem::Requirement
@@ -126,6 +125,34 @@ dependencies:
126
125
  - - "~>"
127
126
  - !ruby/object:Gem::Version
128
127
  version: '1.1'
128
+ - !ruby/object:Gem::Dependency
129
+ name: redis
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 4.0.1
135
+ type: :runtime
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 4.0.1
142
+ - !ruby/object:Gem::Dependency
143
+ name: turbo-rails
144
+ requirement: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '1.0'
149
+ type: :runtime
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '1.0'
129
156
  description: Studio Engine is a non-isolated Rails engine that ships an opinionated
130
157
  authentication + SSO contract, a polymorphic ErrorLog model, a Sluggable concern,
131
158
  a 7-role dynamic theme system with CSS-custom-property generation, and an S3-backed
@@ -169,6 +196,7 @@ files:
169
196
  - app/mailers/application_mailer.rb
170
197
  - app/mailers/user_mailer.rb
171
198
  - app/models/concerns/sluggable.rb
199
+ - app/models/concerns/studio/broadcastable.rb
172
200
  - app/models/current.rb
173
201
  - app/models/error_log.rb
174
202
  - app/models/image_cache.rb
@@ -239,6 +267,7 @@ files:
239
267
  - db/migrate/20260623130000_create_studio_enumerals.rb
240
268
  - lib/studio-engine.rb
241
269
  - lib/studio.rb
270
+ - lib/studio/cable.rb
242
271
  - lib/studio/color_scale.rb
243
272
  - lib/studio/email.rb
244
273
  - lib/studio/email_smoke.rb
@@ -246,6 +275,7 @@ files:
246
275
  - lib/studio/image_cache.rb
247
276
  - lib/studio/link_token.rb
248
277
  - lib/studio/mail_transport.rb
278
+ - lib/studio/redis.rb
249
279
  - lib/studio/s3.rb
250
280
  - lib/studio/theme_resolver.rb
251
281
  - lib/studio/ui_primitives.rb
@@ -263,7 +293,6 @@ metadata:
263
293
  source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
264
294
  bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
265
295
  changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
266
- post_install_message:
267
296
  rdoc_options: []
268
297
  require_paths:
269
298
  - lib
@@ -278,8 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
278
307
  - !ruby/object:Gem::Version
279
308
  version: '0'
280
309
  requirements: []
281
- rubygems_version: 3.5.11
282
- signing_key:
310
+ rubygems_version: 4.0.9
283
311
  specification_version: 4
284
312
  summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
285
313
  image caching