【GitHub Actions】単体テスト自動実行+コメント機能を作ってみた

# はじめに
Jestでの単体テストを導入しているプロジェクトについて、Github Actionsでテスト自動実行を導入してみたので、得た知見をまとめておきます。

テストはプルリク時に実行させています。
また、テストの結果をコメントする機能も付けてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3640003/2999fbea-65b4-f708-792c-9e8e179eecf8.png)

テスト失敗時にはGithub Actionsのジョブ実行結果の詳細画面へのリンクも載せています。

![スクリーンショット 2024-12-09 9.28.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3640003/35d66d8d-68cf-4a32-5c27-873f442c5bdf.png)

リンクをクリックすると、以下の画面に遷移します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3640003/0aee112a-e900-255f-9dac-a57fc2e03f16.png)


# 完成コード

コードは主にGithub Actions公式ドキュメントのものをベースにして書きました。
公式ドキュメントには、他の言語の場合のサンプルも掲載されているので、とても参考になります。

https://docs.github.com/ja/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs

以下が今回作成したGithub Actionsのworkflowのymlファイルです。
実行環境は`Node.js`、パッケージマネージャーは`yarn`、テストフレームワークは`Jest`を使用しています。

```yml
# Jestのテストを実行します
# mainとdevブランチに対して、プルリクエスト時に実行します

name: Run Tests

on:
  pull_request:
    branches:
      - main
      - dev
    types: [opened, reopened, synchronize, ready_for_review]

jobs:
  run-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:

    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Use Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20.10.0'
        cache: 'yarn'
        cache-dependency-path: yarn.lock

    - name: Install dependencies
      run: yarn

    - name: Run tests with Jest
      run: yarn jest

    - name: Post comment on success
      if: ${{ success() }}
      uses: actions/github-script@v7
      with:
        script: |
          const message = "## テスト結果 ✅\nテストが成功しました！✨"
          github.rest.issues.createComment({
            issue_number: context.payload.pull_request.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: message,
          })

    - name: Post comment on failure
      if: ${{ failure() }}
      uses: actions/github-script@v7
      with:
        script: |
          const response = await github.rest.actions.listJobsForWorkflowRun({
            owner: context.repo.owner,
            repo: context.repo.repo,
            run_id: context.runId,
          })
          const currentJob = response.data.jobs.find(job => job.name === context.job)
          const jobDetailUrl = currentJob.html_url
          
          const message = `## テスト結果 ❌\n### ⚠️テストが失敗しました。\n詳細を確認してください。\n[${jobDetailUrl}](${jobDetailUrl})`
          github.rest.issues.createComment({
            issue_number: context.payload.pull_request.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: message,
          })
```

ちなみに、導入したプロジェクトでジョブを実行させたところ、ジョブの実行時間は3~4分といった感じでした。
ただ、依存関係のインストールで2分半程度かかっているので、結局ここ次第だと思います。
（キャッシュが存在しなかった場合、依存関係のインストールで4分、トータルで5~6分程度）

あとは、余裕があればビルドがきちんとできるかを検証するstepがあるとなお良いですね。

# 解説
## トリガーとなるイベントの指定

`main`ブランチと`dev`ブランチに対してのプルリク時に実行させます。
プルリクの作成時、再open時、コードがpushされてプルリク内容に変更が生じた時、`ready for review`となった時にテストを実行させています。

```yml
on:
  pull_request:
    branches:
      - main
      - dev
    types: [opened, reopened, synchronize, ready_for_review]
```

https://docs.github.com/ja/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request


## チェックアウト
最初にリポジトリのコードを取得します。

```yml
- name: Checkout repository
  uses: actions/checkout@v4
```


## Node.jsの使用
次にNode.jsを使えるようにします。

`node-version`には、プロジェクトで使われているバージョンを指定するのが無難かと思います。
また、`npm`や`Yarn`の依存関係をキャッシュすることもできるようなので、`cache: 'yarn'`で`Yarn`の依存関係をキャッシュしています。

```yml
- name: Use Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20.10.0'
    cache: 'yarn'
    cache-dependency-path: yarn.lock
```

`cache-dependency-path`には依存関係が記録されているファイルを指定しています[^1]。
ルートディレクトリにのみ`yarn.lock`（`npm`の場合は`package-lock.json`）がある場合はおそらく省略できると思います。
一方、モノレポなどで依存関係のファイルが複数ある場合は、以下のように指定しておくと良いと思います。

[^1]:[Usage](https://github.com/marketplace/actions/setup-node-js-environment#usage)を見ると、`cache-dependency-path`で指定したファイルからハッシュを生成して、キャッシュのキーとしているようです。

```yml
cache-dependency-path: |
  yarn.lock
  packages/foo/yarn.lock
```

https://docs.github.com/ja/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs#example-caching-dependencies


## 依存関係のインストール
続いてプロジェクトの依存関係をインストールします。
なお、Github Actionsの公式ドキュメントによると、GitHubホストランナーには依存関係マネージャーの`npm`と`Yarn`がインストールされているとのことなので、`Yarn`自体のインストールはここでしていません。

```yml
- name: Install dependencies
  run: yarn
```

https://docs.github.com/ja/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs#installing-dependencies


## テストの実行
ここで、いよいよ`Jest`のテストを実行させます。

```yml
- name: Run tests with Jest
  run: yarn jest
```

## テスト成功時のコメント送信

最終段階として、コメントの送信をしていきます。
まず、`if`を使ってジョブの実行を制御しています。
ステータスチェック関数の`success()`を使って、テストを含むこれまでのステップがすべて成功している場合のみ実行するようにしています[^2]。

[^2]:[公式ドキュメント](https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#status-check-functions)にも記載されていますが、省略した場合は`success()`が適用されるので、ここの`if`は省略可能です。

コメント送信については、公式Actionの`github-script`を使っています。

```yml
- name: Post comment on success
  if: ${{ success() }}
  uses: actions/github-script@v7
  with:
    script: |
      const message = "## テスト結果 ✅\nテストが成功しました！✨"
      github.rest.issues.createComment({
        issue_number: context.payload.pull_request.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: message,
      })
```

`github-script`を使うと、`Github API`（を便利に使えるライブラリである`Octokit`）を直接呼び出せるようになります。
要するに、GitHubの操作が簡単にできるようになります。

https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#status-check-functions

https://github.com/marketplace/actions/github-script

https://octokit.github.io/rest.js/v21/#issues-create-comment

## テスト失敗時のコメント送信

ここでも、`if`を使ってジョブの実行を制御しています。
ステータスチェック関数の`failure()`を使って、これまでのステップのいずれかが失敗した場合にのみ実行するようにしています。
つまりテストが失敗すると、このステップが実行されます。
（厳密には、テスト実行以外のどこかのステップが失敗した場合にも実行されます。）

```yml
- name: Post comment on failure
  if: ${{ failure() }}
  uses: actions/github-script@v7
  with:
    script: |
      const response = await github.rest.actions.listJobsForWorkflowRun({
        owner: context.repo.owner,
        repo: context.repo.repo,
        run_id: context.runId,
      })
      const currentJob = response.data.jobs.find(job => job.name === context.job)
      const jobDetailUrl = currentJob.html_url
          
      const message = `## テスト結果 ❌\n### ⚠️テストが失敗しました。\n詳細を確認してください。\n[${jobDetailUrl}](${jobDetailUrl})`
      github.rest.issues.createComment({
        issue_number: context.payload.pull_request.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: message,
      })
```

`github-script`の部分については、`script`の前半部分でジョブ実行結果の詳細画面へのリンクを取得し、後半部分でコメント送信をしています。

https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#failure

https://octokit.github.io/rest.js/v21/#actions-list-jobs-for-workflow-run


# その他

作業中につまずいた点がいくつかあったので、メモとして残しておきます。

- ディレクトリを移動した場合でも、次のステップ開始時にはカレントディレクトリがリセットされる
- トリガーとなるイベントに`pull_request`を指定した場合、プルリク元かプルリク先のブランチにworkflowのymlファイルが存在しないと実行されない[^3]
- モノレポでローカルのパッケージを使用する場合、（私のプロジェクトでは）以下のようなビルドの実行が必要だった

```yml
- name: Install dependencies
  run: yarn

- name: Build local packages
  run: |
    yarn workspace bar rollup -c
    yarn workspace baz rollup -c

- name: Run tests with Jest
  run: |
    cd packages/foo
    yarn jest
```

[^3]:ただし、[公式ドキュメント](https://docs.github.com/ja/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)に記載されている大半のイベントについては「ワークフローファイルがデフォルトブランチにある場合にのみワークフローの実行をトリガー」するみたいなので、デフォルトブランチにもファイルを置いておくのが定石かもしれないです。