Continuous Integration with Rails, Heroku and Bitbucket Pipelines


What is Bitbucket Pipelines?

If your team is using Bitbucket as a code repository, Pipelines is a good fit for you. Pipeline let’s your team build, test, and deploy from Bitbucket. It is built right within Bitbucket, giving you end-to-end visibility from coding to deployment. With Bitbucket Pipelines there’s no CI server to setup, user management to configure, or repositories to synchronise. Just enable it in one click and you’re ready to go.

The Goal

My goal was to setup a CI that allows us to run the test suite on a pull request and check if the code passes all the tests. Once a pull request is merged, it deploys to the staging or production server depending on which branch the pull request is being merged on. I’ll be using a postgresql database and a redis server when running the test suite. 1

The Setup

There are four files we need to create in order to make this possible:

  • package_and_deploy.sh
  • migrate.sh
  • restart.sh
  • bitbucket-pipelines.yml

For the first three files, we will need to put them inside a /deploy-scripts in the root directory of your Rails app.

/deploy-scripts/package_and_deploy.sh:

#!/bin/bash
#
# FROM: https://bitbucket.org/rjst/heroku-deploy  
# Bash script to deploy to Heroku from Bitbucket Pipelines (or any other build system, with
# some simple modifications)
#
# This script depends on two environment variables to be set in Bitbucket Pipelines
# 1. $HEROKU_API_KEY - https://devcenter.heroku.com/articles/platform-api-quickstart
#

git archive --format=tar.gz -o deploy.tgz $BITBUCKET_COMMIT

HEROKU_VERSION=$BITBUCKET_COMMIT # BITBUCKET_COMMIT is populated automatically by Pipelines
APP_NAME=$1

echo "Deploying Heroku Version $HEROKU_VERSION"

URL_BLOB=`curl -s -n -X POST https://api.heroku.com/apps/$APP_NAME/sources \
-H 'Accept: application/vnd.heroku+json; version=3' \
-H "Authorization: Bearer $HEROKU_API_KEY"`

echo $URL_BLOB | python -c 'import sys, json; print(json.load(sys.stdin))'
PUT_URL=`echo $URL_BLOB | python -c 'import sys, json; print(json.load(sys.stdin)["source_blob"]["put_url"])'`
GET_URL=`echo $URL_BLOB | python -c 'import sys, json; print(json.load(sys.stdin)["source_blob"]["get_url"])'`

curl $PUT_URL  -X PUT -H 'Content-Type:' --data-binary @deploy.tgz

REQ_DATA="{\"source_blob\": {\"url\":\"$GET_URL\", \"version\": \"$HEROKU_VERSION\"}}"

BUILD_OUTPUT=`curl -s -n -X POST https://api.heroku.com/apps/$APP_NAME/builds \
-d "$REQ_DATA" \
-H 'Accept: application/vnd.heroku+json; version=3' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $HEROKU_API_KEY"`

/deploy-scripts/migrate.sh:

#!/bin/bash

mkdir -p tmp/

newDyno=$(curl -n -s -X POST https://api.heroku.com/apps/$1/dynos \
   -H "Accept: application/json" \
   -H "Authorization: Bearer $HEROKU_API_KEY"\
   -H 'Accept: application/vnd.heroku+json; version=3' \
   -H 'Content-Type: application/json' \
   -d '{"command": "rake db:migrate; echo \"MIGRATION COMPLETE\"", "attach": "false"}' | tee tmp/migration_command |
python -c 'import sys, json; myin=sys.stdin; print( json.load(myin)["name"] )')

cat tmp/migration_command

echo "One-Shot dyno created for migration at: $newDyno"

# create a log session so we can monitor the completion of the command
logURL=$(curl -n -s -X POST https://api.heroku.com/apps/$1/log-sessions \
  -H "Accept: application/json" \
  -H "Authorization: Bearer $HEROKU_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/vnd.heroku+json; version=3' \
  -d "{\"lines\": 100, \"dyno\": \"$newDyno\"}" | tee tmp/log_session_command | python -c 'import sys, json; myin=sys.stdin; print(json.load(myin)["logplex_url"])')

cat tmp/log_session_command

/deploy-scripts/restart.sh:

#!/bin/bash

curl -n -s -X DELETE https://api.heroku.com/apps/$1/dynos \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.heroku+json; version=3" \
  -H "Authorization: Bearer $HEROKU_API_KEY"

sleep 10

The last file should be on the root directory of your Rails app. bitbucket-pipelines.yml:

image: ruby:2.4.1

pipelines:
  branches:
    master:
        - step:
            script:
              - deploy-scripts/package_and_deploy.sh prod-leagueside
              - deploy-scripts/migrate.sh prod-app
              - deploy-scripts/restart.sh prod-app
    staging:
      - step:
          script:
            - deploy-scripts/package_and_deploy.sh dev-leagueside
            - deploy-scripts/migrate.sh dev-app
            - deploy-scripts/restart.sh dev-app
  default:
    - step:
        script:
          - apt-get update
          - apt-get install -y build-essential libpq-dev nodejs
          - gem update bundler
          - bundle install
          - mv config/database.ci.yml config/database.yml
          - mv config/secrets.ci.yml config/secrets.yml
          - bundle exec rake db:drop db:create db:migrate --trace RAILS_ENV=test
          - bundle exec rspec
        services:
          - postgres
          - redis
definitions:
   services:
     postgres:
       image: postgres
       environment:
         POSTGRES_DB: app_test
         POSTGRES_USER: test_user
         POSTGRES_PASSWORD: test_user_password
     redis:
       image: redis

Now when you PR, you should see something like this on the PR page. Pull Request Page

This tells if your PR has broken any tests in your test suite.

Make sure you also setup the environment variables for your test suit. 2


  1. The test suite in this case is RSpec. There would probably be minor changes if you use another testing framework. 

  2. Instruction to setup your environment variables in pipelines.