actionable-rb 1.9.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.claude-plugin/marketplace.json +15 -0
  3. data/.claude-plugin/plugin.json +9 -0
  4. data/.rspec +3 -0
  5. data/AGENTS.md +150 -0
  6. data/CHANGELOG.md +766 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +555 -0
  10. data/UPGRADING.md +957 -0
  11. data/USAGE.md +1291 -0
  12. data/docs/getting_started.md +611 -0
  13. data/lib/actionable/action.rb +1220 -0
  14. data/lib/actionable/batch_result.rb +92 -0
  15. data/lib/actionable/errors.rb +36 -0
  16. data/lib/actionable/history.rb +91 -0
  17. data/lib/actionable/input_dispatch.rb +94 -0
  18. data/lib/actionable/job.rb +157 -0
  19. data/lib/actionable/measurement.rb +64 -0
  20. data/lib/actionable/rails/after_commit.rb +142 -0
  21. data/lib/actionable/rails/proxy_validator.rb +59 -0
  22. data/lib/actionable/rails/transactions.rb +95 -0
  23. data/lib/actionable/rails.rb +24 -0
  24. data/lib/actionable/railtie.rb +10 -0
  25. data/lib/actionable/rbs.rb +502 -0
  26. data/lib/actionable/registry.rb +64 -0
  27. data/lib/actionable/result.rb +166 -0
  28. data/lib/actionable/results/failure.rb +43 -0
  29. data/lib/actionable/results/skipped.rb +30 -0
  30. data/lib/actionable/results/success.rb +14 -0
  31. data/lib/actionable/rspec/coverage/report.rb +264 -0
  32. data/lib/actionable/rspec/coverage/tracker.rb +330 -0
  33. data/lib/actionable/rspec/coverage.rb +45 -0
  34. data/lib/actionable/rspec/matchers.rb +266 -0
  35. data/lib/actionable/rspec/stubs.rb +250 -0
  36. data/lib/actionable/rspec.rb +40 -0
  37. data/lib/actionable/rubocop/config.yml +8 -0
  38. data/lib/actionable/rubocop/cop/actionable/verb_not_last.rb +62 -0
  39. data/lib/actionable/rubocop/plugin.rb +31 -0
  40. data/lib/actionable/rubocop.rb +25 -0
  41. data/lib/actionable/runner.rb +300 -0
  42. data/lib/actionable/steps/action.rb +76 -0
  43. data/lib/actionable/steps/base.rb +107 -0
  44. data/lib/actionable/steps/case.rb +124 -0
  45. data/lib/actionable/steps/method.rb +39 -0
  46. data/lib/actionable/steps.rb +27 -0
  47. data/lib/actionable/value_match.rb +22 -0
  48. data/lib/actionable/verb_position.rb +574 -0
  49. data/lib/actionable/version.rb +6 -0
  50. data/lib/actionable-rb.rb +6 -0
  51. data/lib/actionable.rb +59 -0
  52. data/sig/actionable/dsl.rbs +42 -0
  53. data/sig/actionable/rails.rbs +16 -0
  54. data/sig/actionable.rbs +2593 -0
  55. data/skills/actionable/SKILL.md +214 -0
  56. metadata +132 -0
data/UPGRADING.md ADDED
@@ -0,0 +1,957 @@
1
+ # Upgrading Actionable
2
+
3
+ <!-- upgrading-range: 1.5.0..1.9.0 -->
4
+
5
+ This guide takes an app from any **1.5.x** release to **1.9.0**, one version at
6
+ a time or [straight through](#straight-from-15-to-18). It lists every change a
7
+ caller can see, not just the breaking ones. Each entry has the same four parts:
8
+
9
+ - **Symptom**: the exception and message shape, or the spec that fails.
10
+ - **Find**: a copy-pasteable search, or the tool that finds it when no
11
+ search is reliable.
12
+ - **Fix**: before and after.
13
+ - **Why**: one line.
14
+
15
+ The `rg` patterns search `app lib` (code), `spec` and `sig`; point them at your
16
+ own directories and drop any that don't exist. Patterns with `-P` need ripgrep
17
+ built with PCRE2. Check first, in the shell that will run them, with
18
+ `rg --pcre2-version`; an agent's `bash -c` may not see a shell function or alias
19
+ that wraps `rg`. A pattern finds *candidates*; read each hit.
20
+
21
+ **Which Finds to run.** Most changes raise or fail a spec, so the suite finds
22
+ them; their Finds cover code the suite doesn't reach. Seven change behaviour
23
+ without raising, so run their Finds even on a green suite: the
24
+ [nested savepoint](#a-nested-transactional-action-takes-its-own-savepoint),
25
+ [bang verbs in hooks](#a-bang-verb-inside-a-lifecycle-hook-sets-the-result),
26
+ [nested output](#a-nested-action-no-longer-overwrites-parent-fields-it-didnt-set),
27
+ [RSpec stubs](#rspec-stubs-absorb-only-the-fields-you-give-them),
28
+ [redeclared steps](#redeclaring-a-step-or-hook-overrides-it-last-wins),
29
+ [`always` after a halt](#always-hooks-run-even-after-an-outcome-hook-halts) and
30
+ [`case_step` branch guards](#case_step-branch-guards-are-honoured). Every pattern has been checked against the "before" snippet it
31
+ targets. [CHANGELOG.md](CHANGELOG.md) has the
32
+ full list of changes, including internal ones.
33
+
34
+ Agents: follow the numbered procedure in [AGENTS.md](AGENTS.md#upgrading)
35
+ (or the bundled skill), which runs this guide step by step.
36
+
37
+ ## TL;DR
38
+
39
+ **Who must act:** every app moving to 1.8.0 (two new checks raise on code that
40
+ used to run), anyone who keeps hand-written RBS next to `Actionable::RBS`
41
+ output, and Rails apps that nest `transactional` actions. **Already on 1.8.1?**
42
+ Skip to [1.8.1 → 1.8.2](#181--182): one silent change (`case_step` branch
43
+ guards) and two narrow ones. **Already on 1.8.2?** Skip to
44
+ [1.8.2 → 1.9.0](#182--190): the gem is renamed `actionable-rb`.
45
+
46
+ 1. **Bump field_struct first.** actionable >= 1.6 requires `field_struct >=
47
+ 0.12` (1.5.2 needs `>= 0.11`). Upgrade it and follow field_struct's
48
+ [UPGRADING.md](https://github.com/Paymentbox-com/field_struct/blob/main/UPGRADING.md)
49
+ until your suite is green.
50
+ 2. **Bump actionable** to `v1.9.0` in the Gemfile, renaming the line to
51
+ `gem 'actionable-rb'` ([why](#the-gem-is-named-actionable-rb)).
52
+ 3. **Fix verb-position offenses.** If you need a green baseline first, run the
53
+ suite once with `Actionable.verb_position_check = false`, then turn it back
54
+ on. Enable the [`Actionable/VerbNotLast` cop](#a-plain-verb-must-end-its-step-or-hook-method)
55
+ in `.rubocop.yml` to list every offense up front. Fix
56
+ [non-identifier codes](#fail--skip-codes-must-be-identifiers) at the same
57
+ time: that check has no switch.
58
+ 4. **Regenerate the RBS you own** with `Actionable::RBS.generate`, then delete
59
+ hand-written declarations it now covers (see the [1.6.0 entries](#152--160)).
60
+ 5. **Opting in to job retries** (`honor_retry_metadata`)? Under ActiveJob, also
61
+ declare `retry_on Actionable::Job::RetryableFailure`.
62
+ 6. **Using `after_commit`?** It needs ActiveRecord >= 7.2. The rest of the
63
+ Rails adapter is tested on 7.1.
64
+
65
+ **Expected effort:** the fixes take minutes to an hour for most apps (what the
66
+ cop and the code check report). Longer only if you keep hand-written RBS for
67
+ your actions or nest `transactional` actions inside your own transactions. On
68
+ top of that come the full suite runs: a baseline, one after the field_struct
69
+ bump, one after this one, and a final run with the check on. Budget 4× your
70
+ suite's wall time.
71
+
72
+ ## Straight from 1.5 to 1.8
73
+
74
+ You can jump in one bump. Work in this order, so each step starts from a green
75
+ run:
76
+
77
+ 1. Bump field_struct to >= 0.12 (step 1 above). Bundler refuses the actionable
78
+ bump until you do: `Bundler could not find compatible versions for gem
79
+ "field_struct"`.
80
+ 2. Bump actionable to `v1.9.0` as `gem 'actionable-rb'`. Set `Actionable.verb_position_check = false`
81
+ in `spec_helper` (or a `spec/support` file) for now, not in an initializer:
82
+ an initializer also switches the check off in production, and that is easy
83
+ to forget to undo. Use an initializer only if you must deploy before every
84
+ offense is fixed (see [the verb entry](#a-plain-verb-must-end-its-step-or-hook-method)).
85
+ 3. Run the suite. Work through the [1.5.2](#15x--152) and
86
+ [1.7.0](#160--170) entries: they change runtime behaviour (savepoints,
87
+ hook halts, nested output, redeclared steps, `always` hooks).
88
+ 4. Enable the cop, fix every offense, and remove the
89
+ `verb_position_check = false` line. Fix the
90
+ [identifier codes](#fail--skip-codes-must-be-identifiers).
91
+ 5. Regenerate your RBS and work through the [1.6.0](#152--160) entries and
92
+ [adapter signatures](#adapter-signatures-now-ship-in-sig).
93
+ 6. Work through the [1.8.2](#181--182) entries: a `case_step` branch guard
94
+ now takes effect, and that doesn't raise, so run its Find.
95
+ 7. If `.rubocop.yml` loads the cop with `plugins:`, its key is
96
+ [`actionable-rb:`](#the-gem-is-named-actionable-rb) from 1.9.0.
97
+ 8. Run the suite with the check on. Anything still failing:
98
+ [bisect by version](#if-something-else-breaks).
99
+
100
+ ## Symptom index
101
+
102
+ | You see | Version | Entry |
103
+ |---|---|---|
104
+ | Bundler: `could not find compatible versions for gem "field_struct"` | 1.5.2, 1.6.0 | [>= 0.11](#requires-field_struct--011), [>= 0.12](#requires-field_struct--012) |
105
+ | A failed nested `transactional` action's rows are gone | 1.5.2 | [Savepoint](#a-nested-transactional-action-takes-its-own-savepoint) |
106
+ | `fail!` in `on_success` now fails the run / rolls back / retries the job | 1.5.2 | [Bang verb in a hook](#a-bang-verb-inside-a-lifecycle-hook-sets-the-result) |
107
+ | A parent field keeps its value after `step Child` | 1.5.2 | [Nested absorb](#a-nested-action-no-longer-overwrites-parent-fields-it-didnt-set), [stubs](#rspec-stubs-absorb-only-the-fields-you-give-them) |
108
+ | `DefinitionError: … declares step(s) calling :print but does not implement them` | 1.5.2 | [Core method names](#a-step-or-hook-named-after-a-core-ruby-method-is-refused) |
109
+ | `DefinitionError: … set @result to a …` | 1.5.2 | [`@result`](#assigning-result-in-a-step-raises-a-clear-error) |
110
+ | `errors[:output]` gone from a failure or skip | 1.5.2 | [Spurious output error](#failures-and-skips-no-longer-carry-a-spurious-errorsoutput) |
111
+ | `RBS::InstanceVariableDuplicationError` | 1.6.0 | [Output ivars](#generated-rbs-owns-the-output-instance-variables) |
112
+ | `RBS::SuperclassMismatchError` | 1.6.0 | [Superclass chain](#generated-rbs-declares-the-real-superclass-chain) |
113
+ | `ArgumentError: X::InputSchema is already defined …` | 1.6.0 | [`InputSchema`](#inputschema-is-a-reserved-name-on-actions-with-a-fixed-input) |
114
+ | New Steep errors at call sites | 1.6.0 | [Stricter types](#generated-types-are-stricter) |
115
+ | A redeclared step runs different options | 1.7.0 | [Redeclaration](#redeclaring-a-step-or-hook-overrides-it-last-wins) |
116
+ | An `always` hook runs after a hook's `fail!` | 1.7.0 | [`always` after halt](#always-hooks-run-even-after-an-outcome-hook-halts) |
117
+ | `RBS::DuplicatedMethodDefinitionError` for `transactional`, `perform_actionable` … | 1.7.0 | [Adapter signatures](#adapter-signatures-now-ship-in-sig) |
118
+ | `describe[:hooks]` has an extra `after_commit:` key | 1.7.0 | [`describe`](#describe-lists-after_commit-hooks-under-the-rails-adapter) |
119
+ | ``DefinitionError: … calls `fail` with more code after it …`` | 1.8.0 | [Verb position](#a-plain-verb-must-end-its-step-or-hook-method) |
120
+ | `ArgumentError: … code "…" is not an identifier …` | 1.8.0 | [Identifier codes](#fail--skip-codes-must-be-identifiers) |
121
+ | A guarded `case_step` branch no longer runs | 1.8.2 | [Branch guards](#case_step-branch-guards-are-honoured) |
122
+ | `ArgumentError: … both name …Output in the generated RBS` | 1.8.2 | [Output class names](#actionablerbs-refuses-codes-that-share-an-output-class-name) |
123
+ | ``DefinitionError: … calls `fail` …`` for an aliased step | 1.8.2 | [Aliased step](#the-verb-check-reads-an-aliased-steps-original-def) |
124
+ | `Could not find gem 'actionable' in …` from Bundler, or RuboCop ``Failed to load plugin `actionable` `` | 1.9.0 | [Gem renamed](#the-gem-is-named-actionable-rb) |
125
+
126
+ ## 1.5.x → 1.5.2
127
+
128
+ ### Requires field_struct >= 0.11
129
+ <!-- upgrading: field-struct-0-11 -->
130
+
131
+ **Symptom.** `bundle update actionable` fails: `Bundler could not find
132
+ compatible versions for gem "field_struct"`.
133
+
134
+ **Find.**
135
+ ```sh
136
+ rg -n "gem ['\"](field_struct|actionable)['\"]" Gemfile
137
+ ```
138
+
139
+ **Fix.** Raise the field_struct pin in the same change:
140
+ ```ruby
141
+ # before
142
+ gem 'field_struct', git: 'https://github.com/Paymentbox-com/field_struct', tag: 'v0.10.0'
143
+ # after (0.12.0 if you are going on to 1.6+)
144
+ gem 'field_struct', git: 'https://github.com/Paymentbox-com/field_struct', tag: 'v0.12.0'
145
+ ```
146
+
147
+ **Why.** 1.5.2 is tested against field_struct 0.11's numeric and input fixes.
148
+
149
+ ### A nested `transactional` action takes its own savepoint
150
+ <!-- upgrading: transactional-savepoint -->
151
+
152
+ **Symptom.** Inside an open transaction (your own `Model.transaction do`, or a
153
+ transactional parent action), a `transactional` action that ends in a
154
+ `Failure` now rolls back its own writes. A spec that expected those rows to
155
+ exist after the outer block commits now finds them missing.
156
+
157
+ **Find.** List the transactional actions, then check which run inside another
158
+ transaction:
159
+ ```sh
160
+ rg -n '^\s*transactional\b' app lib
161
+ ```
162
+
163
+ **Fix.** Usually nothing: the new behaviour is the correct one. To keep the old
164
+ joining behaviour for one action:
165
+ ```ruby
166
+ # before (1.5.1 joined the outer transaction)
167
+ transactional model: :invoice
168
+ # after, only if you really want a failed run's writes to commit with the outer transaction
169
+ transactional model: :invoice, requires_new: false
170
+ ```
171
+
172
+ **Why.** A recorded `Failure` committed its rows through the outer transaction,
173
+ which ignored the action's rollback.
174
+
175
+ ### A bang verb inside a lifecycle hook sets the result
176
+ <!-- upgrading: hook-bang-verb -->
177
+
178
+ **Symptom.** `fail!` / `skip!` / `succeed!` in `on_success` / `on_failure` /
179
+ `on_skip` / `always` used to be dropped. Now the run ends with that hook's
180
+ result: a spec expecting `Success` gets the `Failure`, a `transactional`
181
+ action rolls back, and a job retries or discards. A nested action used as a
182
+ hook (`on_success AuditPayment`) that fails now fails the run.
183
+
184
+ **Find.** No single pattern. List the hook declarations, then read those
185
+ methods for a bang verb:
186
+ ```sh
187
+ rg -n '^\s*(on_success|on_failure|on_skip|always)\s+:' app lib # hook methods
188
+ rg -n '^\s*(on_success|on_failure|on_skip|always)\s+[A-Z]' app lib # nested actions as hooks
189
+ ```
190
+
191
+ **Fix.** Keep the bang verb if the hook's failure should fail the run (that is
192
+ what the docs always said). If it shouldn't, record nothing: log, or `halt!`
193
+ to stop the remaining hooks and keep the result.
194
+ ```ruby
195
+ # before: silently ignored, the run stayed a Success
196
+ def notify
197
+ fail!(:notify_failed) unless mailer.ok?
198
+ end
199
+ # after, when a mailer problem must not fail the payment
200
+ def notify
201
+ logger.warn('receipt not sent') unless mailer.ok?
202
+ end
203
+ ```
204
+
205
+ **Why.** A hook's `fail!` must not commit a transaction or ack a job as a success.
206
+
207
+ ### A nested action no longer overwrites parent fields it didn't set
208
+ <!-- upgrading: nested-absorb-set-fields -->
209
+
210
+ **Symptom.** After `step Child`, a parent field the child left nil (or at its
211
+ default) keeps the parent's value instead of being reset. A spec expecting the
212
+ nil now sees the old value.
213
+
214
+ **Find.** No reliable pattern; review nested steps without `expose:` whose child
215
+ shares output field names with the parent:
216
+ ```sh
217
+ rg -n '^\s*step\s+[A-Z]' app lib
218
+ ```
219
+
220
+ **Fix.** Have the child set the field explicitly; an assigned ivar (even `nil`)
221
+ counts as set:
222
+
223
+ <!-- doctest -->
224
+ ```ruby
225
+ class ClearNote < Actionable::Action
226
+ output { optional :note, :string }
227
+ step :clear
228
+ def clear = @note = nil # assigned, so the parent absorbs the nil
229
+ end
230
+
231
+ class Edit < Actionable::Action
232
+ output { optional :note, :string }
233
+ step :draft
234
+ step ClearNote
235
+ def draft = @note = 'draft'
236
+ end
237
+
238
+ Edit.run.note # => nil
239
+ ```
240
+
241
+ **Why.** Copying every declared field wiped parent values the child never touched.
242
+
243
+ ### RSpec stubs absorb only the fields you give them
244
+ <!-- upgrading: stub-set-fields -->
245
+
246
+ **Symptom.** A parent spec that stubs a child with partial output
247
+ (`allow_actionable_success(Child, total: 5)`) no longer gets the child's other
248
+ fields reset to nil; it keeps the parent's values, as a real run does.
249
+
250
+ **Find.**
251
+ ```sh
252
+ rg -nP '\b(?:allow|stub)_actionable_(?:success|skip)\b' spec
253
+ ```
254
+
255
+ **Fix.** Pass every field the parent should receive, `nil` included:
256
+ ```ruby
257
+ # before: relied on the stub resetting :note
258
+ allow_actionable_success(Child, total: 5)
259
+ # after
260
+ allow_actionable_success(Child, total: 5, note: nil)
261
+ ```
262
+
263
+ **Why.** Stubs now behave like the real runs they stand in for.
264
+
265
+ ### A step or hook named after a core Ruby method is refused
266
+ <!-- upgrading: core-method-steps -->
267
+
268
+ **Symptom.** First run raises
269
+ `Actionable::DefinitionError: Report declares step(s) calling :print but does not implement them`.
270
+
271
+ **Find.** The runtime check is authoritative; this lists the common names:
272
+ ```sh
273
+ rg -nP '^\s*(?:step|case_step|on_success|on_failure|on_skip|always)\s+:(?:puts|print|p|pp|exit|exit!|abort|sleep|format|sprintf|printf|warn|raise|system|exec|spawn|fork|open|select|test|loop|rand|srand|gets|require|load|eval|binding|caller|catch|throw|trap|display|hash|freeze|dup|clone|inspect|tap|then|itself|send|method|methods|class|name|object_id)\b' app lib
274
+ ```
275
+
276
+ **Fix.** Implement the method on the action, or rename the step:
277
+ ```ruby
278
+ # before: called Kernel#print (or Kernel#exit, which ended the process)
279
+ step :print
280
+ # after
281
+ step :print_report
282
+ def print_report = ...
283
+ ```
284
+
285
+ **Why.** Steps are called with `__send__`, so `always :exit` could reach `Kernel#exit`.
286
+
287
+ ### Assigning `@result` in a step raises a clear error
288
+ <!-- upgrading: result-ivar -->
289
+
290
+ **Symptom.** `Actionable::DefinitionError: Lookup set @result to a Account; @result is reserved for the run's result, so rename the instance variable`
291
+ (before: a `NoMethodError` deep in the runner).
292
+
293
+ **Find.**
294
+ ```sh
295
+ rg -n '@result\s*(\|\|)?=' app lib
296
+ ```
297
+
298
+ **Fix.**
299
+ ```ruby
300
+ # before
301
+ def find = @result = Account.find(input.id)
302
+ # after
303
+ def find = @account = Account.find(input.id)
304
+ ```
305
+
306
+ **Why.** `@result` holds the run's result.
307
+
308
+ Assigning an `Actionable::Result` (a `Failure`, `Skipped` or `Success`) is not
309
+ refused, from a step or from a helper a step calls, and `halt!` is the
310
+ documented way to stop after it ([USAGE](USAGE.md)'s verb table). So an app base
311
+ class that builds a failure carrying ActiveModel errors keeps working:
312
+ ```ruby
313
+ def fail_invalid!(record)
314
+ @result = Actionable::Failure.new(code: :invalid, message: 'Record is invalid', metadata: {}, output: {})
315
+ record.errors.each { |error| @result.errors.add(error.attribute, error.message) }
316
+ halt!
317
+ end
318
+ ```
319
+ Only a non-`Result` value raises.
320
+
321
+ ### Failures and skips no longer carry a spurious `errors[:output]`
322
+ <!-- upgrading: no-spurious-output-error -->
323
+
324
+ **Symptom.** A spec asserting `errors[:output] == ["is invalid"]` on a failure
325
+ or skip of an untyped action fails: the key is gone.
326
+
327
+ **Find.**
328
+ ```sh
329
+ rg -nP "errors\[:output\]|output:\s*\[\s*['\"]is invalid" spec
330
+ ```
331
+
332
+ **Fix.** Delete the assertion; errors the failure recorded itself are still there.
333
+ ```ruby
334
+ # before
335
+ expect(result.errors).to include(output: ['is invalid'])
336
+ # after: assert on the errors your fail call recorded
337
+ expect(result.errors).to include(card: ['expired'])
338
+ ```
339
+
340
+ **Why.** Best-effort output on a failure or skip is unvalidated by design.
341
+
342
+ ### Also fixed in 1.5.2 (no action expected)
343
+
344
+ - `succeed` / `skip` kwargs survive lifecycle hooks: a run that used to end as
345
+ `Failure(:invalid_output)` because a hook dropped a kwarg now succeeds.
346
+ <!-- upgrading: hook-kwargs -->
347
+ - Private step, hook and `case_step` methods work (they raised `NoMethodError`).
348
+ - `Action.input` without a block reads the schema instead of replacing it.
349
+
350
+ ## 1.5.2 → 1.6.0
351
+
352
+ No runtime behaviour changes. Everything here is about **generated RBS** and
353
+ the hand-written sig files that sit next to it. Regenerate first
354
+ (`Actionable::RBS.generate(YourAction)`, one file per action), then run
355
+ `rbs validate` (or Steep) and work through what it reports.
356
+
357
+ ### Requires field_struct >= 0.12
358
+ <!-- upgrading: field-struct-0-12 -->
359
+
360
+ **Symptom.** `Bundler could not find compatible versions for gem "field_struct"`.
361
+
362
+ **Find.**
363
+ ```sh
364
+ rg -n "gem ['\"](field_struct|actionable)['\"]" Gemfile
365
+ ```
366
+
367
+ **Fix.** Pin `tag: 'v0.12.0'` (or later) and follow field_struct's
368
+ [UPGRADING.md](https://github.com/Paymentbox-com/field_struct/blob/main/UPGRADING.md)
369
+ before bumping actionable.
370
+
371
+ **Why.** Generated action RBS takes every field type from
372
+ `FieldStruct::RBS.type_for` (0.12).
373
+
374
+ ### Generated RBS owns the output instance variables
375
+ <!-- upgrading: rbs-output-ivars -->
376
+
377
+ **Symptom.** `rbs validate`:
378
+ ``Duplicated instance variable name `@total` in `::Total` (RBS::InstanceVariableDuplicationError)``.
379
+
380
+ **Find.** Your hand-written ivar declarations; delete those named after an
381
+ output field:
382
+ ```sh
383
+ rg -n '^\s*@\w+\s*:' sig
384
+ ```
385
+
386
+ **Fix.**
387
+ ```rbs
388
+ # before (your sig/total.rbs)
389
+ class Total < ::ApplicationAction
390
+ @total: ::Integer
391
+ end
392
+ # after: delete the declaration; the generated file has `@total: ::Integer?`
393
+ ```
394
+
395
+ **Why.** A step that assigns `@total = ...` now type-checks without hand-written sigs.
396
+
397
+ ### Generated RBS declares the real superclass chain
398
+ <!-- upgrading: rbs-superclass-chain -->
399
+
400
+ **Symptom.** `rbs validate`:
401
+ `Superclass mismatch: ::Billing::BaseAction (RBS::SuperclassMismatchError)`,
402
+ or a duplicate declaration error for a namespace you declared as `module`
403
+ that Ruby defines as a class.
404
+
405
+ **Find.** Your hand-written bases declared straight under
406
+ `Actionable::Action`; compare each with Ruby's `superclass`:
407
+ ```sh
408
+ rg -nP '^\s*class\s+[\w:]+\s*<\s*(?:::)?Actionable::Action\b' sig
409
+ ```
410
+
411
+ **Fix.** Match Ruby's ancestry, or delete the declaration (the generated file
412
+ declares every named ancestor up to `Actionable::Action`):
413
+ ```rbs
414
+ # before (Ruby says: class Billing::BaseAction < ApplicationAction)
415
+ class Billing::BaseAction < ::Actionable::Action
416
+ end
417
+ # after
418
+ class Billing::BaseAction < ::ApplicationAction
419
+ end
420
+ ```
421
+
422
+ **Why.** Actions under an intermediate base were declared `< ::Actionable::Action`,
423
+ hiding their bases from the type checker. See
424
+ [USAGE, Base actions](USAGE.md#base-actions).
425
+
426
+ ### `InputSchema` is a reserved name on actions with a fixed input
427
+ <!-- upgrading: rbs-input-schema -->
428
+
429
+ **Symptom.** `Actionable::RBS.generate` raises
430
+ `ArgumentError: Import::InputSchema is already defined (in Import); Actionable::RBS.generate names the generated input type InputSchema, so rename that constant`.
431
+
432
+ **Find.**
433
+ ```sh
434
+ rg -nw InputSchema app lib
435
+ ```
436
+
437
+ **Fix.**
438
+ ```ruby
439
+ # before
440
+ class Import < ApplicationAction
441
+ InputSchema = Struct.new(:rows)
442
+ end
443
+ # after
444
+ class Import < ApplicationAction
445
+ RowSet = Struct.new(:rows)
446
+ end
447
+ ```
448
+
449
+ **Why.** Generated RBS declares a typed `InputSchema` class; Ruby's anonymous
450
+ runtime schema has no name to clash with.
451
+
452
+ ### Generated types are stricter
453
+ <!-- upgrading: rbs-stricter-types -->
454
+
455
+ **Symptom.** Steep reports new errors in code that calls your actions. Typed
456
+ `.run` keywords (optional ones accept `nil`; a defaulted required field is an
457
+ optional keyword), `def input: () -> InputSchema`, writers typed
458
+ `(T? value) -> T?`, nullable scalar array elements (`::Array[::String?]`), and
459
+ type arguments on bare generics replace `untyped`. A field typed by an
460
+ anonymous nested struct class now raises `ArgumentError` at generation.
461
+
462
+ **Find.** No pattern: Steep (or `rbs validate`) is the finder.
463
+
464
+ **Fix.** Fix the call sites Steep reports, or give the field an `rbs_type:`
465
+ override (honoured since 1.6). Name anonymous nested struct classes:
466
+ ```ruby
467
+ # before
468
+ required :address, Class.new(FieldStruct::Base) { required :zip, :string }
469
+ # after
470
+ Address = Class.new(FieldStruct::Base) { required :zip, :string }
471
+ required :address, Address
472
+ ```
473
+
474
+ **Why.** The types now describe what `run` really accepts and returns.
475
+
476
+ ## 1.6.0 → 1.7.0
477
+
478
+ ### Redeclaring a step or hook overrides it (last wins)
479
+ <!-- upgrading: step-redeclaration -->
480
+
481
+ **Symptom.** An action (or subclass) that declares the same `step :x`,
482
+ `case_step :x`, `step Child` or hook twice with different options now runs the
483
+ **last** options. Before, the second declaration was silently dropped. A
484
+ subclass's override now takes effect, so behaviour you thought was overridden
485
+ (and wasn't) changes.
486
+
487
+ **Find.** Same-file duplicates (two classes in one file sharing a step name
488
+ show up too):
489
+ ```sh
490
+ rg -no --no-heading '^\s*(step|case_step|on_success|on_failure|on_skip|always)\s+:?[A-Za-z_][\w:]*' app lib | sed -E 's/^([^:]+):[0-9]+:[[:space:]]*/\1 /' | sort | uniq -d
491
+ ```
492
+ A subclass redeclaring a parent's step lives in another file: no reliable
493
+ pattern. Grep the step names of each base class you subclass.
494
+
495
+ **Fix.** Keep one declaration with the options you mean:
496
+
497
+ <!-- doctest -->
498
+ ```ruby
499
+ class Ship < Actionable::Action
500
+ output { optional :packed, :boolean }
501
+ step :pack, if: :ready?
502
+ def pack = @packed = true
503
+ def ready? = true
504
+ end
505
+
506
+ class ShipFragile < Ship
507
+ step :pack, unless: :fragile? # replaces the parent's options, keeps its position
508
+ def fragile? = true
509
+ end
510
+
511
+ Ship.run.packed # => true
512
+ ShipFragile.run.packed # => nil
513
+ ```
514
+
515
+ **Why.** Invariant 5 promises a subclass can override inherited steps.
516
+
517
+ ### `always` hooks run even after an outcome hook halts
518
+ <!-- upgrading: always-after-halt -->
519
+
520
+ **Symptom.** An `always` hook that never ran after a `fail!` in `on_success`
521
+ now runs, and sees the adopted `Failure`. Under `transactional` it runs inside
522
+ the transaction, so its writes roll back with the failed run. Specs that
523
+ counted `always` calls, or expected its side effect to be absent, fail.
524
+
525
+ **Find.**
526
+ ```sh
527
+ rg -n '^\s*always\b' app lib
528
+ ```
529
+
530
+ **Fix.** Make each `always` hook correct for every outcome; check `result` when
531
+ the outcome matters:
532
+
533
+ <!-- doctest -->
534
+ ```ruby
535
+ class Sync < Actionable::Action
536
+ output { optional :audited, :string }
537
+ step :work
538
+ on_success :publish
539
+ always :audit
540
+ def work = nil
541
+ def publish = fail!(:publish_failed)
542
+ def audit = @audited = result.failure? ? 'failed' : 'ok'
543
+ end
544
+
545
+ result = Sync.run
546
+ result.code # => :publish_failed
547
+ ```
548
+
549
+ **Why.** One hook's halt silently skipped cleanup and audit hooks.
550
+
551
+ ### Adapter signatures now ship in `sig/`
552
+ <!-- upgrading: adapter-rbs -->
553
+
554
+ **Symptom.** `rbs validate`:
555
+ `::Actionable::Action.transactional has duplicated definitions in sig/mine.rbs:3:4...3:52 (RBS::DuplicatedMethodDefinitionError)`
556
+ when your own sig files declare the Rails, RSpec or Job surface.
557
+
558
+ **Find.**
559
+ ```sh
560
+ rg -nP 'def (?:self\.)?(?:transactional|transaction_config|after_commit|after_commit_hooks|perform_actionable|(?:allow|stub)_actionable_\w+|actionable_\w+_result)\b' sig
561
+ ```
562
+
563
+ **Fix.** Delete your declarations of those methods; the gem's are generated from
564
+ its YARD. They are declared on `Actionable::Action` even when
565
+ `actionable/rails` isn't required (RBS can't make a signature conditional).
566
+
567
+ **Why.** The adapters' public surface is now typed by the gem itself.
568
+
569
+ ### `describe` lists `after_commit` hooks under the Rails adapter
570
+ <!-- upgrading: describe-after-commit -->
571
+
572
+ **Symptom.** With `actionable/rails` loaded, `describe[:hooks]` gains an
573
+ `after_commit:` key (an empty Array when there are none), so a spec comparing
574
+ the whole hash with `eq` fails.
575
+
576
+ **Find.**
577
+ ```sh
578
+ rg -nP '(?<!RSpec)\.describe\b(?!_text)' spec
579
+ ```
580
+
581
+ **Fix.**
582
+ ```ruby
583
+ # before
584
+ expect(Pay.describe[:hooks]).to eq(on_success: [:notify], always: [])
585
+ # after
586
+ expect(Pay.describe[:hooks]).to include(on_success: [:notify], always: [])
587
+ ```
588
+
589
+ **Why.** `describe` is the contract summary; `after_commit` hooks are part of it.
590
+
591
+ ## 1.7.0 → 1.8.0
592
+
593
+ Both checks below raise on code that used to run. `Actionable.verb_position_check`
594
+ switches off the first; nothing switches off the second.
595
+
596
+ ### A plain verb must end its step or hook method
597
+ <!-- upgrading: verb-position-guard -->
598
+
599
+ **Symptom.** An action's first run raises:
600
+ ```
601
+ Actionable::DefinitionError: Charge#check (app/actions/charge.rb:5) calls `fail` with more code after it, which still runs: use fail! to stop here, or make it the method's last expression
602
+ (see USAGE, Control flow)
603
+ ```
604
+ It covers `fail`, `fail_with`, `skip` and `succeed` (and `self.fail` …) in every
605
+ step, case-step branch target and lifecycle hook method (`after_commit`
606
+ included). It is checked once per class, on the first run.
607
+
608
+ **Find.** The cop is the finder; it applies the same rules. Add it to
609
+ `.rubocop.yml` (RuboCop >= 1.72; `require: [actionable/rubocop]` on older):
610
+ ```yaml
611
+ plugins:
612
+ - actionable: # `actionable-rb:` from 1.9.0, see 1.8.2 → 1.9.0
613
+ require_path: actionable/rubocop
614
+
615
+ Actionable/VerbNotLast:
616
+ ActionBaseClasses: # replaces the default, so keep the two defaults
617
+ - Actionable::Action
618
+ - ApplicationAction
619
+ - Billing::BaseAction # your own bases defined in other files
620
+ ```
621
+ then `bundle exec rubocop --only Actionable/VerbNotLast`. The cop sees one file
622
+ at a time. It follows a subclass of a listed base only when both are in the
623
+ same file, so **list every intermediate base defined in its own file**, by full
624
+ name: with `ResponseReadyAction < ApplicationAction` and
625
+ `Orders::BaseAction < ResponseReadyAction` in separate files, list both. To
626
+ find candidates, count superclass names; the ones with many children are your
627
+ bases:
628
+ ```sh
629
+ rg -oNP --no-filename --type ruby '^\s*class\s+[\w:]+\s*<\s*\K[\w:]+' app lib | sort | uniq -c | sort -rn | head -20
630
+ ```
631
+ Steps a parent class declares are left to the runtime check.
632
+ Running the suite with the check on finds the rest. For a quick look before
633
+ enabling the cop, these list the two commonest shapes (not definitive):
634
+ ```sh
635
+ rg -nP '^\s*(?:self\.)?(?:fail|fail_with|skip|succeed)\b(?!!).*\b(?:if|unless)\b' app lib
636
+ rg -nP '\b(?:fail|fail_with|skip|succeed)\b(?!!)[^#\n]*\b(?:and|or)\s+(?:return|next|break)\b' app lib
637
+ ```
638
+
639
+ **Fix.** If the code after the verb should not run, add the bang; if it
640
+ should, make the verb last (or `return` it):
641
+ ```ruby
642
+ # before
643
+ def check
644
+ fail :card_expired if card.expired?
645
+ charge_card
646
+ end
647
+ ```
648
+
649
+ <!-- doctest -->
650
+ ```ruby
651
+ class Charge < Actionable::Action
652
+ step :check
653
+ step :charge_card
654
+ def check
655
+ fail!(:card_expired) if expired? # stops here
656
+ end
657
+
658
+ def charge_card = nil
659
+ def expired? = true
660
+ end
661
+
662
+ Charge.run.code # => :card_expired
663
+ ```
664
+
665
+ `fail(:x) and return` never returns (`fail` returns `false`): write
666
+ `return fail(:x)`. [USAGE, Where a plain verb may go](USAGE.md#where-a-plain-verb-may-go)
667
+ has the full table. If you can't fix every offense before deploying, set
668
+ `Actionable.verb_position_check = false` in an initializer and keep the cop in
669
+ CI until you can turn it back on. The check is skipped on Ruby 3.2 without the
670
+ `prism` gem, and for methods from `eval` / `define_method`; the cop still
671
+ covers those.
672
+
673
+ **Why.** `fail :x if bad?` followed by more code keeps running: almost always
674
+ a `fail!` missing its bang.
675
+
676
+ ### `fail` / `skip` codes must be identifiers
677
+ <!-- upgrading: identifier-codes -->
678
+
679
+ **Symptom.** At the call:
680
+ ```
681
+ ArgumentError: Codes: failure code "card expired" is not an identifier; use a Symbol like :card_expired. Inside an action, fail records a failure, it is not Kernel#fail: to raise an exception, use raise
682
+ ```
683
+ (`skip code ...` for a skip). A code must be a Symbol or String matching
684
+ `/\A[[:alpha:]_][[:alnum:]_]*[?!]?\z/`. Sentences, dotted (`'payment.declined'`)
685
+ and dashed (`:"not-found"`) codes, codes starting with a digit (`'404'`) and
686
+ classes (`fail GatewayError, 'down'`) all raise. Applies to `fail`, `fail!`,
687
+ `fail_with`, `fail_with!` (its `code:`), `skip` and `skip!`. **No opt-out:**
688
+ `verb_position_check = false` doesn't cover it.
689
+
690
+ **Find.** Three patterns: quoted codes, `code:` keywords, and class codes. They
691
+ skip calls on a receiver (`run.fail!(…)` on a model with its own `fail!`) and
692
+ also `self.fail …`, which is rare. A bare `fail` outside an action class
693
+ (`Kernel#fail` in a model or job) is fine to leave:
694
+ ```sh
695
+ rg -nP --type ruby "(?<![\w.])(?:fail|skip)!?[ (]\s*(?:'[^']*[^\w'?!][^']*'|\"[^\"]*[^\w\"?!][^\"]*\"|['\"]\d|:['\"])" app lib
696
+ rg -nP --type ruby "\bcode:\s*(?:'[^']*[^\w'?!][^']*'|\"[^\"]*[^\w\"?!][^\"]*\"|['\"]\d|:['\"])" app lib
697
+ rg -nP --type ruby '(?<![\w.])fail!?[ (]\s*[A-Z]\w*(?:::[A-Z]\w*)*\s*(?:,|\)|$)' app lib
698
+ ```
699
+ A code built at run time (`fail!(code_for(response))`) can't be found
700
+ statically; the runtime check is the finder.
701
+
702
+ **Fix.** Use a Symbol code and pass the sentence as the message; `raise` when
703
+ you meant an exception:
704
+ ```ruby
705
+ # before
706
+ fail('card expired')
707
+ fail(GatewayError, 'gateway down')
708
+ skip('payment.declined')
709
+ ```
710
+
711
+ <!-- doctest -->
712
+ ```ruby
713
+ class Pay < Actionable::Action
714
+ step :charge
715
+ def charge = fail!(:card_expired, 'card expired') # was: fail('card expired')
716
+ end
717
+
718
+ result = Pay.run
719
+ result.code # => :card_expired
720
+ result.message # => "card expired"
721
+ ```
722
+
723
+ **Why.** `fail 'card expired'` is `Kernel#fail` habit; inside an action it
724
+ recorded the sentence as a code and kept running.
725
+
726
+ ## 1.8.0 → 1.8.1
727
+
728
+ No caller action. The `Actionable/VerbNotLast` cop now resolves a superclass by
729
+ its lexical full name, as Ruby does, so two same-named classes in one file no
730
+ longer confuse it: it can only report fewer false offenses, never new ones on
731
+ code it already accepted. Everything else in 1.8.1 is test and tooling.
732
+
733
+ ## 1.8.1 → 1.8.2
734
+
735
+ A patch release, but three fixes change what an app can see. Each is a bug
736
+ fix: the old behaviour contradicted the documentation.
737
+
738
+ | Change | Who is affected | What you see | Why |
739
+ |---|---|---|---|
740
+ | [`case_step` branch guards are honoured](#case_step-branch-guards-are-honoured) | Actions with `on …, if:` / `unless:` inside a `case_step` | **Silent:** a branch whose guard says no no longer runs; the run still succeeds | The guard was accepted and silently ignored |
741
+ | [`Actionable::RBS` refuses colliding codes](#actionablerbs-refuses-codes-that-share-an-output-class-name) | `typed_outcomes` actions with codes like `:ready` + `:ready_`, or non-ASCII codes | `ArgumentError` from `generate`, naming both codes | Both schemas landed in one RBS class, so one was typed wrong |
742
+ | [The verb check reads an aliased step's `def`](#the-verb-check-reads-an-aliased-steps-original-def) | Steps that name an `alias_method` alias of a method with a misplaced `fail` / `skip` | `DefinitionError` on first run | Aliased steps skipped the check that 1.8.0 applies to every other step |
743
+
744
+ **From 1.8.1:**
745
+ 1. Bump the tag to `v1.8.2`.
746
+ 2. Run the `case_step` Find below, and check that each guarded branch should
747
+ really be skipped when its guard says no.
748
+ 3. If you use `typed_outcomes`, regenerate your RBS: `generate` names any clash.
749
+ 4. Run the suite.
750
+ Nothing else changed for callers. Codes ending in `?` / `!` get valid RBS class
751
+ names (`…PredicateFailureOutput`); before, their RBS didn't parse.
752
+
753
+ ### `case_step` branch guards are honoured
754
+ <!-- upgrading: case-branch-guards -->
755
+
756
+ **Symptom.** A `case_step` branch declared with `if:` / `unless:` no longer
757
+ runs when its guard opts out: an output field it set is `nil`, a side effect
758
+ doesn't happen, or a spec that expected the branch to run fails. The run still
759
+ succeeds. A guarded-out branch does **not** fall through to the next `on` or
760
+ to the `default`.
761
+
762
+ **Find.** Guarded branches inside a `case_step` block:
763
+ ```sh
764
+ rg -n '^\s*(on|default)\b.*,\s*(if|unless):' app lib
765
+ ```
766
+
767
+ **Fix.** If the branch should run regardless, drop the guard. If a
768
+ guarded-out branch should fall back to another target, say so in a guard-free
769
+ branch body:
770
+ ```ruby
771
+ # before: the guard was ignored, so 'sms' always sent an SMS
772
+ case_step :kind do
773
+ on 'sms', :send_sms, if: :sms_enabled?
774
+ default :send_email
775
+ end
776
+
777
+ # after: 'sms' with SMS off sends an email
778
+ case_step :kind do
779
+ on 'sms', :route_sms
780
+ default :send_email
781
+ end
782
+
783
+ def route_sms = sms_enabled? ? send_sms : send_email
784
+ ```
785
+
786
+ **Why.** The guard was accepted and silently dropped; a branch now behaves
787
+ like the plain step it is.
788
+
789
+ ### `Actionable::RBS` refuses codes that share an output class name
790
+ <!-- upgrading: rbs-outcome-class-names -->
791
+
792
+ **Symptom.** Under `typed_outcomes`, `Actionable::RBS.generate` raises:
793
+ ```
794
+ ArgumentError: Billing::Charge: skip codes :ready and :ready_ both name ReadySkipOutput in the generated RBS; rename one of the codes
795
+ ```
796
+ (or `... is not a valid RBS class name ...` for a code whose class name would not
797
+ be plain ASCII starting with a capital, e.g. `:日本` or `:foo_é`). Codes ending in `?` / `!` that declare an output
798
+ schema now generate `…PredicateFailureOutput` / `…BangSkipOutput`; before,
799
+ their RBS didn't parse, so nothing can reference the old names.
800
+
801
+ **Find.** Run `Actionable::RBS.generate` (your sigs task) for every action; it
802
+ names each clash. No search finds them reliably.
803
+
804
+ **Fix.** Rename one of the two codes, and its `fail` / `skip` calls:
805
+ ```ruby
806
+ # before
807
+ skips(:ready) { required :id, :integer }
808
+ skips(:ready_) { required :id, :integer }
809
+
810
+ # after
811
+ skips(:ready) { required :id, :integer }
812
+ skips(:ready_later) { required :id, :integer }
813
+ ```
814
+
815
+ **Why.** Both schemas went into one RBS class, so one of them typed wrong.
816
+
817
+ ### The verb check reads an aliased step's original `def`
818
+ <!-- upgrading: verb-check-alias -->
819
+
820
+ **Symptom.** On first run, the same `DefinitionError` as for any
821
+ [verb-position offense](#a-plain-verb-must-end-its-step-or-hook-method),
822
+ naming the step (`Charge#charge`) and the `file:line` of the aliased `def`:
823
+ ```
824
+ Actionable::DefinitionError: Charge#charge (app/actions/charge.rb:4) calls `fail` with more code after it, which still runs: use fail! to stop here, or make it the method's last expression
825
+ ```
826
+
827
+ **Find.** Aliases in action classes; read the `def` each step alias points
828
+ at. The `Actionable/VerbNotLast` cop doesn't follow aliases, so it won't list
829
+ these:
830
+ ```sh
831
+ rg -n '^\s*(alias_method\b|alias\s)' app lib
832
+ ```
833
+
834
+ **Fix.** As for any offense: a bang verb, or make the verb the last
835
+ expression:
836
+ ```ruby
837
+ # before
838
+ step :charge
839
+ def do_charge
840
+ fail :late if late?
841
+ @charged = true
842
+ end
843
+ alias_method :charge, :do_charge
844
+
845
+ # after
846
+ step :charge
847
+ def do_charge
848
+ fail! :late if late?
849
+ @charged = true
850
+ end
851
+ alias_method :charge, :do_charge
852
+ ```
853
+
854
+ **Why.** The check looked the `def` up by the alias's name, found none, and
855
+ skipped the step.
856
+
857
+ ## 1.8.2 → 1.9.0
858
+
859
+ The gem is renamed. Nothing in the library changed.
860
+
861
+ | Change | Who is affected | What you see | Why |
862
+ |---|---|---|---|
863
+ | [The gem is named `actionable-rb`](#the-gem-is-named-actionable-rb) | Every app (the Gemfile line), and apps that load the cop with `plugins:` | `bundle install` fails: "Could not find gem 'actionable' in …"; RuboCop: "Failed to load plugin `actionable`" | The `actionable` name on rubygems.org belongs to an unrelated 2014 gem |
864
+
865
+ **From 1.8.2:**
866
+ 1. In the Gemfile, change `gem 'actionable'` to `gem 'actionable-rb'` and the
867
+ tag to `v1.9.0`, then `bundle install`.
868
+ 2. If `.rubocop.yml` loads the cop with `plugins:`, rename its key to
869
+ `actionable-rb`.
870
+ 3. Run the suite. No code changes: `require 'actionable'` and every constant
871
+ stay the same.
872
+
873
+ ### The gem is named `actionable-rb`
874
+ <!-- upgrading: gem-renamed-actionable-rb -->
875
+
876
+ **Symptom.** `bundle install` (or `bundle update actionable`) stops with
877
+ `Could not find gem 'actionable' in https://github.com/Paymentbox-com/actionable`
878
+ (Bundler matches a git source by the gemspec's name). With the cop loaded by
879
+ `plugins:`, `rubocop` stops with ``Failed to load plugin `actionable` because the
880
+ corresponding plugin class could not be determined``.
881
+
882
+ **Find.**
883
+ ```sh
884
+ rg -n "gem ['\"]actionable['\"]" Gemfile gemfiles
885
+ rg -n -A1 '^\s*-\s*actionable:\s*$' .rubocop.yml
886
+ ```
887
+
888
+ **Fix.**
889
+ ```ruby
890
+ # Gemfile, before
891
+ gem 'actionable', git: 'https://github.com/Paymentbox-com/actionable', tag: 'v1.8.2'
892
+ # after
893
+ gem 'actionable-rb', git: 'https://github.com/Paymentbox-com/actionable', tag: 'v1.9.0'
894
+ ```
895
+ ```yaml
896
+ # .rubocop.yml, before
897
+ plugins:
898
+ - actionable:
899
+ require_path: actionable/rubocop
900
+ # after
901
+ plugins:
902
+ - actionable-rb:
903
+ require_path: actionable/rubocop
904
+ ```
905
+ Keep `require 'actionable'` wherever you have it. A Gemfile that relies on
906
+ `Bundler.require` needs no `require:` option: the gem ships an
907
+ `actionable-rb.rb` that loads the library. The older `require:
908
+ [actionable/rubocop]` form in `.rubocop.yml` keeps working unchanged.
909
+
910
+ **Why.** To publish on rubygems.org, the gem needs a name nobody else holds;
911
+ `actionable` there is an unrelated project from 2014.
912
+
913
+ ## Class-level API an app base class may rely on
914
+
915
+ Unchanged from 1.5.1 to 1.8.1, so a shared `ApplicationAction` that overrides
916
+ or calls these needs no edit: `fails` / `skips` (the declarations),
917
+ `declared_failures` / `declared_skips`, `allows_failure_code?` /
918
+ `allows_skip_code?`, `failure_output_schema` / `skip_output_schema`, `halt!`,
919
+ `fail_with!`, and the `Failure` / `Skipped` / `Success` constructors. `Result`
920
+ only gained an `@api private` accessor. Anything marked `@api private` in the
921
+ YARD docs can change in any release.
922
+
923
+ ## New, opt-in: worth adopting
924
+
925
+ | Since | Feature | One line | Reference |
926
+ |---|---|---|---|
927
+ | 1.5.2 | Savepoint control | `transactional model: :x, requires_new: false` joins an outer transaction instead of taking a savepoint. | [USAGE, Rails adapter](USAGE.md#rails-adapter) |
928
+ | 1.6.0 | Typed `Actionable::RBS` | Typed `.run` keywords, `InputSchema`, output ivars and the real superclass chain for each action. | [USAGE, RBS for your actions](USAGE.md#rbs-for-your-actions) |
929
+ | 1.7.0 | `honor_retry_metadata` | Declared `retry: true` / `retry: false` decides the job's disposition (ActiveJob: `retry_on Actionable::Job::RetryableFailure`). | [USAGE, Background jobs](USAGE.md#background-jobs) |
930
+ | 1.7.0 | `after_commit` | Emails, webhooks and enqueues run only after the outermost transaction commits (ActiveRecord >= 7.2). | [USAGE, Rails adapter](USAGE.md#rails-adapter) |
931
+ | 1.7.0 | RSpec matchers, typed | `perform_actionable` and the `allow_/stub_actionable_*` helpers now have RBS. | [USAGE, RSpec integration](USAGE.md#rspec-integration) |
932
+ | 1.8.0 | `Actionable/VerbNotLast` cop | Lints misplaced plain verbs in CI, before the first run. | [USAGE, Where a plain verb may go](USAGE.md#where-a-plain-verb-may-go) |
933
+
934
+ ## If something else breaks
935
+
936
+ **Bisect by version.** Pin each release in turn and run the failing spec; the
937
+ first tag that fails names the section above to reread:
938
+ ```ruby
939
+ gem 'actionable', git: 'https://github.com/Paymentbox-com/actionable', tag: 'v1.5.2' # then v1.6.0, v1.7.0, v1.8.0
940
+ ```
941
+ From `v1.9.0` the gem is `actionable-rb`, so that line says `gem 'actionable-rb'`.
942
+ Remember the field_struct floor at each step (0.11 for 1.5.2, 0.12 from 1.6.0).
943
+
944
+ **A guard offense or a real bug?**
945
+
946
+ - `DefinitionError` "... calls `fail` with more code after it ..." is the verb
947
+ guard. It goes away with `Actionable.verb_position_check = false`; a real bug
948
+ doesn't. The cop reports the same line.
949
+ - `ArgumentError` "... code ... is not an identifier" is the code check, raised
950
+ at the `fail` / `skip` call itself. The backtrace's first app frame is the
951
+ line to change.
952
+ - `DefinitionError` "... declares step(s) calling ... but does not implement
953
+ them" or "... set @result ..." are the 1.5.2 definition checks.
954
+ - Anything else (a different result code, missing output, a hook that runs or
955
+ doesn't) is behaviour: find the version with the bisect above, then the entry
956
+ in that version's section. If none fits, it's a bug. Open an issue with the
957
+ action and the two versions.