CircleCI Orbsのための共有スクリプトの作成

この記事は元々私のブログこちらで公開されました。

問題点

CircleCI Orbsは、includeというディレクティブを提供しており、コマンド設定にスクリプトを含める際に利用できます。残念ながら、このディレクティブでは、含まれるスクリプトの中で他の共有スクリプトを明確に参照する方法が提供されていません。

実は、includeディレクティブは、参照されたスクリプトのテキスト本体をincludeディレクティブの部分に置き換えるマクロに過ぎません。つまり、共有スクリプティングモジュールを作成するのが難しくなります。

動機

最近、私は自分のプロジェクトのDevOpsツーリングをカスタムCircleCI Orbに集中させています。このorbは、私が現在および将来的に作成を計画している標準化されたリポジトリでの典型的なアクションを抽象化することを目的としています。

Nxプラグインとカスタムワークスペースプリセットを使用したモノリポジェネレータを作業中です。Codecov統合のテストカバレッジレポートのために、モノリポの中の各パッケージごとにカバレッジレポートを動的にアップロードし、それに合ったCodecovフラグを割り当てたいです。

残念ながら、公式のCodecov CircleCI Orbでは、このユースケースを単独では十分に対応できないので、私は彼らのスクリプトのいくつかを使って、Nxワークスペース設定を理解し、パッケージごとにセグメント化されたカバレッジのアップロードを自動的に行う何かを作り出しました。

その過程で、他のスクリプトで再利用できる共有スクリプトの関数を書く能力が欲しくなり、よりモジュール化されたテスト可能なスクリプトを書くため、そしてスクリプトをDRY(Don't Repeat Yourself)に保つためになりました。

解決策

幸いにも、includeディレクティブはorbの設定の中で、特にstepcommandプロパティ内に限らずどこにでも使用できます。

さらに、includeディレクティブを使用して、共有スクリプトの本文を他のコマンドに環境変数として提供することもできます。

推奨されない: 共有スクリプトの本文をevalする

この問題を解決する最初の試みは、共有スクリプトの本文を環境変数として提供し、その内容をevalするというものでした。

明らかに、これは最も安全な方法ではありませんし、この方法でのevalの使用は少し問題があります。しかし、問題は解決しました。

src/commands/upload-monorepo-coverage.yml:

steps:
  - run:
      name: Upload Monorepo Coverage Results
      command: << include(scripts/uploadMonorepoCoverageResults.sh) >>
      environment:
        PARSE_NX_PROJECTS_SCRIPT: << include(scripts/parseNxProjects.sh) >>

src/scripts/parseNxProjects.sh:

#! /usr/bin/env bash

# 他のファイルで使いたい共通関数
parse_nx_projects() {
  # ...
}

src/scripts/uploadMonorepoCoverageResults.sh:

#! /usr/bin/env bash

eval "$PARSE_NX_PROJECTS_SCRIPT"

# `parseNxProjects.sh`からのこの共有関数が呼び出せるようになった
parse_nx_projects

# ...

やや良い: 共有スクリプトの内容をディスクに書き込む

上記のようなevalを使用することは私の感覚に反していました。実際にそれが_実際に_より安全だとは確信していませんが、evalアプローチに触発された異なる方法で結論付けました。それは、includeディレクティブを使用して、共有スクリプトの内容をディスク上の予測可能な場所に書き込み、その共有スクリプトのパスをそれを消費する必要があるスクリプトに提供するというものです。

これにより、私の共有関数をsourceで読み込めるようになり、少し良い感じです。

この目的のために私は、write-shared-scriptと呼ばれる特定のコマンドを書きました。

そのコマンドのソースは以下の通りです:

description: >
  このコマンドは共有スクリプトをディスクに書き込んで、他のスクリプトによって消費されるようにします

parameters:
  script-dir:
    type: string
    default: ~/@chiubaka/circleci-orb/scripts
    description: 共有スクリプトを書き込むディレクトリへのパス。
  script-name:
    type: string
    description: 書き込むスクリプトの名前
  script:
    type: string
    description: 書き込むスクリプト。ここで`include`ディレクティブを使用して含めるべきです。

steps:
  - run:
      name: Write << parameters.script-name >> to disk
      command: << include(scripts/writeSharedScript.sh) >>
      environment:
        SCRIPT: << parameters.script >>
        SCRIPT_DIR: << parameters.script-dir >>
        SCRIPT_NAME: << parameters.script-name >>

そしてwriteSharedScript.shは以下のようになります:

#! /usr/bin/env bash

SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_NAME"

mkdir -p "$SCRIPT_DIR"
echo "$SCRIPT" > "$SCRIPT_PATH"
chmod +x "$SCRIPT_PATH"

そして共有スクリプトが必要なコマンドのステップは以下のようになります:

  - write-shared-script:
      script-name: parseNxProjects.sh
      script: << include(scripts/parseNxProjects.sh) >>
  - run:
      name: Upload Monorepo Coverage Results
      command: << include(scripts/uploadMonorepoCoverageResults.sh) >>
      environment:
        PARSE_NX_PROJECTS_SCRIPT: ~/@chiubaka/circleci-orb/scripts/parseNxProjects.sh

最後に、uploadMonorepoCoverageResults.shスクリプトは以下のようになります:

#! /usr/bin/env bash

source "$PARSE_NX_PROJECTS_SCRIPT"

# `parseNxProjects.sh`からのこの共有関数が呼び出せるようになった
parse_nx_projects

# ...

セキュリティ上の配慮

おそらく、CircleCIの文脈でこれら二つのアプローチの違いは大きくありません。実際には、evalへの入力はCircleCIが正常に動作している限り、常に私のコントロール下にあります。もしCircleCIにセキュリティの侵害があって、攻撃者がこのevalステートメントの入力をコントロールできるようになる場合、ここではまったく異なる脅威モデルを扱って、より大きな問題があります。

技術的には、攻撃者がそのevalステートメントの入力をコントロールできるとしたら、同じ攻撃者はおそらくディスク上の共有スクリプトの内容をコントロールできることになり、これは同様の重大性の攻撃になるでしょう。

それでも、evalを使用するという重大な罪を避ける方が良いと感じますし、少なくともこの方法では私のorbスクリプトは実際にディスクに共有スクリプトが書き込まれるため、本番環境でよりデバッグしやすくなります。

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/chiubaka/writing-shared-scripts-for-circleci-orbs-3c2c