GitHub Actions の if, 否定演算子, そして YAML
GitHub Actions のワークフロー構文に jobs.<job_id>.if
1 や jobs.<job_id>.steps[*].if
2 というものがあり
、その job や step の実行を if
に書かれた条件が満たされた場合だけに限定することができます。
また GitHub Actions のワークフロー構文として "expression syntax" (式構文)3 というものが存在します。
これは ${{ <expression> }}
と書くと <expression>
部分が文字列ではなく式として評価されるようになる、という特殊な構文です。
そしてドキュメントの該当部分 4 を読むと面白いことが書いてあります:
When you use expressions in an
if
conditional, you may omit the expression syntax (${{ }}
) because GitHub automatically evaluates theif
conditional as an expression.
(日本語訳) 5
if
条件の中で式を使用する際には、式構文 (${{ }}
)を省略できます。これは、GitHub がif
条件を式として自動的に評価するためです。
興味深いですね。つまり例えばこういう YAML が書けるわけです:
name: CI on: pull_request: types: - opened - reopened - synchronize jobs: if-omit-expression-syntax-test: runs-on: ubuntu-20.04 steps: - name: Explicit expression syntax if: ${{ github.actor == 'pione30' }} run: echo "Explicitly github.actor is pione30" - name: Expression syntax omitted if: github.actor == 'pione30' run: echo "github.actor is pione30 even though the expression syntax omitted"
上記の steps
はどちらも syntax error など起こさず実行されます:
https://github.com/pione30/github-actions-sandbox/runs/1800678250
ところで GitHub Actions の式の中において利用できる演算子があり、否定演算子 !
もあります 6 。
そこで例えばこんな steps
を追加するとどうでしょうか:
name: CI on: pull_request: types: - opened - reopened - synchronize jobs: if-omit-expression-syntax-test: runs-on: ubuntu-20.04 steps: - name: Explicit expression syntax if: ${{ github.actor == 'pione30' }} run: echo "Explicitly github.actor is pione30" - name: Expression syntax omitted if: github.actor == 'pione30' run: echo "github.actor is pione30 even though the expression syntax omitted" # 以下の steps を追加 - name: Not operator on the head in the explicit expression syntax if: ${{ !startsWith(github.head_ref, 'pione30/') }} run: echo "Explicitly github.head_ref does NOT startsWith 'pione30/'" - name: Not operator on the head without the expression syntax if: !startsWith(github.head_ref, 'pione30/') run: echo "github.head_ref does NOT startsWith 'pione30/'"
……残念ながらこのワークフローは実行されません。最後の step の if: !startsWith(github.head_ref, 'pione30/')
の部分で "You have an error in your yaml syntax on line 24" と言われてしまいます:
https://github.com/pione30/github-actions-sandbox/actions/runs/525066684
どうしてでしょうか?わからないので YAML の仕様書を見に行きましょう。
まず Preview をざっと眺めると Tags 7 という節が目に留まります。
YAML のノード(スカラー、シーケンス、マッピングのいずれか 8 )は !
記号を使って明示的に型を指定することができる、例えば !something
とか !!str
とかのように、と書かれています。
もう少し詳しく BNF による形式的定義 9, 10 を追っていくと、タグは
- Verbatim Tag
- Tag Shorthand
- Non-Specific Tag
の 3 種類に大別されることがわかります。
Verbatim Tag
!< URI として利用可能な文字列 >
の形式。 !<tag:yaml.org,2002:str>
が例として挙げられています。
Tag Shorthand
Tag Shorthand は先頭が Tag Handle、その後 Tag Char の連続と記されています。
Tag Handle は !
, !!
のいずれか 11 、Tag Char は URI として利用可能な文字セットから !
, ,
, [
, ]
, {
, }
を除いたもの 12 です。
(正確には Tag Handle にはさらに !英数字ハイフンの連続!
という形式も含まれますが、Tag Shorthand として使うためには TAG
ディレクティブで指定する必要があり、今回は説明を省きます。)
例としては !foo
!local
!!int
!!str
とかがあります。
Non-Specific Tag
ただの !
が単一でタグになります。そのノードは強制的に tag:yaml.org,2002:seq
, tag:yaml.org,2002:map
, tag:yaml.org,2002:str
のいずれかの型として解釈されます。
というわけで、式構文 (${{ }}
)を省略した上で否定演算子のつもりで先頭に !
をつけると YAML の仕様によりラベルとして解釈されてしまうことがわかりました。
GitHub Actions のドキュメントには明記されていないのですが意外とハマりポイントではないでしょうか 13 。
上記の !startsWith(github.head_ref, 'pione30/')
を解釈しようとすると、Tag Char には ,
が含まれないので ,
の前 !startsWith(github.head_ref
までが Tag Shorthand としてパースされるが、その直後に(Collection の開始記号 [
or {
およびエントリー無しに),
が来てしまうので syntax error となったのですね 14(YAML 初心者なので間違っていたら教えてください)。
そうすると、否定演算子としての意味は無くなってしまいますが
if: !<tag:yaml.org,2002:str> startsWith(github.head_ref, 'pione30/')
とか
if: !!str always()
とか書いても構文的には合法な気がしたので試してみたのですが、"The workflow is not valid. .github/workflows/ci.yml: Unexpected tag 'tag:yaml.org,2002:str'" と怒られてしまいました。僕にはもうよくわかりません。
おわりに
GitHub Actions、ロジックを記述するのになんで YAML の構文で書くことになってしまったのか???
-
https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idif↩
-
https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsif↩
-
https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions↩
-
https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#about-contexts-and-expressions↩
-
https://docs.github.com/ja/actions/reference/context-and-expression-syntax-for-github-actions#about-contexts-and-expressions↩
-
https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#operators↩
-
ちなみに式構文を省略していても式全体を
()
で括って論理グループ化してif: (!startsWith(github.head_ref, 'pione30/'))
のように書けば正しく式として解釈されます: https://github.com/pione30/github-actions-sandbox/runs/1800723422↩