Github ActionsでUnityアプリをビルドする前に知っておきたかったこと

# 概要
Unityのアプリはアセットサイズが膨らみがちです。そのため冪等性や保守コストはある程度諦めてJenkinsを使ってビルドする、ということも多かったです。

さて、そんな人が「最近Jenkinsのプラグインも保守大変だし、流行りのGithub Actionsっていうのに移行しようかな…でもクラウドのMacは高そうだし手元のMacをビルドマシンにしたいな…」  
みたいなことを考えた時に知っておくと良さそうなことをまとめておきます。

無限にお金がある場合はGithub Hosting RunnerのLarge Runnerをぶん回した方が考えるコストが減るとは思います。

この投稿はKDDIテクノロジーアドベントカレンダーの12/8分の記事です。
他の記事はこちらです。 
https://qiita.com/advent-calendar/2024/kddi-technology　

# 先行事例調査
この3個が特にありがたかったです。

### AWSを使ったクラウドUnityビルド環境の構築～GitHub Actions構築編～
https://synamon.hatenablog.com/entry/2022/05/02/180000  
すごく詳細にまとまっていて、おそらく中規模以上のプロジェクトならこれを参考にするのがコスト戦略的に良い気がしました。

### GitHub ActionsのセルフホストランナーでUnityビルドする
https://tech.framesynthesis.co.jp/github/actions-unity/  
いつもお世話になっている korin さんのサイト。
物理ローカルマシンで動かすときのクリーンするかなどで参考にしました。

### GitHub CI/CD実践ガイド
https://gihyo.jp/book/2024/978-4-297-14173-8
GithubActions自体の仕組みについて参考にしました。皆様も手元にある安心感のために買っておくといいと思います。


# Github Actions自体のベストプラクティス
数週間前の僕が知っておきたかったことを書いておきます。

- **name: は絶対に指定する**（後からの見やすさが違います）
- shellはMacでもbashをデフォルトシェルにしておく
    - そしてActions内のshellのbashはトップレベルで指定する（それにより、ログ出力が詳細になります）
  ```yml
  # ワークフローのトップレベルでシェルオプションを指定する
    defaults:
          run:
            # bashでprofileを読む（Runner標準では読まない、self-hosted特有の処理
            shell: bash -ieo pipefail {0}
    ```

- 複数のymlを作ることを躊躇しない（ios_build.ymlとandroid_build.ymlがあっても良いし build_check.ymlやaction_study.ymlなどがあっても良い）
- ステップは細かく分ける（デバッグに便利です）
- ifはstep単位で行う（よって、先に書いたステップを小分けするのが効いてくる）
  - 例えばDEVビルド限定処理、などを書く時に使う 
- ステップ間の変数受渡はGITHUB_ENVを使う(グローバル変数扱いなので大きいActionsだともっと狭いスコープ用途のGITHUB_OUTPUTを使うのが推奨です）
   ```yml
    step:
	  - name: store_value
        run: echo "RESULT=hello" >> "${GITHUB_ENV}" #グローバルに変数書き出し
	  - name: read_value
        run: echo "${RESULT}" # => hello
     ```
- `# コメントが書ける`ので、**コメントは積極的に使う**
    - コメントがないと後で読めなくなっちゃう！
- VScodeなどでyamlを編集する時にyamlのチェックを入れてインデント崩れなどを避ける
  - https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml これとか
  - https://marketplace.visualstudio.com/items?itemName=github.vscode-github-actions これとか
- 困ったらshellのコマンドがそのまま使えるので、無理に既存アクションを組み合わせる為に消耗する必要は無い
  - シェルのパスは${{ github.workspace }}が何もしてなければリポジトリルートのフルパス
    - ${{ env.GITHUB_WORKSPACE }} ではないことに注意
- ChatGPTなどが詳しい分野なので、機密情報を含まない一般的な範囲であれば活用する
  - ただし、たまにshellの`if`に対応する`fi`を付け忘れる、などの**お茶目なところもある**

# GithubActionsの開発時の作法
開発中のアプリにCIを導入する時を例にします。  
pushして試す、を繰り返すのでコミットが汚れがちなのですがSquash&Mergeでまとめることが出来ます。

小規模プロジェクトならこれが紛れがないんじゃないかな。という手順は以下です。  

1. (Self-Hosted-Runnerなら)クリーンな環境のマシンを用意する
2. Actions開発用ブランチを作る(`feature/github_actions_dev`)
3. (一時的に)リポジトリのdefault branchを `feature/github_actions_dev`に変更する。なぜならActionsはdefault branchの内容で動くため
4. 開発中は`on: workflow_dispatch`を追加する（その方が手動実行できるため）
5. 手元でコードを書き換えて`feature/github_actions_dev`にpushして起動、を繰り返す
6. 無事動いたらdevelopなどのメインブランチにPRを出し、Squash&Mergeでコミットを綺麗にしつつマージする
7. その後リポジトリのdefault branchを元に戻す


default branchではなく特定ブランチのActionsを起動させるテクニックはありますが、この方が明快で楽です。その際**開発メンバーにはbranch変更の事を周知してください！**

また、Runnerを途中で止めてSSH でログインし、シェル操作を試行錯誤すると開発効率が良いです。    https://qiita.com/shonansurvivors/items/cb8902acfe5c3a1b3ca0 

# Github Self-Hosted-Runner特有の注意事項
- 基本的にJenkinsと同じでRubyバージョンなどは**端末内を参照する**
  - 冪等性の問題があるならActions内で明示的にインストールのactionを使う
  - また、Self-Hosted-Runnerはビルドキャッシュなどが（明示的に消さない限り）残る
    - 問題があるならビルド開始時に前回キャッシュを削除、などをshellで記述する
    - 明示的なCleanにすると、UnityプロジェクトでLibrary以下が削除されて **死ぬほどビルドが遅くなる** 
  - 一部のActionsはSelf-Hosted-Runnerだと動かないことがある（Xcodeのインストールアクションなど）
  - runner.environmentの判定をifで行うことでself-hostedとそれ以外に両対応するように書けるが、メンテコストが地獄なのでself-hostedなら**works on my machine** の精神でやってしまうのもあり（Jenkinsの悪夢再びですが）
  - ```yml
    # runner.environment=>'self-hosted' または 'github-hosted'
     if [[ "${{ runner.environment }}" == "self-hosted" ]]; then
            echo "This job is running on a self-hosted runner."
          # Self-hosted ランナー専用のコマンドをここに追加
          else
            echo "This job is *not* running on a self-hosted runner."
              # GitHub ホストのランナー専用のコマンドをここに追加
          fi
    ```
## Self-Hosted-Runnerのセットアップ
- 特定バージョンのXcodeをインストールしたり、特定SDKを入れたAndroidStudioをインストールしたりする
- 手動じゃなくてこれをGithub Actionsで実行できるようにしておいて、setup-self-hosted-runner.yml みたいなファイル名で保存しておくと、ビルドマシンを増やすときにやや楽
- しかし、それほどビルドマシンを増やす予定がないなら手でセットアップしても問題はない

# 署名情報(mobileprovisionやp12)の扱い
iOSアプリやAndroidアプリ、あるいはそれに限らず大事なファイル群やパスワード文字列群はどうするかという話。
パスワードなどは素直にGithubのActions内Secrets(あるいはRepository内Secrets)に保存すれば良いですが、p12ファイルやmobileprovisionファイルなどもgitignoreに登録して保護しましょう。
また、p12ファイルなどはBase64エンコードして文字列として上記パスワードと同じように保護して使うことができます。

- Base64エンコードしてSecretsに登録する
  - `base64 someapp.mobileprovision > someapp.mobileprovision.base64` してMOBILEPROVISION_BASE64として登録する
  - Macなら`openssl base64 -in appkey.keystore -out appkey.keystore.base64` https://superuser.com/a/120815 
  - 実際に登録したSecretsを使うときの話
  - ```yml
      # ↑で保存して登録したMobileProvisionファイルをビルドマシン上で安全に復元する
      # note:この復元したファイルをビルド後に消しておくほうが安全ではある…
      - name: Setup Code Signing Files
        env:
          # ここで環境変数を定義しておく
          MOBILEPROVISION_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }}
          # 単純な文字列は以降 ${SOME_PASSWORD} で参照する
          SOME_PASSWORD: ${{ secrets.SOME_PASSWORD }}
          
        run: |
          # base64デコードしてリポジトリ上にsomeapp.mobileprovisionファイルを作る
          echo "$MOBILEPROVISION_BASE64" | base64 --decode > ./someapp.mobileprovision
          # モバイルプロビジョニングファイルをデバイスにインストール
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp ./someapp.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
    ```

# エラー表示などの表示
```yml
echo "::error::Rubyがインストールされていません"
```
このように表示しておくと目立つので良い

このように書いておくとSummary上でも表示されるので、開発者に明示的なメッセージを伝えるときに使うと良い
```bash
echo "::notice title=現在のRubyバージョン::Current Ruby version: $(ruby -v)"
```
 
notice表示の例
![](https://storage.googleapis.com/zenn-user-upload/8ab160d8db1f-20240721.png)

# Unityプロジェクトのバージョンコード
ProjectSettings.assetにあるので、ここをUnityのビルド前に直接書き換える、なども手かも
```txt
AndroidBundleVersionCode:77
  buildNumber:
    Standalone: 0
    iPhone:101
    tvOS: 0
```

例えばこんな感じで…

```yml
    # iOSのbuildNumberを強制的に書き換える
      - name: Update iPhone build number in ProjectSettings.asset
        run: |
          FILE_PATH='./ProjectSettings/ProjectSettings.asset'
          # ProjectSettings.asset内のbuildNumberの表記を探して行番号を取得
          BUILD_NUMBER_LINE=$(awk '/buildNumber:/{print NR; exit}' $FILE_PATH)
          echo "buildNumber: found on line $BUILD_NUMBER_LINE"
          # そのbuildNumber表記を開始点としてiPhoneの表記を探す
          IPHONE_LINE=$(awk -v start=$BUILD_NUMBER_LINE 'NR>=start && /iPhone:/{print NR; exit}' $FILE_PATH)
          echo "iPhone: found on line $IPHONE_LINE"
          # 今回置き換えるビルドナンバーの文字列を定義
          NEW_BUILD_NUMBER=${{ github.event.inputs.iOSBuildNumber }}
          awk -v line=$IPHONE_LINE -v new_number=$NEW_BUILD_NUMBER 'NR==line{sub(/[0-9]+$/, new_number)} {print}' $FILE_PATH > temp_file
          echo "Before change:"
          awk -v line=$IPHONE_LINE 'NR==line' $FILE_PATH
          echo "After change:"
          awk -v line=$IPHONE_LINE 'NR==line' temp_file
          mv temp_file $FILE_PATH
```

# Self-Hosted-Runner向けTips
## AndroidSdkインストール
これはBuild Script内部で呼びます。往々にしてUnityのバージョンアップをサボっていると、TargetSDKだけが上がっていき、Android Studio内のSDKを参照したりカスタムgradleスクリプトを書くことに…

https://github.com/amyu/setup-android

## ミニマルなキャッシュ戦略
ある程度のアセットがあるUnityアプリの場合は、普通に（？）デフォルトの10GBのキャッシュを使い切るのでこういうのを使います。
Self-Hosted-Runnerを3-4台手元に用意しておいて、ファイルシステムとしてLAN内にぶら下げたNASをマウントしておく、などが良いと思います。

https://github.com/MasterworksIO/action-local-cache

あるいはビルドマシンが１台なら**物理的に同じ端末のSSD内にキーをつけてキャッシュ保存、** でも良いです。

少なくともSelf-Hosted-RunnerのUnityプロジェクトの場合は **Github-Actionsの [cache](https://github.com/actions/cache) は使わない** ことは知っておくといいと思います。

# トラブルシューティング
## ところでGithub ActionsのymlがLFSなんだけど・・・
えええ？そんなことあります？（あった）
影響範囲を最小にするために
- [attr] でnot-lfsを.gitattributesファイルの先頭に追加
- ymlをlfs指定しているすぐ下にworkflows以下のymlをlfs対象外に指定`.github/workflows/*.yml not-lfs`

で乗り切りました。もしすでに書きかけの.github/workflows/build.yml ファイルがlfs管理だったら.github/workflows/super-build.yml とかにリネームしてシンプルにadd-pushするとlfsじゃなくなります。

.gitattributes 内で以下のように設定
```.gitattributes
# ファイル先頭でアトリビュートを定義
[attr]not-lfs -filter -diff -merge -text
# ここにfbxやpngなど様々なものが
# 定義されている、とおもってください

# ここに今回問題になったymlファイルのlfs設定があるので
*.yml filter=lfs diff=lfs merge=lfs -text
# こうして、明示的にworkflow以下はlfs化のホワイトリストに登録します
.github/workflows/*.yml not-lfs
```

## Self-Hosted-RunnerのUnityが変
`Error building player because build target was unsupported`
という文字が出たら、諦めてUnityHubからマウスでぽちぽちして足りてないmoduleを入れましょう。
僕はiOSビルド環境がなぜか入ってなくてハマりました。

## iOSのIL2CPPビルドで失敗する

```bash
Actual result: Xcode project build fails with “tundra: error: Failed to open file
```
みたいなログが出ることがあり、
- 新規プロジェクトでは問題ない
- 手元の開発PCでは問題ない

ということで悩んでいたら、IL2CPPのビルドキャッシュを消して再ビルドすると直るようです。
CIでは Library/Il2cppBuildCache/ を消すのを習慣にしても良いかもしれません。

https://issuetracker.unity3d.com/issues/il2cpp-ios-build-fails-when-building-on-a-different-machine-slash-user-due-to-il2cppbuildcache-caching

## SwitchPlatform前にコンパイルエラー出る
UnityをCIとかでバッチモードで動かす時は

`-ignorecompilererrors`

をつけると良い、ということがわかりました。  
これつけるとStandaloneでコンパイルエラーが出てても -buildTarget Android とかのバッチモード実行がちゃんと動きます。

https://x.com/su10_dev/status/1829927467999731953

つまり、UnityのGameCIを使ってビルドしていて、 `-ignorecompilererrors` を使ってるGithub Actionsのサンプルは信頼できそうですね！