2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iCARE Dev Advent Calendar 2023

Day 2

特定のファイルが変更された時のみ Circle CI のジョブを実行する

Posted at

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 が実行されるようにできます。

.circleci/config.yaml
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
.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

また、以下のようなコンフィグダイルにすることで一つのファイルで完結させることもできます。

.circleci/config.yaml
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 のアクセストークンを登録しておき、環境変数として値を参照できるようにします。

.circleci/config.yaml
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
.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

次に、continuePipeline API にリクエスト処理を行う一連の処理を行うシェルスクリプトファイルを作成します。コンフィグファイルの job.steps.run に直接することも可能です。また、Python スクリプトファイルを Python コンテナ上で実行するといった方法も可能です。
(必要最低限の処理しか記述していないので適宜環境に併せて編集してください。)

path-filtering.sh
#!/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 を直接利用することで特定のファイルの変更を検知してパイプラインパラメーターを変更するだけでなくさまざまな状況に併せて実行するコンフィグファイルを切り替える、パイプラインパラメーターの値を変更するといったことができますが、メンテナンスコストが増えてしまします。
ここはトレードオフなので環境に応じて使い分ける必要があります。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?