🏋️

GitHub Actionsで変更のあるパッケージだけGoテストを走らせる

2024/03/19に公開

TL;DR

  • 一部パッケージの自動テストにおいてdockertestを導入しているため、コード変更があるたびに全てのパッケージのGoテストを行うとWorkflowの実行に時間がかかる。
  • git diffを活用して、マージ先のブランチから変更のあるパッケージを取得し、そのパッケージのみのテストを実行することで自動テストのWorkflowにかかかる時間を短縮した。
  • 詰まりどころ: git diffで取得しできる変更のあるパッケージ一覧が複数行の文字列になるため、適切にフォーマットしてWorkflowの環境変数に格納する必要があった。

背景

私の参加しているプロジェクトでは、dockertestを用いることで、RDBMSやRedisといったインフラ環境に関わるテストをモックコードを使わずに行なっていました。
dockertestを用いると、テスト環境に必要なコンテナの立ち上げから削除までをコードベースで管理できるようになるため、本番に近い環境でのテストを気軽に行えるようになります。
dockertestの詳細な使い方は他の記事を参照してください。

一方で、dockertestを使用しているパッケージのテストでは、コンテナの起動を伴うためテストの実行に時間がかかるという問題も抱えていました。

func TestMain(m *testing.M) {
    // docker containerを立ち上げるのに時間がかかる.
    pool, err := dockertest.NewPool("")
    ...
}

もちろん、重要なパッケージにおいては時間をかけてでも正確にテストを行うべきですが、些細な変更やdockertestを使用していないパッケージの変更の際には自動テストの実行を高速化したいというモチベーションがありました。

やったこと

git diffコマンドを使用してマージするブランチとHEADブランチ間の差分を取得し、変更のあったパッケージに対してのみ自動テストを実行するようにしました。

最終的な設定ファイルは以下のようになります。

name: Test

on:
  pull_request:
    paths:
      - 'cmd/**.go'
      - 'internal/**.go'
      - 'go.mod'
      - 'go.sum'

env:
  GO_VERSION: 1.22

jobs:
  test:
    name: test
    runs-on: ubuntu-latest
    services:
      dind:
        image: docker:23.0-rc-dind-rootless
        ports:
          - 2375:2375
    defaults:
      run:
        working-directory: .
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v4
        with:
          go-version: ">=${{ env.GO_VERSION }}"
      - name: Get changed packages
        id: get_changed_packages
        run: |
          git fetch origin ${{ github.base_ref }}
          {
            echo "changed_packages<<\n"
            git diff origin/${{ github.base_ref }} HEAD --name-only | grep -E 'cmd|internal' | sed -E 's|/[^/]+$||' | sort -u
            echo "\n"
          } >> $GITHUB_OUTPUT
      - name: Test with Docker
        run: |
          readarray -t packages <<EOF
          ${{ steps.get_changed_packages.outputs.changed_packages }}
          EOF

          if [ -z ${{ steps.get_changed_packages.outputs.changed_packages }} ]; then
            echo "No changes in cmd or internal"
          else
            for pkg in "${packages[@]}"; do
              go test -v ./$pkg/...
            done
          fi

1番のポイントは差分を取得し、変更のあるパッケージに対してだけテストを実行する以下の部分です。

  - name: Get changed packages
    id: get_changed_packages
    run: |
      git fetch origin ${{ github.base_ref }}
      {
        echo "changed_packages<<\n"
        git diff origin/${{ github.base_ref }} HEAD --name-only | grep -E 'cmd|internal' | sed -E 's|/[^/]+$||' | sort -u
        echo "\n"
      } >> $GITHUB_OUTPUT
  - name: Test with Docker
    run: |
      readarray -t packages <<EOF
      ${{ steps.get_changed_packages.outputs.changed_packages }}
      EOF

      if [ -z ${{ steps.get_changed_packages.outputs.changed_packages }} ]; then
        echo "No changes in cmd or internal"
      else
        for pkg in "${packages[@]}"; do
          go test -v ./$pkg/...
        done
      fi

実装の際のつまりどころとしては、git diff ...以下のコマンドで差分のファイル一覧を取得し、grepなどで成形して得られるパッケージ一覧が改行文字\nで区切られた複数行の文字列になるため、Workflowの環境変数に設定する際に注意が必要であるという点です。

ドキュメントによると、複数行の値をWorkflowの環境変数に設定する際には

{name}<<{delimiter}
{value}
{delimiter}

という形式で値を設定する必要があるようでした。

今回の改行文字で区切られた複数行の文字列のケースでは、

{
    echo "changed_packages<<\n"
    git diff origin/${{ github.base_ref }} HEAD --name-only | grep -E 'cmd|internal' | sed -E 's|/[^/]+$||' | sort -u
    echo "\n"
} >> $GITHUB_OUTPUT

のような形でWorkflowの環境変数に設定することでうまくいきました。

まとめ

複数行の出力をWorkflowの環境変数に設定する際には適切にフォーマットする必要あり!

参考

GENDA

Discussion