Unityでエディタ拡張を作っている人向けの「GitHub Actionsを使ったCI構築」

タイトルの通りです。

# 背景
自分はUnityを使ってVRコンテンツを作っている人間なのですが、近ごろはVRChatやClusterといった、「Unityでコンテンツを作るプラットフォーム」が勢いを増してきており、日々様々な方が各種プラットフォーム向けのコンテンツを開発しているようです。
一方で、UnityというIDEは拡張性の高さでも知られており、Unity自体の拡張というのが一定のコミュニティやマーケットを形成するプラットフォームでもあります。実際、自分も拡張機能やコンテンツ作成の支援ツールをいくつか作ったり、公開したりしています。

今までは、それらツールのソースコードはGitHubのプライベートリポジトリに雑に置いていたのですが、近ごろGitHubの料金体系が変更され、より気軽に間口が広がったのを受け、改めてGitHubで出来ることを調べてみました。

GitHubにはGitHub Actionsという機能があります。これはyamlファイルにWorkflowを記述し、CI/CDを始めとした様々な自動化を行うことが出来ます。
今回それに実際に触れてみたところ、思いのほか簡潔に書けて便利だった為、早速簡単なUnityのCI環境を作ってみました。

# この記事の対象
- UnityでEditor拡張を作っている人

3Dモデルやテクスチャ等容量が大きい(>100MB)コンテンツを作っている人は、GitHubに乗せるためにはGit LFS(Large File Storage)という仕組みを使う必要がある為、少々ハードルが高いかもしれません。

# この記事の目的
- GitHub Actionsの威力（お手軽さ）を紹介する
- GitHub Actionsを使ってUnityのCI/CDをやりたい人が記法の調査で一週間を潰さないように、とりあえず動くサンプルを提供する

# この記事でやらないこと
記法への踏み込んだ解説はしません。公式ドキュメントがかなり詳しく解説してくれています。
https://help.github.com/ja/actions/reference/workflow-syntax-for-github-actions

また、Workflowの性質上製作者を異にするActionが大量に登場するのですが、そちらに関しても必要最低限のみ解説します。

# 環境
- Unity 2018.4.20f1
- 2020年6月22日時点のGitHub及びUnity公式サイト

# 注意事項
## Unityが実行されるのはLinux上
今回はLinuxサーバー上で動くWorkflowを構築します。Windows環境を前提として作ったプロジェクトは正常に動作しない可能性があります。
Windows環境で動かそうとするともう2倍くらい手順がややこしくなりますし、まだ手元で構築できていません。

## 非公開リポジトリを選択する場合の制限
非公開リポジトリを選択すると、GitHubのサーバー側でビルド等の処理を行える時間に制限がかかります。
具体的には一月あたり2000分、5分かかるビルド処理をGitHub側に任せると月に400回までしかビルド出来ません。（1日13回）
公開リポジトリであれば上記の制限はありません。

# 構築開始
リポジトリのルートに`.github/workflows/`フォルダを作ります。以降、すべてのWorkflowはこのフォルダ内に記述します。

## GitHubActionsで動かすUnityのライセンス認証
Workflowを実行するとき、Unityは自分の手を離れてどこか知らないところにあるマシンの上で実行されます。
そのパソコンにはあなたのライセンス情報が入っていません。なので与えてやる必要があります。

Unityのライセンス認証を行うにはいくつか方法がありますが、今回は比較的簡単で安全な **alf, ulfファイルを使った認証** を行います。

### まずはライセンスファイル(.alf)を取得する
ライセンスファイルを取得するには、まずサーバー上で一度Unityを実行してやる必要があります。早速ですが以下のファイルをリポジトリに追加し、Pushしましょう。

これは https://github.com/webbertakken/unity-request-manual-activation-file のサンプルをカスタマイズしたものです。

```..github/workflows/activation.yml
name: Acquire activation file
on: push
env:
  UNITY_VERSION: 2018.4.20f1
jobs:
  requestManualActivationFile:
    name: Request manual activation file
    runs-on: ubuntu-latest
    steps:
      - name: Create manual activation file
        id: createManualLicenseFile
        uses: webbertakken/unity-request-manual-activation-file@v1.1
        with:
          unityVersion: ${{ env.UNITY_VERSION}}

      - name: Expose as artifact
        uses: actions/upload-artifact@v1
        with:
          name: ${{ steps.createManualLicenseFile.outputs.filePath }}
          path: ${{ steps.createManualLicenseFile.outputs.filePath }}
```

ファイルを一つ追加するだけです。階層は以下のようになります。

```
[Root]
└[.github]
　└[workflows]
　　└activation.yml
```

Push後、少し待ってからGitHubリポジトリのActionsタブを開くと以下の画像のようになっているはずです。
![GitHub action result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248202/2c7d0c51-e0c1-84ef-596b-345abb7fa777.png)

コミットメッセージの箇所をクリックすると以下の画像のページに遷移します。
以下の画像のようにUnity_vXXXX.alfファイルが出来ているハズなので、これをダウンロードしましょう。
![Acquire activation file result 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248202/1a045f8f-7117-4dea-00e2-d7e3eaa5c504.png)

### 毎回ライセンスファイル(.alf)が作られるのを止める
次のステップに進む前に後片付けをします。
先ほど作ったWorkflowはライセンスファイルを作る為のものでしたが、今後プッシュするたびにライセンスファイルが作り直されていては無駄に実行時間を消費します。なので先ほど作成した`activation.yml`は削除しておきましょう。
現在、Workflowを手動で実行出来るようにする簡単な方法は無いようです。[^2]
[^2]: 特定のIssueが開いたことによるトリガーや、webhookトリガーを使う事が出来ます。

### Unity公式サイトでライセンス認証し、ライセンスファイル(.ulf)を入手する
以下のUnityのWebサイトでlicenseファイルを取得します。先ほどダウンロードした.alfファイルを利用します。zip圧縮されているので展開してから使いましょう。
https://license.unity3d.com/manual

案内に従ってアクティベーションを完了すると、今度は拡張子が.ulfのファイルを入手できます。
![download .ulf file](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248202/ed2a12f1-5a37-fd72-e6d5-9dc9aed83cde.png)

中身はXMLなので、適当なテキストエディタで中を見ることが出来ます。
![ulf file.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248202/6d8a3ab1-9616-83a2-10bd-a3f20cdd4141.png)


次はこのファイルををGitHubに設定しましょう。

### ライセンスファイル(.ulf)をSecretsに設定する
.ulfファイルの中身をGitHubに与え、workflowで使用できる状態にします。
リポジトリのSettings>Secretsページに移動し、ページ右上のNew secretボタンを押してください。
![before set secrets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248202/1f8ab2a6-4c08-e915-53e6-88a5b1c96682.png)

.ulfファイルの中身をそのまま貼り付けます。
![setting secrets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248202/6e23a192-f830-39ad-ff13-4a6d6a90db71.png)

名前はUNITY_LICENSEとしてください。後のサンプルファイル内でこの名前を使って参照します。
![after set secrets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248202/0ffb5e25-22b9-c62c-4335-8bd3bdb08df5.png)

以上でライセンス認証の準備は完了です。お疲れさまでした。

## CIの構築
準備ができたのでworkflowを作りましょう。
（紆余曲折）
作ったものが以下のサンプル達になります。

以下に紹介するスニペットを所定の場所に配置してプッシュするだけで動作するはずです。

（以降のファイルは https://github.com/webbertakken/unity-actions のサンプルをカスタマイズしたものです。）

### 各種テストを実行し、結果を成果物として保存するサンプル

```..github/workflows/test.yml
name: CI-Test
on: push
env:
  UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
  PROJECT_ROOT: .
jobs:
  testAllModes:
    name: Test in ${{ matrix.testMode }} on version ${{ matrix.unityVersion }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        unityVersion:
          - 2018.4.20f1
        testMode:
          - playmode
          - editmode
    steps:
      - uses: actions/checkout@v2
        with:
          lfs: true

      - uses: actions/cache@v1.1.0
        with:
          path: ${{ env.PROJECT_ROOT }}/Library
          key: Library-${{ env.PROJECT_ROOT }}

      - uses: webbertakken/unity-test-runner@v1.4
        id: tests
        with:
          projectPath: ${{ env.PROJECT_ROOT }}
          unityVersion: ${{ matrix.unityVersion }}
          testMode: ${{ matrix.testMode }}
          artifactsPath: ${{ matrix.testMode }}-artifacts
          
      - uses: actions/upload-artifact@v1
        with:
          name: Test results for ${{ matrix.testMode }}
          path: ${{ steps.tests.outputs.artifactsPath }}
```


### .unitypackageを出力するサンプル
C#スクリプトをworkflowから呼び出しています。細かいところはC#内でカスタマイズし、最終的に`Build.unitypackage`を出力すればOKです。
以下の2ファイルを配置してください。

```Assets/Editor/CI/Builder.cs
using UnityEditor;

namespace CI
{
    public static class Builder 
    {
        public static void Build()
        {
            AssetDatabase.ExportPackage("Assets/Scenes", "Build.unitypackage");
        }
    }
}
```

```..github/workflows/build.yml
name: CI-Build
on: 
  push:
    branches:
      - master
env:
  UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
  PROJECT_ROOT: .
jobs:
  createUnityPackage:
    name: Create UnityPackage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          lfs: true

      - uses: actions/cache@v1.1.0
        with:
          path: ${{ env.PROJECT_ROOT }}/Library
          key: Library-${{ env.PROJECT_ROOT }}

      - name: Get Unity editor version
        id: getUnityEditorVersion
        uses: CumulusDS/get-yaml-paths-action@v0.1.0
        with:
          file: ${{ env.PROJECT_ROOT }}/ProjectSettings/ProjectVersion.txt
          versionOutput: m_EditorVersion

      - uses: webbertakken/unity-builder@v1.0
        with:
          unityVersion: ${{ steps.getUnityEditorVersion.outputs.versionOutput }}
          projectPath: ${{ env.PROJECT_ROOT }}
          buildMethod: CI.Builder.Build

      - uses: actions/upload-artifact@v1
        with:
          name: Build.unitypackage
          path: Build.unitypackage
```

以下のリポジトリで全てのサンプルが動作しています。
https://github.com/sokuhatiku/GithubActionsTest/actions

# おわりに
この記事を踏み台にすることで、とりあえず動くUnityのCI/CD環境を構築することが出来るはずです。そこから先はリファレンスを読んで、必要に応じてカスタマイズして下さい。
この記事が皆さんのコードやビルド環境の健全性を保つ礎となれたら幸いです。

指摘や質問あればコメント欄に書いてください。