現職ではCIの一つとしてCircleCI を使っており、静的解析・フォーマットチェック・ユニットテスト(フロントエンド・バックエンド)などを実行しています。
CIにpush するたびに全てのテストを実行すると、変更差分に関係のないテストが実行されてしまい、コードベースが大きくなると非効率な面が際立ちます。
ここでは、CircleCIのセットアップワークフローを使って差分ビルドを行う方法を紹介します。
セットアップワークフローとは
- 簡単に言うと、ユーザーが動的に設定ファイルを生成し、それに基づいて実行内容を決定できるというものです。
- この機能以前も、設定ファイルに書かれた内容を条件分岐によって実行内容を変えることはできましたが、より柔軟に実行内容を設定することができます。
- 使用するにはドキュメントに記載の通り、CircleCI Web上で
Enable dynamic config using setup workflowsを有効にし、.circleci/config.ymlにsetup:trueを追加します。 - これでセットアップワークフローが使えるようになったので、次は設定ファイルの動的生成を実装します。
設定ファイルの動的生成
- lenet のフロントエンドアプリケーションは、ディレクトリを分けて管理されています。
- CIでどのような処理を行うかはアプリケーション毎に異なるため、それぞれにCirlceCI設定ファイルを用意して、モジュール管理者が自由に設定できるようにしたいですね。
- そして、変更があったコードが属するモジュールのCirlceCI設定ファイルに基づいてCIを実行する、というのがやりたいことです。
.
├── app-1
│ └── .circleci
│ └── config.yml
├── app-2
│ └── .circleci
│ └── config.yml
└── app-3
└── .circleci
└── config.yml
- これは設定ファイル分割とパスフィルタリングを組み合わせたもので、CircleCI support ページで紹介されている circle-makotom/circle-advanced-setup-workflow を参考にしました。
- やっていることは大きく3つあり、「変更差分の検知」「設定ファイルのマージ」「ワークフローの実行依頼」です。実装概要を以下に説明します。
step1. 変更差分の検知
- プロジェクトルートにある
.circleci/config.ymlのworkflowsを以下のように記述します。 - これは、インラインOrbで定義している
config-splitting/setup-dynamic-configジョブを呼び出し、必要なパラメータを渡しています。-
base-revision:ファイル差分を検出する比較元ブランチ名 -
shared-config:共通設定を定義したファイルパス -
modules:変更差分を検出したいモジュールパス
-
workflows:
setup-workflow:
jobs:
- config-splitting/setup-dynamic-config:
base-revision: origin/master
shared-config: .circleci/shared-config.yml
modules: |
app-1
app-2
app-3
- 呼び出された
config-splitting/setup-dynamic-configでは、origin/masterと作業ブランチの差分ファイルをgit diff --name-onlyで取得し、modulesに設定されたディレクトリパスに該当するかどうかを調べ、該当するディレクトリパスを一時ファイルに書き出します。 - 例えば、
app-1とapp-2に差分ファイルがあった場合、以下のような中身を持つ一時ファイルが生成されます。
/tmp/modules-filtered.txt
app-1
app-2
step2. 設定ファイルのマージ
- 次に、一時ファイルに書き込まれた各ディレクトリパスの末尾に、
/.circleci/config.ymlを追記し、更に先ほどshared-configパラメータで指定した共通設定ファイルのパスを追記します。
/tmp/modules-filtered.txt
app-1/.circleci/config.yml
app-2/.circleci/config.yml
.circleci/shared-config.yml
- そして、YAMLファイルのマージツールであるyqを使って、一時ファイルに書かれたファイルをマージし、設定ファイルを生成します。
- マージの例を見てみましょう。
# app-1/.circleci/config.yml
jobs:
app1-job:
docker:
- image: cimg/base:2023.03
steps:
- common-echo
- run:
name: app1 echo
command: echo 'app1-job'
workflows:
app1-workflow:
jobs:
- app1-job
# app-2/.circleci/config.yml
jobs:
app2-job:
docker:
- image: cimg/base:2023.03
steps:
- common-echo
- run:
name: app2 echo
command: echo 'app2-job'
workflows:
app2-workflow:
jobs:
- app2-job
# .circleci/shared-config.yml
version: 2.1
commands:
common-echo:
steps:
- run:
name: common-echo
command: echo 'common-echo'
# マージ後のファイル
version: 2.1
commands:
common-echo:
steps:
- run:
name: common-echo
command: echo 'common-echo'
jobs:
app1-job:
docker:
- image: cimg/base:2023.03
steps:
- common-echo
- run:
name: app1 echo
command: echo 'app1-job'
app2-job:
docker:
- image: cimg/base:2023.03
steps:
- common-echo
- run:
name: app2 echo
command: echo 'app2-job'
workflows:
app1-workflow:
jobs:
- app1-job
app2-workflow:
jobs:
- app2-job
- ここまでで、設定ファイルの動的生成が完了しました。
step3. ワークフローの実行依頼
- 最後に continuation という Orb を使って、CircleCI のワークフロー実行用APIに先ほど生成した設定ファイルを渡して叩きます。
- すると、設定ファイルに記述された内容でワークフローが実行されます。
- 以上が実装概要になります。
リソースの重複定義対策
- yqコマンドで設定ファイルをマージする時、リソースの重複定義があった場合に統合されてしまいます。
- 例えば
workflows定義に、同名のワークフローが複数のファイルに定義されていた場合、そのうちの1つだけが残り他の定義は無視されてしまいます。
# app-1/.circleci/config.yml
jobs:
app1-job:
docker:
- image: cimg/base:2023.03
steps:
- run:
name: app1 echo
command: echo 'app1-job'
workflows:
app1-workflow:
jobs:
- app1-job
# app-2/.circleci/config.yml
jobs:
app2-job:
docker:
- image: cimg/base:2023.03
steps:
- run:
name: app2 echo
command: echo 'app2-job'
workflows:
app1-workflow: # app-1/.circleci/config.yml の workflow と同じ名前
jobs:
- app2-job
# マージ後のファイル
jobs:
app1-job:
docker:
- image: cimg/base:2023.03
steps:
- run:
name: app1 echo
command: echo 'app1-job'
app2-job:
docker:
- image: cimg/base:2023.03
steps:
- run:
name: app2 echo
command: echo 'app2-job'
workflows:
app1-workflow: # app-2/.circleci/config.yml のワークフローだけになってしまった
jobs:
- app2-job
- これでは
app-1/.circleci/config.ymlで定義したワークフローが実行されなくなってしまいますね。 - モジュール毎の設定ファイルが増えるほど、重複定義は起こりやすくなります。
- これを防ぐために、lenet では重複チェック用のシェルスクリプトを作っています。
duplicateDefinition=$(xargs -a "$1" yq '. | to_entries[] | select(.value | type == "object") | .value | keys[]' | sort | uniq -d)
if [ -n "$duplicateDefinition" ]; then
echo "Error: duplicate definition found: $duplicateDefinition"
exit 1
fi
echo "No duplicates found."
- step2の設定ファイルマージを行う前のファイルパスを
$1に渡してスクリプトを実行し、yqコマンドでYAMLファイルをパースします。 - 重複チェック対象にするのは、
workflowsやjobsなどの定型キーの子要素に当たるユーザ定義のリソース名で、同名のリソースが複数定義されていた場合にエラーメッセージを出力し処理を終了させています。 - これで重複定義の心配なく、設定ファイルを書いていけるようになりました。