はじめに
GitHub Actionsを使い込んでいると、リファレンスに明示されていない部分が気になることが多々ある (個人の見解です) 。
タスクに忙殺されていると「動くから良し」の判断のもと、碌な検証や検討がされていない不適切な実装のままWorkflowが運用されてしまうこともしばしばある (個人の見解です) 。
より深くGitHub Actionsの仕様を理解し、より適切に実装するということがスムーズに行えるよう、調査と結果をこの記事に記録する。
検証環境はGitHub Enterprise Server 3.14.4 である。github.comではない点に注意されたい。
式
まず前提として、GitHub Actionsでは「式」が書ける。
https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions
式として以下が書ける。
- 即値 (リテラル)
- 単項演算 (
!
) - 二項演算 (
<
<=
>
>=
==
!=
&&
||
) - 関数呼び出し
- プロパティ参照 ※右辺にはプロパティ名しかとれない
- context
また ( 式 )
も式であり、式の評価順序を明示することができる。
型
式を評価すると値になる。
値は以下のいずれかの型に分類できる
- null
- boolean
- string
- number
- array
- object
暗黙変換
値はいくつかの文脈でいくつかの型へ暗黙変換される。
例えば、異なる型同士の比較演算を行う場合に両辺が number へ暗黙変換される。
string への暗黙変換
-
null
-> 空文字列 -
false
->'false'
-
true
->'true'
- number -> 数値を10進数表記した文字列、桁数が大きい場合は指数表記となる
- 配列、オブジェクト -> 変換はできず、エラーを生じる
number への暗黙変換
-
null
->0
-
false
->0
-
true
->1
- 文字列
- 空文字列 ->
0
- JSONにおける数値表現を満たす文字列 -> その文字列を数値へ変換した結果
- その他の文字列 ->
NaN
- 空文字列 ->
- 配列、オブジェクト ->
NaN
falsy values と non-falsy values
暗黙変換とは少し性質が異なるが、すべての値は falsy values か non-falsy values に分類される。
falsy values に分類される値は boolean として評価する文脈において false
相当の振る舞いをする。
falsy values でない値はすべて non-falsy values に分類され true
相当の振る舞いをする。
falsy values に分類されるのは以下の値。
false
null
-
0
、-0
- 空文字列
リテラル
リテラルとして以下の型の値を記述できる。
- boolean :
true
またはfalse
- null :
null
- number : JSONでサポートされるフォーマットの数値
- string : シングルクォート
'
で括った文字列、シングルクォートをエスケープする場合は2つ連続させる
以下はリテラルを用いて環境変数を定義する例。
env:
myNull: ${{ null }}
myBoolean: ${{ false }}
myIntegerNumber: ${{ 711 }}
myFloatNumber: ${{ -9.2 }}
myHexNumber: ${{ 0xff }}
myExponentialNumber: ${{ -2.99e-2 }}
myString: Mona the Octocat
myStringInBraces: ${{ 'It''s open source!' }}
なお、リファレンスでは単純な文字列について ${{ '文字列' }}
の形式ではなく直接文字列を記述する、と記載がある。
個人的には ${{ '文字列' }}
の形式の方にする方が、統一性があり良いのではないかと思う。
関数
式として関数呼び出しを記述できる。
https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#functions
なお一部の関数は特定の文脈でしか定義できない為注意が必要である。
https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#context-availability
リファレンスには Some functions cast values to a string to perform comparisons.
との記載がある。
to perform comparisons
の意味するところが判然としないが、 join
関数の第2引数と toJSON
関数の第1引数以外のすべての引数は関数呼び出し時に string へ暗黙変換されているように思う。
いくつかの関数が提供されているが、この記事では特に fromJSON(value)
と toJSON(value)
に注目する。
fromJSON(value)
は value
をJSON文字列として解釈し、そのオブジェクトや値を返す関数である。
逆に toJSON(value)
は value
をJSON文字列に変換し、その文字列を返す関数である。
fromJSON(value)
はJSONやJavaScriptで扱える以下の型の値を返す。
- string
- boolean
- null
- array
- object
fromJSON(value)
を用いることでリテラルでは直接表現できない型の値も表現することができる。
オブジェクト
GitHub Actionsではオブジェクトに対し [ ]
演算子によるインデックスアクセスと、 .
演算子によるプロパティ参照の2つの方法でプロパティの参照ができる。
.
演算子では右辺にプロパティ名しか取れないが、[ ]
演算子の場合にはインデックスに式を取ることができ、より複雑な表現が可能である。
env:
# Error: The template is not valid. <workflow path> (Line: N, Col: M): A mapping was not expected
# SAMPLE_MY_VAR_1: ${{ fromJSON( '{"hoge":"value"}' ) }}
SAMPLE_MY_VAR_1: ${{ fromJSON( '{"hoge":"value"}' )['hoge'] }}
SAMPLE_MY_VAR_2: ${{ fromJSON( '{"hoge":"value"}' ).hoge }}
※環境変数の定義では string への暗黙変換が発生するため、 SAMPLE_MY_VAR_1
の定義はエラーを生じる
存在しないプロパティを参照した場合は null
が返る。
これは context のリファレンスの記載 If you attempt to dereference a nonexistent property, it will evaluate to an empty string.
とは明らかに異なる。
また、 null
に対するプロパティの参照はエラーとはならず、 null
を返す。
env:
SAMPLE_MY_VAR_1: ${{ fromJSON( '""' ) }}
SAMPLE_MY_VAR_2: ${{ toJSON(fromJSON( '""' )) }}
SAMPLE_MY_VAR_3: ${{ null }}
SAMPLE_MY_VAR_4: ${{ toJSON(null) }}
SAMPLE_MY_VAR_5: ${{ fromJSON( '{}' ).hoge }}
SAMPLE_MY_VAR_6: ${{ toJSON(fromJSON( '{}' ).hoge) }}
SAMPLE_MY_VAR_7: ${{ github.hoge }}
SAMPLE_MY_VAR_8: ${{ toJSON(github.hoge) }}
SAMPLE_MY_VAR_9: ${{ toJSON(fromJSON( 'null' ).hoge) }}
上記の例の評価結果は以下の通りになる。
SAMPLE_MY_VAR_1:
SAMPLE_MY_VAR_2: ""
SAMPLE_MY_VAR_3:
SAMPLE_MY_VAR_4: null
SAMPLE_MY_VAR_5:
SAMPLE_MY_VAR_6: null
SAMPLE_MY_VAR_7:
SAMPLE_MY_VAR_8: null
SAMPLE_MY_VAR_9: null
配列
配列に対し [ ]
演算子によるインデックスアクセスができる。
(リファレンスに記載がないため推定だが)インデックスは number へ暗黙に変換される。
インデックスに対応する要素が無い場合は null を返す。
env:
# Error: The template is not valid. <workflow path> (Line: N, Col: M): A sequence was not expected
# SAMPLE_MY_VAR_1: ${{ fromJSON( '["A", "B", "C"]' ) }}
SAMPLE_MY_VAR_2: ${{ fromJSON( '["A", "B", "C"]' )['1'] }} # B
SAMPLE_MY_VAR_3: ${{ fromJSON( '["A", "B", "C"]' )[2] }} # C
SAMPLE_MY_VAR_4: ${{ fromJSON( '["A", "B", "C"]' )[false] }} # A
SAMPLE_MY_VAR_5: ${{ fromJSON( '["A", "B", "C"]' )[''] }} # A
SAMPLE_MY_VAR_6: ${{ fromJSON( '["A", "B", "C"]' )[null] }} # A
SAMPLE_MY_VAR_7: ${{ toJSON(fromJSON( '["A", "B", "C"]' )[3]) }} # null
SAMPLE_MY_VAR_8: ${{ toJSON(fromJSON( '["A", "B", "C"]' )[-1]) }} # null
※環境変数の定義では string への暗黙変換が発生するため、 SAMPLE_MY_VAR_1
の定義はエラーを生じる
実際に動作させて確認した限り、配列はプロパティを持っていなかった。
そのため、ある配列の要素数を GitHub Actions の式で知る術がない。
比較演算
典型的な6種の比較演算 <
<=
>
>=
==
!=
が利用できる。
リファレンスには比較演算に関して重要な仕様が記載されている。
- 両辺の型が一致しない場合、 number へ暗黙変換を行ってから比較演算を行う
-
NaN
に対する比較演算は、比較対象の値に拠らず常にfalse
となる - string の比較を行う場合、大文字小文字は区別されない
- オブジェクトと配列は、同一インスタンスの場合にのみ等価と扱われる
string 同士の比較は大文字小文字が区別されない以外、文字コード順の比較に思える。
単純な例で、以下の結果が得られた。
文字コードに対する取り扱いがどのようになっているかについては調査、確認していない。
(たとえばWorkflowの定義をShift-JISで記述し、Unicodeに変換できない文字列リテラルを記述して比較演算を行う場合には何が起きるのだろうか。)
env:
SAMPLE_MY_VAR_1: ${{ 'A' < 'B' }} # true
SAMPLE_MY_VAR_2: ${{ 'A' == 'B' }} # false
SAMPLE_MY_VAR_3: ${{ 'A' > 'B' }} # false
SAMPLE_MY_VAR_4: ${{ 'A' < 'a' }} # false
SAMPLE_MY_VAR_5: ${{ 'A' == 'a' }} # true
SAMPLE_MY_VAR_6: ${{ 'A' > 'a' }} # false
オブジェクトや配列に対しては、同一のインスタンスであるかという比較のみ行うことができる。
そのため、以下のような結果が得られる。
env:
SAMPLE_MY_VAR_1: ${{ fromJSON('{"key":"A"}') == fromJSON('{"key":"A"}') }} # false
SAMPLE_MY_VAR_2: ${{ fromJSON('{"key":"A"}') <= fromJSON('{"key":"B"}') }} # false
SAMPLE_MY_VAR_3: ${{ fromJSON('{"key":"A"}') >= fromJSON('{"key":"B"}') }} # false
SAMPLE_MY_VAR_4: ${{ fromJSON('["A"]') == fromJSON('["A"]') }} # false
SAMPLE_MY_VAR_5: ${{ fromJSON('["A"]') <= fromJSON('["B"]') }} # false
SAMPLE_MY_VAR_6: ${{ fromJSON('["A"]') >= fromJSON('["B"]') }} # false
SAMPLE_MY_VAR_7: ${{ github == github }} # true
論理演算
論理積 &&
と論理和 ||
が利用できる。
論理演算の評価結果は boolean ではなく両辺のどちらかの値となる。
この性質により、論理演算は三項演算子の代わりとして利用できる。
env:
SAMPLE_MY_VAR_1: ${{ (inputs.value == 'hoge') && 'fuga' || 'piyo' }}
この例の場合、条件式 (inputs.value == 'hoge')
の評価結果が真であれば SAMPLE_MY_VAR_1 は 'fuga'
、そうでなければ 'piyo'
となる。
env:
SAMPLE_MY_VAR_1: ${{ inputs.flag && 0 || 1 }}
SAMPLE_MY_VAR_2: ${{ inputs.flag && inputs.value1 || inputs.value2 }}
論理演算を用いた疑似的な三項演算は、真値 (条件式が真だった場合に返したい値) が falsy value とならないよう注意して扱う必要がある。
この例の SAMPLE_MY_VAR_1 は、 0
が falsy value であるため inputs.flag
の値に拠らず 1
で定義されてしまう。
また SAMPLE_MY_VAR_2 も、 inputs.value1
が空文字列の場合それは falsy value であり、 inputs.flag
の値に拠らず inputs.value2
で定義されてしまう。
context
Workflow の run や runner 、リポジトリ変数等々を Workflow 定義で参照する手段として context が提供されている。
https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs
全ての context があらゆる文脈で利用できるわけでは無いため、注意する必要がある。
https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#context-availability
context はオブジェクトとして扱える。
通常はそのプロパティを参照するために用いる。
inputs と型
Workflow は inputs 属性によって入力を与えることができる。
Workflow の入力は on.workflow_call.inputs.<input_id> または on.workflow_dispatch.inputs.<input_id> で定義される。
Workflow の入力は context を用いて inputs.<input_id>
として参照できる。
github.event.inputs.<input_id>
としても参照できるが、こちらは値が常に string であることに注意する必要がある。
inputs の型の指定
Workflow の入力は on.<workflow_call|workflow_dispatch>.inputs.<input_id>.type でその型を指定できる。
しかし workflow_call と workflow_dispatch では、有効な選択肢や指定による効果が大きく異なる。
typeの値 | workflow_call | workflow_dispatch |
---|---|---|
(指定が必須か?) | 必須 | 任意 (デフォルトは string 指定) |
string | inputs.<input_id>が必ず string となる。 | inputs.<input_id>が string か null となる。 |
boolean | inputs.<input_id>が必ず boolean となる。 | inputs.<input_id>が必ず boolean となる。 |
number | inputs.<input_id>が必ず number となる。 | inputs.<input_id>が string か null となる。数値に変換不可能な文字列を与えてもそのまま受理される。 |
choice | 選択不可。 | on.workflow_dispatch.inputs.<input_id>.optionsで定義した選択肢からなるドロップダウンリストで入力を選択可能となる。inputs.<input_id>が必ず string となる。 |
environment | 選択不可。 | リポジトリに定義された environment からなるドロップダウンリストで入力を選択可能となる。inputs.<input_id>が必ず string となる。 |
workflow_call の場合、 type 指定とその効果が明確で扱いやすい。
一方 workflow_dispatch の場合、特に number を指定した場合はその挙動に注意が必要である。
数値であることのバリデーションが行われないどころか、そもそも string で値が返ってくる。
workflow_call の場合、必ず type 指定に従った型の値が定義されるのに対し、 workflow_dispatch の場合一部の指定で null を返すケースがある。
これは値が渡されなかった場合に、そもそも inputs.<input_id> が定義されない為に生じる。
特に入力フォームが空の場合、空文字列が定義されるのではなく、プロパティが定義されず参照すると null が返ることに注意が必要である。
inputs の型検査
workflow_call の入力は uses.with 属性で与えることとなる。
このとき、 inputs の型が期待されたものであるか検査され、型の不一致に対してエラーを吐くケースがある。
type で string を指定した場合、 string への暗黙変換が成功すれば受理される。
これに対して boolean と number を指定した場合、それ以外の型の値を渡すと The workflow is not valid.
というエラーによって job の起動に失敗する。
特に step や job の outputs は必ず string に暗黙変換され定義されることから、以下のようなワークフロー呼び出しは失敗することに注意が必要である。
jobs:
first_job:
# outputs は string に暗黙変換される
outputs:
string_value: ${{ 'ABC' }}
number_value: ${{ 123 }} # '123' と等価
boolean_value: ${{ true }} # 'true' と等価
steps:
- run: echo ""
second_job:
needs: [ first_job ]
uses: ./.github/workflows/XXX.yml
with:
string_input: ${{ needs.first_job.outputs.string_value }} # string 指定に string が渡る => OK
number_input: ${{ needs.first_job.outputs.number_value }} # number 指定に string が渡る => Error
boolean_input: ${{ needs.first_job.outputs.boolean_value }} # boolean 指定に string が渡る => Error
それだけでは100%の動作を保証できないが、型を一致させてエラーを回避するために fromJSON(value)
を用いる方法がある。
string_value
は元から string の為 fromJSON(value)
を用いてはいけない (不正なJSONのパースとしてエラーとなる) 。
with:
string_input: ${{ needs.first_job.outputs.string_value }}
number_input: ${{ fromJSON(needs.first_job.outputs.number_value) }}
boolean_input: ${{ fromJSON(needs.first_job.outputs.boolean_value) }}