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!
Top comments (4)
About yarn, I never got any problems with this:
You don't really need to have the very latest version of yarn.
Your yarn.lock file is usually enough to handle the dependencies, etc.
One thing that I was hoping to find here are post-build notifications.
Ideally, after a deploy step... Last time I checked the docs, it was not possible on the version 2 API
Hi there,
Thanks for this, it looks like you're using the webpacker gem to install yarn here so that might introduce some differences. Perhaps webpacker shields you from temporary issues when installing yarn from their install script? It could also be that your parallelism is lower than ours as we would typically see most containers work but sometimes 1-4 of them might not install yarn correctly.
The problem we were having is actually installing yarn, not caching the dependencies post install.
You're right in that you don't typically need an up-to-date yarn BUT without caching yarn's executables we would have seen the same issue of it sometimes not installing.
We are not using webpacker (yet) so I'm unsure if the protections you're receiving installing yarn are a lower parallelism or webpacker itself providing some protections (which would totally make sense).
Please let me know if I'm missing something as I could just update our config and the articles for future humans :)
Using webpacker indeed but the gem itself does not manage the installation of yarn (its actually a prerequisite: github.com/rails/webpacker#prerequ...)
The core difference I wanted to point out is this
I cannot explain why this works, but yeah, installs yarn without issues.
Also, yeah, parallelism is 1 and docker image is "circleci/ruby:2.5-node"
I'd be interested in trying this in the future though I really, really think that it would not fix the problem we had whereby when yarn is installed from a remote script the remote script would return invalid content or a 4x/5x error.
This dependencies key was part of Circle 1.0 and we actually used it there, but under Circle 2.0 the dependencies key is not documented as far as I can see (circleci.com/docs/2.0/configuratio...).
The image you're using will have yarn pre-installed (circleci.com/docs/2.0/yarn/) and because you're not checking its version I could see that your configuration might not even install yarn but use the image's pre-installed version which would circumvent completely the issue we had to fix.
My thoughts on why you would not have experienced the issue we did was because you're not installing yarn but using the image's version.