GitHub Actionsのif節をミスなく安全に運用するには

## はじめに

こんにちは、最近毎朝ブルーベリースムージーを作っている、2023年新卒入社のこうざい（[@yukhy_BE](https://x.com/yukhy_BE)）です。おすすめのスムージーレシピを募集しています！
主にSchooのプロダクトのインフラエンジニアとして働いています。

Schooでは、CI/CDの一部にGitHub Actionsを利用しています。最近、GitHub Actions内のif節で少し厄介な問題に遭遇しました。この記事では、その経験から学んだことと、GitHub Actionsのif節を正しく使用するためのポイントを共有したいと思います。

## GitHub Actionsのif節

GitHub Actionsは、GitHubのリポジトリでソフトウェア開発ワークフローを自動化するためのCI/CDプラットフォームです。その中でif節は、ワークフロー内の特定のステップやジョブが実行されるかどうかを条件に基づいて制御するための機能です。

基本的な使用例は以下の通りです。

```yaml
steps:
  - name: 条件付きステップ
    if: 条件
    run: echo "条件が真の場合に実行される"
```

## 遭遇した問題

既存のActionsファイルに以下のようなif条件がありました。

```yaml
if: "(env.environment == 'hoge') && (github.ref_name == 'hoge')"
```

これに新しい条件を追加しようとして、次のように書きました。

```yaml
if: "((env.environment == 'hoge') && (github.ref_name == 'hoge')) || ((${{ env.environment }} == 'fuga') && (github.ref_name == 'fuga'))"
```

ここで問題が発生しました。OR条件の2つ目の部分で、`env.environment`を`${{}}`で囲んでいます。
if節の条件式の一部を`${{}}`で囲むと、条件式の内容に関わらず、そのif文は常に真を返してしまいます。

## GitHub Actionsにおける`${{}}`の扱い

GitHub Actionsでは、`${{}}`は式を評価するために使用されます。つまり、`${{}}`で囲まれた部分は文字列としてではなく、式として解釈されます。

例えば、

```yaml
env:
  sample1: endsWith("Schoo", "o")
  sample2: ${{ endsWith("Schoo", "o") }}
```

この場合、`sample1`には`endsWith("Schoo", "o")`という文字列が代入されますが、`sample2`にはBool値の`true`が代入されます。

:::note info
`endsWith()`は文字列が指定した文字列で終わる場合に`true`を返す組み込み関数です。
:::

## if節の正しい書き方

if節の条件式を書く場合、基本的に`${{}}`の記載が必要です。ただし、一部の条件下では省略が可能です。

GitHubのドキュメントによると、

> if 条件の中で式を使う際には、任意で式構文 `${{ }}` を省略できます。これは、GitHub Actions が if 条件を式として自動的に評価するためです。ただし、この例外はどこでも適用されるわけではありません。
_https://docs.github.com/ja/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idif_

つまり、以下のステップは全て同じ挙動になります。

```yaml
steps:
  - name: Test1
    if: ${{ false }}
    run: echo "Test1 Pass"
    
  - name: Test2
    if: "${{ false }}"
    run: echo "Test2 Pass"
    
  - name: Test3
    if: false
    run: echo "Test3 Pass"
```

いずれの場合も、if節は偽と評価されるため、runの項目は実行されません。

特に注意が必要なのは、否定演算子`!`を使う場合です。

> `!` は YAML 形式で予約された表記であるため、必ず `${{ }}` 構文の式を使用するか、式が `!` で始まる場合は `''`、`""`、または `()` でエスケープする必要があります。
_https://docs.github.com/ja/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idif_

以下のTest4、5はTest1~3と同様に動作しますが、Test6はActionsに含まれているだけで構文エラーになります。

```yaml
steps:
  - name: Test4
    if: ${{ !true }}
    run: echo "Test4 Pass"
    
  - name: Test5
    if: "!true"
    run: echo "Test5 Pass"
    
  - name: Test6
    if: !true
    run: echo "Test6 Pass"
```

## 検証

以下のActionsを用意し、実行しました。

```yaml
steps:
  - name: Test7 # 式全体をエスケープ、一部を${{}}でネスト
    if: "${{ false }} == true"
    run: echo "Test7 Pass"
    
  - name: Test8 # 一部を${{}}でネスト
    if: ${{ false }} == true
    run: echo "Test8 Pass"
    
  - name: Test9 # 式全体を${{}}でネスト
    if: ${{ false == true }}
    run: echo "Test9 Pass"
    
  - name: Test10 # 式全体をエスケープ、式の一部を${{}}でネスト、式に否定演算子
    if: "${{ !true }} == true"
    run: echo "Test10 Pass"
    
  - name: Test11 # 式の一部を${{}}でネスト、式に否定演算子
    if: ${{ !true }} == true
    run: echo "Test11 Pass"
    
  - name: Test12 # 式全体を${{}}でネスト、式に否定演算子
    if: ${{ !true == true }}
    run: echo "Test12 Pass"
```
### 実行結果
| step名 | runが実行されるかどうか |
| :--: | :--: |
| Test7 | ◯ |
| Test8 | ◯ |
| Test9 | × |
| Test10 | ◯ |
| Test11 | ◯ |
| Test12 | × |

### わかったこと

1. 条件式の一部だけを`${{}}`で囲むと、予期せぬ結果になります。
   - 例：Test7, 8, 10, 11
2. 条件式全体を`${{}}`で囲むと、正しく評価されます。
   - 例：Test9, 12

## 結論

GitHub Actionsのif節を使う際は、以下のルールを守ることで、人間にとって理解しやすく、かつ予期せぬ結果を避けることができます。

**条件式全体を必ず`${{}}`で囲む。**

このルールを守ることで、以下のメリットがあります。
- 否定演算子の有無に関わらず、式全体を`${{}}`で囲むことでネストし忘れがなくなる。
- 式の一部を`${{}}`で囲んでしまうミスがなくなる。

## 最後に

今回の検証に使用したコードは、以下のリポジトリで公開しています。

https://github.com/schoo-kozai/actions-if-condition-test

GitHub Actionsをより安全に使用する際の参考になれば幸いです。

## 参考資料

https://docs.github.com/ja/actions

https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions

## We're Hiring!

Schooでは一緒に働く仲間を募集しています！

https://corp.schoo.jp/careers

https://corp.schoo.jp/careers/graduates