Skip to content

Instantly share code, notes, and snippets.

@jdickey
Last active October 30, 2021 16:16
Show Gist options
  • Select an option

  • Save jdickey/5f06dd10252ddd148b9660f4dfe0abe9 to your computer and use it in GitHub Desktop.

Select an option

Save jdickey/5f06dd10252ddd148b9660f4dfe0abe9 to your computer and use it in GitHub Desktop.
Attempting Automation of Rails App Generation with .railsrc and App Generator
## Intro
(NB: Forgive the many content-free revisions; it has been many moons since I have created a Gist with attachments, and getting the
ordering currect is maddeningly non-trivial.)
After too many years doing this, never mind how many, I'm attempting to automate the generation of Rails apps using a
`.railsrc` file and an app-generation template. The `.railsrc` file, largely cribbed from numerous others, appears to work
splendidly.
The template file, which should automate setup of the newly-created app to current shop standards, is being developed incrementally; increments largely separated by *the next wall I hit*. The current "wall" has had me stopped cold for two days; my Stack Overflow skills and Google-fu are clearly not up to the task.
## Possibly Relevant Background Items
1. Ruby is installed by way of [`rbenv`](https://github.com/rbenv/rbenv#readme), which we have used for *years* without known issues. We are presently on Ruby 3.0.2p107.
2. I'm attempting to continue our long-time shop-standard usage of [`rbenv-gemsets`](https://github.com/jf/rbenv-gemset#readme), which has hitherto (before attempted use of an app generator) saved our proverbial bacon often enough that I still have *some* hair. It supports our experimentation and evolution of candidate Gems for use, without polluting the system Gem repository.
## Problem Statement
When I generate a new app using the template, everything appears to work fine setting up the new app up to and including when `bundle install` is run. However, immediately after bundling reports success, the Gems are installed not in the Gemset, but in the `rbenv` Gem repository for the current Ruby version. Running the installation process manually stores the Gems in the expected place.
When I started writing this Gist, I had several open questions that I was seeking help for; I found answers to most of them on my own, save for two:
1. Why are the bundled Gems being installed in the `rbenv` Gem repository rather than my gemset, as occurs when I proceed manually?
2. Why has `app/javascript/packs/application.js` changed after completion of the template, forcing `public/packs/manifest.json` to also change?
Help?
# This is my ~/.railsrc file. There are many like it, but this one is mine.
# known defaults
--no-api
--no-dev
--no-edge
--no-no-rc # obviously...
--no-skip-action-cable
--no-skip-active-record
--no-skip-gemfile
--no-skip-git
--no-skip-javascript
--no-skip-keeps
--no-skip-listen
--no-skip-puma
--no-skip-sprockets
--no-skip-turbolinks
--no-skip-yarn
--skip-coffee
--skip-namespace
--skip-system-test # ditto
--skip-test # Don't use MiniTest; we'll add RSpec later
# --ruby=/path/to/executable/ruby
# --template=/path/to/app/template
# Custom/non-default values
--database=postgresql
--skip-action-mailer
--skip-active-storage
--skip-bootsnap
--skip-bundle
--skip-spring
--webpack=stimulus
# --template=/path/to/template.rb
# frozen_string_literal: true
# DRAFT Rails Application Template
# Copyright 2021, Jeff Dickey/Seven Sigma Agility
# Licensed for public use under the MIT License.
#
# This template sets up a newly-created Rails app, in conjunction with the
# associated `.railsrc` file. This (mostly; see below) automates processes that
# have been in use for a decade or so, over dozens of apps. Better late than
# never, yes? B-)
#
# Sample usage would look something like
# ```
# rails new the_app -m ./template.rb
# ```
# where `the_app` would be replaced by the actual name of your new application.
#
# First, we set up our Gemfile. This is somewhat abbreviated from our standard
# since this whole script runs essentially as a hook within Rails' template
# processing, which has already created a Gemfile by the time we get here. It
# Would Be Very Nice if that was documented a bit more clearly in the Rails
# Guides, but oh well.
#
# This setup assumes `zsh`, `rbenv`, and `rbenv-gemset`. It further asdumes that
# it is being run from what will become the parent directory of the new app, in
# which exists a `.ruby-version` file, and possibly an `.rbenv-gemset` directory
# that will be **DELETED** if it exists.
################################################################################
# FIXME: Outdated, hardcoded Node packages are *evil*.
gem 'redis', '~> 4.0'
gem 'bcrypt', '~> 3.1.7'
gem "bundler-audit", "~> 0.9.0"
gem "dotenv-rails", "~> 2.7"
gem "dry-monads", "~> 1.4"
gem "ice_nine", "~> 0.11.2"
gem "slim-rails", "~> 3.3"
gem "foreman", "~> 0.87.2"
gem "sassc-rails", "~> 2.1"
gem "bundler", "~> 2.2"
gem "database_cleaner", "~> 2.0"
gem "pry-byebug", "~> 3.8"
gem "pry-doc", "~> 1.2"
gem "pry-rails", "~> 0.3.9"
# gem "stimulus_reflex", "~> 3.4"
gem "nokogiri", ">= 1.12.5"
gem "view_component", "~> 2.40", require: 'view_component/engine'
gem_group :development, :test do
gem "flay", "~> 2.12"
gem "flog", "~> 4.6"
gem "inch", "~> 0.8.0"
gem "reek", "~> 6.0"
gem "rubocop-rails", "~> 2.12"
gem "yard", "~> 0.9.26"
gem "fabrication", "~> 2.22"
gem "simplecov", "~> 0.21.2"
gem "ffaker", "~> 2.19"
gem "rspec-html-matchers", "~> 0.9.4"
gem "html2slim", "~> 0.2.0"
end
gem_group :test do
gem 'capybara', '>= 3.26'
gem 'selenium-webdriver'
# Easy installation and use of web drivers to run system tests with browsers
gem 'webdrivers'
gem "rspec-rails", "~> 5.0"
end
#
# Gemfile created; on to
# * initial setup of the `rbenv` gemset; and
# * running (what as far as we care is the first) `bundle install`.
#
run "cp ../.ruby-version ."
run 'rm -f .rbenv-gemset'
run "mkdir -p bin tmp/gemset"
run "rbenv gemset delete #{ENV['RBENV_VERSION']} ./tmp/gemset 2>&1 > /dev/null"
run "rbenv gemset create #{ENV['RBENV_VERSION']} ./tmp/gemset"
run 'rm -f .rbenv-gemset'
run "rbenv gemset init ./tmp/gemset"
# run "echo ./tmp/gemset > .rbenv-gemset"
run 'cd #{@app_name}; rbenv rehash; echo "Active gemsets: " `rbenv gemset active`'
run "bundle install"
#
# Make sure we've got all our binstubs set up so that Rails will simply use them
# (instead of complaining that they're set up for BUndler.)
#
run "bundle binstubs --all --force"
run "(yes | rails app:update:bin)", capture: true
rails_command 'app:update:bin'
#
# Create the database role/user for the app.
#
run "createuser -dw #{@app_name}"
#
# Tell Rails to use the `fabrication` Gem fixture data. It's pretty cool that
# Rails/Thor has the `inject_data_file` method; this used to be the first of
# two steps performed manually by directly editing files.
#
inject_into_file "config/application.rb",
after: " config.generators.system_tests = nil\n" do <<-EOS
config.generators do |gen|
gen.test_framework :rspec, fixture_replacement: :fabrication
gen.fixture_replacement :fabrication, dir: "spec/fabricators"
end
EOS
end
#
# Fix the generated `Rails.root.join` call in the development environment, else
# Rubocop will kvetch about it whenever its Rake task is called, either directly
# or as part of the default processing. (See the Rake task setup below.)
#
gsub_file('config/environments/development.rb',
"Rails.root.join('tmp', 'caching-dev.txt')",
"Rails.root.join('tmp/caching-dev.txt')")
#
# Create the initial (empty) development and test databases.
#
rails_command "db:create"
rails_command "db:migrate"
rails_command "db:seed"
#
# Now is a good time to get RSpec set up
#
rails_command 'generate rspec:install'
#
# Create a Procfile for `foreman` to use to run multiple processes, including
# the web server running our app.
#
create_file "Procfile", <<-ENDIT
webpack: bin/webpack-dev-server
web: bin/rails server -p 3000
ENDIT
# TBD: Find out why Foreman changes ports & we must explicitly specify 3000
run 'bin/foreman check'
#
# Set up Git and stage all existing files not specified in `.gitignore`. Note
# that we aren't committing yet; the idea is that we'll have one single commit
# after all steps in this generator script (and the surrounding Rails processing
# code) will have been completed.
#
# TBD: Figure out how to define an after-all-generator-steps-completed hook. Not
# just for this script; for the whole generator that this runs within.
#
run 'rm -rf .git'
git init: '-b main'
git add: '.'
insert_into_file 'spec/spec_helper.rb', before: 'RSpec.configure do |config|' do <<-EOS
require 'simplecov'
SimpleCov.start do
coverage_dir './tmp/coverage'
add_filter '/lib/tasks'
add_filter '/spec'
add_filter '/tmp'
end
EOS
end
git add: 'spec/spec_helper.rb'
#
# Set up Webpacker.
# TBD: In future, we'll likely add Stimulus JS, but not at present.
#
rails_command 'dev:cache'
rails_command 'webpacker:install'
rails_command 'webpacker:check_binstubs'
git add: '.'
#
# We use Slim for view templates, not ERb as generated by default. Convert the
# app-wide base view template.
#
run 'erb2slim -d app/views/layouts/application.html.erb'
git add: 'app/views/layouts/'
#
# Add configuration files for Flay, Reek, and Rubocop
#
# Add .flayignore
create_file '.flayignore', <<-ENDIT
./spec/**/*.rb
./tmp/**/*
ENDIT
git add: '.flayignore'
# Add Reek config
create_file 'config.reek', <<-ENDIT
---
detectors:
IrresponsibleModule:
enabled: false
MissingSafeMethod:
enabled: false
NestedIterators:
max_allowed_nesting: 2
ignore_iterators:
- lambda
UncommunicativeVariableName:
exclude:
- Conversagence::Application
UnusedPrivateMethod:
enabled: true
UtilityFunction:
public_methods_only: true
LongParameterList:
max_params: 4 # If it's good enough for Sandi, it's good enough for us
ENDIT
git add: 'config.reek'
# Add Rubocop config
create_file '.rubocop.yml', <<ENDIT
require:
- rubocop-rails
AllCops:
TargetRubyVersion: 2.6
Bundler/OrderedGems:
Exclude:
- 'Gemfile'
Metrics/BlockLength:
Max: 300
Include:
- 'test/**/*.rb'
Metrics/ModuleLength:
Max: 300
Include:
- 'test/**/*.rb'
# Style/AsciiComments:
# Exclude:
# - 'spec/web/views/home/index_spec.rb'
Style/CommentedKeyword:
Enabled: false
# This silences 'Missing top-level class documentation comment.', which we
# particularly want to do on (initially-)generated files.
Style/Documentation:
Enabled: false
Include:
- 'app/**/*.rb'
- 'lib/**/*.rb'
- 'test/**/*.rb'
###
### New cops added as of rubocop 0.85.1 and rubocop-rails 2.6.0
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Lint/DeprecatedOpenSSLConstant:
Enabled: true
Lint/MixedRegexpCaptureTypes:
Enabled: true
Lint/RaiseException:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Style/ExponentialNotation:
Enabled: true
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: true
Style/RedundantRegexpEscape:
Enabled: true
Style/SlicingWithRange:
Enabled: true
ENDIT
git add: '.rubocop.yml'
#
# Add Rake task definitions, and the default set and order of tasks to be run
# by `rake` when invoked without a task name.
#
## For Flay
create_file "lib/tasks/flay.rake", <<-ENDIT
# frozen_string_literal: true
require 'flay'
require 'flay_task'
FlayTask.new do |t|
t.verbose = true
t.dirs = %w[app config lib]
end
ENDIT
git add: 'lib/tasks/flay.rake'
## For Flog
create_file 'lib/tasks/flog.rake', <<-ENDIT
# frozen_string_literal: true
require 'flog'
require 'flog_task'
class FlogTask < Rake::TaskLib
attr_accessor :methods_only
end
FlogTask.new do |t|
t.verbose = true
t.threshold = 200 # default is 200
t.methods_only = true
t.dirs = %w[app config lib] # Look, Ma; no specs! Run the tool manually every so often for those.
end
ENDIT
git add: 'lib/tasks/flog.rake'
## For Inch
create_file "lib/tasks/inch.rake", <<-ENDIT
# frozen_string_literal: true
require 'inch/rake'
Inch::Rake::Suggest.new do |t|
t.args = ['--no-undocumented', '--pedantic']
end
ENDIT
git add: 'lib/tasks/inch.rake'
## For Reek
create_file "lib/tasks/reek.rake", <<-ENDIT
# frozen_string_literal: true
require 'reek/rake/task'
Reek::Rake::Task.new do |t|
t.config_file = 'config.reek'
t.source_files = '{app,config,lib}/**/*.rb'
t.reek_opts = '--sort-by smelliness --no-progress -s'
# t.verbose = true
end
ENDIT
git add: 'lib/tasks/reek.rake'
## For Rubocop
create_file "lib/tasks/rubocop.rake", <<-ENDIT
# frozen_string_literal: true
require 'rubocop/rake_task'
RuboCop::RakeTask.new(:rubocop) do |task|
task.patterns = [
'app/**/*.rb',
'config/**/*.rb',
'lib/**/*.rb',
'spec/**/*.rb'
]
task.formatters = ['simple']
task.fail_on_error = true
task.requires << 'rubocop-rails'
task.options << '--config=.rubocop.yml'
task.options << '--display-cop-names'
end
ENDIT
git add: 'lib/tasks/rubocop.rake'
## For Bundler sub-tasks
create_file "lib/tasks/bundler-audit.rake", <<-ENDIT
# frozen_string_literal: true
namespace :bundler do
desc 'Run bundler-audit, updating its database and then verbosely checking'
task :audit do
system('bundler-audit check -uv')
end
end
ENDIT
git add: 'lib/tasks/bundler-audit.rake'
## Defaults
# frozen_string_literal: true
create_file "lib/tasks/defaults.rake", <<-ENDIT
# frozen_string_literal: true
default_tasks=%i[spec flog flay reek inch rubocop:auto_correct rubocop]
bundler_tasks=[]
if ENV['RAKE_BUNDLER'] || ENV['RAKE_BUNDLER_GRAPH']
bundler_tasks = %i[bundler:audit bundler:dependencies:count]
if ENV['RAKE_BUNDLER_GRAPH']
bundler_tasks = %i[bundler:audit bundler:dependencies:graph bundler:dependencies:count]
end
default_tasks += bundler_tasks
end
# Rake::Task['default'].clear
task default: default_tasks
default_tasks = nil
bundler_tasks = nil
ENDIT
git add: 'lib/tasks/defaults.rake'
#
# Install TailwindCSS et al
#
run 'yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9'
git add: 'package.json yarn.lock'
insert_into_file 'postcss.config.js', after: ' plugins: [' do <<-EOS
require('tailwindcss'),
EOS
end
git add: 'postcss.config.js'
run 'node_modules/.bin/tailwindcss init'
git add: 'tailwind.config.js'
insert_into_file 'app/javascript/packs/application.js', after: 'Turbolinks.start()' do <<-EOS
import "tailwindcss/tailwind.css"
EOS
end
git add: 'app/javascript/packs/application.js'
gsub_file('app/views/layouts/application.html.slim', 'stylesheet_link_tag',
'stylesheet_pack_tag')
git add: 'app/views/layouts/application.html.slim'
#
# Let's just make everything Rubocop-clean before we're done.
#
rails_command 'rubocop:auto_correct'
git add: 'app/ config/ spec/'
run 'git add --force public/packs/manifest.json'
#
# NOTE: Prior to the initial commit, you'll need to run `git add .` from the
# command line before manually making the initial commit. The only file seemingly
# affected is `public/packs/manifest.json`. We need to figure out how to automate
# this. Is there an "after-everything" hook supported by the Rails app generator?
#
# We'll run `hr` a couple of times so that it's visually apparent in the output
# from generating the app where the end of our template is, and the start of more
# commands executed by the base generator.
#
run "hr"
run "hr"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment