GitHub Actions の if, 否定演算子, そして YAML

GitHub Actions のワークフロー構文に jobs.<job_id>.if 1jobs.<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 the if conditional as an expression.

(日本語訳) 5

if 条件の中で式を使用する際には、式構文 (${{ }})を省略できます。これは、GitHubif 条件を式として自動的に評価するためです。

興味深いですね。つまり例えばこういう 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 となったのですね 14YAML 初心者なので間違っていたら教えてください)。

そうすると、否定演算子としての意味は無くなってしまいますが

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 の構文で書くことになってしまったのか???