CircleCI 2.1 設定サンプル（Rails + Rspec + Rubocop + Jest + ESLint + Yarn + Postgres）

# 環境

- Circle CI 2.1
- Rails 5 + PostgresSQL 9.6 + Yarn
  - 主にCircle CIの設定のため、上記環境には大きく依存していません。

# やっていること

- テストの実行（`rspec`、`jest`）
  - [simplecov](https://github.com/colszowka/simplecov) を使って、rspec 実行結果の code coverage を CircleCI 上の`ARTIFACTS`に残しています。
- Linterの実行（`rubocop`、`eslint`）
- セキュリティチェック（`brakeman`） 
- デプロイ
  - `develop`や`master`ブランチへマージされた時に、`capistrano`を使って各staging、production環境にデプロイ。
  - 結果を`slack`に通知。
  - `capistrano`の設定ファイル（`config/deploy/staging.rb`等）で、`set :branch, ENV['BRANCH'] || 'develop'`等として、ブランチの設定がされている必要があります。
- 上記に付随する処理（ソースコード、ライブラリのインストール及びキャッシング）

# 出来ていないこと

- `fingerprint`を環境変数で設定すべき

# 設定例

```yaml:.circleci/config.yml
version: 2.1

orbs:
  slack: circleci/slack@3.3.0

executors:
  default:
    working_directory: ~/repo # default: ~/project
    docker:
      - image: circleci/ruby:2.6.3-node
        environment:
          # BUNDLE_PATH=/usr/local/bundle is set in the original docker image. Should be overwritten.
          # https://hub.docker.com/layers/circleci/ruby/2.6.3-node/images/sha256-371f81f654d9a2d5e3b71bbf3616ac076686358838bc123d27ba35ff6d8df321?context=explore
          BUNDLE_PATH: vendor/bundle
          RAILS_ENV: test
  extended:
    working_directory: ~/repo
    docker:
      - image: circleci/ruby:2.6.3-node
        environment:
          BUNDLE_PATH: vendor/bundle
          RAILS_ENV: test
          PGHOST: 127.0.0.1
          PGUSER: root
      - image: circleci/postgres:9.6.2-alpine
        environment:
          POSTGRES_USER: root
          POSTGRES_DB: circle-test

commands:
  restore_source_code:
    steps:
      - restore_cache:
          name: Restore source code cache
          keys:
            - v1-source-code-{{ .Environment.CIRCLE_SHA1 }}
            - v1-source-code-
  save_source_code:
    steps:
      - save_cache:
          key: v1-source-code-{{ .Environment.CIRCLE_SHA1 }}
          paths:
            - ~/repo
  restore_node_dependencies:
    steps:
      - restore_cache:
          name: Restore node dependencies cache
          keys:
            - v1-node-dependencies-{{ checksum "yarn.lock" }}
            - v1-node-dependencies-
  install_node_dependencies:
    steps:
      - run:
          name: Install node dependencies
          command: yarn install --frozen-lockfile
  save_node_dependencies:
    steps:
      - save_cache:
          name: Save node dependencies cache
          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
          paths:
            - ~/repo/node_modules
  restore_bundle_dependencies:
    steps:
      - restore_cache:
          name: Restore bundle dependencies cache
          keys:
            - v1-bundle-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-bundle-dependencies-
  install_bundler:
    steps:
      - run:
          name: Install bundler
          command: |
            pwd # debug

            BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")
            echo $BUNDLER_VERSION # debug
            gem install bundler:$BUNDLER_VERSION
  install_bundle_dependencies:
    steps:
      - run:
          name: Install bundle dependencies
          command: |
            pwd # debug

            bundle install --jobs=4 --retry=3
  save_bundle_dependencies:
    steps:
      - save_cache:
          key: v1-bundle-dependencies-{{ checksum "Gemfile.lock" }}
          paths:
            - ~/repo/vendor/bundle
  wait_for_db_start_up:
    steps:
      - run:
          name: Wait for db start up
          command: dockerize -wait tcp://127.0.0.1:5432 -timeout 1m
  set_up_database:
    steps:
      - run:
          name: Set up database
          command: |
            pwd # debug
            echo $(cat Gemfile.lock | tail -1 | tr -d " ") # debug

            bin/rails db:create --trace
            bin/rails db:migrate --trace
  run_rubocop:
    steps:
      - run:
          name: Run rubocop
          command: bundle exec rubocop
  run_brakeman:
    steps:
      - run:
          name: Run Brakeman
          command: bundle exec brakeman -A -w1
  run_rspec:
    steps:
      - run:
          name: Run rspec
          command: bundle exec rspec --format progress --format RspecJunitFormatter -o test_results/rspec.xml
  run_eslint:
    steps:
      - run:
          name: Run eslint
          command: |
            pwd # debug

            yarn lint --format junit -o test_results/eslint.xml
  run_jest:
    steps:
      - run:
          name: Run jest
          command: yarn test --maxWorkers=2 --testResultsProcessor="jest-junit"
          environment:
            JEST_JUNIT_OUTPUT: ~/test_results/jest.xml

jobs:
  checkout_code:
    executor: default
    steps:
      - checkout
      - save_source_code
  node_dependencies:
    executor: default
    steps:
      - restore_source_code
      - restore_node_dependencies
      - install_node_dependencies
      - save_node_dependencies
  bundle_dependencies:
    executor: default
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - install_bundle_dependencies
      - save_bundle_dependencies
  rubocop_job:
    executor: default
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - run_rubocop
  brakeman_job:
    executor: default
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - run_brakeman
  eslint_job:
    executor: default
    steps:
      - restore_source_code
      - restore_node_dependencies
      - run_eslint
  jest_job:
    executor: default
    steps:
      - restore_source_code
      - restore_node_dependencies
      - run_jest
  rspec_job:
    executor: extended
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - wait_for_db_start_up
      - set_up_database
      - run:
          name: Copy application.yml.sample
          command: cp -p config/application.yml.sample config/application.yml
      - run_rspec
      # see: https://circleci.com/docs/2.0/code-coverage/
      # You can check the code coverage at coverage/index.html on ARTIFACTS of this job on CircleCI.
      - store_artifacts:
          path: coverage
  deploy:
    executor: default
    parameters:
      env:
        type: enum
        enum: ['staging', 'production']
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - add_ssh_keys:
          fingerprints: <your_fingerprint_comes_here>
      - run:
          name: Deploy to << parameters.env >>
          command: bundle exec cap << parameters.env >> deploy
      - slack/status:
          mentions: channel

workflows:
  build_test_and_deploy:
    jobs:
      - checkout_code
      - node_dependencies:
          requires:
            - checkout_code
      - bundle_dependencies:
          requires:
            - checkout_code
      - rubocop_job:
          requires:
            - bundle_dependencies
      - brakeman_job:
          requires:
            - bundle_dependencies
      - eslint_job:
          requires:
            - node_dependencies
      - jest_job:
          requires:
            - node_dependencies
      - rspec_job:
          requires:
            - bundle_dependencies
      - deploy_to_staging:
          name: Deploy to staging
          env: staging
          requires:
            - rubocop_job
            - brakeman_job
            - eslint_job
            - jest_job
            - rspec_job
          filters:
            branches:
              only: develop
      - deploy_to_production:
          name: Deploy to production
          env: production
          requires:
            - rubocop_job
            - brakeman_job            - 
            - eslint_job
            - jest_job
            - rspec_job
          filters:
            branches:
              only: master
```

# ポイント

- [こちらをどうぞ](https://qiita.com/luigiii/items/6a6b7e61c90deabf4c5c#%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88)
- ソースコードもキャッシュしていますが、常にキャッシュした方が速いとは限りません。
  > とはいえ、ソースコードのキャッシュの有無によってビルド速度が向上するかどうかは検証した方が良い場合もあります。例えば restore_cache するより git clone を実行する方が高速な場合も多々あります。
  > https://circleci.com/docs/ja/2.0/caching/#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5

- ベースのDockerイメージ（`circleci/ruby:2.6.3-node`）で、`BUNDLE_PATH`が設定されているので、`BUNDLE_PATH: vendor/bundle`として上書きしています。
- `gem install bundler`時に、`Gemfile.lock`で使われている`bundler`のバージョンを指定してインストール方法として、[ここ]( https://discuss.circleci.com/t/using-bundler-2-0-during-ci-fails/27411/3)を参考にして、以下の方法を試しましたが、何故か最新のものがインストールされてしまったので、`gem install bundler:$BUNDLER_VERSION`としています。

  ```
  echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
  source $BASH_ENV
  gem install bundler
  ```

# 参照先

- [こちらをどうぞ](https://qiita.com/luigiii/items/6a6b7e61c90deabf4c5c#%E5%8F%82%E7%85%A7%E5%85%88)