Continuous Integration

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:

…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