1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unity の iOS / Android ビルド の CI / CD 環境を Free で構築する(iOS編)

1
Last updated at Posted at 2025-10-19

日時 2025年10月 の話

先行(1ヶ月早い)して公開した Android編 へのリンクを載せておきます

最初の構想では 1 記事にするつもりだったので
Android 編といいながら、こちらの iOS 編の説明が書いてあったりするので、、

今回の目標

  • Unity iOS プラットフォーム 対象
  • Github Actionsfastlane にて ipa 作成 + TestFlight アップロード
    (この時点で Github / TestFlight 使用確定)
  • fastlane は match / gym / upload_to_testflight 等を使用して
    全てのビルド処理をまかなう
  • Github Actions は Mac ランナーのセットアップをまかなう

iOS ビルドは コード署名 が鬼門

その仕組みやハマった点を説明すると
とんでもなく長い解説になるので iOS 編は答えから

Github Actions 用 YAML

name: Build Unity for iOS

on:
  workflow_dispatch:

jobs:
  build_ios:
    name: Build and Upload iOS
    runs-on: macos-latest
    env:
      # unity-builder がデフォルトで build/{プラットフォーム} に出力 + Unity が iOS に出力する
      BUILD_LOCAL_PATH: 'build/iOS/iOS'
      
    steps:
      # LFS 使用
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          lfs: true

      # キャッシュを設定(ビルド時間短縮のため)
      - name: Cache Unity Library
        uses: actions/cache@v4
        with:
          path: Library
          key: Library-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
          restore-keys: Library-

      # XCode は最新にする
      - name: Select Xcode version
        id: select_xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable

      # Unity のバージョンは ProjectVersion.txt をみてもらう
      # 指定したければ unityVersion: で指定する
      - name: Build Xcode project with Unity
        uses: game-ci/unity-builder@v4
        env:
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
        with:
          targetPlatform: iOS

      # Xcode プロジェクト出力先に fastlane 用のファイルをコピーする
      - name: Copy fastlane configuration
        env:
          BUILD_FULL_PATH: ${{ format('{0}/{1}', github.workspace, env.BUILD_LOCAL_PATH) }}
        run: |
          mkdir -p ${{ env.BUILD_FULL_PATH }}/fastlane
          cp ConfigFastlane/Gemfile ${{ env.BUILD_FULL_PATH }}
          cp ConfigFastlane/Gemfile.lock ${{ env.BUILD_FULL_PATH }}
          cp ConfigFastlane/Fastfile ${{ env.BUILD_FULL_PATH }}/fastlane/
          cp ConfigFastlane/Appfile ${{ env.BUILD_FULL_PATH }}/fastlane/
          cp ConfigFastlane/Matchfile ${{ env.BUILD_FULL_PATH }}/fastlane/

      # match 用の SSH 秘密鍵を設定する
      - name: Setup SSH key for match
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.MATCH_GIT_SSH_KEY }}

      # fastlane 用の ruby セットアップ
      # 3.3 系に留めた
      - name: Set up Ruby and Bundler
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3.9'
          bundler-cache: true
          working-directory: ${{ env.BUILD_LOCAL_PATH }}

      # fastlane に全てやってもらう
      - name: Run fastlane to build and upload
        uses: maierj/fastlane-action@v3.1.0
        env:
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
          APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
          MATCH_KEYCHAIN_NAME: ${{ secrets.MATCH_KEYCHAIN_NAME }}
          APPLE_IDENTIFIER: ${{ secrets.APPLE_IDENTIFIER }}
          APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        with:
          lane: 'build_and_upload'
          subdirectory: ${{ env.BUILD_LOCAL_PATH }}

Fastfile

Fastfile
default_platform(:ios)

platform :ios do
  desc "Run Xcode to archive the ipa and upload to TestFlight"
  lane :build_and_upload do |options|
    # 基本的にビルド番号は日時より自動で作成だが(必ず番号が大きくなる)
    # 引数指定も用意しておく
    build_number = options[:build_number] || Time.now.strftime("%Y%m%d%H%M")

    # Mac ランナーではデフォルトのキーチェーンではパスワードを聞かれるなど止まってしまうので
    # 自身で新しくキーチェーンを作成して使用する
    create_keychain(
      name: ENV["MATCH_KEYCHAIN_NAME"],
      password: ENV["MATCH_KEYCHAIN_PASSWORD"],
      default_keychain: true,
      unlock: true,
      timeout: 1200
    )

    # キーチェーンの検索リストを再設定する
    # create_keychain だけで上手く行かない
    sh "security list-keychains -s #{ENV['MATCH_KEYCHAIN_NAME']} login.keychain"

    # 自動署名だとトラブルが多いのでマニュアル署名
    match(
      type: "appstore",
      app_identifier: ENV["APPLE_IDENTIFIER"],
      team_name: ENV["APPLE_TEAM_NAME"],
      team_id: ENV["APPLE_TEAM_ID"],
      keychain_name: ENV["MATCH_KEYCHAIN_NAME"],
      keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"],
      readonly: true
    )

    # キーチェーンの秘密鍵にアクセス権を付与
    sh "security set-key-partition-list -S apple-tool:,apple: -k '#{ENV['MATCH_KEYCHAIN_PASSWORD']}' #{ENV['MATCH_KEYCHAIN_NAME']}"

    # Unity の設定がどうなっていても Xcode をマニュアル署名になる様にする
    update_code_signing_settings(
      use_automatic_signing: false,
      path: "Unity-iPhone.xcodeproj",
      targets: ["Unity-iPhone"],
      team_id: ENV["APPLE_TEAM_ID"],
      code_sign_identity: "Apple Distribution",
      profile_name: "match AppStore #{ENV['APPLE_IDENTIFIER']}",
      bundle_identifier: ENV["APPLE_IDENTIFIER"]
    )

    increment_build_number(build_number: build_number)

    gym(
      export_method: "app-store",
      clean: true,
      derived_data_path: "derived_data",
      export_options: {
        provisioningProfiles: { ENV["APPLE_IDENTIFIER"] => "match AppStore #{ENV['APPLE_IDENTIFIER']}" }
      }
    )

    # TestFlight アップロード様に API Key(p8)を設定
    app_store_connect_api_key(
      key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
      issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
      key_content: ENV["APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64"],
      is_key_content_base64: true
    )

    # TestFlight iへ ipa をアップロード
    upload_to_testflight(skip_waiting_for_build_processing: true)
  end

  # ローカルビルド用
  # ローカル Mac は 個人設定がされているはずなので fastlane もシンプル
  desc "(LOCAL)Run Xcode to archive the ipa and upload to TestFlight"
  lane :local_build_and_upload do |options|
    # 基本的にビルド番号は日時より自動で作成だが(必ず番号が大きくなる)
    # 引数指定も用意しておく
    build_number = options[:build_number] || Time.now.strftime("%Y%m%d%H%M")

    match(
      type: "appstore",
      readonly: true
    )

    increment_build_number(build_number: build_number)

    gym(
      export_method: "app-store",
      clean: true
    )

    upload_to_testflight(skip_waiting_for_build_processing: true)
  end

  # 念の為 処理の最後とエラー時に作成したキーチェーンを削除
  after_all do |lane|
    if is_ci?
      delete_keychain(name: ENV["MATCH_KEYCHAIN_NAME"])
    end
  end

  error do |lane|
    if is_ci?
      delete_keychain(name: ENV["MATCH_KEYCHAIN_NAME"])
    end
  end
end

Github Actions Secrets

名前 内容 説明
UNITY_EMAIL Unity アカウント
のメールアドレス
game-ci/unity-builder
で必要
UNITY_PASSWORD Unity アカウント
のパスワード
game-ci/unity-builder
で必要
UNITY_LICENSE Unity ライセンス ファイル game-ci/unity-builder
で必要
*Personal は注釈あり
APPLE_IDENTIFIER アプリの Bundle Identifier 最初に決めないと
動けないやつ
APPLE_TEAM_NAME Apple Developer
でのチーム名
個人の場合は
名前になっているはず
APPLE_TEAM_ID Apple Developer
のチーム ID
Apple Developer
のチーム ID
APP_STORE_CONNECT_API_KEY_ID API Key ID TestFlight アップロード用
APP_STORE_CONNECT_API_ISSUER_ID Issuer Key ID TestFlight アップロード用
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 p8 を base64 エンコードしたもの TestFlight アップロード用
MATCH_GIT_SSH_KEY match リポジトリへの
SSH 秘密鍵
リポジトリ専用 SSH(Deploy keys)
がお勧め
MATCH_PASSWORD match リポジトリ
のパスワード
match 構築時に設定
MATCH_KEYCHAIN_NAME 一時的に作成する
キーチェーンの名前
好きな名前で OK
MATCH_KEYCHAIN_PASSWORD 一時的に作成する
キーチェーンのパスワード
好きなパスワードで OK

名前は

  • UNITY_EMAIL
  • UNITY_PASSWORD
  • UNITY_LICENSE

以外は 好きな名前で良いはずです
(上記 3 つは 固定名 で要求される)

ローカルビルド用シェル(一応)

実はローカルビルドだと Development 署名も出来る様にしていますが
Fastfile からは lane を消してます(混乱させない様に)

#!/bin/bash

# 自身の使用している Unity へのパス
UNITY_EDITOR_PATH="/Applications/Unity/Hub/Editor/6000.2.7f2/Unity.app/Contents/MacOS/Unity"
UNITY_PROJECT_PATH=$(pwd)
FASTLANE_CONFIG_PATH="${UNITY_PROJECT_PATH}/ConfigFastlane"
LOG_FILE="${UNITY_PROJECT_PATH}/unity_build_ios.log"
# Script でパスを指定してる場合は合わせるなりセットするなりする
# (Github Actions と合ってないのは気にしない)
BUILD_PATH="${UNITY_PROJECT_PATH}/Build/iOS"

# シェルの使い方
# 第一引数で dev or app を選び、第二引数にビルド番号を指定できる(オプション)
if [ $# -eq 0 ]; then
  echo "Error: No arguments specified."
  echo "Example:"
  echo "  build-ios.sh dev"
  echo "  build-ios.sh app [Build Number]"
  exit 1
fi

# 第1引数をモードとして取得 (dev or app)
MODE=$1

# スクリプトが失敗した時点で処理を中断する設定
set -e

echo "----- Starting Unity Build iOS -----"

# 既存のビルドフォルダを削除してクリーンな状態にする
rm -rf $BUILD_PATH

# ログファイルをクリア
rm -f $LOG_FILE

# Unityをバッチモードで実行
# BatchModeBuilder.BuildIos が私のビルドメソッド
$UNITY_EDITOR_PATH -quit -batchmode -projectPath $UNITY_PROJECT_PATH -executeMethod BatchModeBuilder.BuildIos -logFile $LOG_FILE

if [ $? = 0 ]; then
  echo "Unity Build iOS Succeeded."
else
  echo "Unity Build iOS Failed. Check log file: $LOG_FILE"
  exit 1
fi

echo "----- fastlane configuration -----"

# fastlane用のフォルダを作成し、設定ファイルをコピーする
mkdir -p "${BUILD_PATH}/fastlane"
cp "${FASTLANE_CONFIG_PATH}/Gemfile" "${BUILD_PATH}"
cp "${FASTLANE_CONFIG_PATH}/Gemfile.lock" "${BUILD_PATH}"
cp "${FASTLANE_CONFIG_PATH}/Fastfile" "${BUILD_PATH}/fastlane/"
cp "${FASTLANE_CONFIG_PATH}/Appfile" "${BUILD_PATH}/fastlane/"
cp "${FASTLANE_CONFIG_PATH}/Matchfile" "${BUILD_PATH}/fastlane/"

echo "Copied configuration files."
echo "----- Starting fastlane Build -----"

# Xcodeプロジェクトのディレクトリに移動
cd $BUILD_PATH

if [ "$MODE" == "dev" ]; then
  echo "Building for Development..."
  fastlane build_dev

elif [ "$MODE" == "app" ]; then
  # betaモードの場合、第2引数(ビルド番号)が必須
  if [ -z "$2" ]; then
    echo "Uploading to TestFlight without a specified build number."
    fastlane local_build_and_upload
  else
    BUILD_NUMBER=$2
    echo "Uploading to TestFlight with build number: $BUILD_NUMBER"
    fastlane local_build_and_upload build_number:$BUILD_NUMBER
  fi

else
  echo "Error: An invalid mode was specified: $MODE"
  echo "Available modes: dev, app"
  exit 1
fi

# 元のディレクトリに戻る
cd $UNITY_PROJECT_PATH

echo "All build processes completed successfully!"

一度ローカルビルドを成功させる必要がある

Fastfile や Gemfile.lock などが存在前提なので
お分かりだと思いますが、一度ローカルでビルドを成功させて
それらを用意する必要があります

Homebrew

Mac なら とりあえず入れとけなんじゃないでしょうか
rbenv や fastlane は Homebrew でインストールするのがお勧めです

この辺りの環境構築は、今回は割愛してさらっといきます
(0 から始めるととんでもなく長くなるので、、)

rbenv

Mac で Unity ビルドする時に
OS に初期インストールされている Ruby で上手く行った覚えはないんですよね

今回は rbenv で 3.3.9 (3.3 系)を指定しています
因みに Local 指定で自分の Unity プロジェクト だけにしています

3.3.9 にした理由は
3.4 系だと Github Actions の環境でエラーが出たからです

下記は今も出ている警告

vendor/bundle/ruby/3.3.0/gems/highline-2.0.3/lib/highline/import.rb:10: warning: abbrev was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 3.4.0.
You can add abbrev to your Gemfile or gemspec to silence this warning.

まあ Gemfile と Gemfile.lock に abbrev の対応をすれば良いみたいなんですが
Gemfile と Gemfile.lock を作った後だったので 3.3.9 にすることで回避した感じです

CocoaPods

残念ながら、現状の私のプロジェクトの状態はゲーム部分しかできておらず
ライブラリなど入っていないので CocoaPods に関してはスルーです

Xcode プロジェクトも Unity-iPhone.xcodeproj です
Unity-iPhone.xcworkspace になる方は CocoaPods を使用していると思いますが
その辺りの処理は、私の記述からご自身で改変して頑張って下さい

CocoaPods のインストール設定や Fastfile の指定など
で行けると思われます

このあたりより コード署名に係る部分が嵌り所だと思いますので
私の情報は そちらを重視して頂ければと考えています

Gemfile & Gemfile.lock

事前に ローカル Mac 環境の Xcode プロジェクトがあるディレクトリに
Gemfile を配置

bundle install

を行い Gemfile.lock を作成しておきます

因みに Gemfile は本当に Fastlane のためだけです

Gemfile
source "https://rubygems.org"

gem "fastlane"

Fastlane

最初は Github Actions の方で、
コード署名や Xcode での ipa アーカイブなど行おうと思っていたのですが
ローカル Mac で Fastlane を使っていたので
ビルド動作を別々にする事はないと思い統一して使用する事にしました

一番最初のローカルビルド時は、
Xcode プロジェクトが出力されたディレクトリで

fastlane init

として、対話形式で Fastfile や Appfile などを作成します

これで fastlane ディレクトリが作成されて
Fastfile など fastlane 用のファイルが作成されます

これを特定のディレクトリに保存しておいて
毎回 コピーして使う様にします

今回は Fastfile は提示しているので
適当に作成しても 私の Fastfile をコピーして使って頂けば良いかと

Match

Fastlane を使おうと思った一番の理由です

iOS ビルドにおけるコード署名と、
それらの p12 ファイルとプロビジョニング プロファイルの管理を
Match に任せてしまう感じです

つまり ローカル Mac で Match を使い
p12 ファイルとプロビジョニング プロファイル用の Github リポジトリの作成
(途中のリポジトリのパスワード設定)まで作成しておく必要があります

この辺りもさらっと流します
(ご自身で調べて下さい、Free の AI でも十分教えてくれます)

fastlane match init

Matchfile を作成して、
自身の Github の署名用のデータのリポジトリを設定します
(因みに私は、development の署名用データも追加しています)

Match リポジトリの SSH Key

私も初めて知ったんですが
自身の Github アカウントに SSH を設定するのは普通だと思いますが
リポジトリ専用の SSH があったんですね

  • Deploy keys

リポジトリの Settings から Deploy keys という項目です

Github Actions では、いくら Secrets への設定といっても
アカウント全体で有効な SSH の秘密鍵を登録するのは避けた方が良いですしね

.zprofile(ローカルビルド用)

これは あくまで ローカルビルドを成功させるための
各種ツールやライブラリの設定で記述が必要になるってだけです

rbenv や match をセットアップしていけば
自ずと記入する必要が出てくるはずです

Apple Developer Program

Github Actions の Fastlane で
TestFlight に ipa をアップロードするのに必要な
p8 (App Store Connect への Auth API Key)ファイルを作成する必要があります

この辺りも作成方法は割愛します
(ご自身で調べて下さい、こちらも Free の AI で十分教えてくれるはずです)

p8 ファイルは一度しか DL 出来ないみたいなので少し注意が必要です

具体的な使い方は
p8 ファイルは SSH の秘密鍵の様に 複数行で構成されるテキスト内容なので
base64 エンコードをかけて Github Secrets に登録します

Mac なら簡単です

base64 -i <入力p8ファイル> -o <出力base64化ファイル>

とすれば良いだけです
後は出力したファイルをテキストエディターなどで開いて中身をコピーすれば OK
(一行のテキスト情報になっているはず)

Github Actions 解説

はじめに何故ローカルビルドの話をしたかというと
Gemfile.lock や Fastfile や Matchfile を作る目的もあったのですが
ビルドを併用する目的もあります

Android 編でも説明していますが
Github Actions の Free 枠の Mac ランナーの使用時間は 200 分 (/月)だけなんですね

これ 例えば 現状の私のプロジェクト レベルで言うと
タイトルだけ、しかも適当にしか出来てない様な、ほぼ空の Unity プロジェクトでも
Github Actions の実行時間は 20 分 ほどかかっています

つまり 月に 10 回(200 / 20)ほどしか実行出来ない算段です、、
(Unity プロジェクトが作り込まれれば 順次 ビルド時間は増大するでしょう)

なので、私のプランとしては
Mac でビルドが許容できない時だけ Github Actions を実行して
後は Mac でビルドを行う、としています
(まあ、Github Actions で CI / CD の入門を、という側面も強いですが)

*自分の Mac を Github Actions 用の
 セルフ ホスト ランナー にする気はありませんでした
 (ローカル ビルドで十分かなと)

YAML

Github Actions の YAML を少しづつ解説していきます

まずは先頭部分

name: Build Unity for iOS

on:
  workflow_dispatch:

jobs:
  build_ios:
    name: Build and Upload iOS
    runs-on: macos-latest
    env:
      # unity-builder がデフォルトで build/{プラットフォーム} に出力 + Unity が iOS に出力する
      BUILD_LOCAL_PATH: 'build/iOS/iOS'

name: で YAML 全体の名前をつけて
on: で動作を設定します

今回は、Actions タブ内から 手動で動作させたかったので
workflow_dispatch: としています

ここを色々と変えれば、
コミットをトリガーにとか、オプション項目を選んでとか出来ます

次に、ワークフローのジョブとして
jobs:build_ios: と ID を設定します
(これは任意の名前で良い)

name: で色々な箇所に名前をつけられるの
細かく name: は設定した方が良いです

理由は、ワークフローが実行されると
この name: でログのタイトルが出るので追いやすくなります

runs-on: で最新(Actions)の MacOS を指定しています
ここは、特定の MacOS がいいなら、それらを設定できます
(例によって調べてね)

env: で環境変数を設定します
env: はステップの各処理毎に設定できるので
ここでは全体で使う BUILD_LOCAL_PATH だけ設定しています

BUILD_LOCAL_PATH

ここが最初に大嵌りした内容です

まず前提として GameCI
Github Actions 用のカスタムアクションである unity-builder を使います

更に ビルド用の static メソッドは用意せずに
通常の Unity のビルドを行う様にしました
つまり ビルドの出力パスを指定しませんでした
(これが嵌った最大の理由かも、、)

unity-builder のドキュメント
buildsPath の説明

buildsPath

Path where the builds should be stored.

In this folder a folder will be created for every targetPlatform.

required: false default: build

つまり

build/iOS

に Xcode のプロジェクトが出力されると思っていました

でも 何故か上手く行かない
エラーが Xcode プロジェクトが見つからないって言うんですね、、

で、エラーログを追っていて気が付いたんですが

build/iOS/iOS

に出力してるんです

つまり unity-builder のデフォルト出力パスの後に
Unity が iOS ってディレクトリを作ってたんです

これ気が付くまで、Mac ランナーの時間をかなり消費しましたw

ビルド用の static メソッド用意して
ビルドパスを ./ (ルート)にしても良かったんですが
今回は同じ様に嵌る人もいるかと考え、あえてこのままで対応しました

リポジトリの checkout ~ Xcode バージョン指定

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          lfs: true

      # キャッシュを設定(ビルド時間短縮のため)
      - name: Cache Unity Library
        uses: actions/cache@v4
        with:
          path: Library
          key: Library-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
          restore-keys: Library-

      - name: Select Xcode version
        id: select_xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable

ここから steps: の記述になっていきます

actions/checkou

まずは Mac ランナーにリポジトリを checkout させるのに
actions/checkout を使います

v4 などのバージョン指定は
私が YAML を作成した際に ドキュメントサイト に載っていた値になります
(以降 全て)

LFS を使用しているので、そちらも指定します

actions/cache

次に、ここが良く分かってなく 申し訳ないのですが
ビルド時間を短縮するためのキャッシュ設定となるんですが、、
これ WEB とかで検索して、ほぼコピペです

使用する理由としては、ビルド時間を短くして
Mac ランナーの使用時間を少しでも短くする らしいのですが、、

個人的は Unity ビルドでのキャッシュはいい思い出がないので
無い方がいい感じなんですよね、、
(Reimport 覚悟で Library ディレクトリを消すって一度はやってません?w)

maxim-lobanov/setup-xcode

最後は Xcode のバージョン指定です

実は、この環境を構築しているときに
自分の MacOS を Tahoe にして Xcode も 26 に上げたんですね

なので MacOS も Xcode も latest ってしてるだけなんです
もし ご自身の環境で バージョン指定が必要な方は 適宜 変えて下さい

Unity ビルド

      - name: Build Xcode project with Unity
        uses: game-ci/unity-builder@v4
        env:
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
        with:
          targetPlatform: iOS

game-ci/unity-builder

GameCI の unity-builder を使って iOS ビルドを行います

重要なのは Unity の ライセンス認証 です
重要というより に近いですw

Unity 歴が長い方は ご存じだと思いますが
以前は Unity のライセンス形態を問わず(Personal でも)シリアルキー と言うのが発行されて
1 シリアルキーで 2 環境(PC) まで認証可能だったんです

ライセンス ファイル

通常は ネットワークでシリアルキーを送る事でアクティベート出来るんですが
それが上手く行かない状況用に マニュアル アクティベート と言うのかあって
Unity のサイトで シリアルキーを使用して、ライセンス ファイル なる物を作成し
その ライセンス ファイルをローカルで使用して アクティベートを行う、というものです

Unity Personal はシリアルキーが無くなった

GameCI の unity-builder は、その ライセンス ファイル を利用して
Unity のアクティベートを行う仕組みになっているのですが、、

なんと!
2025年09月 の時点で Unity は Personal アカウントでは
シリアルキーを廃止して
Unity Hub 起動時のアクティベートに切り替えているみたいなんです

つまり
GameCI の unity-builder の Unity ライセンス ファイルが手に入らない!

より詳細に

具体的には、マニュアル アクティベート フローにおいて
最初に ライセンス リクエスト ファイル(拡張子 .alf)を作成するんですが
これは 作れます(Unity Hub で作れます)

次に、この ライセンス リクエスト ファイル(拡張子 .alf)を使用して
Unity の サイトで ライセンス ファイル(拡張子 .ulf)を作成するのですが
サイトの UI で シリアルキー の入力を求められるんです

ここに 以前の Personal アカウント時のシリアルキーを入力しても
エラーになって弾かるんです

*因みに Pro より上の契約では、いままで通りシリアルキーは発行されてます

GameCI 公式が対応

ここで困って考えたんですが

  • Unity の正式(公式)の挙動である
  • GameCI だって有名なコミュニティー&機能

これ 放置されてる状況のわけないだろ! って事で
GameCI 側や Stack Overflow とかで議論ぐらいされていると踏んで調べたら
GameCI のサイトに説明がありました

Unity no longer supports manual activation of Personal licenses

詳しくは、上記の GameCI サイトの注意書きを読んで欲しいのですが
Unity のサイトで ライセンス ファイル(拡張子 .ulf)を使い
ライセンス ファイル(拡張子 .ulf)を作る時に
シリアルキーの入力を求められる UI になったら
ブラウザの開発者モードで HTML を表示して 一部を書き換えると
シリアルキーの入力なしで ライセンス ファイル(拡張子 .ulf) 作成のボタンが押せる
って感じです

これ 良いんかいな、、と思わなくもない手法ですが、、
GameCI が公式サイトに載せてますし、、と 心を Void にして進みます

ライセンス認証に必要な情報

環境変数名 情報
UNITY_EMAIL Unity アカウント の メールアドレス
UNITY_PASSWORD Unity アカウント の パスワード
UNITY_LICENSE Unity ライセンス ファイル(拡張子 .ulf)

これらを Github Actions の Mac ランナーに
特定の名前の環境変数(env:)としてセットします

        env:
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}

で、ここで出てくるのは、secrets. って記述です
これは Github リポジトリの機能で
ここに Actions 用の情報を予めセットしておけるやつです

${{ secrets.xxxxx }}
YAML で secrets. の値を使用する時の定番の書き方見たいです

Secrets

Github のアカウントではなく、リポジトリの Settings 内に
Secrets and variables のプルダウン項目があり
その中に Actions があるので選択します

image.png

image.png

image.png

New repository secret ボタンを押して登録出来ます
今回は 環境変数名と合わせて(同じ名前)登録しています

使用する Unity のバージョン

今回は 指定していませんので
Unity プロジェクト内にある
ProjectSettings/ProjectVersion.txt
のバージョンが使用されます

もし指定したい場合は unityVersion: で指定します

        # Unity バージョン指定の例
        uses: game-ci/unity-builder@v4
        with:
          targetPlatform: iOS
          unityVersion: 6000.2.7f2

fastlane 用の準備

      - name: Copy fastlane configuration
        env:
          BUILD_FULL_PATH: ${{ format('{0}/{1}', github.workspace, env.BUILD_LOCAL_PATH) }}
        run: |
          mkdir -p ${{ env.BUILD_FULL_PATH }}/fastlane
          cp ConfigFastlane/Gemfile ${{ env.BUILD_FULL_PATH }}
          cp ConfigFastlane/Gemfile.lock ${{ env.BUILD_FULL_PATH }}
          cp ConfigFastlane/Fastfile ${{ env.BUILD_FULL_PATH }}/fastlane/
          cp ConfigFastlane/Appfile ${{ env.BUILD_FULL_PATH }}/fastlane/
          cp ConfigFastlane/Matchfile ${{ env.BUILD_FULL_PATH }}/fastlane/

      - name: Setup SSH key for match
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.MATCH_GIT_SSH_KEY }}

      - name: Set up Ruby and Bundler
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3.9'
          bundler-cache: true
          working-directory: ${{ env.BUILD_LOCAL_PATH }}

fastlane 用のファイル群をコピーする

ファイル名 コピー場所 作成方法
Gemfile Xcode プロジェクトと同じディレクトリ 手動で作成
Gemfile.lock Xcode プロジェクトと同じディレクトリ 事前に bundle install
Fastfile Xcode プロジェクトのディレクトリに
fastlane ディレクトリを作成
事前に fastlane init
Appfile Xcode プロジェクトのディレクトリに
fastlane ディレクトリを作成
事前に fastlane init
Matchfile Xcode プロジェクトのディレクトリに
fastlane ディレクトリを作成
事前に fastlane match init

私の場合は、事前にリポジトリのルートに
ConfigFastlane ディレクトリを作成して
その中に 上記 5 ファイルを置いてあります

それを 絶対パス 指定で処理していきます
(github.workspace で取得できます)

まず GemfileGemfile.lock をコピー

次に mkdir -p コマンドで fastlane ディレクトリを作成して
Fastfile / Appfile / Matchfile をコピーします

webfactory/ssh-agent

これは match リポジトリ専用の SSH 設定 Deploy keys で登録した
SSH 公開鍵と対になる 秘密鍵Secrets に登録して指定します
(この 秘密鍵 は そのまま登録で OK)

*基本的に base64 エンコードしている情報は
 Secrets の名前に _BASE64 と付けているので参考にして下さい

fastlanematch を実行した時にエラーが出たので設定しています

ruby/setup-ruby

Ruby (fastlane用)のバージョンを指定します
基本は、自身のローカルビルドを行った環境と
同じバージョンにするとトラブルは少ないはずです

        with:
         bundler-cache: true

GemfileGemfile.lock を用意しているので
bundler-cache: truebundle install コマンドを省略できます
(毎回 インストールしないで済む)

        with:
         working-directory: ${{ env.BUILD_LOCAL_PATH }}

working-directory:Gemfile.lock が置かれている場所を指定します

fastlane 実行

      - name: Run fastlane to build and upload
        uses: maierj/fastlane-action@v3.1.0
        env:
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
          APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
          TMP_KEYCHAIN_NAME: ${{ secrets.TMP_KEYCHAIN_NAME }}
          APPLE_IDENTIFIER: ${{ secrets.APPLE_IDENTIFIER }}
          APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        with:
          lane: 'build_and_upload'
          subdirectory: ${{ env.BUILD_LOCAL_PATH }}

maierj/fastlane-action

実際に fastlane を実行させます

subdirectory
Optional The relative path from the project root to the subdirectory where the fastlane folder is located.

後は Fastfile の中の説明に入っていきます
env: で設定している環境変数は、全て Fastfile で使うためのものです
なので Fastfile の解説の方で説明します

Fastfile

ここが最も手こずった箇所です

エラー解決まで トライ & エラー しまくって
月 Free 枠の 200 分 を超えてしまい
Github に課金したのは、まあ勉強代ってことでw
(この情報を公開することで、皆は Free の CI / CD 環境ライフを!)

個人的には
情報が 少ない & 古い情報しか無い事象は、
AI はハルシネーションを起こしやすいと痛感させられた
(AI の賢い使い方も大分覚えた)

default_platform(:ios)

platform :ios do
  desc "Run Xcode to archive the ipa and upload to TestFlight"
  lane :build_and_upload do |options|
    # 基本的にビルド番号は日時より自動で作成だが(必ず番号が大きくなる)
    # 引数指定も用意しておく
    build_number = options[:build_number] || Time.now.strftime("%Y%m%d%H%M")

Xcode で ipa を archive して、それを TestFlight へアップロードする
lane の最初の部分です

build_number は後述する
increment_build_number(ビルド番号の指定)で使うのですが
最初は 引数( |options| )で指定したのですが、途中から 面倒になってw
省略すると 日時 を使い 絶対に数値が戻らない値を自動でセットする様にしました

create_keychain

    create_keychain(
      name: ENV["TMP_KEYCHAIN_NAME"],
      password: ENV["MATCH_KEYCHAIN_PASSWORD"],
      default_keychain: true,
      unlock: true,
      timeout: 1200
    )

    # キーチェーンの検索リストを再設定する
    sh "security list-keychains -s #{ENV['TMP_KEYCHAIN_NAME']} login.keychain"

Mac で言う キーチェーン を新たに作成して、そこに match で取得した
署名用の p12 や プロビジョニングプロファイルを設定できる様にします

なんで、その様な事をするかというと
Mac ランナーに初めからあるキーチェーンのパスワードが分からなかったからです
(match が使おうとすると パスワード を聞かれて lane が止まってしまう)

AI は、仮想ランナーのデフォルトのキーチェインのパスワードは "" (空)です
なんて大嘘を平気で言いますw

ここで大いに参考になったのは
Github Actions のドキュメントです

Xcode 開発用の macOS ランナーに Apple 証明書をインストールする

基本は fastlane で出来る事は置き換えて
出来ない事は、ドキュメントのコードをパクる
方針で上手く行きました

ENV[ ] で使用している環境変数は
全て env: 経由で指定している Secrets に設定しているものです
(以降全て)

TMP_KEYCHAIN_NAME
MATCH_KEYCHAIN_PASSWORD

この 2 つは、
好きな キーチェーン名キーチェーンのパスワード を設定しているだけです

    # キーチェーンの検索リストを再設定する
    sh "security list-keychains -s #{ENV['TMP_KEYCHAIN_NAME']} login.keychain"

これは、キーチェーンを作るだけでは上手く行かず
新しく作ったキーチェーンを Mac が検索するときに最初に見つける様にする
おまじないの様なものです
(これ Github のドキュメントが無かったら一生解決していなかったw)

sh は Fastfile の中でシェルを使うコマンド?です

match

    match(
      type: "appstore",
      app_identifier: ENV["APPLE_IDENTIFIER"],
      team_name: ENV["APPLE_TEAM_NAME"],
      team_id: ENV["APPLE_TEAM_ID"],
      keychain_name: ENV["TMP_KEYCHAIN_NAME"],
      keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"],
      readonly: true
    )

    # キーチェーンの秘密鍵にアクセス権を付与
    sh "security set-key-partition-list -S apple-tool:,apple: -k '#{ENV['MATCH_KEYCHAIN_PASSWORD']}' #{ENV['TMP_KEYCHAIN_NAME']}"

いよいよ match の解説です
fastlane を使う最大の理由でした
そして 一番 大嵌りした箇所ですw

、、と言うか match 自体に解説なんていらない簡単な物のはずです
(だからこそ使いたかった)
解説するのは match 自体を成功させるまでの設定ですね

Manual 署名

初め Automatically でなんとか動作させようと頑張ったのですが諦めました
(ローカル Mac では Automatically で署名は動作したので)

結局 マニュアル署名 で実装しています
つまり match も全て指定して動作させています

ipa の目的は、
開発中のアプリを TestFlight から配布(DL)したい
(App Store Connect に接続する)なので
プロビジョニングプロファイルの種類は Ad Hoc ではなく
App Store にする 必要があります

後は

  • アプリの Bunble Identifier
  • Apple Developer の チーム名(*)
  • Apple Developer の チーム ID
  • 先程作成した キーチェーンの名前
  • 先程作成した キーチェーンのパスワード

を指定します
(それぞれ Secrets 経由で設定)

Apple Developer の チーム名

これだけ注意が必要です

現状で 個人の Apple Developer Program 契約では
ダッシュボード上を探してもチーム名という表示ありません
(法人契約だとチーム名は表示されている?)

しかし チーム名は存在するんですw

私の場合は
ローカル Mac で falstlane を動作させた時のログから見つけました
AI も 個人の Developer 契約だと、個人名になると間違わなかったです

readonly: true

match は便利過ぎて
プロビジョニングプロファイル の更新が必要な場合には
(例えば テスト端末の登録を追加した等)
自動で更新をしてくれます

しかし そこまでノー管理だとトラブルが起きやすいのかな?と思い
readonly: true を指定する事で読み取り専用にしています

    # キーチェーンの秘密鍵にアクセス権を付与
    sh "security set-key-partition-list -S apple-tool:,apple: -k '#{ENV['MATCH_KEYCHAIN_PASSWORD']}' #{ENV['TMP_KEYCHAIN_NAME']}"

これも Github のドキュメント様様です
コメントの通りです

これが無いと
エラー(パスワード聞かれて lane が止まってしまう)が起きて
上手く動作しないんです

update_code_signing_settings

    update_code_signing_settings(
      use_automatic_signing: false,
      path: "Unity-iPhone.xcodeproj",
      targets: ["Unity-iPhone"],
      team_id: ENV["APPLE_TEAM_ID"],
      code_sign_identity: "Apple Distribution",
      profile_name: "match AppStore #{ENV['APPLE_IDENTIFIER']}",
      bundle_identifier: ENV["APPLE_IDENTIFIER"]
    )

    increment_build_number(build_number: build_number)

update_code_signing_settings
Xcode の署名設定をマニュアル設定にします

リポジトリの Unity プロジェクト自体の設定は
ローカルビルド用に Automatically を設定しているので
ビルド後の Xcode 設定は Automatically なので 変更します
(ローカル ビルドの Automatically は変えたくなかったんですw)

ここで解説すべきは 2 つですかね

update_code_signing_settings を使う理由は
正しい(確立した)操作で Xcode プロジェクトの操作を行わないと
全て(内部の構造も)をマニュアル署名設定にしてしまい、エラーが出たからです

要は Unity-iPhone.xcodeproj だけマニュアル設定にしたい
って感じです

残りの 1 つは、profile_name: です
これも、
ローカル Mac で falstlane を動作させた時のログから引っ張ってきました

increment_build_number

これは前述したビルド番号(build_number)を指定しているだけで
特に説明はいらないはずです

gym

    gym(
      export_method: "app-store",
      clean: true,
      derived_data_path: "derived_data",
      export_options: {
        provisioningProfiles: { ENV["APPLE_IDENTIFIER"] => "match AppStore #{ENV['APPLE_IDENTIFIER']}" }
      }
    )

gym で ipa をアーカイブします

ここで真に必要なのは export_options: です
これで マニュアル署名で、どのをプロファイルを使うかを指定しないと
上手く動作しませんでした

clean: true
Xcode の Clean Build Folder と同義です

derived_data_path: "derived_data"
は AI に教えてもらったテクニックで、Xcode が実行した際の中間ファイルなどを
"derived_data" というディレクトリに出力してね、って指定です

で、今の処理って Github Actions の Mac ランナー上で行われているフローで
一度きりの環境なはずで毎回消えるので "derived_data" も絶対に消えるので
変なキャッシュに 100% 悩まされる事はないって話(AI談)です

app_store_connect_api_key

    app_store_connect_api_key(
      key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
      issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
      key_content: ENV["APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64"],
      is_key_content_base64: true
    )

    upload_to_testflight(skip_waiting_for_build_processing: true)

app_store_connect_api_key
TestFlight にアクセス用の p8 ファイル を設定します
(p8 ファイルの作り方は割愛します)

通常 base64 エンコードした環境変数は
デコードして元に戻すして使用するのですが
is_key_content_base64: true というオプションがあり
base64 エンコードのまま使用できました

key_id:issuer_id:
p8 ファイルを作成したら App Store Connect で表示されているはずです

upload_to_testflight

実際に ipa を TestFlight へアップロードします

skip_waiting_for_build_processing: true
非同期処理化的な指定です

つまり 実際のアップロード成功を待たずに
upload_to_testflight が成功したら終わりにします

これは Mac ランナーの使用時間も軽減出来ますし
そもそも最後の工程なんで、実際のアップロードの成否を知りたければ
Actions のログでも、
実際の TestFlight のダッシュボードでも調べようは沢山あるはずです

気になる話

それより
私が WEB で見かけて気になっているのテクニックがあり
ipa のアーカイブまでは Mac でしか出来ないですが
TestFlight へのアップロードは Mac でなくても良いって話です

つまり ipa を作成したら
一旦 保存しておき、次は Linux なりの 非 Mac ランナーで
TestFlight へアップロードするというテクニックです
(Mac ランナーの使用時間を減らすテクニック)

結論は
コスパが悪そうと判断して見送りましたw

行う作業や処理に対して
劇的に Mac ランナーの使用時間が減るか? と考えたら
エラーやトラブルの可能性と比較してコスパ悪そうと思ってしまいました
(でも気になるw)

作成したキーチェーンの削除

  # 念の為 処理の最後とエラー時に作成したキーチェーンを削除
  after_all do |lane|
    if is_ci?
      delete_keychain(name: ENV["MATCH_KEYCHAIN_NAME"])
    end
  end

  error do |lane|
    if is_ci?
      delete_keychain(name: ENV["MATCH_KEYCHAIN_NAME"])
    end
  end

一応、念のため程度ですが Github Actions で fastlane が実行された場合のみ
(とエラー終了した場合)
キーチェーン名を指定で削除する処理を入れています
(Mac ランナーに残るとは言い切れませんが、なんちゃってセキュリティ風w)

ただ注意があり
私の場合は ローカル Mac とのビルドで併用しているので
is_ci を見て Github Actions で実行された場合のみ、という条件を入れています

いくら 名前が違うとはいえ
自身の Mac でキーチェーン削除コマンドを呼ぶのは 行うべきではありませんね

Test Flight

後は、App Store Connect 側で
Test Flight にアップした ipa の承認がされるまで 暫し待てば DL 可能になるはずです

最後に

これで Unity の iOS ビルド(ipa 作成)と TestFlight へのアップロードを
Free(無料)の環境で行う話は、一通り終わりです

思っている事は
Github Actions で Unity iOS ビルドに手を出すレベルの方なら
コピペして終わりなどという場合は少ないと思っており
それぞれが 手こずっている挙動 や エラーを解決するための 理屈 として
参考にして頂けたらと思っています
(Android 編もそうですが、結構 冗長に説明しているつもりです)

最後の最後に、どうしても Github Actions の Mac ランナーの
Free で使える 月の時間 200 分 がネックな気がします

私は前述した通り Github に課金してしまったので
会社のつもりでバンバン実行ボタンを押しそうで怖いですw

最後に書き終わって気が付いたのですが
同じ従量制課金をするなら Unity Automation Build にすれば
もっと簡単に iOS ビルド環境が手に入ったはずだなぁとw

  • Unity Automation Build の iOS ビルドは初めから有料
  • 専用の仕組みなので、基本の設定は ポチポチ指定で完了する(はず)
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?