Help us understand the problem. What is going on with this article?

【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を試してみましょう。

私が研究したリポジトリです。
スマホからはActionsタブが見えないのでPCから御覧ください。

GitHub Actions v2とは

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

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

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

私達はこれから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の成果物をアーティファクトと呼びます。
ここでは2つライセンス認証に必要なALFファイルをアーティファクトとして出力しましょう。

適当にCreateALFと命名してリポジトリを作ります。
そして .github/workflows以下にCreateLicenseALF.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を1つだけ定義しています。
container: docker://gableroux/unity3d:${{ matrix.unity-tag }}はDocker Hubの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

ULFファイル入手

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

ULFファイルを使ってUnityの手動アクティベートを行うのですが、これをそのまま公開リポジトリに置くのは不味いです。
参考文献とかでは暗号化してそれを復号していました。

私はプライベートリポジトリを用意して、そこにulfをアップロードしopensslに暗号化してもらい、暗号化処理後のファイルをアーティファクトに出力しました。(もはやCygwin不要なのでは?)
image.png

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

最小構成で走らせる

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テストを行う

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

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に結果報告してみましょう。
ワークフローは次のようになります。

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してきます。

- 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データを投げて結果報告を行います。

- 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 }}
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=$?で握りつぶします。
$?は直前のシェルスクリプトのリターンコードを表す特殊変数です。

curl -X POST -H 'ContentX-type:application/json' --data "$(cat slackJson)" $SLACK

slackJsonの内容をcatで読み出して、SlackのIncoming WebHookにPostで投げます。
Incoming WebHookの仕様についてはこちらをご覧ください。

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

Jobを2つに分けた理由

containerをgableroux/unity3dにしたJobではcurlコマンドを使用できなかったからです。

Slackから見た結果

image.png

ちなみにわざと失敗するテストのコメントアウトを解除した場合
image.png

青いリンクをクリックするとこのような画面に飛びます。
image.png

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

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

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

GitHub Actionの仮想環境は公開されています。
Unity Cloud Buildのそれはちょっとよくわかりませんでした。

感想兼宣伝

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

UniNativeLinqのEditorModeテストを手元のPCだと現実的時間で行えないのでクラウドの力で解決しようと思います。
UniNativeLinqが完成したらよろしくお願いします。
マネージドヒープに対するアロケーションがゼロなNativeArray<T>に対するLINQライクなライブラリです。

参考文献

CircleCIでUnityをテスト/ビルドする、或いは.unitypackageを作るまで
CircleCIでUnityのTest&Buildを雰囲気理解で走らせた

謝辞

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

pCYSl5EDgo
C#!好き!!(語彙不足) Unityやヴァーレントゥーガ関連の投稿をしていく予定です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした