Configuring Circle CI 2.0 for a Real Rails Application

By ozzyaaron

Tuesday, Aug 7, 2018

To all you developers looking for a quick guide to getting a real Rails App running on Circle CI, feel free to rip and modify this as needed.

For others that are looking to find out about some unusual aspects of Circle’s new 2.0 infrastructure that we’ve had to contend with, please read on after this giant code block!

version: 2
jobs:
  build:
    parallelism: 8
    working_directory: ~/path/to/your/app
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
          CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
          RAILS_ENV: test
          YARN_VERSION: 1.7.0
          PGHOST: 127.0.0.1
          PGUSER: root

      - image: circleci/postgres:10.3-alpine-ram
        environment:
          POSTGRES_USER: root
          POSTGRES_DB: circle-test_test
    steps:
      - checkout
      - restore_cache:
          name: Restore Bundler cache
          keys:
            - iex-app-{{ checksum "Gemfile.lock" }}
            - iex-app-

      - run:
          name: Install NVM
          command: |
            set +e
            touch $BASH_ENV
            curl --retry 10 --retry-max-time 30 -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
            export NVM_DIR="$HOME/.nvm"
            [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
            nvm install 10.6.0

            echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
            echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
            echo 'nvm alias default 10.6.0' >> $BASH_ENV

      - restore_cache:
          name: Restore Yarn Binary
          keys:
            - iex-app-yarn-{{ .Environment.YARN_VERSION }}
            - iex-app-yarn-

      - run:
          name: Prepare Yarn Path
          command: echo 'export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"' >> $BASH_ENV

      - run:
          name: Install Yarn
          command: |
            if [[ ! -e ~/.yarn/bin/yarn || $(yarn --version) != "${YARN_VERSION}" ]]; then
              echo "Download and install Yarn."
              echo "Current Yarn at `which yarn`"
              echo "Current yarn version `yarn --version`"
              echo "Current YARN_VERSION `echo $YARN_VERSION`"
              curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
              curl --retry 10 --retry-max-time 30 -o- -L https://yarnpkg.com/install.sh | bash -s -- --version $YARN_VERSION
            else
              echo "The correct version of Yarn is already installed."
            fi

      - save_cache:
          name: Cache Yarn Binary
          key: iex-app-yarn-{{ .Environment.YARN_VERSION }}
          paths:
            - ~/.yarn

      - restore_cache:
          name: Restore Yarn Package Cache
          keys:
            - yarn-packages-{{ checksum "yarn.lock" }}

      - run:
          name: Install node packages & build assets
          command: yarn -v && yarn install && yarn heroku-postbuild

      - save_cache:
          name: Save Yarn Package Cache
          key: yarn-packages-{{ checksum "yarn.lock" }}
          paths:
            - node_modules/

      - run:
          name: APT Installs (QT, PDFtk, psql-client)
          command: |
            sudo apt-get update
            sudo apt-get install -y software-properties-common
            sudo apt install -y gcc g++ make qt5-default libqt5webkit5-dev ruby-dev zlib1g-dev
            sudo apt-get install pdftk
            sudo apt install postgresql-client

      - run:
          name: Bundle Install
          command: bundle install --path=vendor/bundle --jobs 4 --retry 3 --without development database_management
          no_output_timeout: 30m

      # Store bundle cache
      - save_cache:
          key: iex-app-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle
            
      - run:
          name: Wait for DB
          command: dockerize -wait tcp://localhost:5432 -timeout 1m

      - run:
          name: Database Setup
          command: |
            bundle exec rake db:create
            bundle exec rake db:structure:load

      - run:
          name: Create Artifacts Directory
          command: |
            mkdir tmp/artifacts
            echo 'export CI_ARTIFACTS="tmp/artifacts"' >> $BASH_ENV

      - run: mkdir ~/rspec

      - run:
          name: Run Specs
          command: |
            TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
            bundle exec rspec --format progress --format RspecJunitFormatter -o ~/rspec/rspec.xml --tag ~wip -- ${TESTFILES}

      - store_test_results:
          path: ~/rspec

      - store_artifacts:
          path: ~/rspec

      - run: mkdir -p ~/cucumber

      - run:
          name: Run Cukes without Failing
          command: |
            TESTFILES=$(circleci tests glob "features/**/*.feature" | circleci tests split --split-by=timings)
            DONT_FAIL=true bundle exec cucumber --tags 'not @wip' --format rerun --out rerun.txt --format pretty --format json --out ~/cucumber/tests.cucumber ${TESTFILES}

      - run:
          name: Run Cukes that Failed
          command: |
            if [ -f rerun.txt ]; then
              bundle exec cucumber @rerun.txt
            fi

      - store_test_results:
          path: ~/cucumber

      - store_artifacts:
          path: ~/cucumber

      - store_artifacts:
          path: tmp/artifacts

Some odd things you might notice:

  • concatenating output to $BASH_ENV
  • caching yarn’s binary folder
  • we have to tell Circle to wait a half hour for a bundle install

Outside of this a lot of things are pretty standard, including install nvm, building assets, running tests and so on.

Concatenating to $BASH_ENV

At first you might expect commands under the 2.0 infrastructure to be a series of commands that are run one after another. It appears that they’re actually a series of isolated commands where the ENV is reloaded each time. When you output to $BASH_ENV, you’re essentially adding setup that is to be done by each subsequent command.

For this reason, we export yarn’s path prior to checking yarn’s version, as well we setup NVM so that for other commands NVM is setup properly. It isn’t enough to export inside that command and continue, as subsequent commands will no longer have what you have exported.

Caching yarn’s Binary Folder

One new issue we ran into was that yarn would quite often fail to install when we used the suggested mechanism of piping the output of a web request to bash. We tried a number of potential fixes, such as curl’s retry, but the final step that truly isolated our tests from a failure of yarn to install was to cache the installed files.

It appears many people were having this problem under Circle 2.0 but I couldn’t find a posted solution but this has worked well for us.

Interestingly, even though we were installing a new yarn version under Circle’s 1.0 infrastructure, we never had this issue.

Why Do We Need to Wait 30 Minutes for Bundler?!

I don’t know. I had a support ticket open with Circle about this but they had no answer for me. They did tell me that the new images under the 2.0 infrastructure allocate half the memory of the 1.0 images so it could simply be the increase in swapping.

Locally a fresh installation is done in under a minute, under Circle CI 1.0 it was 2-3 minutes.

The guidance I was given was to just add that timeout configuration option. There was no other solution.

The culprit in most cases appeared to be compiling native extensions such as are needed by capybara-webkit or nokogiri, both of which are virtually ubiquitous libraries when discussing Rails.

Other Notables

You might also notice that we’re using an in-memory Postgres instance. This is just a docker image that Circle kindly provides. It makes setup in this system much easier.

We install some apt packages such as pdftk so that can test PDF creation.

We install QT so that we can test using capybara-webkit.

We have a retry on our Cucumber features such that only failed tests will be re-run. These failures show up in the test summary, as you would expect.

Outside of this our configuration is quite busy, but from what I’ve seen not entirely unusual. The busyness is just an artifact of Circle’s 2.0 infrastructure requiring more configuration and setup by its users than under their 1.0 offering.

Next Steps

One thing that might be apparent to many is the amount of duplicate setup that is done when these tests are run in parallel.

Circle now offers workflows and a feature that allows you to share data between jobs in those workflows. In the future, this should allow us to have a job to install dependencies and compile assets which can then be distributed to each of the parallel test jobs.

I attempted to get this going, but at the time the documentation did not match reality and I was told by Circle’s support that the method the documentation used to try and achieve something like we wanted did not work. There is a new feature that appeared less documented at the time that might. This is something I’ll look at in the future.

Summary

In my opinion, Circle 1.0 was actually quite nice as if you had a normal Rails App; very little to no configuration was required. With Circle CI 2.0, it appears that by providing a more open platform, it has made the setup of CI for fairly normal applications significantly harder. In many cases the provided documentation was nowhere even close to what was required and our setup is nothing unusual for most Rails applications I’ve worked on or consulted to.

As an aside, we also build our website and an old PHP app on Circle and the same can be said for them. Very easy to setup under 1.0 but quite a bit more difficult under 2.0.

In the end I’m only writing this article because I hope it helps someone! I don’t want this to read too much like a go at Circle as for the most part we’re able to offload a lot of work we don’t want to do to them - maintaining CI servers, integrating with slack, integrating with Github and so on. I will say this process was a lot harder than getting setup under their previous infrastructure and other CI systems I had used previously. I realise that if you want to increase your customer base in this space your hands are somewhat tied - it still didn’t make the experience any nicer as a customer.

We were able to get our builds running within a day or two, the reason it took so long is mostly due to having to read documentation and understand the new system. Core components such as yarn and nvm required output to $BASH_ENV which was something I only found whilst trawling discussion forums.

After this first win it took us a few weeks of bug fixing intermittent issues to get to something we all consider pretty stable. These little fixes are the main things I hope this article can help with.

I hope this helps someone!