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を作ります。
以下のように中身を書きます。
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ファイルができあがりです。
#ULFファイル入手
https://license.unity3d.com/manual を開いてアーティファクトから落としてきたalfファイルをアップロードして質問に答えます。そうするとulfファイルをダウンロードできるようになりますので保存しましょう。
ULFファイルを使ってUnityの手動アクティベートを行うのですが、これをそのまま公開リポジトリに置くのは不味いです。
参考文献とかでは暗号化してそれを復号していました。
私はプライベートリポジトリを用意して、そこにulfをアップロードしopensslに暗号化してもらい、暗号化処理後のファイルをアーティファクトに出力しました。(もはやCygwin不要なのでは?)
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
なりする方が素直な気がします。
#最小構成で走らせる
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コマンドを使用できなかったからです。
#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ビルドできることを教えてくださりありがとうございました。