Writing a step
Table of contents
The contract
Every step (lib/opinionated_rails/steps/{core,addons}/*.rb) follows the same shape:
# frozen_string_literal: true
module OpinionatedRails
module Steps
module Addons
class ExampleStep < Step
def self.id = :example
def self.tier = :addon
def self.needs = []
def self.summary = "Configure Example"
def self.description = "One sentence on what it adds."
def self.prompts = "Enable Example? (default: no)"
EXAMPLE_VERSION = "1.2.3".freeze
def if_condition(_context) = true
def prompt(context)
shell = context.shell
choice = shell.ask("Enable Example? (default: no):").to_s.strip
choice = "no" if choice.empty?
loop do
case choice.downcase
when "yes", "y" then context.answer(:example, true); break
when "no", "n" then context.answer(:example, false); break
else
shell.say("Invalid. Enter 'yes' or 'no'.", :red)
choice = shell.ask("Enable Example? (default: no):").to_s.strip
choice = "no" if choice.empty? # a re-ask must also fall back to the default
end
end
end
def generate(context)
enabled = context.get(:example) == true
# edit the generated app here
context.set_output(:example, { example: enabled })
end
def outputs(context) = context.output(:example)
end
end
end
end
Rules the code and specs rely on:
outputs(context)just reads backcontext.output(:step_id).- A step with no question still defines
promptas a no-op. - Re-asks fall back to the default. After an invalid answer, an empty reply (or end of input) must take the default, or the loop never ends.
prompt_resilience_spec.rbfails any step that violates this. - Pin exact versions in
*_VERSIONconstants. generateis idempotent: running it twice must not duplicate output. Check before appending to files; use the group-aware Gemfile helpers the other steps use.- Respect
skip_bundle_install: when true, only write files and Gemfile lines; do not shell out tobundle, generators oryarn. - Shell out safely:
Bundler.with_unbundled_env { system("cmd", "arg") }(argument array, never a string), run from the app directory, and raiseOpinionatedRails::Erroron failure. - Never silently skip an edit to Rails’ files. Warn with the exact setting to apply by hand.
needs has two meanings
- Ordering only.
cineedsrubocopandtestingso those have run first. - A real read.
mailerneedssecrets_handlingand branches oncontext.output(:secrets_handling, :secrets_handling).
If a step only wants to peek at another step’s output, do not add it to needs: read it defensively (context.output(:js_lint, :js_lint) || false), so a skipped upstream step just means “skip the optional behavior”. Keep needs honest: staging needs solid_stack, mailer and docker_kamal because it copies files those steps edit.
Skipping Rails’ own files
If a step should be the only author of files rails new would otherwise create, pass the matching flag in CLI#build_rails_new_flags (see --skip-docker, --skip-kamal, --skip-ci) instead of overwriting afterwards.
Static templates
Verbatim content (PR templates, service base classes, the generated STYLE.md) lives in lib/opinionated_rails/templates/ and is copied with Generators::FileWriter. Edit the template file; do not rebuild it inline. Generated Ruby must pass the generated app’s RuboCop (templates are excluded from this repo’s lint, so check by generating).
Checklist for adding or changing an option
- The step, following the rules above. Pin every new gem or npm package exactly;
bundle exec rake pins:checkmust pass (map a new*_VERSIONconstant to its gem name inPins::GEM_NAMES). - Non-interactive use. Add its prompt label to
AnswerShell::LABELSand its accepted values toAnswerShell::CHOICES.answer_choices_spec.rbfails if a listed value is not accepted by the step. - Unit specs for the step: the default path, each choice, idempotency, an invalid answer.
- A real-app combo. Add
spec/fixtures/combos/<name>.ymland its assertions ingenerated_apps_slow_spec.rb, run it (see Testing), and add it to the matrix in.github/workflows/ci.yml. - Docs. Add it to Options and Config file, then run
bundle exec rake docs:stepsto refresh the step reference.docs_spec.rbfails if the docs and code disagree. CHANGELOG.md.