こんにちは。virapture株式会社のもぐめっとです。
この記事を書いてるときは冬なんですけど、もう自分の写真がなさすぎて夏までさかのぼってしまいました。
本日はCI/CD with Unity, GitHub Actions, and Fastlaneという記事を参考にUnityでの自動ビルドとストア提出を作ってみたのでそのメモ書きを残しておきます。
概要
UnityにはUnity Cloud Buildという公式のCIがあるのですが、いかんせんfastlaneとの連携ができないので、証明書を引っ張ってきたり、ストアにアップロードというのができません。
そこで探してみたところ、GameCIが出しているunity-builderというgithub actionsを使ってビルドを行い、デプロイにはfastlaneを使用することでうまくできました。
このunity-builderが結構優秀で、ライセンスの認証とかもやってくれる。そして、ビルドが終わった後はライセンスをリターンしてくれるという優れもの。
もはやUnity Cloud Build使わなくてもCIができちゃいます。
構築手順
github actions workflowの設置
記事の人のリポジトリのファイルをベースにしながら作りました。
ios,android以外にもmac, windowsなどもあったのですが、邪魔だったのでそのへんは抹消しました。
こんなワークフローになってます。
iOSのビルド長すぎぃぃ。。
お待ちかね実体ファイルはこんな感じになってます。
name: Test, Build, and Release CGS
on:
# push: { branches: [master] } #masterにpushされたときにやりたい人はこちらをお使いください
release: { types: [published] }
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
BUILD_NUMBER: ${{ github.run_number }}
jobs:
tests:
name: Test Code Quality
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout Repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache Library
uses: actions/cache@v2
with:
path: Library
key: Library
- name: Run EditMode and PlayMode Tests
uses: game-ci/unity-test-runner@main
- name: Publish Test Results
if: ${{ always() }} # Avoid skipping on failed tests
uses: davidmfinol/unity-test-publisher@main
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
buildWithLinux:
name: Build for ${{ matrix.targetPlatform }} by Unity
runs-on: ubuntu-latest
timeout-minutes: 90
needs: tests
strategy:
fail-fast: false
matrix:
targetPlatform:
- Android
- iOS
steps:
- name: Checkout Repository
uses: actions/checkout@v2
with:
fetch-depth: 0
lfs: true
- name: Cache Library
uses: actions/cache@v2
with:
path: |
Library
build/${{ matrix.targetPlatform }}
key: Library-${{ matrix.targetPlatform }}-
restore-keys: Library-
- name: Free Disk Space for Android
if: matrix.targetPlatform == 'Android'
run: |
sudo swapoff -a
sudo rm -f /swapfile
sudo apt clean
docker rmi $(docker image ls -aq)
df -h
- name: Build Unity Project
uses: game-ci/unity-builder@main
with:
customParameters: -buildNumber ${{ github.run_number }}
targetPlatform: ${{ matrix.targetPlatform }}
buildMethod: Cgs.Editor.BuildCgs.BuildOptions
androidAppBundle: true
androidKeystoreName: keystore.keystore
androidKeystoreBase64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
androidKeystorePass: ${{ secrets.ANDROID_KEYSTORE_PASS }}
androidKeyaliasName: ${{ secrets.ANDROID_KEYALIAS_NAME }}
androidKeyaliasPass: ${{ secrets.ANDROID_KEYALIAS_PASS }}
- name: Upload Build
uses: actions/upload-artifact@v2
if: github.event.ref != 'refs/heads/develop'
with:
name: cgs-${{ matrix.targetPlatform }}
path: build/${{ matrix.targetPlatform }}
releaseToGooglePlay:
name: Release to the Google Play Store
runs-on: ubuntu-latest
timeout-minutes: 60
needs: buildWithLinux
if: github.event.action == 'published'
env:
GOOGLE_PLAY_KEY_FILE: ${{ secrets.GOOGLE_PLAY_KEY_FILE }}
GOOGLE_PLAY_KEY_FILE_PATH: ${{ format('{0}/fastlane/api-finoldigital.json', github.workspace) }}
ANDROID_BUILD_FILE_PATH: ${{ format('{0}/build/Android/Android.aab', github.workspace) }}
ANDROID_PACKAGE_NAME: com.sample.app # 自分のアプリのパッケージ名に合わせよう!
RELEASE_NOTES: ${{ github.event.release.body }}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Download Android Artifact
uses: actions/download-artifact@v2
with:
name: cgs-Android
path: build/Android
- name: Prepare for Upload
run: |
echo "$GOOGLE_PLAY_KEY_FILE" > $GOOGLE_PLAY_KEY_FILE_PATH
echo "$RELEASE_NOTES" > fastlane/metadata/android/en-US/changelogs/default.txt
- name: Upload to Google Play
uses: maierj/fastlane-action@v1.4.0
with:
lane: 'android playstore'
buildIOS:
name: Build Archive for iOS
runs-on: macos-latest
timeout-minutes: 60
needs: buildWithLinux
if: github.event.action == 'published'
env:
APPLE_CONNECT_EMAIL: ${{ secrets.APPLE_CONNECT_EMAIL }}
APPLE_DEVELOPER_EMAIL: ${{ secrets.APPLE_DEVELOPER_EMAIL }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
APPLE_ITC_TEAM_ID: ${{ secrets.APPLE_ITC_TEAM_ID }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_PERSONAL_ACCESS_TOKEN: ${{ secrets.MATCH_PERSONAL_ACCESS_TOKEN }}
IOS_APP_ID: com.sample.app # bundle idを指定します
IOS_BUILD_PATH: ${{ format('{0}/build/iOS', github.workspace) }}
PROJECT_NAME: sampleapp
RELEASE_NOTES: ${{ github.event.release.body }}
MATCH_REPOSITORY_ACCOUNT: ${{ secrets.MATCH_REPOSITORY_ACCOUNT }}
USYM_UPLOAD_AUTH_TOKEN: 'fake' # ビルドが途中でこけるのでfake用に環境変数を追加。
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Cache restore for debug
uses: actions/cache@v2
with:
path: |
Library
build/iOS
key: Library-iOS-
restore-keys: Library-
- name: Download iOS Artifact
uses: actions/download-artifact@v2
with:
name: cgs-iOS
path: build/iOS
- name: Cache restore cocoapods # firebase使ってるとcocoapodsを使うのですが、cocoapodsのキャッシュとらないと毎回時間かかるのでキャッシュしておきます。
uses: actions/cache@v2
if: ${{ always() }}
with:
path: |
build/iOS/iOS/Pods
~/.cocoapods/repos
key: Pods-${{ hashFiles('**/Podfile') }}
restore-keys: Pods-
- uses: actions/setup-ruby@v1
with:
ruby-version: 2.7
- name: Prepare for fastlane # GateKeeper対策
run: |
sudo spctl --master-disable
- name: Archive iOS
uses: maierj/fastlane-action@v2.0.1
with:
lane: 'ios build'
- name: run if fail_step failed # ビルドがコケた原因がわかるようにcatしておきます
if: failure()
run: cat /Users/runner/Library/Logs/gym/*Unity-iPhone.log
- name: Upload Build
uses: actions/upload-artifact@v2
if: github.event.ref != 'refs/heads/develop'
with:
name: ipa
path: |
${{ github.workspace }}/*.ipa
${{ github.workspace }}/*.dSYM.zip
releaseToAppStore:
name: Release to the App Store
runs-on: macos-latest
timeout-minutes: 60
needs: buildIOS
if: github.event.action == 'published'
env:
APPLE_CONNECT_EMAIL: ${{ secrets.APPLE_CONNECT_EMAIL }}
APPLE_DEVELOPER_EMAIL: ${{ secrets.APPLE_DEVELOPER_EMAIL }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
APPLE_ITC_TEAM_ID: ${{ secrets.APPLE_ITC_TEAM_ID }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_PERSONAL_ACCESS_TOKEN: ${{ secrets.MATCH_PERSONAL_ACCESS_TOKEN }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
IOS_APP_ID: com.sample.app # bundle idを指定します
IOS_BUILD_PATH: ${{ format('{0}/build/iOS', github.workspace) }}
PROJECT_NAME: sampleapp
RELEASE_NOTES: ${{ github.event.release.body }}
MATCH_REPOSITORY_ACCOUNT: ${{ secrets.MATCH_REPOSITORY_ACCOUNT }}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Download iOS Artifact
uses: actions/download-artifact@v2
with:
name: ipa
path: |
${{ github.workspace }}/*.ipa
${{ github.workspace }}/*.dSYM.zip
- name: Upload to the App Store
uses: maierj/fastlane-action@v1.4.0
with:
lane: 'ios release'
このファイルのポイントを話しておくと、linuxでビルドが終わった後、fastlaneを使ってandroid/iosともにビルドもしくはアップロードを行っています。
iosは結構曲者で、UnityでCLIビルドが結構大変なので随所随所にビルドできるような仕組みを置いてます。
fastlaneの設置
fastlaneで使うファイルを参考ファイルをベースにカスタマイズした下記を設置しています。
keychain_name = "temporary_keychain"
keychain_password = SecureRandom.base64
platform :android do
desc "Upload a new Android version to the Google Play Store"
lane :playstore do
upload_to_play_store(
aab: "#{ENV['ANDROID_BUILD_FILE_PATH']}",
track: 'internal',
skip_upload_screenshots: true,
skip_upload_images: true
)
end
end
platform :ios do
desc "Push a new release build to the App Store"
lane :release do
api_key = app_store_connect_api_key(
key_id: ENV['ASC_KEY_ID'], # your key id
issuer_id: ENV['ASC_ISSUER_ID'], # your issuer id
key_content: ENV['ASC_KEY_CONTENT'], # your secret key body
# ex) key_content: '-----BEGIN PRIVATE KEY-----\nfoobar\n-----END PRIVATE KEY-----'
)
upload_to_app_store(
api_key: api_key, # pass api_key
force: true,
skip_screenshots: true,
skip_metadata: true
)
end
desc "Submit a new Beta Build to Apple TestFlight"
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV['ASC_KEY_ID'], # your key id
issuer_id: ENV['ASC_ISSUER_ID'], # your issuer id
key_content: ENV['ASC_KEY_CONTENT'], # your secret key body
# ex) key_content: '-----BEGIN PRIVATE KEY-----\nfoobar\n-----END PRIVATE KEY-----'
)
upload_to_testflight(
api_key: api_key, # pass api_key
skip_waiting_for_build_processing: true
)
end
desc "Create .ipa"
lane :build do
cocoapods(podfile: "#{ENV['IOS_BUILD_PATH']}/iOS/Podfile")
disable_automatic_code_signing(path: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcodeproj")
certificates
update_project_provisioning(
xcodeproj: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcodeproj",
target_filter: "Unity-iPhone",
profile: ENV["sigh_#{ENV['IOS_APP_ID']}_appstore_profile-path"], # より動的にみるようにしています
code_signing_identity: "Apple Distribution: #{ENV['APPLE_TEAM_NAME']} (#{ENV['APPLE_TEAM_ID']})"
)
gym(
workspace: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcworkspace",
scheme: "Unity-iPhone",
clean: true,
#clean: false,
skip_profile_detection: true,
codesigning_identity: "Apple Distribution: #{ENV['APPLE_TEAM_NAME']} (#{ENV['APPLE_TEAM_ID']})",
export_method: "app-store",
export_options: {
method: "app-store",
provisioningProfiles: {
ENV["IOS_APP_ID"] => "match AppStore #{ENV['IOS_APP_ID']}"
}
}
)
end
desc "Synchronize certificates"
lane :certificates do
cleanup_keychain
create_keychain(
name: keychain_name,
password: keychain_password,
default_keychain: true,
lock_when_sleeps: true,
timeout: 3600,
unlock: true
)
match(
type: "appstore",
readonly: true,
keychain_name: keychain_name,
keychain_password: keychain_password
)
end
lane :cleanup_keychain do
if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
delete_keychain(name: keychain_name)
end
end
after_all do
if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
delete_keychain(name: keychain_name)
end
end
end
元ファイルからcocoapodsのインストールや、api keyまわりの設定、provisioning profileの指定方法などが若干違います。
また、問題を切り分けやすくするためにビルドとapp storeにリリースする処理はわけたりしています。
参考元ファイルでは現在、apikey周りは証明書をおいていい感じにやるようにしているみたいです。
他にfastlaneに付随するファイルも置いていきます
- Appfile
for_platform :android do
package_name(ENV["ANDROID_PACKAGE_NAME"])
json_key_file(ENV["GOOGLE_PLAY_KEY_FILE_PATH"])
end
for_platform :ios do
app_identifier(ENV["IOS_APP_ID"])
apple_dev_portal_id(ENV["APPLE_DEVELOPER_EMAIL"]) # Apple Developer Account
itunes_connect_id(ENV["APPLE_CONNECT_EMAIL"]) # App Store Connect Account
team_id(ENV["APPLE_TEAM_ID"]) # Developer Portal Team ID
itc_team_id(ENV["APPLE_ITC_TEAM_ID"]) # App Store Connect Team ID
end
- Deliverfile
submit_for_review false
automatic_release true
force true
skip_screenshots true
release_notes({
'default' => ENV["RELEASE_NOTES"],
'en-US' => ENV["RELEASE_NOTES"]
})
run_precheck_before_submit false
submission_information({
add_id_info_uses_idfa: false,
export_compliance_compliance_required: false,
export_compliance_encryption_updated: false,
export_compliance_app_type: nil,
export_compliance_uses_encryption: false,
export_compliance_is_exempt: false,
export_compliance_contains_third_party_cryptography: false,
export_compliance_contains_proprietary_cryptography: false,
export_compliance_available_on_french_store: false
});
- Matchfile
git_url("https://github.com/iosのcertificateおいてるgitリポジトリ") # sshのurlではなく、httpsで指定してます。
git_basic_authorization(Base64.strict_encode64("#{ENV['MATCH_REPOSITORY_ACCOUNT']}:#{ENV['MATCH_PERSONAL_ACCESS_TOKEN']}"))
storage_mode("git")
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
app_identifier(["com.sample.app"]) # bundleIDを指定しよう
username("apple@example.com") # Your Apple Developer Portal username
projectの準備
BuildCgs.csをAssets/Scripts/Cgs/Editorディレクトリに置いておく。
このファイルがビルド番号の設定などをしてくれている。
もぐめっとの場合、CIのビルド番号とアプリのビルド番号を紐付けしたかったので冒頭部分だけbuildNumberを参照するように少し修正しました。
PlayerSettings.macOS.buildNumber = options["buildNumber"];
PlayerSettings.iOS.buildNumber = options["buildNumber"];
PlayerSettings.Android.bundleVersionCode = int.Parse(options["buildNumber"]);
PlayerSettings.WSA.packageVersion = new Version(options["buildVersion"]);
metadataの準備
metadataがないとfastlane途中でこけるため準備しておく。
androidの準備
fastlane run download_from_play_store json_key:<json path>
jsonファイルはストアにアクセスするために必要なものになるが、こちらの記事に解説は委ねます。
iosの準備
fastlane deliver download_metadata
上記で生成されたファイルをコミットしておく
githubのsecrets設定
先術したymlやfastfileなどを見てもらったとおり、環境変数をふんだんに使うので、その準備をしていきます。
game-ci/unity-builder周りで使う環境変数
細かいことはGame CIのドキュメントをご参照ください
Unityのアカウント周りの情報は下記の変数になります
- UNITY_EMAIL
- UNITY_PASSWORD
- UNITY_SERIAL
unityのserialについてはunityのwebサイトにログインして確認します。
Androidの公開設定周りはこちらの変数になります
- ANDROID_KEYALIAS_NAME
- ANDROID_KEYALIAS_PASS
- ANDROID_KEYSTORE_BASE64
- ANDROID_KEYSTORE_PASS
上記四点はandroidの公開設定でも使うこの辺の情報ですね。
keystoreの作り方は公式に委ねます
fastlaneのアップロード周りで使ってる環境変数
アップロードするときなどに使われるfastlaneのDeliverで使われるAppfileで定義して使ってる一覧です。
- APPLE_CONNECT_EMAIL
App Store Connect Accountにアクセスできるアカウントを指定します
- APPLE_DEVELOPER_EMAIL
Apple Developer Accountにアクセスできるアカウントを指定します。
- APPLE_TEAM_ID
- APPLE_TEAM_NAME
上記2点はDeveloper Portalのサイトでアクセスできる項目を確認します
- APPLE_ITC_TEAM_ID
APPLE_TEAM_IDではなくて、app store connectで使うアカウントのほうのIDになるらしい。とりあえずfastlane使えるならspaceship使えば取得できます。
- ASC_ISSUER_ID
- ASC_KEY_CONTENT
- ASC_KEY_ID
Appleの2段階認証を突破してアップロードできるようにする用の変数です。解説は下記記事に委ねます。
- FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
- FASTLANE_PASSWORD
先程のAppStoreConnect API Keyを使った方法でできると思うので、おそらくこの2点は設定しなくてもいいと思いますが、念の為設定しておきます。
設定方法はこちらに。
- GOOGLE_PLAY_KEY_FILE
metadataをダウンロードする時お話した、jsonファイルの中身をはっつけます。
再掲)
fastlane match周りの環境変数
fastlane matchについてはこちらをご参照ください。証明書を管理するやつです。
- MATCH_PASSWORD
matchに設定してるパスワードを指定します。
- MATCH_PERSONAL_ACCESS_TOKEN
- MATCH_REPOSITORY_ACCOUNT
上記2つはMatchfileで指定したgit_basic_authorizationに使うものなのですが、repositoryのcloneがgithub action上でできるようにするために使っています。
MATCH_PERSONAL_ACCESS_TOKENはgithubのaccess tokenを指定します。
MATCH_REPOSITORY_ACCOUNTはMatchのリポジトリにアクセスできるアカウントを指定します。(access tokenを発行したアカウント名ですね)
使い方
ようやく設定がおわりました。
このworkflowはreleaseを切ったときに初めて発火されます。
Draft a new releaseボタンからリリースを作りましょう!
成功すればreleaseで書いた内容でappstoreとplay storeにリリースされるはずです!
まとめ
というわけでgame ciさんのunity-builderを使うことでgithub actionsでunityのCIを回すことができるようになりました!
unityはビルドに時間かかるので結構めんどくさかったのですがこの辺が省略できるようになるのはとても楽になりました。
みなさんの開発にあてる時間がこれで増えたら幸いです!
最後に、ワンナイト人狼オンラインというゲームを作ってます!よかったら遊んでね!
他にもcameconやoffchaといったサービスも作ってるのでよかったら使ってね!