Continuous Integration (CI) is the process of regularly merging code into a shared repository and then validating that code via an automated build. CI is a key part of Instrument’s toolkit, helping us guarantee quality and stability across projects and teams.
Thoughtworks has this to say about Continuous Integration:
Integrate at least daily
Continuous Integration (CI) is a development practice that requires developers to integrate code into a shared repository several times a day. Each check-in is then verified by an automated build, allowing teams to detect problems early.
By integrating regularly, you can detect errors quickly, and locate them more easily.
Our CI Process
Our primary CI tool is CircleCI. It has a great web interface and its YAML-based configuration makes it easy to set up and adapt to new project requirements.
We hook CircleCI into Github so that when a new Pull Request is opened, CircleCI builds the project and runs any checks specified. These checks typically include:
- Linting
- Unit Tests
- Automated accessibility checks
…and anything else that this specific project needs.
If all check pass on CircleCI, we’re good to merge the PR. If checks fail, we are blocked from merging.
Example CI File
Most CI files can be fairly basic but you can accomplish a great deal through the automation of CI. This is an example CircleCI configuration file showing the build and deploy process for a Google App Engine hosted project. Some interesting details of this process:
- Both Node.js and Python dependencies are cached upon installation, using a checksum of the relevant package list file. Subsequent builds will use the cached dependencies unless the package file changes.
- This project features multiple time-based phases. To support the QA process, when the Circle build spots that it is building the qa branch, it first does a normal build, but then fans out to deploy 6 different versions of the site… one locked to each phase. This makes it possible to review and test the content that would normally only be visible during certain time ranges.
- Production deploys are automatically kicked off when a tag following a certain pattern is pushed (the script called during this process further checks that the tag was created by an approved deployer)
version: 2
jobs:
# build_and_test_website job confirms all tests and linting passes,
# and outputs a ready to deploy build to the workspace
build_and_test_website:
docker:
- image: circleci/python:2.7.15-node-browsers
working_directory: ~/repo
steps:
- checkout
# Attempt to restore Python dependencies from the cache
- restore_cache:
name: Restore Python Dependency Cache
keys:
- v1-pydep-{{ checksum "requirements_build.txt" }}-{{ checksum "requirements.txt" }}
# fallback to using the latest cache if no exact match is found
- v1-pydep-
# Install Python dependencies
- run:
name: Install Python Build Dependencies
command: |
mkdir -p ./venv
virtualenv ./venv
. venv/bin/activate
bin/install-python-deps
# Cache Python dependencies
- save_cache:
name: Save Python Dependency Cache
paths:
- third_party/py
- ./venv
key: v1-pydep-{{ checksum "requirements_build.txt" }}-{{ checksum "requirements.txt" }}
# Attempt to restore Node dependencies from the cache
- restore_cache:
name: Restore Node.js Dependency Cache
keys:
- v1-nodedep-{{ checksum "package.json" }}-{{ checksum "appengine/website/package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-nodedep-
# Install Node dependencies
- run:
name: Install Node.js Dependencies
command: |
bin/install-node-deps
# Cache Node dependencies
- save_cache:
name: Save Node.js Dependency Cache
paths:
- node_modules
- appengine/website/node_modules
key: v1-nodedep-{{ checksum "package.json" }}-{{ checksum "appengine/website/package.json" }}
# Lint the codebase; break the build on errors
- run:
name: Run Website Linters
command: |
. venv/bin/activate
./bin/lint
# Install App Engine dependencies
- run:
name: Install App Engine Dependencies
command: |
curl -o $HOME/google_appengine_1.9.73.zip https://storage.googleapis.com/appengine-sdks/featured/google_appengine_1.9.73.zip
unzip -q -d $HOME $HOME/google_appengine_1.9.73.zip
rm $HOME/google_appengine_1.9.73.zip
# Write client secrets file; if we are deploying a tag, use environment
# variable that contains the production version
- run:
name: Write Client Secrets File
command: |
if [[ -n "$CIRCLE_TAG" ]]; then
echo "Using production version of client secrets!"
OAUTH_CLIENT_SECRETS_ENCODED=$PROD_OAUTH_CLIENT_SECRETS_ENCODED
fi
echo $OAUTH_CLIENT_SECRETS_ENCODED | base64 --decode > appengine/website/client_secrets.json
- run:
name: Install Grunt
command: |
sudo npm install grunt-cli -g
# Run the unit tests; break the build on failures
- run:
name: Run Tests
command: |
. venv/bin/activate
GAE=$HOME/google_appengine
export GAE
./bin/tests
# Build the codebase to prepare for deploy
- run:
name: Build Application
command: |
. venv/bin/activate
export GAE=$HOME/google_appengine
bin/build-ci
# Persist the built codebase to a workspace to share with later steps
- persist_to_workspace:
root: ./
paths:
- out
- bin/deploy-ci
# deploy_website job takes the build artifacts from the workspace, and
# deploys to the App Engine project.
deploy_website: &deploy
docker:
- image: google/cloud-sdk:latest
steps:
- attach_workspace:
at: ./
- run:
name: Login to GCP
command: |
if [[ -n "$CIRCLE_TAG" ]]; then
echo "Using production deploy credentials!"
GCLOUD_SERVICE_KEY=$PROD_GCLOUD_SERVICE_KEY
GCLOUD_PROJECT=$PROD_GCLOUD_PROJECT
fi
echo $GCLOUD_SERVICE_KEY | base64 --decode > ${HOME}/gcloud-service-key.json
gcloud auth activate-service-account --key-file ${HOME}/gcloud-service-key.json
gcloud --quiet config set project $GCLOUD_PROJECT
- run:
name: Deploy to GAE
command: |
./bin/deploy-ci
# The following jobs are variants on the normal deploy_website job.
# They deploy the site locked to a specific phase, to support easy QA of the
# different phases of the project.
deploy_phase1:
<<: *deploy
environment:
DEPLOY_PHASE: phase1
deploy_phase2:
<<: *deploy
environment:
DEPLOY_PHASE: phase2
deploy_phase3:
<<: *deploy
environment:
DEPLOY_PHASE: phase3
deploy_phase4:
<<: *deploy
environment:
DEPLOY_PHASE: phase4
deploy_phase5:
<<: *deploy
environment:
DEPLOY_PHASE: phase5
deploy_phase6:
<<: *deploy
environment:
DEPLOY_PHASE: phase6
workflows:
version: 2
build_all:
jobs:
# Build tag runs for all branch commits, and also for tags matching our
# versioning scheme. Example tags: v2018.1.3; v2018.2.4rc1
- build_and_test_website:
filters:
tags:
only: /^v20\d{2}[.]\d+[.]\d+([A-Za-z0-9]*)$/
# The regular deploy task runs for all branches except qa, and also for
# tags matching our versioning scheme.
- deploy_website:
requires:
- build_and_test_website
filters:
tags:
only: /^v20\d{2}[.]\d+[.]\d+([A-Za-z0-9]*)$/
branches:
only: /^(?!qa).*$/
# These subsequent deploy tasks fan out to deploy phase-locked versions
# of the site for the qa environment
- deploy_phase1:
requires:
- build_and_test_website
filters:
branches:
only: qa
- deploy_phase2:
requires:
- build_and_test_website
filters:
branches:
only: qa
- deploy_phase3:
requires:
- build_and_test_website
filters:
branches:
only: qa
- deploy_phase4:
requires:
- build_and_test_website
filters:
branches:
only: qa
- deploy_phase5:
requires:
- build_and_test_website
filters:
branches:
only: qa
- deploy_phase6:
requires:
- build_and_test_website
filters:
branches:
only: qa