🤖

GitHub ActionsにおけるIf文の挙動を勘違いしていた話

2024/12/08に公開

本記事は、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