rails_ops 1.7.8 → 1.8.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: d40d2ec6a8a45a3725040a4f9fd0837ca7e42d36625827d1253e1722589e2e21
4
- data.tar.gz: 53c235fe982863f62f0e6a756f6d7be9f127ad7b7693b1f46f4100b7b914bec8
3
+ metadata.gz: d983d20a0a0bf3e1b7e967d0dbd16aec64f253c8a7a12e7c6b72f9f6f676b604
4
+ data.tar.gz: 00a7b268b2337ba5b2cf57855b9d611a7c9dbe3492cd9484345ad1f7956b2b27
5
5
  SHA512:
6
- metadata.gz: d226ffc592c1c46ed0c536d6e33e3f54342afc59ef6e162670437b7d2847df8d05d213b62ff128de3cb1084e53470a8bd5c7fa37ac875c9e65610ab54585633d
7
- data.tar.gz: 611fbd338f724d311d32d300677bf378d031891543bced3f074134793a45e106c337ea8a07958a36d6696f355965d0f23f004f14e2b881605cb68c1de58ac257
6
+ metadata.gz: 9a50ef1e82a5a82e1adddb76435389babd2f34a6fe3e4f80c06bf3ec061b26235fa80541cfbfdab6fdc43f253245b388b1b376d8032e090c510fe1a4f2edc591
7
+ data.tar.gz: fca76d3c3c65cc564744596dd298e8a52e69320cd0eb5952750e689dc879b21dce5dab13dd6372456108ea367aa536a62e40a58d4c069af5ab78eb214167c3be
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.0 (2026-05-07)
4
+
5
+ * `Operation#run` (non-bang) now wraps the call to `run!` in a
6
+ SAVEPOINT whenever a database transaction is already open. This
7
+ ensures that partial writes performed before a validation error are
8
+ rolled back even though `run` swallows the error and returns
9
+ `false`. `run_sub` benefits from the same protection transitively.
10
+ Behavior outside of an open transaction is unchanged, and the
11
+ behavior of `run!` / `run_sub!` is unchanged.
12
+ * Deprecate `with_rollback_on_exception`. The helper now emits an
13
+ `ActiveSupport::Deprecation` warning via `RailsOps.deprecator` and
14
+ will be removed in RailsOps 2.0. The savepoint added to `run` makes
15
+ it obsolete for the common "save then do more work" pattern. For
16
+ non-validation errors that should trigger a rollback, raise
17
+ `RailsOps::Exceptions::RollbackRequired` directly.
18
+
3
19
  ## 1.7.8 (2026-03-12)
4
20
 
5
21
  * Add practical examples throughout README covering non-model operations,
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_ops (1.7.8)
4
+ rails_ops (1.8.0)
5
5
  active_type (>= 1.3.0)
6
6
  minitest
7
7
  rails (> 4)
@@ -132,7 +132,7 @@ GEM
132
132
  mini_mime (1.1.5)
133
133
  minitest (5.27.0)
134
134
  mutex_m (0.3.0)
135
- net-imap (0.4.20)
135
+ net-imap (0.4.24)
136
136
  date
137
137
  net-protocol
138
138
  net-pop (0.1.2)
@@ -158,8 +158,8 @@ GEM
158
158
  psych (5.1.2)
159
159
  stringio
160
160
  racc (1.8.1)
161
- rack (3.2.5)
162
- rack-session (2.1.1)
161
+ rack (3.2.6)
162
+ rack-session (2.1.2)
163
163
  base64 (>= 0.1.0)
164
164
  rack (>= 3.0.0)
165
165
  rack-test (2.1.0)
@@ -241,7 +241,7 @@ GEM
241
241
  sqlite3 (1.6.2-x86_64-linux)
242
242
  stringio (3.1.0)
243
243
  thor (1.3.1)
244
- timeout (0.6.0)
244
+ timeout (0.6.1)
245
245
  tzinfo (2.0.6)
246
246
  concurrent-ruby (~> 1.0)
247
247
  unicode-display_width (3.1.4)
data/README.md CHANGED
@@ -720,24 +720,22 @@ module Operations::Article
720
720
  model.published_at = Time.current
721
721
  super # Save the article
722
722
 
723
- with_rollback_on_exception do
724
- run_sub! Operations::Cache::Rebuild, rebuild_counters: true
725
- run_sub! Operations::Notification::Send,
726
- template: 'article_published',
727
- record_id: model.id,
728
- record_type: 'Article'
729
- end
723
+ run_sub! Operations::Cache::Rebuild, rebuild_counters: true
724
+ run_sub! Operations::Notification::Send,
725
+ template: 'article_published',
726
+ record_id: model.id,
727
+ record_type: 'Article'
730
728
  end
731
729
  end
732
730
  end
733
731
  ```
734
732
 
735
- Note the use of `with_rollback_on_exception`: if any sub-operation fails after
736
- `super` has already saved the article, the exception is re-raised as
737
- `RailsOps::Exceptions::RollbackRequired`, which is not caught by `run` and
738
- therefore causes a surrounding transaction to roll back. Without it, the article
739
- could remain saved even if the notification fails (since `run` catches standard
740
- validation errors). See section *Transactions* for more details.
733
+ Validation errors raised by either sub-operation propagate up — `run_sub!`
734
+ re-raises any `validation_errors` as
735
+ `RailsOps::Exceptions::SubOpValidationFailed`, which escapes the parent's
736
+ `run` and rolls back the surrounding transaction. If the parent is invoked
737
+ via `run` (non-bang), the savepoint added in 1.8.0 ensures `super`'s save
738
+ is rolled back too. See section *Transactions* for more details.
741
739
 
742
740
  ## Contexts
743
741
 
@@ -2210,28 +2208,67 @@ end
2210
2208
  Typically though, transactions are opened on a higher level and outside of
2211
2209
  operations, e.g. in controller methods.
2212
2210
 
2213
- ### Rollback on Exception
2211
+ ### Automatic Savepoint Around `run`
2214
2212
 
2215
- When using `run` (without bang), validation errors are caught and may not cause
2216
- transaction rollback. The `with_rollback_on_exception` helper ensures that
2217
- exceptions within its block are re-raised as `RollbackRequired`, which will
2218
- cause a rollback even when using `run`:
2213
+ Since version 1.8.0, `RailsOps::Operation#run` (the non-bang variant)
2214
+ automatically wraps the operation in a SAVEPOINT whenever a database
2215
+ transaction is already open. If the operation raises a validation error
2216
+ partway through `perform`, the savepoint is rolled back before `run`
2217
+ catches the error and returns `false`. This eliminates the most common
2218
+ reason to reach for `with_rollback_on_exception`.
2219
2219
 
2220
2220
  ```ruby
2221
- class Operations::User::ComplexUpdate < RailsOps::Operation::Model::Update
2221
+ class Operations::User::Create < RailsOps::Operation::Model::Create
2222
2222
  def perform
2223
- ActiveRecord::Base.transaction do
2224
- super # Saves the user
2223
+ super # Saves the user
2224
+ something_else! # If this raises ActiveRecord::RecordInvalid, the
2225
+ # save above is rolled back automatically when the
2226
+ # operation is invoked through `run`.
2227
+ end
2228
+ end
2225
2229
 
2226
- # Without with_rollback_on_exception, validation errors here won't
2227
- # roll back the transaction when the operation is called with run
2228
- with_rollback_on_exception do
2229
- model.profile.bio = params[:bio]
2230
- model.profile.save! # If this fails, transaction is rolled back
2230
+ # Caller (controller, job, etc.):
2231
+ ActiveRecord::Base.transaction do
2232
+ if Operations::User::Create.run(user: { name: 'Alice' })
2233
+ # success path
2234
+ else
2235
+ # validation error — partial writes have already been rolled back
2236
+ end
2237
+ end
2238
+ ```
2231
2239
 
2232
- model.settings.notifications = params[:notifications]
2233
- model.settings.save! # If this fails, transaction is rolled back
2234
- end
2240
+ A savepoint is only created if a transaction is already open at the time
2241
+ of the call. Without an outer transaction (rake tasks, console sessions,
2242
+ some background jobs), `run` calls `run!` directly and behavior is
2243
+ unchanged: the caller is expected to open a transaction if it wants
2244
+ atomicity. `run!` itself is never wrapped — exceptions propagate naturally
2245
+ and the surrounding transaction (if any) rolls back.
2246
+
2247
+ `run_sub` (non-bang) benefits from the same protection transitively, since
2248
+ it delegates to `.run` on the sub-operation.
2249
+
2250
+ ### Rollback on Exception (Non-Validation Errors)
2251
+
2252
+ The savepoint above only protects against **validation errors**
2253
+ (`RailsOps::Exceptions::ValidationFailed` and
2254
+ `ActiveRecord::RecordInvalid`). For other `StandardError` subclasses that
2255
+ should also trigger a rollback when `run` is used, the
2256
+ `with_rollback_on_exception` helper re-raises them as
2257
+ `RailsOps::Exceptions::RollbackRequired`, which is not part of
2258
+ `validation_errors` and therefore propagates up through `run` and rolls
2259
+ back the surrounding transaction:
2260
+
2261
+ ```ruby
2262
+ class Operations::User::ComplexUpdate < RailsOps::Operation::Model::Update
2263
+ def perform
2264
+ super # Saves the user — validation errors here are handled by the
2265
+ # automatic savepoint in `run`.
2266
+
2267
+ with_rollback_on_exception do
2268
+ ExternalApi.call!(model) # raises a custom StandardError on failure;
2269
+ # converted into RollbackRequired so the
2270
+ # outer transaction rolls back even when
2271
+ # the operation is invoked via `run`.
2235
2272
  end
2236
2273
  end
2237
2274
  end
@@ -2250,7 +2287,7 @@ end
2250
2287
  ```
2251
2288
 
2252
2289
  **Important**: `with_rollback_on_exception` only works within an existing
2253
- transaction. It doesn't create a transaction - it just ensures exceptions
2290
+ transaction. It doesn't create a transaction it just ensures exceptions
2254
2291
  cause rollback:
2255
2292
 
2256
2293
  ```ruby
@@ -2260,7 +2297,9 @@ class Operations::Order::Process < RailsOps::Operation::Model::Update
2260
2297
  super # Order is saved and committed in its own transaction
2261
2298
 
2262
2299
  with_rollback_on_exception do
2263
- model.line_items.each { |item| item.update!(status: 'processed') }
2300
+ ExternalApi.charge!(model) # Raises a custom StandardError on failure;
2301
+ # the order has already been committed and
2302
+ # cannot be rolled back.
2264
2303
  end
2265
2304
  end
2266
2305
  end
@@ -2269,10 +2308,13 @@ end
2269
2308
  class Operations::Order::Process < RailsOps::Operation::Model::Update
2270
2309
  def perform
2271
2310
  ActiveRecord::Base.transaction do
2272
- super # Order is saved
2311
+ super # Order is saved within the surrounding transaction
2273
2312
 
2274
2313
  with_rollback_on_exception do
2275
- model.line_items.each { |item| item.update!(status: 'processed') }
2314
+ ExternalApi.charge!(model) # Custom StandardError is converted into
2315
+ # RollbackRequired, which propagates
2316
+ # through `run` and rolls back the
2317
+ # transaction including `super`.
2276
2318
  end
2277
2319
  end
2278
2320
  end
@@ -2322,9 +2364,13 @@ complete.
2322
2364
 
2323
2365
  ### Important Notes on Transactions
2324
2366
 
2325
- 1. **Validation Errors**: When using `run` (without bang), validation errors
2326
- are caught and won't roll back the transaction. Use `run!` for
2327
- sub-operations to ensure transaction rollback on validation errors.
2367
+ 1. **Validation Errors**: When `run` (without bang) is called inside an
2368
+ open transaction, RailsOps wraps the operation in a SAVEPOINT so that
2369
+ any partial writes are rolled back before the caught validation error
2370
+ is converted into a `false` return value. `run_sub` benefits from the
2371
+ same protection. Use `run!` / `run_sub!` when you want validation
2372
+ errors to propagate and roll back the surrounding transaction
2373
+ directly.
2328
2374
 
2329
2375
  2. **External Services**: Be careful when calling external services within
2330
2376
  transactions. Long-running external calls can cause database locks:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.8
1
+ 1.8.0
@@ -14,6 +14,12 @@ class RailsOps::Operation
14
14
  Symbol
15
15
  ].freeze
16
16
 
17
+ # Tracks which call sites have already emitted the deprecation warning
18
+ # for {with_rollback_on_exception}. Keyed by `"<path>:<lineno>"` so the
19
+ # warning fires at most once per call location across the process,
20
+ # avoiding log spam in long-running servers and per-request hot paths.
21
+ WITH_ROLLBACK_DEPRECATION_SEEN = Concurrent::Map.new
22
+
17
23
  attr_reader :params
18
24
  attr_reader :context
19
25
 
@@ -102,8 +108,26 @@ class RailsOps::Operation
102
108
 
103
109
  # Runs the operation using {run!} but rescues certain exceptions. Returns
104
110
  # `true` on success, otherwise `false`.
111
+ #
112
+ # If a database transaction is already open when {run} is called, the call
113
+ # to {run!} is wrapped in a savepoint via
114
+ # `ActiveRecord::Base.transaction(requires_new: true)`. This ensures that
115
+ # any database writes performed by the operation are rolled back if a
116
+ # validation error is raised, even though that error is then caught here
117
+ # and converted into a `false` return value. This eliminates the most
118
+ # common reason for using {with_rollback_on_exception}.
119
+ #
120
+ # When no transaction is open, behavior is identical to calling {run!}
121
+ # directly: the caller is responsible for atomicity.
105
122
  def run
106
- run!
123
+ if ActiveRecord::Base.connection.transaction_open?
124
+ ActiveRecord::Base.transaction(requires_new: true) do
125
+ run!
126
+ end
127
+ else
128
+ run!
129
+ end
130
+
107
131
  return true
108
132
  rescue *validation_errors
109
133
  return false
@@ -186,44 +210,37 @@ class RailsOps::Operation
186
210
  end
187
211
  end
188
212
 
189
- # Yields the given block and rethrows any possible exception as a
213
+ # Yields the given block and rethrows any possible `StandardError` as a
190
214
  # {RailsOps::Exceptions::RollbackRequired} exception.
191
215
  #
192
- # For illustration of potential use cases, consider the following example:
216
+ # @deprecated Since 1.8.0, validation errors raised inside {run} no
217
+ # longer leak partial database writes: {run} wraps the call to
218
+ # {run!} in a SAVEPOINT whenever an outer transaction is open, so
219
+ # any prior `model.save!` is rolled back automatically before
220
+ # {run} returns `false`. This helper is therefore obsolete for
221
+ # the common "save then do more work" pattern and will be
222
+ # removed in RailsOps 2.0.
193
223
  #
194
- # class User::Create < RailsOps::Operation::Model::Create
195
- # def perform
196
- # super # Saves the user
224
+ # To convert a non-validation `StandardError` into a rollback
225
+ # signal that escapes {run}'s rescue, raise
226
+ # {RailsOps::Exceptions::RollbackRequired} directly, e.g.
227
+ # `fail RailsOps::Exceptions::RollbackRequired, e, e.backtrace`.
197
228
  #
198
- # model.some_field = 'some value'
199
- # model.save! # Throws validation error
200
- # end
201
- # end
202
- #
203
- # User::Create.run(user: { some: :values })
204
- #
205
- # Since this operation is run without the bang method, validation errors are
206
- # caught and won't result in the transaction beeing rolled back. However, the
207
- # `super` call already saved the user while the exception happens only at
208
- # the manual call to `model.save!`. Thus the user will still be in the DB,
209
- # despite the fact that the second update didn't run.
210
- #
211
- # The correct example would therefore be:
212
- #
213
- # class User::Create < RailsOps::Operation::Model::Create
214
- # def perform
215
- # super # Saves the user
216
- #
217
- # with_rollback_on_exception do
218
- # model.some_field = 'some value'
219
- # model.save! # Throws validation error
220
- # end
221
- # end
222
- # end
223
- #
224
- # This method is one possible solution for issue #28535. There might be a more
225
- # elegant and transparent approach as explained in the issue.
229
+ # Originally introduced for issue #28535.
226
230
  def with_rollback_on_exception(&_block)
231
+ location = caller_locations(1, 1)&.first
232
+ location_key = location && "#{location.path}:#{location.lineno}"
233
+ if location_key.nil? || WITH_ROLLBACK_DEPRECATION_SEEN.put_if_absent(location_key, true).nil?
234
+ RailsOps.deprecator.warn(
235
+ '`with_rollback_on_exception` is deprecated and will be removed ' \
236
+ 'in RailsOps 2.0. Validation errors raised inside `run` are now ' \
237
+ 'rolled back automatically via a SAVEPOINT, so this helper is no ' \
238
+ 'longer required for the common "save then do more work" pattern. ' \
239
+ 'For non-validation errors that should trigger a rollback, raise ' \
240
+ '`RailsOps::Exceptions::RollbackRequired` directly.',
241
+ caller_locations(1)
242
+ )
243
+ end
227
244
  yield
228
245
  rescue StandardError => e
229
246
  fail RailsOps::Exceptions::RollbackRequired, e, e.backtrace
@@ -1,14 +1,16 @@
1
1
  module RailsOps
2
2
  # @private
3
3
  class Railtie < Rails::Railtie
4
- initializer 'rails_ops' do |app|
5
- # ---------------------------------------------------------------
6
- # Register deprecator
7
- # ---------------------------------------------------------------
4
+ # Register the deprecator early so that app-level deprecation config
5
+ # (`config.active_support.deprecation`,
6
+ # `config.active_support.report_deprecations`, …) is applied to it.
7
+ initializer 'rails_ops.deprecator', before: :load_environment_config do |app|
8
8
  if app.respond_to?(:deprecators)
9
9
  app.deprecators[:rails_ops] = RailsOps.deprecator
10
10
  end
11
+ end
11
12
 
13
+ initializer 'rails_ops' do
12
14
  # ---------------------------------------------------------------
13
15
  # Load hookup config eagerly at application startup unless
14
16
  # in development mode.
data/lib/rails_ops.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'active_type'
2
+ require 'concurrent/map'
2
3
  require 'schemacop'
3
4
  require 'request_store'
4
5
  require 'ostruct'
data/rails_ops.gemspec CHANGED
@@ -1,20 +1,20 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: rails_ops 1.7.8 ruby lib
2
+ # stub: rails_ops 1.8.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "rails_ops".freeze
6
- s.version = "1.7.8"
6
+ s.version = "1.8.0"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["Sitrox".freeze]
11
- s.date = "2026-03-12"
12
- s.files = [".github/workflows/rubocop.yml".freeze, ".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "Appraisals".freeze, "CHANGELOG.md".freeze, "CLAUDE.md".freeze, "Gemfile".freeze, "Gemfile.lock".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "gemfiles/rails_6.0.gemfile".freeze, "gemfiles/rails_6.1.gemfile".freeze, "gemfiles/rails_7.0.gemfile".freeze, "gemfiles/rails_7.1.gemfile".freeze, "gemfiles/rails_7.2.gemfile".freeze, "gemfiles/rails_8.0.gemfile".freeze, "lib/generators/operation/USAGE".freeze, "lib/generators/operation/operation_generator.rb".freeze, "lib/generators/operation/templates/controller.erb".freeze, "lib/generators/operation/templates/controller_wrapper.erb".freeze, "lib/generators/operation/templates/create.erb".freeze, "lib/generators/operation/templates/destroy.erb".freeze, "lib/generators/operation/templates/load.erb".freeze, "lib/generators/operation/templates/update.erb".freeze, "lib/generators/operation/templates/view.erb".freeze, "lib/rails_ops.rb".freeze, "lib/rails_ops/authorization_backend/abstract.rb".freeze, "lib/rails_ops/authorization_backend/can_can_can.rb".freeze, "lib/rails_ops/configuration.rb".freeze, "lib/rails_ops/context.rb".freeze, "lib/rails_ops/controller_mixin.rb".freeze, "lib/rails_ops/exceptions.rb".freeze, "lib/rails_ops/hooked_job.rb".freeze, "lib/rails_ops/hookup.rb".freeze, "lib/rails_ops/hookup/dsl.rb".freeze, "lib/rails_ops/hookup/dsl_validator.rb".freeze, "lib/rails_ops/hookup/hook.rb".freeze, "lib/rails_ops/log_subscriber.rb".freeze, "lib/rails_ops/mixins.rb".freeze, "lib/rails_ops/mixins/authorization.rb".freeze, "lib/rails_ops/mixins/log_settings.rb".freeze, "lib/rails_ops/mixins/model.rb".freeze, "lib/rails_ops/mixins/model/authorization.rb".freeze, "lib/rails_ops/mixins/model/nesting.rb".freeze, "lib/rails_ops/mixins/param_authorization.rb".freeze, "lib/rails_ops/mixins/policies.rb".freeze, "lib/rails_ops/mixins/require_context.rb".freeze, "lib/rails_ops/mixins/routes.rb".freeze, "lib/rails_ops/mixins/schema_validation.rb".freeze, "lib/rails_ops/mixins/sub_ops.rb".freeze, "lib/rails_ops/model_mixins.rb".freeze, "lib/rails_ops/model_mixins/ar_extension.rb".freeze, "lib/rails_ops/model_mixins/marshalling.rb".freeze, "lib/rails_ops/model_mixins/parent_op.rb".freeze, "lib/rails_ops/model_mixins/sti_fixes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb".freeze, "lib/rails_ops/model_mixins/virtual_has_one.rb".freeze, "lib/rails_ops/model_mixins/virtual_model_name.rb".freeze, "lib/rails_ops/operation.rb".freeze, "lib/rails_ops/operation/model.rb".freeze, "lib/rails_ops/operation/model/create.rb".freeze, "lib/rails_ops/operation/model/destroy.rb".freeze, "lib/rails_ops/operation/model/load.rb".freeze, "lib/rails_ops/operation/model/update.rb".freeze, "lib/rails_ops/profiler.rb".freeze, "lib/rails_ops/profiler/node.rb".freeze, "lib/rails_ops/railtie.rb".freeze, "lib/rails_ops/scoped_env.rb".freeze, "lib/rails_ops/virtual_model.rb".freeze, "rails_ops.gemspec".freeze, "test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze]
11
+ s.date = "2026-05-07"
12
+ s.files = [".github/workflows/rubocop.yml".freeze, ".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "Appraisals".freeze, "CHANGELOG.md".freeze, "CLAUDE.md".freeze, "Gemfile".freeze, "Gemfile.lock".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "gemfiles/rails_6.0.gemfile".freeze, "gemfiles/rails_6.1.gemfile".freeze, "gemfiles/rails_7.0.gemfile".freeze, "gemfiles/rails_7.1.gemfile".freeze, "gemfiles/rails_7.2.gemfile".freeze, "gemfiles/rails_8.0.gemfile".freeze, "lib/generators/operation/USAGE".freeze, "lib/generators/operation/operation_generator.rb".freeze, "lib/generators/operation/templates/controller.erb".freeze, "lib/generators/operation/templates/controller_wrapper.erb".freeze, "lib/generators/operation/templates/create.erb".freeze, "lib/generators/operation/templates/destroy.erb".freeze, "lib/generators/operation/templates/load.erb".freeze, "lib/generators/operation/templates/update.erb".freeze, "lib/generators/operation/templates/view.erb".freeze, "lib/rails_ops.rb".freeze, "lib/rails_ops/authorization_backend/abstract.rb".freeze, "lib/rails_ops/authorization_backend/can_can_can.rb".freeze, "lib/rails_ops/configuration.rb".freeze, "lib/rails_ops/context.rb".freeze, "lib/rails_ops/controller_mixin.rb".freeze, "lib/rails_ops/exceptions.rb".freeze, "lib/rails_ops/hooked_job.rb".freeze, "lib/rails_ops/hookup.rb".freeze, "lib/rails_ops/hookup/dsl.rb".freeze, "lib/rails_ops/hookup/dsl_validator.rb".freeze, "lib/rails_ops/hookup/hook.rb".freeze, "lib/rails_ops/log_subscriber.rb".freeze, "lib/rails_ops/mixins.rb".freeze, "lib/rails_ops/mixins/authorization.rb".freeze, "lib/rails_ops/mixins/log_settings.rb".freeze, "lib/rails_ops/mixins/model.rb".freeze, "lib/rails_ops/mixins/model/authorization.rb".freeze, "lib/rails_ops/mixins/model/nesting.rb".freeze, "lib/rails_ops/mixins/param_authorization.rb".freeze, "lib/rails_ops/mixins/policies.rb".freeze, "lib/rails_ops/mixins/require_context.rb".freeze, "lib/rails_ops/mixins/routes.rb".freeze, "lib/rails_ops/mixins/schema_validation.rb".freeze, "lib/rails_ops/mixins/sub_ops.rb".freeze, "lib/rails_ops/model_mixins.rb".freeze, "lib/rails_ops/model_mixins/ar_extension.rb".freeze, "lib/rails_ops/model_mixins/marshalling.rb".freeze, "lib/rails_ops/model_mixins/parent_op.rb".freeze, "lib/rails_ops/model_mixins/sti_fixes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes.rb".freeze, "lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb".freeze, "lib/rails_ops/model_mixins/virtual_has_one.rb".freeze, "lib/rails_ops/model_mixins/virtual_model_name.rb".freeze, "lib/rails_ops/operation.rb".freeze, "lib/rails_ops/operation/model.rb".freeze, "lib/rails_ops/operation/model/create.rb".freeze, "lib/rails_ops/operation/model/destroy.rb".freeze, "lib/rails_ops/operation/model/load.rb".freeze, "lib/rails_ops/operation/model/update.rb".freeze, "lib/rails_ops/profiler.rb".freeze, "lib/rails_ops/profiler/node.rb".freeze, "lib/rails_ops/railtie.rb".freeze, "lib/rails_ops/scoped_env.rb".freeze, "lib/rails_ops/virtual_model.rb".freeze, "rails_ops.gemspec".freeze, "test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze, "test/unit/rails_ops/railtie_test.rb".freeze]
13
13
  s.homepage = "https://github.com/sitrox/rails_ops".freeze
14
14
  s.licenses = ["MIT".freeze]
15
15
  s.rubygems_version = "3.4.6".freeze
16
16
  s.summary = "An operations service layer for rails projects.".freeze
17
- s.test_files = ["test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze]
17
+ s.test_files = ["test/db/models.rb".freeze, "test/db/schema.rb".freeze, "test/dummy/Rakefile".freeze, "test/dummy/app/assets/config/manifest.js".freeze, "test/dummy/app/assets/images/.keep".freeze, "test/dummy/app/assets/javascripts/application.js".freeze, "test/dummy/app/assets/javascripts/cable.js".freeze, "test/dummy/app/assets/javascripts/channels/.keep".freeze, "test/dummy/app/assets/stylesheets/application.css".freeze, "test/dummy/app/channels/application_cable/channel.rb".freeze, "test/dummy/app/channels/application_cable/connection.rb".freeze, "test/dummy/app/controllers/application_controller.rb".freeze, "test/dummy/app/controllers/concerns/.keep".freeze, "test/dummy/app/controllers/group_controller.rb".freeze, "test/dummy/app/helpers/application_helper.rb".freeze, "test/dummy/app/jobs/application_job.rb".freeze, "test/dummy/app/mailers/application_mailer.rb".freeze, "test/dummy/app/models/ability.rb".freeze, "test/dummy/app/models/animal.rb".freeze, "test/dummy/app/models/application_record.rb".freeze, "test/dummy/app/models/bird.rb".freeze, "test/dummy/app/models/cat.rb".freeze, "test/dummy/app/models/computer.rb".freeze, "test/dummy/app/models/concerns/.keep".freeze, "test/dummy/app/models/cpu.rb".freeze, "test/dummy/app/models/dog.rb".freeze, "test/dummy/app/models/flower.rb".freeze, "test/dummy/app/models/group.rb".freeze, "test/dummy/app/models/mainboard.rb".freeze, "test/dummy/app/models/nightingale.rb".freeze, "test/dummy/app/models/phoenix.rb".freeze, "test/dummy/app/models/user.rb".freeze, "test/dummy/app/views/layouts/application.html.erb".freeze, "test/dummy/app/views/layouts/mailer.html.erb".freeze, "test/dummy/app/views/layouts/mailer.text.erb".freeze, "test/dummy/bin/bundle".freeze, "test/dummy/bin/rails".freeze, "test/dummy/bin/rake".freeze, "test/dummy/bin/setup".freeze, "test/dummy/bin/update".freeze, "test/dummy/bin/yarn".freeze, "test/dummy/config.ru".freeze, "test/dummy/config/application.rb".freeze, "test/dummy/config/boot.rb".freeze, "test/dummy/config/cable.yml".freeze, "test/dummy/config/database.yml".freeze, "test/dummy/config/environment.rb".freeze, "test/dummy/config/environments/development.rb".freeze, "test/dummy/config/environments/production.rb".freeze, "test/dummy/config/environments/test.rb".freeze, "test/dummy/config/hookup.rb".freeze, "test/dummy/config/initializers/application_controller_renderer.rb".freeze, "test/dummy/config/initializers/assets.rb".freeze, "test/dummy/config/initializers/backtrace_silencers.rb".freeze, "test/dummy/config/initializers/cookies_serializer.rb".freeze, "test/dummy/config/initializers/filter_parameter_logging.rb".freeze, "test/dummy/config/initializers/inflections.rb".freeze, "test/dummy/config/initializers/mime_types.rb".freeze, "test/dummy/config/initializers/rails_ops.rb".freeze, "test/dummy/config/initializers/wrap_parameters.rb".freeze, "test/dummy/config/locales/en.yml".freeze, "test/dummy/config/puma.rb".freeze, "test/dummy/config/routes.rb".freeze, "test/dummy/config/secrets.yml".freeze, "test/dummy/config/spring.rb".freeze, "test/dummy/db/schema.rb".freeze, "test/dummy/lib/assets/.keep".freeze, "test/dummy/log/.keep".freeze, "test/dummy/package.json".freeze, "test/dummy/public/404.html".freeze, "test/dummy/public/422.html".freeze, "test/dummy/public/500.html".freeze, "test/dummy/public/apple-touch-icon-precomposed.png".freeze, "test/dummy/public/apple-touch-icon.png".freeze, "test/dummy/public/favicon.ico".freeze, "test/dummy/tmp/.keep".freeze, "test/test_helper.rb".freeze, "test/unit/rails_ops/generators/operation_generator_test.rb".freeze, "test/unit/rails_ops/hookup_test.rb".freeze, "test/unit/rails_ops/mixins/controller_test.rb".freeze, "test/unit/rails_ops/mixins/model/deep_nesting_test.rb".freeze, "test/unit/rails_ops/mixins/model/marshalling_test.rb".freeze, "test/unit/rails_ops/mixins/param_authorization_test.rb".freeze, "test/unit/rails_ops/mixins/policies_test.rb".freeze, "test/unit/rails_ops/operation/auth_test.rb".freeze, "test/unit/rails_ops/operation/model/create_test.rb".freeze, "test/unit/rails_ops/operation/model/destroy_test.rb".freeze, "test/unit/rails_ops/operation/model/load_test.rb".freeze, "test/unit/rails_ops/operation/model/sti_test.rb".freeze, "test/unit/rails_ops/operation/model/update_test.rb".freeze, "test/unit/rails_ops/operation/model_test.rb".freeze, "test/unit/rails_ops/operation/update_lazy_auth_test.rb".freeze, "test/unit/rails_ops/operation_test.rb".freeze, "test/unit/rails_ops/profiler_test.rb".freeze, "test/unit/rails_ops/railtie_test.rb".freeze]
18
18
 
19
19
  s.specification_version = 4
20
20
 
@@ -193,8 +193,168 @@ class RailsOps::OperationTest < ActiveSupport::TestCase
193
193
  end
194
194
  end
195
195
  end.new
196
- assert_raises RailsOps::Exceptions::RollbackRequired do
196
+ RailsOps.deprecator.silence do
197
+ assert_raises RailsOps::Exceptions::RollbackRequired do
198
+ op.run
199
+ end
200
+ end
201
+ end
202
+
203
+ def test_with_rollback_on_exception_emits_deprecation_warning
204
+ # Use a fresh call site to bypass the per-process deduplication cache
205
+ # used by `with_rollback_on_exception`.
206
+ op = Class.new(RailsOps::Operation) do
207
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
208
+ def perform
209
+ with_rollback_on_exception do
210
+ # No-op; we only care about the deprecation warning being
211
+ # emitted on entry.
212
+ end
213
+ end
214
+ RUBY
215
+ end.new
216
+
217
+ messages = []
218
+ previous_behavior = RailsOps.deprecator.behavior
219
+ RailsOps.deprecator.behavior = ->(msg, _callstack, _name, _gem) { messages << msg }
220
+ begin
197
221
  op.run
222
+ ensure
223
+ RailsOps.deprecator.behavior = previous_behavior
224
+ end
225
+
226
+ assert_match(/with_rollback_on_exception.*deprecated/, messages.join("\n"))
227
+ end
228
+
229
+ def test_with_rollback_on_exception_deduplicates_per_call_site
230
+ op = Class.new(RailsOps::Operation) do
231
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
232
+ def perform
233
+ with_rollback_on_exception do
234
+ # No-op
235
+ end
236
+ end
237
+ RUBY
238
+ end
239
+
240
+ messages = []
241
+ previous_behavior = RailsOps.deprecator.behavior
242
+ RailsOps.deprecator.behavior = ->(msg, _callstack, _name, _gem) { messages << msg }
243
+ begin
244
+ op.new.run
245
+ op.new.run
246
+ op.new.run
247
+ ensure
248
+ RailsOps.deprecator.behavior = previous_behavior
249
+ end
250
+
251
+ assert_equal 1, messages.size, 'expected the warning to fire once per call site'
252
+ end
253
+
254
+ # When `run` is called inside an outer transaction and the operation does a
255
+ # save followed by a validation error, the save must be rolled back even
256
+ # though `run` swallows the error and returns `false`. Without the savepoint
257
+ # wrapping inside `run`, the partial write would leak into the outer
258
+ # transaction.
259
+ def test_run_rolls_back_partial_writes_when_inside_outer_transaction
260
+ op_class = Class.new(RailsOps::Operation::Model::Create) do
261
+ model Group
262
+
263
+ def perform
264
+ super
265
+ fail RailsOps::Exceptions::ValidationFailed, 'post-save check failed'
266
+ end
267
+ end
268
+
269
+ ActiveRecord::Base.transaction do
270
+ count_before = Group.count
271
+ refute op_class.run(group: { name: 'partial', color: 'red' })
272
+ assert_equal count_before, Group.count, 'expected save to be rolled back'
273
+ end
274
+ end
275
+
276
+ # `run!` keeps its existing behavior: validation errors propagate, the
277
+ # savepoint logic in `run` is not on the call path.
278
+ def test_run_bang_still_raises_inside_transaction
279
+ op_class = Class.new(RailsOps::Operation::Model::Create) do
280
+ model Group
281
+
282
+ def perform
283
+ super
284
+ fail RailsOps::Exceptions::ValidationFailed, 'post-save check failed'
285
+ end
286
+ end
287
+
288
+ assert_raises RailsOps::Exceptions::ValidationFailed do
289
+ ActiveRecord::Base.transaction do
290
+ op_class.run!(group: { name: 'partial', color: 'red' })
291
+ end
292
+ end
293
+ end
294
+
295
+ # Successful `run` calls inside a transaction still commit the model save.
296
+ def test_run_persists_model_on_success_inside_transaction
297
+ op_class = Class.new(RailsOps::Operation::Model::Create) do
298
+ model Group
299
+ end
300
+
301
+ ActiveRecord::Base.transaction do
302
+ count_before = Group.count
303
+ assert op_class.run(group: { name: 'fine', color: 'green' })
304
+ assert_equal count_before + 1, Group.count
305
+ end
306
+ end
307
+
308
+ # `run_sub` (non-bang) delegates to `run`, so a child operation that saves
309
+ # and then fails validation must not leak partial state into the parent
310
+ # transaction.
311
+ def test_run_sub_rolls_back_partial_writes_in_child_op
312
+ child_op = Class.new(RailsOps::Operation::Model::Create) do
313
+ model Group
314
+
315
+ def perform
316
+ super
317
+ fail RailsOps::Exceptions::ValidationFailed, 'post-save check failed'
318
+ end
319
+ end
320
+
321
+ parent_op = Class.new(RailsOps::Operation) do
322
+ attr_reader :sub_result
323
+
324
+ define_method(:perform) do
325
+ @sub_result = run_sub(child_op, group: { name: 'child', color: 'blue' })
326
+ end
327
+ end
328
+
329
+ ActiveRecord::Base.transaction do
330
+ count_before = Group.count
331
+ op = parent_op.new
332
+ op.run!
333
+ refute op.sub_result, 'expected run_sub to return false on child validation error'
334
+ assert_equal count_before, Group.count, 'expected child op save to be rolled back'
335
+ end
336
+ end
337
+
338
+ # The same protection applies when the failing exception is
339
+ # `ActiveRecord::RecordInvalid` (raised by `model.save!` on a real
340
+ # model validation failure), which is the more common production case.
341
+ def test_run_rolls_back_partial_writes_on_active_record_record_invalid
342
+ op_class = Class.new(RailsOps::Operation::Model::Create) do
343
+ model Group
344
+
345
+ def perform
346
+ super
347
+ # Trigger a real ActiveRecord::RecordInvalid on a separate record.
348
+ invalid = Group.new
349
+ invalid.errors.add(:base, 'invalid')
350
+ fail ActiveRecord::RecordInvalid, invalid
351
+ end
352
+ end
353
+
354
+ ActiveRecord::Base.transaction do
355
+ count_before = Group.count
356
+ refute op_class.run(group: { name: 'partial', color: 'red' })
357
+ assert_equal count_before, Group.count, 'expected save to be rolled back'
198
358
  end
199
359
  end
200
360
 
@@ -0,0 +1,29 @@
1
+ require 'test_helper'
2
+
3
+ class RailsOps::RailtieTest < ActiveSupport::TestCase
4
+ include TestHelper
5
+
6
+ def test_deprecator_is_registered_with_rails
7
+ skip 'Rails.application.deprecators is unavailable on this Rails version' \
8
+ unless Rails.application.respond_to?(:deprecators)
9
+
10
+ registered = Rails.application.deprecators[:rails_ops]
11
+ assert_same RailsOps.deprecator, registered,
12
+ 'expected RailsOps.deprecator to be registered as Rails.application.deprecators[:rails_ops]'
13
+ end
14
+
15
+ def test_deprecator_honors_silenced_flag_set_via_app_config
16
+ skip 'Rails.application.deprecators is unavailable on this Rails version' \
17
+ unless Rails.application.respond_to?(:deprecators)
18
+
19
+ previous_silenced = RailsOps.deprecator.silenced
20
+ begin
21
+ Rails.application.deprecators.silenced = true
22
+ assert RailsOps.deprecator.silenced,
23
+ 'expected RailsOps.deprecator to be silenced when ' \
24
+ 'Rails.application.deprecators.silenced = true'
25
+ ensure
26
+ Rails.application.deprecators.silenced = previous_silenced
27
+ end
28
+ end
29
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_ops
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.8
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-12 00:00:00.000000000 Z
10
+ date: 2026-05-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: active_type
@@ -258,6 +258,7 @@ files:
258
258
  - test/unit/rails_ops/operation/update_lazy_auth_test.rb
259
259
  - test/unit/rails_ops/operation_test.rb
260
260
  - test/unit/rails_ops/profiler_test.rb
261
+ - test/unit/rails_ops/railtie_test.rb
261
262
  homepage: https://github.com/sitrox/rails_ops
262
263
  licenses:
263
264
  - MIT
@@ -374,3 +375,4 @@ test_files:
374
375
  - test/unit/rails_ops/operation/update_lazy_auth_test.rb
375
376
  - test/unit/rails_ops/operation_test.rb
376
377
  - test/unit/rails_ops/profiler_test.rb
378
+ - test/unit/rails_ops/railtie_test.rb