First commits in a Ruby on Rails app
Configure Rails app using automated quality checks, security audits, and best practices for maintainability and consistency in solo or side projects
I had the idea for this project (Ruby and Ruby on Rails courses over email) three months ago and created the Ruby on Rails repo around that time.
I was unsure what form the product would have, so I focused first on setting up coding styles, static analysis checks, and some other defaults that I consider essential when starting a project.
In this article, I will explore what I added and why I chose that specific tool, talking specifically from the perspective of a single-person side project.
What I wanted to achieve with my first commits
There is a kind of inertia to the code design: new code tends to be similar to existing code.
Here is a better quote from Edmond Lau book called “The Effective Engineer” that shows the positive side of this inertia:
The code quality also self-propagates; new engineers model their own code based on the excellent code that they see, creating a positive feedback loop.
When working alone on your project, you can replace “new engineer” with “you in the future,” the quote will still be relevant.
My initial commits aimed to set up automated checks for code quality and security. I want to help my future self be fast and write quality code.
Side projects developed while having a full-time job have a unique characteristic worth noting. The time dedicated to working on the side project is not continuous. For instance, you may work on it for 1-2 hours on Saturday, and the next opportunity to work on it may only arise a week later.
It is then essential to make the code quality built-in and use as much automation as possible.
First commits
My first commits were about:
adding Rubocop to correct and enforce coding styles automatically
check code quality with Rubycritic
security audits with brakeman, bundler-audit and importmap audit
enable Rails strict loading
make Rails console run on sandbox mode in production
Add Rubocop
I have a Rubocop configuration that I like to use in my side projects.
The first commit sets up Rubocop and adds my configuration to the .rubocop.yml
file.
Why add Rubocop
I have a lot of thoughts about what cop I want enabled and what cop disabled, and sometimes I change my mind about some of them, but for this project, consistency is more important than a specific cop.
It is essential to have a consistent style while coding. I also like to have the editor auto-format my code, so I chose Rubocop because I want to use the new Ruby syntax, and thus, I can switch on/off rules that will impede this.
What I added to the commit
1. Rubocop gems
I am using the following Rubocop gems:
rubocop-rails - “Automatic Rails code style checking tool”
rubocop-performance - “A collection of RuboCop cops to check for performance optimizations in Ruby code”
rubocop-minitest - “Automatic Minitest code style checking tool”
rubocop-capybara - “Code style checking for Capybara test files (RSpec, Cucumber, Minitest)”
rubocop-rubycw - “Integrate RuboCop and ruby -cw. You can get Ruby's warning as a RuboCop offense by rubocop-rubycw”
2. Apply Rubocop autocorrection to files generated by Rails generate
I want to automatically run Rubocop with autocorrect on any .rb
file generated by the Rails generate command. This will ensure that any gem that generates Ruby files or any I generate will have the style automatically corrected.
I added the following code to the config/application.rb
to do this:
config.generators.after_generate do |files|
parsable_files = files.filter { |file| file.end_with?(".rb") }
unless parsable_files.empty?
system("bundle exec rubocop -A --fail-level=E #{parsable_files.shelljoin}", exception: true)
end
end
I took this code from Rubocop Rails Configuration tip and you can see it added to the current Rails master
And I made Rubocop run on Github CI with by adding the following content to the file under .github/workflows/rubocop.yml
:
name: Rubocop Linter
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
bundler-cache: true
- name: Install dependencies
run: bundle install
- name: Checking Rubocop Styles
run: bundle exec rubocop
This will run Rubocop on the following two cases:
When code is pushed to the main branch.
When a pull request is made to the main branch.
Code Quality with Rubycritic
The third commit adds Rubycritic as a code quality static analysis.
Why add Rubycritic
I added this because I want to ensure I create simple code. And while Rubycritic does not guarantee the code will be simple, it can help maintainability. I see maintainability as an essential metric in the context of a side project.
The commit
I don’t want to run Rubycritic in CI mostly because I think about having little time and trying to be fast with feature releases, and sometimes refactoring to remove a code smell can take quite some time.
But I do want to run this locally, so I just created a file under bin/quality_check
with the following content:
#!/bin/bash -e
bundle exec rubycritic
Why a file and not run directly rubycritic
? I’d like to add more tools like this here or configure Rubycritic in a specific way and having a bash script allows me this flexibility.
Rubycritic uses reek under the hood so I added a reek config files at .reek.yml
with the following content:
---
detectors:
IrresponsibleModule:
enabled: false
LongParameterList:
enabled: true
exclude: []
max_params: 4
overrides:
initialize:
max_params: 5
DataClump:
max_copies: 3
min_clump_size: 3
### Excluding directories
exclude_paths:
- test/
- config/
I mostly disabled IrresponsibleModule
because I don’t want to add a description to all classes/modules I create.
I also allowed a maximum of 4 parameters for methods and 5 for the initializers. I also allowed a maximum of 3 methods in an object to have the same parameters, and the check happens only from 3 parameters up. I can rationalize this decision, but it is mainly based on my experience with extracting objects from a long list of parameters and finding a balance when to make this effort.
Security checks on CI
The fourth commit is concerned with adding security checks to the CI.
Why add security checks
I want to run on CI a couple of security audits that I grouped under the name of Static Analysis
:
Brakeman - “Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis”
Bundle audit - “Patch-level verification for Bundler”
Importmap audit - “checks the NPM registry for known security issues”
The commit
Here is the CI config for Github to run all these tools in .github/workflows/static_analysis.yml
:
name: Static Analysis
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
static_analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
bundler-cache: true
- name: Install Bundler
run: gem install bundler
- name: Install dependencies
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Run bundler-integrity check
run: bundle exec bundler-integrity
- name: Run Brakeman
run: bundle exec brakeman
- name: Run Bundle Audit
run: bundle exec bundle-audit check --update
- name: Run Importmap audit
run: bundle exec bin/importmap audit
Enable Rails strict loading
Here is what strict_loading
does (source):
Add #strict to any record to prevent lazy loading of associations. strict will cascade down from the parent record to all the associations to help you catch any places where you may want to use preload instead of lazy loading.
Why enable strict loading
This is an excellent way to prevent N+1 and ensure that any needed association is included or preloaded.
The commit
I enabled strict_loading
in all environments:
# config/environments/development.rb
config.active_record.strict_loading_by_default = true
# config/environments/test.rb
config.active_record.strict_loading_by_default = true
# config/environments/production.rb
config.active_record.strict_loading_by_default = true
Make Rails console run on sandbox mode in production
Why make console run in sandbox mode
It is a good practice to run the rails console in production in the sandbox box to ensure any change is intentional.
I use rails console
quite often in production, but I want to make sure that if I want to make any changes, that should be intentional and not accidental.
The commit
This is a simple switch that I can toggle in the Rails config for production:
# config/environments/production.rb
config.sandbox_by_default = true
If this is enabled, then to make changes, you have to run rails console --no-sandbox
Conclusion
I made the first commits in this repository intentional about code quality and automatic checks.
It helps with moving fast while keeping a good code quality. A lot can be done in this area, but I tried to add the minimum that would help me.
A note about eager loading code
A previous version of this article included a section about configuring CI to run bin/rails zeitwerk:check
to check if the code loads correctly.
Xavier Noria correctly pointed out that running this in CI is unnecessary.
First, there is a config setting called config.eager_load
, and if you put that on true
in your test environment and run any test that will eagerly load your entire application. This setting exists already in Rails 6.0 (and even before 6.0).
Since Rails 7.0 the generated config/environment/test.rb
includes the following line:
config.eager_load = ENV["CI"].present?
That means if you run your tests in Github CI (or similar CIs) that sets the CI
environment variable, it will eagerly load your application.
So there is no need to execute rails zeitwerk:check
separately. Either set the config.eager_load
to true on CI, or you already have that if you generated your app with Rails >= 7.0 and run your tests on CI.
Enjoyed this article?
👐 Subscribe to my Ruby and Ruby on rails courses over email at learn.shortruby.com- effortless learning anytime, anywhere
👉 Join my Short Ruby News newsletter for weekly Ruby updates from the community and visit rubyandrails.info, a directory with learning content about Ruby.
🤝 Let's connect on Ruby.social or Linkedin or Twitter where I post mainly about Ruby and Rails.
🎥 Follow me on my YouTube channel for short videos about Ruby