Github Actions for Ruby on Rails CI

Owen Tran
5 min readJul 7, 2020

--

As a bootstrapped developer, you probably don’t have the cash (yet) for any whiz-bang commercial products or time/patience to configure Jenkins. If you’re starting with Rails and using Github as your code repository, you’re in luck with Github actions.

Actions is Github partial answer to Gitlab’s CI/CD. First off, CI stands for Continuous Integration that allows you to automagically test your code as soon as you commit it. This is great because sometimes (okay, a lot of times) you may not run your tests because you’re cranking out the next great feature for MVP. Never fear, machines aren’t as fickle as humans and will dutiful run tests AND avoid deploying the code if anything fails. Consider it your Goose… “I’ll be your wingman anytime”.

Now that you know why you need it, let’s use all the cool free stuff out there to make it happen for a Ruby on Rails app. Unsurprisingly, to follow these instructions you will need (1) a free Github account, (2) a Ruby on Rails app, and (3) a simple test, preferably a model test that requires the database (in our example we use Postgresql, because it rocks).

Github action workflow file

Click on Actions and start a new flow. You can setup a Ruby flow which is pretty decent start.

You’ll get placed into the Github editor (pretty slick for online editing) and you can start editing it. We can start with the branch. By default it’s set to “master” (NOTE: Github is working on to update this main). This means for any push or pull to the “master” branch the job will run.

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

Setting the version of Ruby

The default is 2.6 but let’s do the latest and greatest version with 2.7.1 (as of this writing).

- name: Set up Ruby
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
# change this to (see https://github.com/ruby/setup-ruby#versioning):
# uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
with:
ruby-version: 2.7.1

Caching gems

If you use sass or nokogiri or any of the native C gems, you can find yourself waiting 4–5 minutes for all your gems to compile and install. Normally this is great since you get a clean build every time, but I’m just not that patient for a clean room type experience. Github has a cache feature (https://github.com/actions/cache) that lets you store dependencies between builds. Here’s the snippet to get your gems cached:

- name: Cache Ruby Gems
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-

- name: Bundle Install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3

I also parallelized the bundle install with 4 jobs, because I’m not a very patient developer and it’s all FREE. NOTE: the gems will only be cached on a successful build, so if you messing around with failing tests or other parts of the file, it will have to bundle fresh every time until you have a clean build.

Testing with a database

Ok, here’s the tricky part that took me a few hours to figure out. The Github documentation almost works (https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-postgresql-service-containers), but goes on and on about containers, VM’s, and other virtualization hocus pocus.

Here’s what worked for me, first set up the postgresql service (this is for version 12) and also be sure to include the options that check to make sure the service is up and running.

services:
postgres:
image: postgres:12
ports: ["5432:5432"]
env:
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

Next, after ruby is installed, install the pg client libraries. I’m not sure if this is necessary, but I consider it good luck.

- name: Install PostgreSQL 12 client
run: |
sudo apt-get -yqq install libpq-dev

Then change the running of the test to pass all the PostgreSQL configuration as ENV variables to rails.

- name: Run Tests
env:
PG_DATABASE: postgres
PG_HOST: localhost
PG_USER: postgres
PG_PASSWORD: password
RAILS_ENV: test
WITH_COVERAGE: true
DISABLE_SPRING: 1
run: |
bin/rails db:setup
bundle exec rake test

In your rails app, you need to make changes to config/database.yml to take ENV vars if specified. You should be able to still run rake test locally and pass all tests.

test:
<<: *default
database: <%= ENV.fetch('PG_DATABASE', 'myapp_test') %>
host: <%= ENV.fetch('PG_HOST', 'localhost') %>
username: <%= ENV.fetch('PG_USER', nil) %>
password: <%= ENV.fetch('PG_PASSWORD', nil) %>
port: <%= ENV.fetch('PG_PORT', 5432) %>

Adding code coverage

Actions also support artifacts. No, not like the Rosetta Stone or Excalibur, but more like output from code coverage reports or logs. I like simplecov and have it wired into my rake test. It spits out an index.html and associated files to a coverage directory. We can make Github upload that directory with every run with the upload artifact action (https://github.com/actions/upload-artifact).

- name: Upload Code Coverage
uses: actions/upload-artifact@v2
with:
name: code-coverage
path: coverage/

And the gravy on top

You can also make code formatting and security checks automated (which I’m sure you’ll comment out after it fails for the first time). We will use (1) rubocop for code formatting, (2) brakeman for security violations, and (3) bundle-audit for known security issues with any gems. For all this sanity goodness just add the following snippet:

- name: Formatting and Security Checks
run: |
bundle exec rubocop
bundle exec brakeman -z
gem install bundle-audit
bundle-audit update
bundle-audit

The Finale

So the final file will be located in your project repo under .github/workflows/ruby.yml. It should look like this… (note I changed the Github action name of test to build in my file).

name: Rubyon:
push:
branches: [ master]
pull_request:
branches: [ master]
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12
ports: ["5432:5432"]
env:
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
with:
ruby-version: 2.7.1
- name: Install PostgreSQL 12 client
run: |
sudo apt-get -yqq install libpq-dev
- name: Cache Ruby Gems
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-

- name: Bundle Install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Run Tests
env:
PG_DATABASE: postgres
PG_HOST: localhost
PG_USER: postgres
PG_PASSWORD: password
RAILS_ENV: test
WITH_COVERAGE: true
DISABLE_SPRING: 1
run: |
bin/rails db:setup
bundle exec rake test
- name: Upload Code Coverage
uses: actions/upload-artifact@v2
with:
name: code-coverage
path: coverage/
- name: Formatting and Security Checks
run: |
bundle exec rubocop
bundle exec brakeman -z
gem install bundle-audit
bundle-audit update
bundle-audit

You can take a peek at the Action logs by clicking on the latest build that lists the output by each named step in the workflow YAML you defined above. You can re-run jobs or use it for debugging. Best of luck to you and remember to if you break it fast, fix it faster.

--

--

Responses (1)