Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
14
Help us understand the problem. What is going on with this article?
@mogmet

github actionsでunityビルドしてiOSとAndroidのストアに自動的に提出する

こんにちは。virapture株式会社もぐめっとです。

mogmet.jpg

この記事を書いてるときは冬なんですけど、もう自分の写真がなさすぎて夏までさかのぼってしまいました。

本日は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などもあったのですが、邪魔だったのでそのへんは抹消しました。

こんなワークフローになってます。

image.png

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を参照するように少し修正しました。

修正後BuildCgs.cs
            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サイトにログインして確認します。
スクリーンショット 2021-03-21 15.40.06.png

Androidの公開設定周りはこちらの変数になります

  • ANDROID_KEYALIAS_NAME
  • ANDROID_KEYALIAS_PASS
  • ANDROID_KEYSTORE_BASE64
  • ANDROID_KEYSTORE_PASS

上記四点はandroidの公開設定でも使うこの辺の情報ですね。
image.png

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のサイトでアクセスできる項目を確認します

スクリーンショット 2021-03-21 16.00.14.png

  • 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を切ったときに初めて発火されます。

リポジトリの右側にあるreleasesから
image.png

Draft a new releaseボタンからリリースを作りましょう!
image.png

成功すればreleaseで書いた内容でappstoreとplay storeにリリースされるはずです!

まとめ

というわけでgame ciさんのunity-builderを使うことでgithub actionsでunityのCIを回すことができるようになりました!
unityはビルドに時間かかるので結構めんどくさかったのですがこの辺が省略できるようになるのはとても楽になりました。

みなさんの開発にあてる時間がこれで増えたら幸いです!

最後に、ワンナイト人狼オンラインというゲームを作ってます!よかったら遊んでね!
他にもcameconoffchaといったサービスも作ってるのでよかったら使ってね!

14
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
mogmet
virapture株式会社CEO。アプリからインフラまでなんでもやります。firebase使ってサクッと作るのが好き。サクッとサービスを作ったりする相談や技術顧問も受け付けております。 ワンナイト人狼作ってます。 あとはこのへんみてください。 https://mogmet.com/ https://virapture.com/

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
14
Help us understand the problem. What is going on with this article?