QualiArtsでUnity用のライブラリ開発をしつつ、その他Unity用の開発環境の整備を行っているUnityエンジニアの @asuuma です。
この記事はQualiArts Advent Calendar 2019の18日目の記事です。
TL;DR
- Gitのタグ付けをトリガーに、GitHub ActionsでReleaseを作成
- npm publishで自社のUnity Package Registryにアップロード
- unitypackageも生成してReleaseの成果物に
- unitypackageの構造を解析して、pythonで生成することでUnityレスを実現
GitHub Actionsとは
GitHub上で直接動作するCI/CDをサポートするためのワークフローを自動化するための機能です。
https://github.com/features/actions
2019年の11月にGAになったばかりです。
その特徴としては、以下のようなことが挙げられます。
- インターフェイスがGitHub上に完全に統合されている
- 無料枠が設定されている
- マーケットプレイスを通して、ワークフローを構築するための部品が世界中で開発されている
- 通常はGitHubが用意するVM上で実行されるが、オンプレなどに自分用の実行環境を用意して実行させることができる
Unity開発におけるGitHub Actionsへの期待
Unity開発などのクライアントアプリの開発は、CI/CD環境を自分たちで用意する必要があります。 1
特にUnityを用いたiOS/Android向けのゲームを制作する際は、自前で用意したMacマシンにJenkinsをインストールして、
CI/CD環境を構築していることが多いかと思います。これは以下の事情に依るところが大きいです。
- Unityエディタがインストールされている環境がマネージドサービスに少ないため
- iOSアプリのビルドのためにXcodeがインストールされたMacマシンが必要
- Unityアプリのビルドはハイスペックなマシンでも30-60分ほどかかる
ところがJenkins環境を維持することは簡単なことではありません。
マシンのメンテナンスなどUnityエンジニアの本来の仕事とは遠いインフラ的な業務が中心になるからです。
またJenkinsの設定や、プラグインの管理なども求められてきます。
多くの場合仮想化されていない環境での構築になるため、一度壊れると復旧するのに大変な時間を要し、その間のプロジェクトの開発は止まってしまいます。
そんな環境に身を委ねていては夜も眠れなくなってしまいます。
そこで登場したのがGitHub Actionsです。
個人的には以前から脱Jenkinsを目指していたので、GitHub標準のCI/CDはまさに待望のサービスでした。
Unityのライブラリ開発をGitHub Actionsで自動化
QualiArtsでのライブラリ開発について
QualiArtsではライブラリ開発が盛んに行われています。
小規模な物を含めるとざっと20ぐらいはありそうです。
そこでいつも問題となるのが、ライブラリの配布方法です。主に
- git submoduleでの取り込み
- unitypackageでの配布
- 自社Unity Package Registry経由での配布
のパターンがあります。どれもメリデメがあるのでケースバイケースで使い分けています。
ライブラリのリリースフロー
主にGitHubのRelease機能を通してリリースします。
git-flowなどの開発を通して開発していき、リリースするタイミングになったらgitのタグを打ってプッシュします。
そしてGitHubのReleaseを作成し、changelogとライブラリの成果物と共にリリースします。
また最近はUnity2019以降で開発しているプロジェクト用にUnity Package Registryも社内に立て、そこにもnpm publishで成果物をアップロードします。
リリース先が2箇所あるのと、リリースのたびにchangelogなどを整形して、成果物をビルドして、リリースするのが地味に大変でした。
絶対的な工数はそこまでかからないものの、細かい作業も多くリリースする頻度が多くなると時間を奪われ、開発モチベーションも低下してしまいます。
なのでGitHub Actionsでなるべく自動化を頑張ってみました。
リリース作業の自動化
早速GtiHub Actuonsで上記のリリースフローを自動化していきます。
タグが打たれたらGitHub Releaseを作る
これはGitHubの基本的な機能を利用することで簡単に実現できます。
name: Release
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
uses: actions/upload-release-asset@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: your.unitypackage
asset_name: your.unitypackage
asset_content_type: application/gzip
ここではいったんyour.unitypackageがあるものとしてて成果物としてアップロードしています。
次のステップで、unitypackageを作ります。
Unityエディタレスでunitypackageを生成する
通常unitypackageを作る場合、Unityエディタが必要になります。
ただしUnityエディタを利用するためには、ライセンス認証が必要だったりととても手間がかかるのと、
そもそもUnityエディタをCI/CD環境に用意しないといけません。
そこで何とかUnityエディタ無しでunitypackageを生成する方法を模索してみます。
unitypackageの構造
unitypackage自体はtar.gz形式です。
実際に解凍してみるとわかりますが、中のファイル構造は比較的シンプルです。
ある1つのアセットをunitypackageにすると、以下のような形で圧縮されます。
-rw-r--r-- 1 a13440 CATK\Domain Users 284 4 23 2018 ./e3cd09c8238fd4842b0c87ef7c1ee257/asset.meta
-rw-r--r-- 1 a13440 CATK\Domain Users 4220 11 16 2018 ./e3cd09c8238fd4842b0c87ef7c1ee257/asset
-rw-r--r-- 1 a13440 CATK\Domain Users 59 12 12 16:09 ./e3cd09c8238fd4842b0c87ef7c1ee257/pathname
フォルダ名はアセットのguidそのものです。
assetはアセット本体、asset.metaはそのmetaファイル、pathnameはAssets/で始まる相対パスがテキストで書かれたファイルです。
これがアセット数分繰り返されるだけなので、これを再現してあげればunitypackageとして認識されます。
unitypackage生成するためのpythonスクリプト
#!/usr/bin/env python3
import sys
import os
import io
import argparse
import os.path
import tarfile
import yaml
import glob
parser = argparse.ArgumentParser(description='Create unitypackage without Unity')
parser.add_argument('-r', '--recursive', action='store_true')
parser.add_argument('targets', nargs='*', help='Target directory or file to pack')
parser.add_argument('-o', '--output', required=True, help='Output unitypackage path')
args = parser.parse_args()
print('Targets:', args.targets)
print('Output unitypackage:', args.output)
print('Is recursive', args.recursive)
for target in args.targets:
if not os.path.exists(target):
print("Target doesn't exist: " + target)
sys.exit(1)
def filter_tarinfo(tarinfo):
tarinfo.uid = tarinfo.gid = 0
tarinfo.uname = tarinfo.gname = "root"
return tarinfo
def add_file(tar, metapath):
filepath = metapath[0:-5]
print(filepath)
with open(metapath, 'r') as f:
try:
guid = yaml.safe_load(f)['guid']
except yaml.YAMLError as exc:
print(exc)
return
# dir
tarinfo = tarfile.TarInfo(guid)
tarinfo.type = tarfile.DIRTYPE
tar.addfile(tarinfo=tarinfo)
if os.path.isfile(filepath):
tar.add(filepath, arcname=os.path.join(guid, 'asset'), filter=filter_tarinfo)
tar.add(metapath, arcname=os.path.join(guid, 'asset.meta'), filter=filter_tarinfo)
# path: {guid}/pathname
# text: path of asset
tarinfo = tarfile.TarInfo(os.path.join(guid, 'pathname'))
tarinfo.size= len(filepath)
tar.addfile(tarinfo=tarinfo, fileobj=io.BytesIO(filepath.encode('utf8')))
with tarfile.open(args.output, 'w:gz') as tar:
for target in args.targets:
add_file(tar, target + '.meta')
if args.recursive:
for meta in glob.glob(os.path.join(target, '*.meta')):
add_file(tar, meta)
for meta in glob.glob(os.path.join(target, '**/*.meta'), recursive=True):
add_file(tar, meta)
pythonを使ってアセットを探し、tar.gzに固めていくスクリプトです。
引数は3種類ありますが、そんなに難しくないと思うので読めばなんとなくわかるかなと思います。
これでunitypackageをUnityを使わずに作れるようになりました。
unitypackageのビルドとアップロード
先程のmain.ymlを少し改造して、unitypackageのビルドも同時にやるようにしたのが以下です。
name: Release
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up python
uses: actions/setup-python@master
with:
python-version: '3.x'
- uses: actions/cache@v1
if: startsWith(runner.os, 'Linux')
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name : Pip install
run: pip install PyYAML
- name: Create unitypackage
run: python3 create_unitypackage.py -r -o you.unitypackage Assets/Path/To/Library
- uses: actions/upload-artifact@v1
with:
name: unitypackages
path: your.unitypackage
github_release:
needs: [build]
runs-on: [ubuntu-latest]
steps:
- uses: actions/download-artifact@master
with:
name: unitypackages
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
uses: actions/upload-release-asset@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: unitypackages/your.unitypackage
asset_name: your.unitypackage
asset_content_type: application/gzip
具体的に増えたstepは以下です。
- python3環境のセットアップ
- pipのインストールと高速化のためのキャッシュ設定
- unitypackageのビルド
- GitHub Actionsのartifact機能を利用した成果物のアップロード・ダウンロード
自社Unity Package Registryへのアップロード
最後にUnity Package Registryへのアップロード部分のstep抜粋です。
deploy:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Node.js
uses: actions/setup-node@master
with:
node-version: '12.x'
registry-url: 'http://your.registry.example.com'
- name: Copy README.md
run: cp README.md Assets/Path/To/Library
- name: Publish if version has been updated
run: npm publish Assets/Path/To/Library
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} # You need to set this in your repo settings
ポイントは、npmの認証用のトークンを事前にsecretsの中に設定しておくのと、
もしレジストリをIP制限などかけている場合はGitHub Actionsのソースレンジを許可しておく必要があります。
ただし現状のGitHub Actionsのソースレンジはドキュメント記載の通り、
AzureのEast US 2リージョン丸ごとであり、かつ随時変わる可能性があるためIP制限はおすすめできません。
どうしてもIP制限が必要な場合は、セルフホステッドランナーを利用することをオススメします。
GitHub Actionsで自動化を実現出来なかったこと
実は更に自動化することも検討していました。
例えば各種バージョン表記の更新、CHANGELOG.mdの自動生成などです。
前者はスクリプトまでは書いたのですが、 GitHub Actionsの何をトリガーにそれを実行するかを決められずに手動で実行することになりました。
後者を実現するGitHub Actionsのstepがマーケットプレイスにもあるのですが、commitメッセージをルール通りに書く必要があるなど、導入ハードルが高いため断念しました。
まとめ
Jenkinsの利用を段階的に減らしていくために、GitHub Actions化をいろいろと頑張りました。
まずはライブラリ開発の地味に大変だったところを自動化しました。
今後は実際のゲームのビルドなど、頻繁に走るビルドをGitHub Actionsのセルフホステッドランナーに寄せられないか検証を進めていいきたいと思います。
-
大手のクラウドサービスにはマネージドなCI/CDサービスがあるため、サーバー開発ではそれを利用することが多い。またUnity社もクラウドビルドサービスを提供しているが、大規模開発では使いづらい面がある。 ↩