Skip to content

Actionable v1.8.0 — verb guard and upgrade guide

Choose a tag to compare

@aemadrid-twx aemadrid-twx released this 23 Oct 01:06
· 36 commits to main since this release
5f8d24d

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 a fail! 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_commit with the Rails adapter) is parsed with Prism, and a plain
    fail / fail_with / skip / succeed (or self.fail …) that isn't the
    method's last expression raises DefinitionError naming the action, method, file:line,
    verb and fix. Branches, return fail(...), a verb followed by return,
    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 the prism gem; no new dependency) and for methods with no
    parseable source (eval, define_method).
  • Actionable.verb_position_check = false turns that first-run check off
    for the process (on by default; Actionable.verb_position_check? reads it).
    Off, run never 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 / skip codes 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 by require 'actionable'; enable it with
    plugins: [{actionable: {require_path: actionable/rubocop}}] (RuboCop >=
    1.72, via the gem's lint_roller plugin) or require: [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's ActionBaseClasses (default
    Actionable::Action, ApplicationAction) or a class defined in the same
    file whose superclass chain reaches one, so a dry-transaction class with
    its own step gets no false positives (owner ruling R10).
  • rake bench gains fail! action and skip! action scenarios.

Changed

  • fail / skip codes 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
    ArgumentError at the call, suggesting raise for Kernel#fail habit.
    Applies to fail / fail! / fail_with / fail_with! / skip / skip!.

Upgrading: both checks raise on code that used to run.

  • DefinitionError "… calls fail with 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 = false in 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')),
    or raise if you meant an exception.

Tests / CI

  • Real-backend lanes. Two new CI jobs feed the required ci check:
    jobs (ActiveJob + Sidekiq) runs the suite with gemfiles/jobs.gemfile, so
    spec/actionable/job_backends/ proves Job + honor_retry_metadata on a
    real ActiveJob (retry_on Actionable::Job::RetryableFailure: retry: true
    re-enqueues until attempts run out, retry: false discards or acks, the
    default is unchanged) and that RetryableFailure propagates out of a Sidekiq
    job in inline testing mode (no Redis). activerecord 7.1 runs the suite on
    ActiveRecord 7.1 (gemfiles/activerecord_7_1.gemfile, Ruby 3.3):
    transactional and savepoints work there, and declaring after_commit
    raises the DefinitionError naming 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 an rg
    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
    invisible upgrading: 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 on Actionable::Action
    even when actionable/rails isn't required: RBS can't make a signature
    conditional and rbs loads a gem's whole sig/.