Circle CI を使用していると、このジョブは特定のファイルが変更されて時だけ実行したいなと思う時がありませんか?ありますよね?そう、あるんですよ。
そこで、特定のファイルが変更された時のみ Circle CI のジョブを実行する方法「Dinamic Configuration」を紹介します。
Dynamic Configration とは
プロジェクトで一つのコンフィグファイルを使用して CI の設定をするのではなく、特定のパイプラインパラメーターやファイルパスに応じて、設定ファイルを作成・実行する方法です。
詳細は公式ドキュメントを参照してください。
Dynamic Configration の使用方法
主に以下の方法を使用して Dynamic Configration を使用することができます。
continuePipelin API はリクエストボディに Circle CI のパイプライン設定および、パイプラインパラメーターをセットしてリクエストを送ることで、動的に Circle CI を実行することができる機能です。
continuePipelin API の簡単な例
continuation_key=<continuation key>
configration=<<-YAML
version: 2.1
parameters:
test-param:
type: string
jobs:
print-test-param:
steps:
run: echo "<< parameters.test-param >>"
workflows:
version: 2
test-workflow:
- print-test-param
YAML
parameters='{"test-param": "テストパラメーターです"}'
curl --request POST \
--url https://circleci.com/api/v2/pipeline/continue \
--header 'authorization: Basic REPLACE_BASIC_AUTH' \
--header 'content-type: application/json' \
--data '{"continuation-key":"$continuation_key","configuration":"$configration","parameters":"$parameters"'
# Circle CI で test-workflow が実行され、「テストパラメーターです」と出力される。
circleci/continuation
orb は continuePipeline API の呼び出し処理を簡単にできるようにした Orb です。
また、circleci/path-filtering
orb は circleci/continuation
orb を特定のファイルに変更があった際に、そのことがわかるパイプラインパラメーターをセットすることで、ジョブの実行をコントロールすることを目的とした Orb です。
circleci/path-filtering orb の簡単な例
circleci/continuation
orb と circleci/path-filtering
orb の使用方法はほぼ同じです。
以下のような設定ファイルを作成することで、main ブランチと比較して **/__test__/*.spec.js
にマッチするファイルが変更された時のみ JEST が実行されるようにできます。
version: 2.1
# Dynamic Configration の使用に必須
setup: true
orbs:
path-filtering: circleci/path-filtering@1.0.0
workflows:
always-run:
jobs:
- path-filtering/filter:
name: check-updated-files
mapping: |
**/__test__/*.spec.js run-jest true
# 変更の比較対象となるブランチ名
base-revision: main
config-path: .circleci/continue.yaml
version: 2.1
parameters:
run-jest:
type: boolean
default: false
jobs:
run-jest:
when: <<parameters.run-jest >>
steps:
- run: npm run jest
workflows:
version: 2
test:
- run-jest
また、以下のようなコンフィグダイルにすることで一つのファイルで完結させることもできます。
version: 2.1
# Dynamic Configration の使用に必須
setup: true
orbs:
path-filtering: circleci/path-filtering@1.0.0
parameters:
continuation:
type: boolean
default: false
run-jest:
type: boolean
defailt: false
jobs:
run-jest:
when: <<parameters.run-jest >>
steps:
- run: npm run jest
workflows:
always-run:
when:
not: << parameters.continuation >>
jobs:
- path-filtering/filter:
name: check-updated-files
mapping: |
**/__test__/*.spec.js run-jest true
# 変更の比較対象となるブランチ名
base-revision: main
config-path: .circleci/continue.yaml
continuation-run:
when: << parameters.continuation >>
jobs:
- run-jest
circleci/path-filtering orb の問題点
circleci/path-filtering orb はどのブランチと比較して、ファイルが変更されたかというのを動的に設定することができません。
プロジェクト毎にルートブランチを切って、そのブランチに PR を作成してマージしていくような方法をとっている場合には、せっかく特定のファイルが変更された時にのみジョブが実行されるようにできたのに、恩恵をほとんど受けることができません。
動的に基準となるブランチを変更するには、continuePipeline API を使用します。Orb を使用する場合とくらべメンテするコストが増えますがより柔軟に設定を行うことができるようになります。
まず、以下のようなコンフィグファイルを作成します。また、Circle CI のコンテキストに GitHub のアクセストークンを登録しておき、環境変数として値を参照できるようにします。
version: 2.1
# Dynamic Configration の使用に必須
setup: true
jobs:
path-filtering:
description: ベースブランチを決定し、パラメータを引き継いでジョブを継続するコマンド
parameters:
config-path:
description: continuePipeline API で使用するコンフィグファイルのパス
type: string
mapping:
description: ファイルとパラメータのマッピング設定
type: string
steps:
- run:
command: bash .circleci/scripts/path-filtering.sh
environment:
CONFIG_PATH: << parameters.config-path >>
MAPPING: << parameters.mapping >>
workflows:
version: 2
continuation:
jobs:
- path-filtering:
context: global
config-path: .circleci/continue.yaml
mapping: |
**/__test__/*.spec.js run-jest true
version: 2.1
parameters:
run-jest:
type: boolean
default: false
jobs:
run-jest:
when: <<parameters.run-jest >>
steps:
- run: npm run jest
workflows:
version: 2
test:
- run-jest
次に、continuePipeline API にリクエスト処理を行う一連の処理を行うシェルスクリプトファイルを作成します。コンフィグファイルの job.steps.run に直接することも可能です。また、Python スクリプトファイルを Python コンテナ上で実行するといった方法も可能です。
(必要最低限の処理しか記述していないので適宜環境に併せて編集してください。)
#!/bin/bash
set -euo pipefail
continue_parameters="{}"
get_pr_base_branch() {
if [[ ! -v GITHUB_TOKEN ]]; then
return
fi
pr_url="${1}"
gh_repo_slug="$(awk -F/ '{ print $4 "/" $5; }' <<<"${pr_url}")"
gh_pr_id="$(cut -d/ -f7 <<<"${pr_url}")"
gh_api_endpoint="https://api.github.com/repos/${gh_repo_slug}/pulls/${gh_pr_id}"
api_resp="$(curl -L -H "Authorization: Bearer ${GITHUB_TOKEN}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${gh_api_endpoint}" | tee /dev/stderr)"
base_branch="$(jq -r .base.ref <<<"${api_resp}")"
}
examine_diffs_and_generate_params() {
diffs=$(
cat <<EOD
$(git diff --name-only "origin/${base_branch}")
$(git diff --name-only HEAD~1 || git ls-tree -r --name-only HEAD)
EOD
)
continue_parameters="$(jq --arg base_branch "$base_branch" '.["base-branch"] = $base_branch' <<<"${continue_parameters}")"
while read -r cond; do
read -r pattern param_name <<<"${cond}"
if [[ -z "${param_name}" ]]; then
continue
fi
if [[ "${FORCE_ALL}" == 'true' ]] || grep -E "^${pattern}\$" <<<"${diffs}"; then
export param_name
continue_parameters="$(jq '.[$ENV.param_name] = true' <<<"${continue_parameters}")"
unset param_name
fi
done <<<"${PARAMETER_CONDITIONS}"
}
if [[ ! -v CIRCLE_CONTINUATION_KEY ]]; then
exit
fi
if [[ -v CIRCLE_PULL_REQUEST ]]; then
get_pr_base_branch "${CIRCLE_PULL_REQUEST}"
fi
git fetch origin "${base_branch}"
examine_diffs_and_generate_params
continue_body="$(jq \
--rawfile config "${CONTINUE_CONFIG_PATH}" \
-s '{
"continuation-key": $ENV.CIRCLE_CONTINUATION_KEY,
configuration: $config,
parameters: .[0]
}' <<<"${continue_parameters}" | tee /dev/stderr)"
[[ $(curl \
-X POST \
-H "Content-Type: application/json" \
--data-binary "${continue_body}" \
-w '\n%{http_code}\n' \
"https://circleci.com/api/v2/pipeline/continue" |& tee /dev/stderr | tail -n 1) == "200" ]]
まとめ
Orb または API を使用することで特定のファイルが変更された時のみジョブを実行する方法を紹介しました。
Orb を使用する場合は、continuePipeline API がラップされており、メンテナンスは Ciecle CI がしてくれるので楽ですが、柔軟性に欠けます。continuePipeline API を直接利用することで特定のファイルの変更を検知してパイプラインパラメーターを変更するだけでなくさまざまな状況に併せて実行するコンフィグファイルを切り替える、パイプラインパラメーターの値を変更するといったことができますが、メンテナンスコストが増えてしまします。
ここはトレードオフなので環境に応じて使い分ける必要があります。