Repository navigation
Actionable v1.8.0 — verb guard and upgrade guide
A guard against silently continuing runs, plus upgrade guidance. A plain
fail / fail_with / skip / succeed must now be the last thing its step
or hook does — checked once per action class on first run (Prism; turn it off
with Actionable.verb_position_check = false) and by a shipped RuboCop cop,
Actionable/VerbNotLast. fail / skip codes must be identifiers, so the
fail 'card expired' Kernel habit raises instead of recording a code. Both can
raise on code that used to run: read UPGRADING.md, which walks 1.5 → 1.8
change by change. CI now also runs real ActiveJob/Sidekiq and ActiveRecord 7.1
lanes. Still requires field_struct >= 0.12.
Added
- A plain recording verb must end its step or hook method (owner rulings
R5, R10).fail :card_expired if card.expired?followed by more code keeps
running — almost always afail!missing its bang. On an action's first
run, every step method (case-step branch targets included) and lifecycle
hook method (on_success/on_failure/on_skip/always, and
after_commitwith the Rails adapter) is parsed with Prism, and a plain
fail/fail_with/skip/succeed(orself.fail…) that isn't the
method's last expression raisesDefinitionErrornaming the action, method,file:line,
verb and fix. Branches,return fail(...), a verb followed byreturn,
and blocks whose call is last are allowed; USAGE (Control flow, "Where a
plain verb may go") has the full table. Checked once per class: later runs
pay one instance-variable read. Skipped without Prism >= 1.0 (Ruby 3.2
without theprismgem; no new dependency) and for methods with no
parseable source (eval,define_method). Actionable.verb_position_check = falseturns that first-run check off
for the process (on by default;Actionable.verb_position_check?reads it).
Off,runnever parses source or loads Prism. It is an escape hatch while
upgrading, and leaves the cop as the only guard. The identifier check on
fail/skipcodes stays on: it's a per-call regex, not a parse.Actionable/VerbNotLast, an opt-in RuboCop cop with the same rules
(one shared analyzer). Not loaded byrequire 'actionable'; enable it with
plugins: [{actionable: {require_path: actionable/rubocop}}](RuboCop >=
1.72, via the gem's lint_roller plugin) orrequire: [actionable/rubocop].
It checks a class whose superclass is an action base and that declares
step/case_step, and the methods that class names in its declarations.
An action base is one of the cop'sActionBaseClasses(default
Actionable::Action,ApplicationAction) or a class defined in the same
file whose superclass chain reaches one, so a dry-transaction class with
its ownstepgets no false positives (owner ruling R10).rake benchgainsfail! actionandskip! actionscenarios.
Changed
fail/skipcodes must be identifiers. A code that isn't a Symbol
or String matching/\A[[:alpha:]_][[:alnum:]_]*[?!]?\z/('card expired',:"not-found",'payment.declined', a class) raises
ArgumentErrorat the call, suggestingraiseforKernel#failhabit.
Applies tofail/fail!/fail_with/fail_with!/skip/skip!.
Upgrading: both checks raise on code that used to run.
DefinitionError"… callsfailwith more code after it …" on an action's
first run: if the code after the verb should not run, use the bang verb
(fail!); if it should, move the verb to the end of the method (or
return fail(...)). Run the cop over your app to find them all up front.
If you can't fix them all before deploying, set
Actionable.verb_position_check = falsein an initializer until you have.ArgumentError"… code "…" is not an identifier": use a Symbol code and
pass the sentence as the message (fail(:card_expired, 'card expired')),
orraiseif you meant an exception.
Tests / CI
- Real-backend lanes. Two new CI jobs feed the required
cicheck:
jobs (ActiveJob + Sidekiq)runs the suite withgemfiles/jobs.gemfile, so
spec/actionable/job_backends/provesJob+honor_retry_metadataon a
real ActiveJob (retry_on Actionable::Job::RetryableFailure:retry: true
re-enqueues until attempts run out,retry: falsediscards or acks, the
default is unchanged) and thatRetryableFailurepropagates out of a Sidekiq
job in inline testing mode (no Redis).activerecord 7.1runs the suite on
ActiveRecord 7.1 (gemfiles/activerecord_7_1.gemfile, Ruby 3.3):
transactionaland savepoints work there, and declaringafter_commit
raises theDefinitionErrornaming 7.2. These specs skip in the default
suite, where the gems or versions they need are absent.
Documentation
UPGRADING.md: how to move an app from 1.5.x to 1.8.0 (owner ruling
R9). A TL;DR with the order of work (field_struct first, then actionable,
the cop, RBS), a straight 1.5-to-1.8 path, a symptom index, and a Symptom /
Find / Fix / Why entry for every change a caller can see, each Find anrg
pattern checked against its "before" code or the tool that finds it. AGENTS.md
and the skill gain a numbered upgrade procedure for agents; README and
getting_started link to it, and the gem ships it.spec/upgrading_spec.rb
keeps it honest: every CHANGELOG upgrade note since 1.5.2 carries an
invisibleupgrading:slug comment that the guide must carry in that
version's section, and the guide's declared range must reach the next
release. Its runnable examples are doctested.- README "Types (RBS)" says the adapter signatures (
transactional,
after_commit, the RSpec helpers,Job) are declared onActionable::Action
even whenactionable/railsisn't required: RBS can't make a signature
conditional andrbsloads a gem's wholesig/.