【CircleCI / Monorepo】変更されたファイルに応じてworkflowを実行する

# 環境

- CircleCI version 2.1
- [circleci/path-filtering@1.0.0](https://circleci.com/developer/orbs/orb/circleci/path-filtering)

# やりたいことの前提

- Monorepo
  - 具体的には、以下のようなディレクトリがレポジトリ配下に存在し、ファイルの変更されたPATHに応じて、必要なworkflowのみを実行する。
    - `frontend`
    - `backend`
- 管理のため、それぞれのcircle configも分ける。

# CircleCI の設定の前提

[ここ](https://circleci.com/docs/dynamic-config/#getting-started-with-dynamic-config-in-circleci)にある通り、`Enable dynamic config using setup workflows` が `on` になっていること。

上記の設定をしても、今まで通り単一の `config.yml` で書くことは可能。
`config.yml` のトップレベルで `setup: true` と定義することにより、上記で有効にした `dynamic config using setup workflows` が有効になる。


# CircleCI Config ファイル構成

※分かりやすいように、前述の `backend`, `frontend` というディレクトリも記載している。


```text
├── .circleci
│   ├── config.yml
│   ├── _base.yml # ファイル名は任意
│   ├── packages # PATH、及びその配下のファイル名は任意
│       ├── backend_config.yml
│       ├── frontend_config.yml
│
├── backend
├── frontend
```

# 各 yml ファイル

## .circleci/_base.yml（ファイル名は任意）

```.circleci/_base.yml
version: 2.1

orbs:

parameters:

executors:

commands:

jobs:

workflows:
```

## .circleci/config.yml

`setup: true` の定義により、dynamic configuration が有効になる。

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

# this allows you to use CircleCI's dynamic configuration feature
setup: true
orbs:
  path-filtering: circleci/path-filtering@1.0.0

jobs:
  merge-config:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: "Generate yaml"
          # https://mikefarah.gitbook.io/yq/operators/reduce#merge-all-yaml-files-together
          command: |
            mkdir -p /tmp/workspace
            yq eval-all '. as $item ireduce ({}; . * $item )' .circleci/_base.yml .circleci/packages/*.yml > /tmp/workspace/merged.yml
            cat /tmp/workspace/merged.yml
      - persist_to_workspace:
          root:  /tmp/workspace
          paths:
            - merged.yml

workflows:
  generate-config:
    jobs:
      - merge-config
      - path-filtering/filter:
          requires:
            - merge-config
          pre-steps:
            - attach_workspace:
                at: /tmp/workspace
          base-revision: develop
          config-path: /tmp/workspace/merged.yml
          mapping: |
            backend/.* run_backend_jobs true
            frontend/.* run_frontend_jobs true
```

## .circleci/packages/backend_config.yml

```.circleci/packages/backend_config.yml
version: 2.1

parameters:
  run_backend_jobs:
    type: boolean
    default: false

executors:
  backend_default:
    working_directory: ~/repo # default: ~/project
    docker:
      - image: cimg/ruby:3.2.2
        environment:
          BUNDLER_VERSION: 2.4.15
          BUNDLE_APP_CONFIG: ~/repo/backend/.bundle
          RAILS_ENV: test

commands:
  restore_bundle_dependencies:
    steps:
      - restore_cache:
          name: Restore bundle dependencies cache
          keys:
            - v1-bundle-dependencies-{{ checksum "backend/Gemfile.lock" }}
            - v1-bundle-dependencies-
  install_bundler:
    steps:
      - run:
          name: Install bundler
          working_directory: backend
          command: gem install bundler -v $BUNDLER_VERSION
  set_bundle_config:
    steps:
      - run:
          name: Set bundle config
          working_directory: backend
          command: bundle config --local path 'vendor/bundle'
  install_bundle_dependencies:
    steps:
      - run:
          name: Install bundle dependencies
          working_directory: backend
          command: |
            bundle install --jobs=4 --retry=3
  save_bundle_dependencies:
    steps:
      - save_cache:
          key: v1-bundle-dependencies-{{ checksum "backend/Gemfile.lock" }}
          paths:
            - backend/vendor/bundle
  run_rubocop:
    steps:
      - run:
          name: Run rubocop
          working_directory: backend
          command: bundle exec rubocop

jobs:
  backend_setup:
    executor: backend_default
    steps:
      - checkout
      - restore_bundle_dependencies
      - install_bundler
      - set_bundle_config
      - install_bundle_dependencies
      - save_bundle_dependencies
      - persist_to_workspace:
          root: ~/repo
          paths:
            - backend
  backend_lint_rubocop:
    executor: backend_default
    steps:
      - attach_workspace:
          at: ~/repo
      - restore_bundle_dependencies
      - install_bundler
      - run_rubocop

workflows:
  backend:
    when: << pipeline.parameters.run_backend_jobs >>
    jobs:
      - backend_setup
      - backend_lint_rubocop:
          requires:
            - backend_setup
```


## .circleci/packages/frontend_config.yml

```yml:.circleci/packages/frontend_config
version: 2.1

parameters:
  run_frontend_jobs:
    type: boolean
    default: false

executors:
  frontend_default:
    working_directory: ~/repo # default: ~/project
    docker:
      - image: cypress/base:18.16.1
        environment:
          YARN_CACHE_FOLDER: ~/repo/frontend/yarn_packages
          TZ: Asia/Tokyo

jobs:
  frontend_setup:
    executor: frontend_default
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-yarn-packages-{{ checksum "frontend/yarn.lock" }}
            - v1-yarn-packages
      - restore_cache:
          keys:
            - v1-node-modules-{{ checksum "frontend/package.json" }}
            - v1-node-modules
      - run:
          name: Install yarn packages
          working_directory: frontend
          command: yarn install
      - save_cache:
          key: v1-yarn-packages-{{ checksum "frontend/yarn.lock" }}
          paths:
            - frontend/yarn_packages
            - .cache
      - save_cache:
          key: v1-node-modules-{{ checksum "frontend/package.json" }}
          paths:
            - frontend/node_modules
            - .cache
      - persist_to_workspace:
          root: ~/repo
          paths:
            - frontend
            - .cache
  frontend_lint_eslint:
    executor: frontend_default
    steps:
      - attach_workspace:
          at: ~/repo
      - run:
          name: Run eslint
          working_directory: frontend
          command: yarn run lint

workflows:
  frontend:
    when: << pipeline.parameters.run_frontend_jobs >>
    jobs:
      - frontend_setup
      - frontend_lint_eslint:
          requires:
            - frontend_setup
```

# 参考

- [Dynamic Configuration](https://circleci.com/docs/dynamic-config/)
  - CircleCI の config を Pipeline values やファイルPATH に応じて、動的に生成する場合の説明（monorepo 時に有用）。 
- [Using dynamic configuration](https://circleci.com/docs/using-dynamic-configuration/) 
- [circleci/path-filtering@1.0.0](https://circleci.com/developer/orbs/orb/circleci/path-filtering)
- [6 optimization tips for your CI configuration](https://circleci.com/blog/six-optimization-tips-for-your-config/)
- https://qiita.com/algas/items/d0e4d460b938d924ae8f