GitHub ActionsにおけるIf文の挙動を勘違いしていた話
本記事は、GitHub Advent Calendar 2024 の 8 日目の記事です。
GitHub - Qiita Advent Calendar 2024 - Qiita
突然ですが、下記のような GitHub Actions の Workflow があったとします。この Workflow が "default branch 以外" で実行された場合にどのような挙動になるでしょうか? hello world
の文字列は Actions のログに出力されるでしょうか?
---
name: hello world
"on":
push:
jobs:
hello:
runs-on: ubuntu-latest
steps:
- name: hello
if: github.ref_name == ${{ github.event.repository.default_branch }}
run: |
echo "hello world"
exit 1
push event の場合は、github.ref_name
を利用して refs が含まれていないブランチ名やタグ名単体を取得できます。
ワークフロー実行に関するコンテキスト情報へのアクセス - GitHub Docs
また、github.event.repository.default_branch
を利用することでデフォルトブランチ名を取得できます。
continuous integration - GitHub actions: default branch variable - Stack Overflow
「ということは、default branch 以外" で起動してるのだから、hello の step はスキップされるのでは?」と思われた方もいらっしゃるかもしれません。実際に、適当なブランチを切って上記の workflow ファイルを push して確認してみると、hello の step は実行されます。
GitHub Actions のデバッグログを添付しますが、一番最後に hello world の文字列が出力されていることがわかります。
##[debug]Evaluating condition for step: 'hello'
##[debug]Evaluating: (success() && format('github.ref_name == {0}', github.event.repository.default_branch))
##[debug]Evaluating And:
##[debug]..Evaluating success:
##[debug]..=> true
##[debug]..Evaluating format:
##[debug]....Evaluating String:
##[debug]....=> 'github.ref_name == {0}'
##[debug]....Evaluating Index:
##[debug]......Evaluating Index:
##[debug]........Evaluating Index:
##[debug]..........Evaluating github:
##[debug]..........=> Object
##[debug]..........Evaluating String:
##[debug]..........=> 'event'
##[debug]........=> Object
##[debug]........Evaluating String:
##[debug]........=> 'repository'
##[debug]......=> Object
##[debug]......Evaluating String:
##[debug]......=> 'default_branch'
##[debug]....=> 'main'
##[debug]..=> 'github.ref_name == main'
##[debug]=> 'github.ref_name == main'
##[debug]Expanded: (true && 'github.ref_name == main')
##[debug]Result: 'github.ref_name == main'
##[debug]Starting: hello
##[debug]Loading inputs
##[debug]Loading env
Run echo "hello world"
どうしてこのようなことになるのでしょうか? GitHub Actions のドキュメントを確認してみましょう。
GitHub Actionsの仕様
hello の step が実行される直前のログを確認してみましょう。 よく見ると、==
を含む文字列として評価されているように見えないでしょうか?
##[debug]Expanded: (true && 'github.ref_name == main')
##[debug]Result: 'github.ref_name == main'
##[debug]Starting: hello
ここで if 文の仕様をおさらいしておきましょう。if 文を利用して条件式を定義して条件式が true
として評価された場合にその step が実行されるというものです。文字列を式として評価させるためには、${{ <expression> }}
のような構文を利用する必要がありますが、if 文においては構文を省略できます。
ワークフローとアクションで式を評価する - GitHub Docs
if 条件が true の場合は、ステップが実行されます。
ある式を、文字列型として扱うのではなく式として評価するためには、特定の構文を使って GitHub に指示する必要があります。
${{ <expression> }}
この規則の例外は、if 句で式を使用する場合です。通常、${{ と }} を任意で省略することができます。
今回の場合は ${{ github.event.repository.default_branch }}
のような形で明示的に構文を利用しているので、github.event.repository.default_branch
は文字列ではなく GitHub Actions の評価の対象となります。デバッグログを確認すると、最終的に default branch (main) に解決されていることがわかるでしょう。
一方で、'github.ref_name
はどうでしょうか? ログを見ると、Evaluating String
と記載があるように、github.ref_name == {0}
という文字列として評価されているように見えます。
{0}
のところには解決された default branch (main) の文字列が割り当てられて、最終的には github.ref_name == main
のような文字列となります。
if 文にある文字列は最終的に true として扱われ、結果として hello の step が実行されたというわけです。
ワークフローとアクションで式を評価する - GitHub Docs
条件では、偽の値 (false、0、-0、""、''、null) が false に強制的に適用され、真値 (true とその他の非偽の値) が true に強制されることに注意してください。
構文を誤って利用すると、「式として評価したい文字列」をそのまま文字列として扱ってしまうことがわかります。
正しく動作するように修正する
では、正しく動作させるためにはどのようにしたら良いでしょうか。下記のように修正してみました。(デバッグログを出力するために、わざと失敗する debug の step を追加しています。)
jobs:
hello:
runs-on: ubuntu-latest
steps:
- name: hello
if: github.ref_name == github.event.repository.default_branch
run: |
echo "hello world"
exit 1
- name: debug
run: |
echo ${{ github.ref_name }}
echo ${{ github.event.repository.default_branch }}
exit 1
このようにすることで、github.ref_name == github.event.repository.default_branch
の全体を式として評価できます。デバッグログを確認すると、github.ref_name
, ithub.event.repository.default_branch
の両方がブランチ名に解決されていることがわかるでしょう。
##[debug]Evaluating condition for step: 'hello'
##[debug]Evaluating: (success() && (github.ref_name == github.event.repository.default_branch))
##[debug]Evaluating And:
##[debug]..Evaluating success:
##[debug]..=> true
##[debug]..Evaluating Equal:
##[debug]....Evaluating Index:
##[debug]......Evaluating github:
##[debug]......=> Object
##[debug]......Evaluating String:
##[debug]......=> 'ref_name'
##[debug]....=> 'test/if-statement'
##[debug]....Evaluating Index:
##[debug]......Evaluating Index:
##[debug]........Evaluating Index:
##[debug]..........Evaluating github:
##[debug]..........=> Object
##[debug]..........Evaluating String:
##[debug]..........=> 'event'
##[debug]........=> Object
##[debug]........Evaluating String:
##[debug]........=> 'repository'
##[debug]......=> Object
##[debug]......Evaluating String:
##[debug]......=> 'default_branch'
##[debug]....=> 'main'
##[debug]..=> false
##[debug]=> false
##[debug]Expanded: (true && ('test/if-statement' == 'main'))
##[debug]Result: false
この場合は、最終的に hello の step は意図通りスキップされていることがわかります。
Discussion