【Unity】GitHub Actions v2でUnity Test Runnerを走らせて、結果をSlackに報告する【入門】

9月24日、象のインディラが上野動物園に到着した日には初投稿です。

#はじめに
GitHub Actionsがパブリックベータ版となってしかもCI/CD機能を手に入れました。
CIをUbuntu上で動作させることが可能です。
ならば当然Unity Test RunnerをGitHub Actionsで動かしたいものです。

長いものには巻かれろと言いますし、今の所最も無料範囲でできることが多いCI/CD環境であるGitHub Actionsを試してみましょう。

私が研究した[リポジトリ](https://github.com/pCYSl5EDgo/UnityGithubActionsExample)です。
スマホからはActionsタブが見えないのでPCから御覧ください。

#GitHub Actions v2とは

パブリックリポジトリ完全無料のWindows/Mac/Ubuntuの3環境で動作するCI/CDサービスです。
プライベートリポジトリも１リポジトリ当たり月2000分まで無料です。
20ジョブまで並列にCIできます。
ヤバイですね。

なおUnityをCIするのに一番楽な環境はDockerがまともに動くLinux環境です。（Unity2019.3b4からIL2CPPビルドも可能です。全方面最強にとうとう進化したUnity Linux Editorは最高だぜ。）

#最初の一歩：ALFファイル生成

私達はこれから[gableroux/unity3d](https://hub.docker.com/r/gableroux/unity3d/)というLinuxのDockerコンテナ上で作業します。

ローカルにDocker環境がある方はそこで作業すると良いのですが、たとえば私のような **Windows 10 HomeユーザーはDocker for Windowsをインストールできません**。

そういう方もGitHub Actionsなら適当なプライベートリポジトリを作成してそこで作業すれば大丈夫です。
**プライベートリポジトリも月2000分まで無料なので安心してパスワードを含んだ作業ができますね。**


さて、Unityはどんな使い方をするのにもライセンス認証が必須なソフトです。
Unityはインターネットが使えない状況でもライセンス認証を行える仕組みとして、「オフライン/ 手動アクティベーション」という仕組みを用意しています。（参考URL： https://docs.unity3d.com/ja/2018.1/Manual/ManualActivationGuide.html ）
GitHub Actionsは普通にポートが開放されてネット接続もできますし、パスワードやユーザー名も普通に隠せますのでわざわざオフライン/手動アクティベートする必要も薄いのでは？と思いますが、先例にならってオフライン/手動アクティベートしましょう。

## リポジトリ作成

GitHub ActionsではCIの成果物をアーティファクトと呼びます。
ここでは２つライセンス認証に必要なALFファイルをアーティファクトとして出力しましょう。

適当に[CreateALF](https://github.com/pCYSl5EDgo/CreateALF)と命名してリポジトリを作ります。
そして .github/workflows以下にCreateLicenseALF.yamlを作ります。
以下のように中身を書きます。

```yaml:CreateLicenseALF.yaml
name: Create ALF File

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        unity-tag: [2018.4.9f1, 2019.2.5f1]
    
    container: docker://gableroux/unity3d:${{ matrix.unity-tag }}
    
    steps:
    - run: mkdir artifact
    - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -logfile -createManualActivationFile || exit 0
    - run: cp "Unity_v$UNITY_VERSION.alf" artifact
      env:
        UNITY_VERSION: ${{ matrix.unity-tag }}
    - uses: actions/upload-artifact@master
      with:
        name: ${{ matrix.unity-tag }}
        path: artifact
```

buildというJobを１つだけ定義しています。
container: docker://gableroux/unity3d:${{ matrix.unity-tag }}はDocker Hubの[gableroux/unity3d](https://hub.docker.com/r/gableroux/unity3d/)からTagが matrix.unity-tag なimageを元にしてJobを走らせるという意味です。
こうすることで私達はUnity Linux EditorをGitHub Actionsで利用することができます。

そしてsteps以下で実際にshellを叩いていきます。

 - mkdir artifact
  - alfファイルを出力する先のフォルダを作ります。
  - GitHub公式のactions/upload-artifactを安全に使うにはワーキングディレクトリ直下に新しくフォルダを作り、そこを利用するべきです。
  - 適当なActionで生成されたフォルダをアーティファクトにしようとするとできないことがあります。
 - /opt/Unity/Editor/Unity -quit -batchmode -nographics -logfile -createManualActivationFile || exit 0
  - /opt/Unity/Editor/UnityがUnityエディタに実行ファイルです。
  - これにコマンドライン用の引数を渡して手動アクティベート用のファイルを出力させます。
  - || exit 0 は、Unityが何故か正常に動作してもリターンコードが1を出しますので、それを握り潰すための処理です。
  - GitHub Actionsでは特に設定をしない限り全てのstepにおいてリターンコードが0でないとJobが即死します。
 - cp "Unity_v$UNITY_VERSION.alf" artifact
  - artifact以下に移しています。
 - uses: actions/upload-artifact@master
  - usesのwithは関数に対する引数ですね。
  - ここでは入力フォルダと出力アーティファクトの名前を設定しています。

はい！　おしまい！
合計2分22秒でalfファイルができあがりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256047/20703a9a-4b9f-b3eb-1704-35120e9a4d71.png)

#ULFファイル入手

https://license.unity3d.com/manual を開いてアーティファクトから落としてきたalfファイルをアップロードして質問に答えます。そうするとulfファイルをダウンロードできるようになりますので保存しましょう。

ULFファイルを使ってUnityの手動アクティベートを行うのですが、これをそのまま公開リポジトリに置くのは不味いです。
[参考文献](http://neue.cc/2019/04/08_574.html)とかでは暗号化してそれを復号していました。

私はプライベートリポジトリを用意して、そこにulfをアップロードしopensslに暗号化してもらい、暗号化処理後のファイルをアーティファクトに出力しました。（もはやCygwin不要なのでは？）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256047/2fb3c99f-2787-adc5-13a1-25754f77ae48.png)

```yaml:CreateULF.yaml
name: Create Unity License File

on:
  push:
    branches:
    - master

jobs:
  job:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@master
      - run: mkdir -p artifact
      - run: openssl aes-256-cbc -e -in Unity_v2018.x.ulf -out artifact/Unity_v2018.x.ulf-cipher -k ${{ secrets.cypherkey }}
      - run: openssl aes-256-cbc -e -in Unity_v2019.x.ulf -out artifact/Unity_v2019.x.ulf-cipher -k ${{ secrets.cypherkey }}
      - uses: actions/upload-artifact@master
        with:
          name: ciphers
          path: artifact
```

今回 `${{ secrets.cypherkey }}`という見慣れない構文が出てきましたね。
Settings>SecretsでKey-Valueのペアを定義できます。
そしてその値を `${{ secrets.キー名 }}`としてGitHub Actionsから扱うことができます。
Valueは複数行も扱えますのでulfファイルの中身をここに書いて、それをGitHub Actionsのstepで`echo ${{ secrets.ulf }} > ulf` なりする方が素直な気がします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256047/5f82fc60-f529-f3bd-a87e-ab5fd4307649.png)

#最小構成で走らせる

```yaml:minimum.yaml
name: UnityTestRunnerExampleSlack

on:
  push:
    branches:
    - master

jobs:
  editorTestJob:
    runs-on: ubuntu-latest
    container: docker://gableroux/unity3d:${{ matrix.unity-tag }}
    strategy:
      matrix:
        unity-tag: [2018.4.9f1]

    steps:
    - uses: actions/checkout@master
    - run: openssl aes-256-cbc -d -in Unity_v2018.x.ulf-cipher -k ${CYPHER_KEY} >> /Unity_v2018.x.ulf
      env:
        CYPHER_KEY: ${{ secrets.cypherkey }}
    - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile /Unity_v2018.x.ulf || exit 0
```

「-manualLicenseFile ライセンスファイルのパス」で手動アクティベートします。
一度手動アクティベートすれば以後同一コンテナで作業をする限り再アクティベートは不要だそうです。
そしてエディタGUIが起動されても困るので-nographicsと-batchmodeと-quitを渡してあげましょう。
そして訳がわからないUnityの挙動なのですが、ライセンス認証を正常に処理してもリターンコード1が返ってきます。
|| exit 0で握りつぶしてあげましょう。

これで手動アクティベートまでは完了です！

#EditModeテストを行う

下記のようなテストを用意します。
適当なテストですのでパスするもの２つとわざと失敗させるもの１つですね。

```csharp
using NUnit.Framework;
namespace Tests
{
    public class NewTestScript
    {
        [Test] public void IsTrueTest() => Assert.IsTrue(true);
        [Test] public void IsFalseTest() => Assert.IsFalse(false);
        //[Test] public void FailProof() => Assert.IsTrue(false);
    }
}
```

そしてこれをGitHub Actionで実行し、成功したか否かをSlackに結果報告してみましょう。
ワークフローは次のようになります。

```yaml
name: UnityTestRunnerExampleSlack

on:
  push:
    branches:
    - master

jobs:
  editorTestJob:
    runs-on: ubuntu-latest
    container: docker://gableroux/unity3d:${{ matrix.unity-tag }}
    strategy:
      matrix:
        unity-tag: [2018.4.9f1]

    steps:
    - uses: actions/checkout@master
    - run: mkdir -p path/to/artifact
    - run: openssl aes-256-cbc -d -in Unity_v2018.x.ulf-cipher -k ${CYPHER_KEY} >> /Unity_v2018.x.ulf
      env:
        CYPHER_KEY: ${{ secrets.cypherkey }}
    - run: /opt/Unity/Editor/Unity -manualLicenseFile /Unity_v2018.x.ulf -batchmode -nographics -quit || exit 0
    - run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -logFile -projectPath . -runEditorTests -editorTestsResultFile path/to/artifact/results.xml || exit 0
    - uses: actions/upload-artifact@master
      with:
        name: test_results
        path: path/to/artifact
  reportResultJob:
    runs-on: ubuntu-latest
    needs: editorTestJob
    steps:
    - uses: actions/download-artifact@master
      with:
        name: test_results
        path: artifact
    - run: git clone https://github.com/pCYSl5EDgo/NUnitXmlReporter.git
    - uses: actions/setup-dotnet@v1.0.2
      with:
        dotnet-version: '3.0.100'
    - name: report result to slack
      run: |
        cd NUnitXmlReporter
        dotnet run ../artifact/results.xml ../slackJson --slack-block  $GITHUB_REPOSITORY $GITHUB_SHA || INPUT_RESULT=$?
        cd ..
        curl -X POST -H 'ContentX-type:application/json' --data "$(cat slackJson)" $SLACK
        exit $INPUT_RESULT
      env:
        SLACK: ${{ secrets.slackhook }}
```

##テスト実行Job editorTestJob
**run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -logFile -projectPath . -runEditorTests -editorTestsResultFile path/to/artifact/results.xml || exit 0**
手動アクティベート直後にEditorModeTestを行い、そのテスト結果をresults.xmlに格納しています。
テストを全パスしたらリターンコードは0ですが、何か失敗したら139とか2とか返されます。
とはいえ、今回はresults.xmlをアーティファクトにして後から見れるようにしたいので、ひとまず || exit 0で握りつぶします。

##Slack結果報告Job reportResultJob
GitHub ActionのJobは基本設定では全て並列実行されます。Job間に順序関係を構築したい場合はneedsを使用します。
``` needs: editorTestJob```
という風に記述することでeditorTestJob完了後にreportResultJobが実行開始されます。

###actions/download-artifactでアーティファクトをダウンロードします。
今回はartifactフォルダを作成してそこにresults.xmlをダウンロードしてきます。

そしてSlackに報告するためにxmlをいい感じにjson化するためのdotnet coreプロジェクトをgit clone [https://github.com/pCYSl5EDgo/NUnitXmlReporter.git](https://github.com/pCYSl5EDgo/NUnitXmlReporter)してきます。

```yaml
- uses: actions/setup-dotnet@v1.0.2
  with:
    dotnet-version: '3.0.100'
```

###dotnet core 3.0をインストールします。

dotnet coreをインストールするGitHub公式のActionであるaction/setup-dotnetは、 **最新版でインストールするとdotnet core 3.0でdotnet runするとエラーを吐いて即死します。**

v1.0.2を使いましょう。

###xmlをいい感じに解釈し、Slackにcurlでjsonデータを投げて結果報告を行います。

```yaml
- name: report result to slack
    run: |
    cd NUnitXmlReporter
    dotnet run ../artifact/results.xml ../slackJson --slack-block  $GITHUB_REPOSITORY $GITHUB_SHA || INPUT_RESULT=$?
    cd ..
    curl -X POST -H 'ContentX-type:application/json' --data "$(cat slackJson)" $SLACK
    exit $INPUT_RESULT
    env:
    SLACK: ${{ secrets.slackhook }}
```

```bash
dotnet run ../artifact/results.xml ../slackJson --slack-block  $GITHUB_REPOSITORY $GITHUB_SHA || INPUT_RESULT=$?
```
これはgit cloneしてきたNUnitXmlReporterにxmlファイルを渡し、変換後のjsonをslackJsonに格納しています。
その際にリポジトリのURLとコミットのSHAを渡すことでいい感じにSlackにリンクを結果報告できるようにしています。
results.xmlを読むと全パスしたのか失敗したのかわかりますので、リターンコードでそれを表現しています。
ただ、この段階でCIにコケられるとSlackに報告できないので、一旦INPUT_RESULT=$?で握りつぶします。
$?は直前のシェルスクリプトのリターンコードを表す特殊変数です。

```bash
curl -X POST -H 'ContentX-type:application/json' --data "$(cat slackJson)" $SLACK
```
slackJsonの内容をcatで読み出して、SlackのIncoming WebHookにPostで投げます。
[Incoming WebHookの仕様についてはこちらをご覧ください。](https://api.slack.com/incoming-webhooks)

$SlackはIncoming WebHookのURLです。
Slackによると公開してはいけないそうなのでこれもまたSecretsに登録しています。

###Jobを2つに分けた理由
containerを[gableroux/unity3d](https://hub.docker.com/r/gableroux/unity3d/)にしたJobではcurlコマンドを使用できなかったからです。

##Slackから見た結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256047/0ea2e2b1-d290-3657-1865-e8264d441618.png)

ちなみにわざと失敗するテストのコメントアウトを解除した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256047/42583a3a-04b2-806b-1c88-1a0a351a6902.png)

青いリンクをクリックするとこのような画面に飛びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256047/ab719c0e-a6e2-64d1-26a2-6ca3e06026d8.png)

#Github ActionsでUnityビルドはプロダクトで使えるか

20ジョブまで並列で実行できるのでどうでしょう。結構行けるのではでは。

一回構築さえすれば、yamlも案外イケル口だと思います。
Unity Cloud Build一度も使ったことがないのでなんとも言えないですが、Linux EditorでIL2CPPビルドできるようになりましたし、PlayModeテストに関しては@neuecc先生の[RuntimeUnitTestToolkit](https://github.com/Cysharp/RuntimeUnitTestToolkit)もありますし、GitHub Actionで全部良いんじゃないかな……という気がします。

[GitHub Actionの仮想環境は公開されています。](https://help.github.com/ja/articles/virtual-environments-for-github-actions)
Unity Cloud Buildのそれはちょっとよくわかりませんでした。

#感想兼宣伝
CI始めて3日目の初心者故、間違っている所もあるでしょう。ご指摘いただけると幸いです。

[UniNativeLinq](https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension)のEditorModeテストを手元のPCだと現実的時間で行えないのでクラウドの力で解決しようと思います。
[UniNativeLinq](https://github.com/pCYSl5EDgo/UniNativeLinq-EditorExtension)が完成したらよろしくお願いします。
マネージドヒープに対するアロケーションがゼロなNativeArray&lt;T&gt;に対するLINQライクなライブラリです。

#参考文献
[CircleCIでUnityをテスト/ビルドする、或いは.unitypackageを作るまで](http://neue.cc/2019/04/08_574.html)
[CircleCIでUnityのTest&Buildを雰囲気理解で走らせた](https://qiita.com/MizoTake/items/2d822e85d33bee359f98)

#謝辞
@SnowCait setup-dotnetの罠と回避法v1.0.2についてご教授いただきありがとうございました。
@mao_ Unity Linux EditorでIL2CPPビルドできることを教えてくださりありがとうございました。