GitHub Actions で複数行の文字列を output にセットする方法

GitHub

GitHub Actions (以下 GHA )で改行文字を含む複数行の文字列をコマンドの output にセットする方法についてです。

追記

2022/11/16

output パラメータをセットする方法が ::setoutput を使った形から環境変数 GITHUB_OUTPUT を使った形に変更されたので各所更新しました。

参考:

前提

この記事は当初 2022 年 05 月に書きました。 時間が経つと GitHub Actions の仕様変更などで情報が古くなるので参考にされる際は注意してください。

問題

通常 GitHub Actions では環境変数 GITHUB_OUTPUT にパスが格納されたファイルに name=value の形式で文字列を出力すると output をセットできるようになっています。

たとえば、キーが action_fruit で値が strawberry のアイテムを output にセットしたいときは次のように書きます:

run: |
  name=action_fruit
  value=strawberry
  echo "${name}=${value}" >> $GITHUB_OUTPUT
shell: bash

しかし、この方法を使うと、 value に改行文字が含まれていると改行文字以降が name の値から抜け落ちてしまうという問題があります。 たとえば、 Hello\nWorld という文字列を value にセットすると output パラメータには Hello だけがセットされてしまいます:

run: |
  name=greeting
  value="Hello\nWorld"
  echo "${name}=${value}" >> $GITHUB_OUTPUT
shell: bash
id: my_command
# => `steps.my_command.outputs.greeting` には `Hello` だけが含まれる

関連イシューなど:

対応方法

GHA で改行文字が含まれうる文字列を output にセットするには次のいずれかの方法を使います

  • 方法 A) 事前にエスケープする
  • 方法 B) 区切り文字を使う
  • 方法 C) actions/github-scriptcore.setOutput() を使う

先に結論ですが、おすすめは方法 C) です。 方法 C) なら利用者は implementation detail な改行問題を意識する必要がありません。

方法 A) 事前にエスケープする

標準出力に出力する前に value 内の改行文字などをエスケープします。 たとえば Bash スクリプトの場合は次のような処理を行うことになります:

value="${value//'%'/'%25'}"
value="${value//$'\n'/'%0A'}"
value="${value//$'\r'/'%0D'}"

参考:

方法 B) 区切り文字を使う

GITHUB_OUTPUT では区切り文字を使うと複数行の文字列をセットできるようになっているのでその記法を使います。 具体的には次のように書きます:

steps:
  - name: Set the value in bash
    id: step_one
    run: |
      echo 'JSON_RESPONSE<<EOF' >> $GITHUB_OUTPUT
      curl https://example.lab >> $GITHUB_OUTPUT
      echo 'EOF' >> $GITHUB_OUTPUT

(これは公式ドキュメントにあるサンプルの $GITHUB_ENV$GITHUB_OUTPUT に変えたものです)

EOF の部分が区切り文字です。 ここでは JSON_RESPONSE という output パラメータに curl コマンドの出力をセットしています。

ただし、このアプローチを使う場合、区切り文字はランダムにするのがセキュリティ上望ましいとされており、実際に使うには注意が必要です:

Warning: Make sure the delimiter you're using is randomly generated and unique for each run. For more information, see "Understanding the risk of script injections".

この点に関して GitHub Community Discussions では次のように区切り文字を動的に生成する方法が紹介されていました:

delimiter="$(openssl rand -hex 8)"
echo "output-name<<${delimiter}" >> "${GITHUB_OUTPUT}"
echo "Some\nMultiline\nOutput" >> "${GITHUB_OUTPUT}"
echo "${delimiter}" >> "${GITHUB_OUTPUT}"

参考:

方法 C) actions/github-scriptcore.setOutput() を使う

GitHub 公式のアクション actions/github-scriptcore.setOutput() を使います。

core.setOutput() はまさに「改行を含みうる文字列を output にセット」するための関数です。 たとえば次のような形で使用します:

# output `greeting` に 'Hello\nWorld' をセットする
- uses: actions/github-script@v6
  with:
    script: |
      const name = 'greeting'
      const value = 'Hello\nWorld'
      core.setOutput(name, value)

core.setOutput() に渡した文字列は内部で方法 A) と同様のエスケープ処理 ↓ が行われてから標準出力に出力されるようになっています。

function escapeData(s: any): string {
  return toCommandValue(s)
    .replace(/%/g, '%25')
    .replace(/\r/g, '%0D')
    .replace(/\n/g, '%0A')
}

シンプルでわかりやすいですね。 ということで、方法 A) B) C) の中では最も C) がシンプルでおすすめです。

ちなみに、 actions/github-script を使う場合は return でオブジェクトを返せば output をセットできるようにもなっているので、その方法を使うという手もあります。 詳細に興味のある方は actions/github-script の README を見てみてください:

もうひとつちなみに、 core.setOutput() を提供している @actions/core のコードとドキュメントは actions/toolkit リポジトリの中にあります:

以上です。

おまけ

「コマンドを 1 つ実行してその結果を output にセットする」というパターンは個人的に頻出なので、そのためだけのかんたんなカスタムアクションを作りました:


アバター
後藤隼人 ( ごとうはやと )

ソフトウェア開発やマーケティング支援などをしています。詳しくはこちら