GitHubActionsでマスタチェックを自動化した話

# はじめに
GitHubActionsでワークフローを書く機会がそれほどないので、忘れないように記事化しておきたいと思い立ちました。
チェックやテストの自動化の情報はなかなかまとまっているものが見つけられず、調査が大変だったので、この機会にまとめたいと思います。

本記事はGitHubActionsのワークフローの話で、どのようにマスタチェックをするかという話は詳しく書かないのでご了承ください。

# マスタデータとは
念の為解説を書きますが、知っている方はスキップしていただいて大丈夫です。
マスタデータとは、ユーザーによって変わらない静的なデータをまとめたもので、クライアントとサーバー両方とも同じデータを参照します。
エクセル・スプレッドシートやCSVで複数のファイルで表形式で管理することが多いです。

# 経緯
マスタデータの更新方法は様々なやり方があると思いますが、今回はcsvファイルをgit管理下において、プルリクでマージし、マスタデータを更新するというフローを前提で話していきます。
このフローでは、正常ではないデータがマージされてしまい、サーバーとクライアントがエラーで止まり確認が進められなくなる状況が発生することが考えられます。
そこで、プルリクの段階でチェックが通らなかった場合、マージをブロックするという仕組みを考えました。

# 環境
.Net Core(C#)でアプリを作成し、それをGitHubActionsのLinux環境で実行してマスタデータをチェックします。
Pythonなどのインタプリタ言語でチェッカーを作成するのも良いと思いますが、クライアントがUnityC#を使っているので、同じコードやクラスが再利用できる点がよいと思いC#を選択しました。
(C#大統一論も叫ばれてますしね)

# ワークフロー
GitHubActionの基本的な型としては以下のようになります。

```yaml
name: [ワークフロー名]

# 実行条件
on:
  pull_request:

# ジョブの中身
jobs:
  # ひとかたまりのジョブを定義(別のジョブになると環境も立て直しになる)
  Job1:
    # 実行する環境を指定
    runs-on: ubuntu-20.04
    # ジョブの中で実行する処理をステップで定義
    steps:
      - uses: actions/checkout@v3
      - name: Hello1
        run: echo Hello1
      - name: Hello2
        run: echo Hello2
  Job2:
    steps:
      - name: Ciao
        run: echo ciao
```

ワークフローはすべて記載せず、１ステップごとにピックアップして解説していきます。

## 基本的なフロー
基本的なフローとしては、プルリクが作成された時と、更新された時に不正なデータがないかチェックを自動で回して、マージ可能かどうかを判定します。

### トリガーの指定
GitHub側の設定は後ほど記載しますが、プルリクが作成された・更新されたときのワークフローを実行させたいときは以下のようになります。

```yaml
on:
  pull_request:
```

**その他Triggerのドキュメント**

https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows

### Checkout

デフォルトの状態では、リポジトリの中身が入っていないので、GitHubが用意したcheckoutアクションを実行する必要があります。

```yaml
    steps:
      - uses: actions/checkout@v3
```

usesは他者が用意した処理を実行するためのものと思っていただければ良いかなと思います。

### マスタチェック実行

```yaml
      - name: Master Check
        run: |
          [チェッカー実行コマンド]
```

複数行のコマンドを実行する際は、runの後に|(パイプ)を入れます。
GitHubActionに実行失敗を伝えるために、成功したときは終了コード0を返し、失敗したときはそれ以外の数字をアプリ側でreturnする必要があります。

### 失敗した理由をプルリクのコメントに追加

```yaml
      - name: Comment if failure
        if: ${{ failure() }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.html_url }}
        run: |
          gh pr comment -F [チェッカーが書き出したログファイルのパス] "${URL}"
```

>if: ${{ failure() }}

こちらで以前のステップで失敗したものがあるかどうかを判定できて、失敗していた場合はそのステップを実行してそれ以外のときはしません。
ifが書かれているstepを判定するだけで、ifが書かれた場所から下すべてに条件がかかるわけではないので注意点です。

ログファイルが生成されていない可能性もあるので、こちらは適宜判定が必要です。

## その他の細かいフロー
基本的のフローは以上ですが、その他に必要な細かい処理があるのでこちらでまとめて書いていきます。

### マスタデータに差分があるかどうかのチェック

GitHubActionsは好きなだけ実行できるわけではなく、privateリポジトリであれば時間制限や追加料金などがかかってくるので、
不要なときはチェックしないようにして、時間を短縮する必要があります。

トリガーで変更を監視して、実行するかどうかを判定することができますが、

```yaml
on:
  pull_request:
    paths:
      - path/to/master/data
```

オープン状態のプルリクのブランチが更新された際に、指定されたパスのファイルに更新がなかった場合、実行されません。

後述でGitHub側でマージ前のテストを設定するのですが、何かしら更新があった時に実行されないと、いつまで立ってもマージが許可されません。
なので、独自に対象フォルダ内のファイルが、マージ先のブランチと比較して差分があるかどうかを判定する必要があります。

判定するステップは以下のとおりです。

```yaml
      - name: Check Diff
        id: diff
        run: |
          git fetch origin ${{ github.base_ref }} --depth=1
          echo "changed-tsv=$(git diff --name-only origin/${{ github.base_ref }} HEAD --relative path/to/master/data/ | wc -l)" >> $GITHUB_OUTPUT
```

「github.base_ref」は、トリガーがpull_requestの場合、マージ先のブランチ名が入ってきます。
「--depth=1」でブランチの先頭のコミットのみを取得して、fetch時間を短縮しています。
反対に、「github.head_ref」を指定すると、マージするブランチ名が取得できます。

次のステップを実行するかどうかの判定で、次のステップで変数を参照できるようにする必要があります。
ステップにはidをつけることができます。(今回はdiffにしています)

変数に値をセットする構文としては以下のように書きます。
```
echo "[変数名]=[セットしたい値]" >> $GITHUB_OUTPUT
```

後のステップでは以下のようにして変数にアクセスすることができます。
```
${{ steps.[ステップID].outputs.[変数名] }}
```

今回の例で実行しているgitコマンドは
```
git diff --name-only origin/${{ github.base_ref }} HEAD --relative path/to/master/data/ | wc -l
```

マージ先のブランチと現在のブランチのマスタデータの差分のファイル名のみ取得して、**wc -l**で列の数を出力するようにしています。

以降のステップ以降に以下を追記すると、そのステップは差分がない場合は実行されなくなります。

```
if: ${{ steps.diff.outputs.changed-tsv != '0' }}
```

### アプリのビルド(.NET Core)
事前にビルドしたアプリを、git管理化に入れておけばこちらのステップは必要ないのですが、私の環境ですと何故かGitHubActionsのUbuntu環境でMacで事前にビルドしたアプリを実行することができなかったのでワークフロー内でビルドすることにしました。
(MacのDocker環境で構築したUbuntuでは実行できた)

まずは、.NETを使用できる環境を作る必要があります。
```yaml
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 6.0.x
```

次にアプリのビルドです。
```yaml
      - name: Build MasterChecker
        run: dotnet build [.NETプロジェクトのパス(slnファイルがあるところ)] -c release -o [ビルド物の出力先]
```

特に難しいことはないですが、このビルド方法の場合、アプリを実行するのに.NET環境が必要になります。

### ビルド済みアプリのキャッシュ
毎回ビルドしていたら、利用可能時間が削られてしまうのと、時間がかかるのでキャッシュを生成したほうが良いです。

調べたところキャッシュはマージ先のブランチで作成しないとキャッシュされないようでしたので、キャッシュはマージされた時に作るようにしました。
マスタチェックのワークフローとは別に、キャッシュ用のワークフローを作成します。
```yaml
name: Create Master Checker Cache

on:
  push:
    branches:
      - main
    paths:
      - [再ビルドが必要と判定するためのパス]

jobs:
  create_master_checker_cache:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET Core SDK
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 6.0.x
      - name: before cache
        run: |
          mkdir -p [ビルド先のパス]
      - name: build cache
        uses: actions/cache@v3
        id: build-cache
        with:
          path: [ビルド先のパス]
          key: ${{ github.ref_name }}-${{ hashFiles('[ハッシュチェックをするファイル(アプリメインのdllなど)]') }}
      - name: Build MasterChecker
        run: dotnet build [.NETプロジェクトのパス(slnファイルがあるところ)] -c release -o [ビルド先のパス]
```

マージされたときのトリガーはpushになるので、今回の場合だとmainブランチがpushされた時と設定しています。
また、複数プロジェクトが入っているリポジトリの場合は対象のディレクトリ内で変更があった時に実行するようにパスを指定します。

一番重要なステップは以下の部分です。
```yaml
      - name: build cache
        uses: actions/cache@v3
        id: build-cache
        with:
          path: [ビルド先のパス]
          key: ${{ github.ref_name }}-${{ hashFiles('[ハッシュチェックをするファイル]') }}
```

actions/cache@v3はgithubが用意したアクションで、ワークフロー内で生成したものをキャッシュすることができます。
node moduleや、unityのライブラリフォルダなど、git管理化には置いていないパッケージ等の依存関係があるファイルをキャッシュしておくことなどにも使えます。
ワークフロー実行するたびに、環境がリセットされるのでこのようなファイルをキャッシュしておくことで、実行時間を削減することができます。

pathで指定しているものが、キャッシュ対象のディレクトリです。

重要なパラメータはkeyで、こちらでキャッシュを適用するかしないかが決まります。
キャッシュはディクショナリ(Map)のように、keyごとで管理されています。

今回は現在のブランチ名とファイルのハッシュ値のペアをkeyにしています。
トリガーがpushの場合は、「github.ref_name」はpushされたブランチ名になります。
ハッシュチェックをするファイルは、何かしらのcsファイルが変更された時に違うキャッシュを使いたいという場合には、
```yaml
'**/*.cs'
```
のように指定します。

こちらのアクションを呼び出しておけば、
指定されたkeyのキャッシュがあれば適用し、なければ、すべてのステップが終了した後にキャッシュが作成されます。
なので、キャッシュが存在しない時にビルドをかけたい場合は、キャッシュアクションの後に以下のようなステップを入れることで、
```yaml
      - name: Build MasterChecker
        if: ${{ steps.build-cache.outputs.cache-hit != 'true'}}
        run: dotnet build [.NETプロジェクトのパス(slnファイルがあるところ)] -c release -o [ビルド先のパス]
```

キャッシュが作成され、次回利用することができます。

steps.build-cache.outputs.cache-hitのbuild-cacheはステップidなので、ワークフローによって変わる部分です。

# GitHub側の設定
マスタチェックが失敗した時にプルリクのマージをブロックしたい場合は、GitHubの設定が必要になります。
リポジトリのAdmin権限が必要です。

Settings/BranchesのBranch protection rulesで、チェック対象のブランチのルールに以下の設定を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188476/f4293d6f-bcd1-bc94-9270-11c648f63020.png)

ワークフロー名ではなく、判定したいジョブ名で検索をかけてStatus checks that are requiredに追加します。

以上で次回のプルリクから、マージチェックが入るようになります。

# 最後に
GitHubActionsは確認が大変なので、トライ＆エラーに結構時間を取られてしまいました。
しかし、慣れて使いこなせるように慣れば、かなり効率化を図れるサービスだと思います。
ビルド処理に関しては、Linux,Windows,Macと各種OSが利用できるので、マルチプラットフォームツールのビルドにはかなり役に立つという印象です。
マスタチェックを例に記事を書きましたが、何かGitHubActionsでやろうと思っている方の参考になれば幸いです。