Goで書かれたアプリケーションをGithub ActionsでCIする

# Github Actionsとは

メタップスアドベントカレンダー8日目の記事です。

私が携わっているプロジェクトでは、CIツールとしてGithub Actionsを利用しています。Github Actionsでは、Jobと呼ばれる小さな単位を組み合わせて、一連の自動化処理であるWorkflowを作成します。Workflowはリモート・リポジトリへのプッシュやプルリクエスト作成をトリガーとして起動します（cronのような書式設定で定期実行もできるようです）。Jobの処理順序や、起動トリガーはYAMLファイルで定義し、所定のディレクトリ（`.github/workflows`）へ設置します。1つのリポジトリには複数のWorkflowを定義可能です。本記事では、プログラミング言語Goのプロジェクトで、ビルドやテストをGithub Actionsを使って自動化してみたので解説を行います。

# 設定ファイルの記述

プロジェクトで使っている設定ファイルを下記に示します。こちらを適当な名前で保存し、　`.github/workflows`　へ保存してpushを行うと、設定に従って処理が行われます。Github Actionsでは、処理が行われるマシンをRunnerと呼びます。Jobは自分で一から作成することができますが、「プロジェクトのチェックアウト」や「ビルド」など、よく使われる処理はGithubにあらかじめ用意されており、Actionと呼ばれます。

```
name: ci

on:
  push:
    branches: [ develop ]
  pull_request:
    branches: [ develop ]

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Go
        uses: actions/setup-go@v3
        with:
          go-version: '>=1.19.0'
          cache: true
          cache-dependency-path: go.sum

      - name: OS Version
        run: cat /etc/os-release

      - name: Go Version
        run: go version

      - name: Go Lint
        uses: golangci/golangci-lint-action@v3

      - name: Setup
        run: |
          go install github.com/golang/mock/mockgen@latest
          go install github.com/onsi/ginkgo/v2/ginkgo@latest
          go get github.com/onsi/gomega/...

      - name: Regenerate mock file
        run: |
          rm -rf mock
          go generate ./...

      - name: Go Build
        run: |
          go build -v ./...

      - name: Go Test
        run: |
          ginkgo -v --cover --coverprofile=coverage.out --race --coverpkg=./... --covermode=atomic --output-dir=/tmp -r test

      - name: Exclude auto-generated files from coverage
        run: |
          cat /tmp/coverage.out | grep -v "_mock.go" | grep -v ".pb.go" > coverage.out
          go tool cover -func coverage.out

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: coverage.out
          fail_ci_if_error: true
          verbose: true
```

## チェックアウト

```yml
      - uses: actions/checkout@v3
```

プロジェクトをRunnerへチェックアウトするには、専用のActionを利用します。

https://github.com/actions/checkout

## Goのインストール

```yml
      - name: Setup Go
        uses: actions/setup-go@v3
        with:
          go-version: '>=1.19.0'
          cache: true
          cache-dependency-path: go.sum
```

GoのセットアップをRunnerに行います。上記の例では、Goのバージョン1.19.0以上の最新版がインストールされます。オプションの詳しい説明は下記をご参照ください。

https://github.com/actions/setup-go

##

```yml
      - name: Go Lint
        uses: golangci/golangci-lint-action@v3
```

Goで書かれたソースコードの静的解析を行います。

https://github.com/golangci/golangci-lint-action

## 必要なツールの追加

```yml
      - name: Setup
        run: |
          go install github.com/golang/mock/mockgen@latest
          go install github.com/onsi/ginkgo/v2/ginkgo@latest
          go get github.com/onsi/gomega/...
```

以下のJob実行に必要なツールを、追加でインストールします。本プロジェクトでは、mockを使った単体テストを行なっています。また、BDDテスティングフレームワークであるginkgoを利用していますので、それぞれ追加しておきます。

## モックファイルの生成

```yml
      - name: Regenerate mock file
        run: |
          rm -rf mock
          go generate ./...
```

mockファイルはinterfaceで定義された仕様に従って生成されますが、仕様を変更するとmockファイルも再生成する必要があります。そのため、仕様を変更したのにmockファイルの再生成を忘れる、といったことが起こりがちです。そちらを防ぐため、このJobでもう一度作成を行います。`go generate` コマンドを利用したmockの作成方法については、こちらをご覧ください。

https://engineering.mercari.com/blog/entry/20210406-gomockhandler/

## プロジェクトのビルド

```yml
      - name: Go Build
        run: |
          go build -v ./...
```

プロジェクト全体をビルドします。Goは静的型付言語ですので、文法エラーのあるプログラムファイルがpushされた場合、この時点で気がつくことができます。

## テスト実行

```yml
      - name: Go Test
        run: |
          ginkgo -v --cover --coverprofile=coverage.out --race --coverpkg=./... --covermode=atomic --output-dir=/tmp -r test
```

テストを実行します。上記の例では、`test`ディレクトリ配下に存在するファイルを、BDDテストフレームワークであるginkgoを使ってテスト実行しています。同時にテストカバレッジを計測し、結果を`/tmp/coverage.out`に保存しています。

## カバレッジ計測に不要な情報を除去

```yml
      - name: Exclude auto-generated files from coverage
        run: |
          cat /tmp/coverage.out | grep -v "_mock.go" | grep -v ".pb.go" > coverage.out
          go tool cover -func coverage.out
```

mockファイルはテストを行うためにあるので、そちらのカバレッジを計測するのは意味がありません。そこで、カバレッジ結果ファイルから、自動生成されたスタブを削除します。gRPC通信のためのスタブファイルのカバレッジ結果も同様に削除します。

## カバレッジをCodecovへアップロード

```yml
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: coverage.out
          fail_ci_if_error: true
          verbose: true
```

テストカバレッジを可視化するCodecovを利用しています。カバレッジをCodecovへのアップロードをしてくれるActionが用意されていますので、そちらを利用します。Codecovを設定すればPull Requestのページ上にカバレッジ率の増減が表示されますので、非常に便利です。

https://github.com/codecov/codecov-action


# まとめ

CIを使うと、コーディング規約違反やテスト漏れといったヒューマンエラーを早期に発見することができます。ソフトウェアの性質上、不具合やミスをできるだけ早く発見することが品質の良いプロダクトを作成する手助けになりますので、めんどくさくてもプロジェクトの開始時などに作成しておくと良いでしょう。