Leveraging Static Code Analysis in a Ruby CI Pipeline
Adding static analysis to your CI pipeline will give you the confidence to quickly iterate, add features, squash bugs, and instantly deploy changes to production.
Join the DZone community and get the full member experience.
Join For FreeContinuous integration (CI) refers to the culture and technologies that enable the continuous merging of features and bug fixes into the main branch of a codebase. Code changes are incorporated immediately after testing, rather than being bunched with other updates in a waterfall release process.
Similarly, continuous delivery (CD) refers to the automatic deployment of the changed code into the target environment, such as pre-production branches into staging, or master into production. CD picks up where CI left off, and they often go hand in hand.
Static code analysis typically falls under the CI aspect of the CI/CD pipeline. Taking the example of a small Ruby project, we'll be setting up a CI workflow to analyze code quality using static analysis in the following areas (with some accompanying examples):
- Consistency, with the widely adopted Ruby style guide.
- Layout, to evaluate unjust spacing or misaligned indentation.
- Linting, examining inadequate permissions or redundant operations.
- Security analysis, catching the use of unsafe methods.
Prerequisites
Creating a Sandbox
Let's make a new directory for our adventure today. Initialize a Git repository in the folder and check it into GitHub. We'll be using GitHub Workflows as our CI tool (more on this later).
$ mkdir proj $ cd proj/ $ touch .gitignore
Setting up Ruby and Bundler
You probably already have Ruby installed on your computer, but I prefer a fresh install of Ruby. An older download can be much older than the latest stable version, and you don't want to miss out on Ruby's newest features. It's also relatively easy to break your system by installing, removing, or updating a critical package.
Don't fret; there's a solution - RVM. I won't get into details of RVM here, but it can install and manage several versions of Ruby on a separate system while keeping the system Ruby pristine.
xxxxxxxxxx
$ rvm install 3.0.0 $ rvm use 3.0.0
Next, we set up Bundler, a fantastic package manager for Ruby. We need it to keep track of our project's dependencies. The install is extremely straightforward.
xxxxxxxxxx
$ gem install bundler $ bundle init
To ensure that our project gems remain localized to our project, we can set up Bundler to install Gems at a given path. Create a directory .bundle/
and a config
file within the directory with the following content.
With this configuration, Bundler will install all gems inside a .gems/
folder within the current project folder proj/
. Add both directories, .bundle/
and .gems/
, to your .gitignore
file so that they are not checked into VCS.
Getting Familiar With Rubocop
For analyzing the code quality in all the areas we mentioned above, we will be using Rubocop, one of the finest linters available for Ruby. Rubocop comes with an extensive collection of rules referred to as 'cops', which are organized in groups of ' departments ' based on their functionality.
To install, add this line to your Gemfile and run bundle install
.
xxxxxxxxxx
# frozen_string_literal: true source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + gem 'rubocop', '~> 1.9', require: false
To list all the offenses in any given file or directory, just pass the names as arguments to rubocop
.
Rubocop is also capable of autocorrecting most errors it reports, which is incredibly helpful. To enable autocorrection, pass the -a
flag. Passing -A
uses a more aggressive auto-correct mode, which is not advisable unless you are confident in what you are doing.
xxxxxxxxxx
$ rubocop -a <file/dir_name> # safe autocorrect, recommended $ rubocop -A <file/dir_name> # unsafe autocorrect, not recommended
You will need to prepend bundle exec
to these commands if Rubocop is not globally installed.
The Code
Now we get to the fun part – scripting in Ruby.
Examine our sample script. It takes a file name as an argument and prints said file's content to STDOUT, similar to the cat
command (hence the name).
It is formatted poorly, violates many rules from the style guide, and even has several gaping security flaws. We'll fix those soon, but let's first do a preliminary scan with Rubocop and observe the output.
Rubocop found seven offenses, six of which it can correct automatically, labeled as [Correctable]
in the above output.
Pipeline
Picking the Infrastructure
Choices abound when it comes to picking a CI/CD infrastructure provider. From Travis CI, a darling of open-source developers, to Jenkins, the tool of choice for enterprise teams who would rather self-host their customized solution, dev-ops engineers are spoiled for choice.
The simplest choice in my experience has been GitHub workflows, a GitHub-native solution. It allows you to set up entire chains of jobs, described as YAML files, that can be initiated based on specific triggers. We can use them throughout the CI/CD pipeline, from running checks on PRs before merge to deploying the code after. There are hundreds of pre-built actions (many officially maintained) that take the effort out of setting up end-to-end pipelines.
Naturally, we'll use GitHub workflows as our CI pipeline infrastructure. The goal is to have linting serve as a check on our PR's and commits. Only PR's that pass those checks would be mergeable.
Adding Lint Workflow
Let's examine how the workflow file would look. Create a new directory .github/
, create another directory within this one named workflows/
, and in this directory, create a file named lint.yml
.
The lint workflow consists of a single job. That job is fired on every push event to the master
branch and performs three steps:
actions/checkout
: Checks out the code repositoryruby/setup-ruby
:- Sets up Ruby version 3.0 and the latest compatible version of Bundler
- Uses Bundler to install all packages in the
Gemfile
run
: Runs Rubocop on the entire current working directory
We get our outcome once the job completes. In this case, it's a big red cross. The workflow execution failed because Rubocop found issues within the code. The logs reveal the same message we saw earlier, listing offenses identified by Rubocop.
We'll get there, eventually.
Fixing Problems
We want our check to pass – nobody likes failing checks. Returning to our local setup, let's use Rubocop's auto-fix feature to quickly resolve these issues. First, have Rubocop handle the automatic processes with the -a
flag.
Now, Rubocop solves most of the reported issues, including one issue introduced during the auto-fix process itself! With six of the seven problems already fixed, we've shaved off about 70% of our work with zero-effort input.
There is still one unsafe auto fix and one security vulnerability that Rubocop cannot automatically fix. We can solve these:
- The frozen string literal comment is missing. That's a reasonable thing to add to the file, so we'll let Rubocop add it using the stronger auto-fix flag
-A
.
xxxxxxxxxx
- file = open(filename) + file = File.open(filename)
Commit and push. We're green now! At this point, pat yourself on the back for a job well done.
Yay!
Staying Clean
Now that you've achieved peak code quality, we need to keep it that way. We must ensure that no PR negatively affects our codebase quality. To run the check on every incoming PR, add the pull_request
event to our lint workflow.
xxxxxxxxxx
on: on: push: branches: - master + pull_request: + branches: + - master
To test that our check is working as expected, we need to make a PR with some code that Rubocop would flag. Let's refactor the lines in our script concerned with reading the file to use a block.
xxxxxxxxxx
# cat.rb # frozen_string_literal: true filename = ARGV[0] - file = File.open(filename) - list = file.read - file.close + File.open(filename) do |file| + list = file.read + end list.each_line do |line| puts line end
Check out a new branch from master
. Commit and push to this branch and open a PR. You'll see that the checks fail; the PR cannot be merged unless overridden by an administrator.
Our check is working just fine!
Brain-teaser: Can you identify why the updated code, using a block, is being flagged by Rubocop?
Hint: Here's the Rubocop output for the PR:
xxxxxxxxxx
$ bundle exec rubocop cat.rb Inspecting 1 file W Offenses: cat.rb:7:3: W: Lint/UselessAssignment: Useless assignment to variable - list. list = file.read ^^^^ 1 file inspected, 1 offense detected
Answer: Defining list
inside the block means that it is not accessible outside the block. This makes the assignment useless, and it will lead to a bug in the subsequent use of the variable.
Lesson: Though indirectly, code analysis can sometimes help identify potential bugs!
DeepSource
We've invested considerable time and effort, and even created checks and actions to monitor code quality in our repo. But what if we don't have time to spare? We're busy developers, after all!

Consider using DeepSource. It continuously scans the code on every commit and pull request through various static code analyzers (including linters and security analyzers), and it can automatically fix some. DeepSource also has its own custom-built analyzers for most languages that are constantly improved and kept up-to-date.
It's incredibly easy to set up! You need only add a .deepsource.toml
file in your repository root for DeepSource to work. It takes much less effort, and the result is more polished than that of the several workflows set up in GitHub.
xxxxxxxxxx
version = 1 [[analyzers]] name = "ruby" enabled = true [[transformers]] name = "rubocop" enabled = true
Automate the Tedium Away

CI/CD pipelines are quintessential to the agile development workflow. The ability to add features, squash bugs, and add instant changes in production can make a significant difference. Startups live or die based on how often they iterate.
Integrating static analysis into the CI pipeline ensures that only the cleanest and most compliant code makes its way into production. For something that takes so little time to set up, consumes minimum resources, and doesn't significantly affect test/build timings, static analysis can add tremendous confidence to your build process.
Confident iterations await. 'Till next time!
Published at DZone with permission of Dhruv Bhanushali. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments