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 +4 -4
- data/CHANGELOG.md +47 -0
- data/Gemfile +16 -14
- data/app/models/concerns/studio/broadcastable.rb +40 -0
- data/lib/studio/cable.rb +35 -0
- data/lib/studio/redis.rb +41 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +2 -0
- data/studio-engine.gemspec +6 -1
- metadata +38 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d4a633071c04010dadfb92d3beef1a7e075a90c94e55ab5761a03a114f48f40
|
|
4
|
+
data.tar.gz: fc17d48b30cfcbde79e4c3bdc5bf618789a7765dbc359a134b28800cc772429e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
data/lib/studio/cable.rb
ADDED
|
@@ -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
|
data/lib/studio/redis.rb
ADDED
|
@@ -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
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
data/studio-engine.gemspec
CHANGED
|
@@ -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.
|
|
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.
|
|
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:
|
|
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.
|
|
18
|
+
version: '7.2'
|
|
20
19
|
- - "<"
|
|
21
20
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: '8.
|
|
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.
|
|
28
|
+
version: '7.2'
|
|
30
29
|
- - "<"
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '8.
|
|
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:
|
|
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
|