Unity Cloud Build 自动化打包 IPA 并上传 App Store Connect 踩坑实录

Unity Cloud Build 自动化打包 IPA 并上传 App Store Connect 踩坑实录

最近给 Unity 项目搭建 iOS 自动化发布流程,原本规划是使用 GitHub Actions 完成整个流程:

GitHub Actions
    ↓
Unity Build
    ↓
生成 IPA
    ↓
上传 App Store Connect

看起来很美好,但实际落地过程中踩了不少坑。最终放弃 GitHub Actions 打包,转而采用 Unity Cloud Build(UCB)方案。

这篇文章记录几个最关键的问题,以及最终的解决方案。

Unity License

最开始打算直接使用 GitHub Actions 构建 Unity 项目。

结果第一步就被 Unity License 卡住了。

Personal License

网上很多教程都会提到:

CI 环境激活 Unity License

但大多数教程默认使用的是:

Unity Pro
Unity Plus

而我使用的是:

Unity Personal

问题就在这里:Personal License 不再支持离线激活12。Unity Personal License 的激活方式只剩下:Unity Hub 登录,无法像Pro License 那样通过 License 文件进行稳定的离线激活。

也就是说:

本地电脑
↓
Unity Hub 登录
↓
激活成功

这样是没有问题的,但是:

GitHub Actions
↓
无界面环境
↓
无法正常使用 Unity Hub

那么问题就来了。

社区方案失效

也查到的一些方案,例如:

unity-builder
game-ci

大多都涉及 .alf → .ulf 的 License 激活流程。Unity 关闭Personal License后,这类方案貌似也失效了,就没有继续测试了。

Editor 体积和缓存问题

除了刚刚的License问题,Github Action 打包还有一个更严重的问题。Unity Editor可执行高达5G(以我们现在用的6000.3.15f1为例)要是每次去下载,浪费太多时间。即使缓存下来,每次恢复也是不小的耗时。

Unity 项目最大的时间消耗其实是:

Library

理论上可以缓存。但实际上:

升级 Unity
升级 Package
修改依赖

之后缓存很容易失效。最终构建时间可能不会太理想。

UCB

这种情况,索性直接切换到:Unity Cloud Build(UCB)

因为:

  • Unity 官方托管
  • 不需要处理 License
  • 不需要安装 Editor
  • 不需要维护构建环境
  • 直接支持 ipa 导出

构建稳定性明显提升。基本不需要做什么操作。添加代码仓库后,添加构建目标,很少的操作就能完成构建。具体操作就不讨论了。

UCB 打包后上传 App Store Connect:Fastlane 的 AppStoreInfo.plist 坑

解决打包问题后,下一步就是上传 IPA。最开始选择:

Fastlane

毕竟是目前流行的方案。

AppStoreInfo.plist

刚开始尝试FlastLane上传,毛病就来了(FlastLane具体操作方法不在本文讨论范文,改天重开一篇写一下),我是用wsl操作的。

user@ubuntu:~/Vermilion$ fastlane pilot upload \
--ipa "/home/user/app/33263704908528-app-default-ios-2.ipa" \
--api_key_path "/home/user/app/api_key.json"
[✔] 🚀
[16:38:21]: Get started using a Gemfile for fastlane https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile
[16:38:22]: Creating authorization token for App Store Connect API
[16:38:23]: Ready to upload new build to TestFlight (App: xxxx)...
[16:38:23]: Going to upload updated app to App Store Connect
[16:38:23]: This might take a few minutes. Please don't interrupt the script.
[16:38:23]: AppStoreInfo.plist is required for uploading .ipa files on non-macOS platforms.
[16:38:23]: Expected AppStoreInfo.plist in the same directory as the .ipa file.
[16:38:23]: Generate it by running either 'fastlane gym [...] --generate_appstore_info'
[16:38:23]: Or add '<key>generateAppStoreInformation</key><true/>' in an options.plist then run 'fastlane gym [...] --export_options options.plist'.

[!] Missing required AppStoreInfo.plist file for iTMSTransporter upload

看到这里,人直接懵了,之前github action操作也是用 fastlane 上传啊,怎么这里不对了?需要AppStoreInfo.plist,这是个啥文件?UCB 导出的产物里面根本没有这个文件啊。

继续搜索资料后发现。很多人给出的解决方案是:

找一台 Mac
Archive 一次
导出 AppStoreInfo.plist

于是专门借了一台 Mac。按操作Archive后,确实拿到了AppStoreInfo.plist。把这个文件放到 IPA 同目录后,Fastlane 确实能继续往下执行。但是,实际测试发现根本不是这么回事。因为这个文件里面包含大量构建相关信息:

CFBundleVersion
CFBundleShortVersionString
Team 信息
签名信息
Provisioning Profile
导出配置
图标配置
...

AppStoreInfo.plist并不是一个简单的配置文件。文件内容很复杂,别的mac导出的,大概率是不能直接为ucb新生成的ipa使用的。最终还是放弃了,不折腾他了。

第三方上传工具

后来也试过,开心上架(App Uploader),但是,它依赖 Apple ID + App 专用密码(App-Specific Password)。而我更希望整个流程基于:

API Key
Issuer ID
p8 私钥

实现完全自动化。因此最终也没有采用这类方案。

Post-build script

多方查阅,发现ucb提供了 Post-build script3,可以在 ucb 构建完成后执行一个脚本。这不就简单了么。

在 ucb 的配置里,添加构建目标,添加三个环境变量:

APPLE_API_KEY_ID: API_KEY_ID
APPLE_API_ISSUER_ID: ISSUER_ID
APPLE_API_KEY_CONTENT_BASE64: p8文件base64值

添加以下脚本到 Assets\Editor\PostBuild.bash

#!/bin/bash

echo "========================================================="
echo "  Starting App Store Connect Auto-Upload Flow (V3)      "
echo "========================================================="

# 1. 动态探测并定位 IPA 文件的绝对路径 (主动去 workspace 深度搜索)
echo "Searching for generated IPA file..."
PATH_TO_IPA=$(find "$WORKSPACE/.build/last" -name "*.ipa" 2>/dev/null | head -n 1)

if [ -z "$PATH_TO_IPA" ] || [ ! -f "$PATH_TO_IPA" ]; then
    echo "[ERROR] 在 $WORKSPACE/.build/last 目录下没有找到任何 .ipa 文件!"
    echo "请确认 Unity 编译阶段是否成功结束。"
    exit 1
fi

# 2. 还原 p8 文件
PRIVATE_KEYS_DIR="$HOME/.private_keys"
mkdir -p "$PRIVATE_KEYS_DIR"
PRIVATE_KEY_FILE="$PRIVATE_KEYS_DIR/AuthKey_${APPLE_API_KEY_ID}.p8"

echo "Cleaning and Decoding Apple Auth Key file from Base64..."
echo "$APPLE_API_KEY_CONTENT_BASE64" | base64 -D > "$PRIVATE_KEY_FILE"

# 3. 校验文件大小
if [ ! -s "$PRIVATE_KEY_FILE" ]; then
    echo "[ERROR] Base64 解码失败,解出来的密钥文件内容为空!"
    exit 1
fi

# 4. 调用苹果官方 altool 交付工具
if xcrun altool --upload-app \
    -t ios \
    -f "$PATH_TO_IPA" \
    --apiKey "$APPLE_API_KEY_ID" \
    --apiIssuer "$APPLE_API_ISSUER_ID" ; then
    
    echo "========================================================="
    echo " 🎉 TestFlight 上传成功!"
    echo "========================================================="
    rm -f "$PRIVATE_KEY_FILE"
    exit 0
else
    echo "========================================================="
    echo " ❌ App Store Connect 交付失败,请检查上方苹果返回的错误码。"
    echo "========================================================="
    rm -f "$PRIVATE_KEY_FILE"
    exit 1
fi

构建配置里,Post-build script,设置成 Assets/Editor/PostBuild.bash。

Bingo! 搞定。

CFBundleVersion 自动递增问题

上传成功后又遇到苹果经典错误:

CFBundleVersion must be higher than previous version

应用市场要求(不止是苹果)CFBundleVersion必须递增(ios是CFBundleVersion,Android是versionCode),对于普通ios项目、flutter,可以通过脚本修改Info.plist信息,实现CFBundleVersion的增加。Unity项目里除了在项目设置里,还真没发现在哪里修改。如果每次手动改,每次更新前总是会忘记,尤其多人协作项目,不能依赖人操作。

使用时间戳作为 Build Number

解决方法就是,在 UCB 的 Pre-Export method 阶段自动执行:

using System;
using System.IO;
using UnityEditor;
using UnityEngine;

#if UNITY_IOS
using UnityEditor.iOS.Xcode;
#endif

/// <summary>
/// Unity Cloud Build (UCB) 自动化构建回调处理类
/// </summary>
public static class CloudBuildProcessor
{
    /// <summary>
    /// 【UCB 配置】请填入 UCB 后台的 Pre-Export Method 输入框:CloudBuildProcessor.OnPreExport
    /// 执行时机:Unity 开始导出工程之前
    /// </summary>
    public static void OnPreExport()
    {
        SetIOSBuildNumber();
    }

    /// <summary>
    /// 内部功能:将 iOS 的 Build 号 (CFBundleVersion) 自动配置为当前时间戳
    /// </summary>
    private static void SetIOSBuildNumber()
    {
        string buildNumber = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        PlayerSettings.iOS.buildNumber = buildNumber;
        Debug.Log($"[UCB Pre-Export] iOS CFBundleVersion / Build Number 已设置为: {buildNumber}");
    }


}

效果:

1752847001
1752933401
1753019801

天然递增,永不重复。苹果对这个字段没有最大值限制,可以一直用下去。在UCB后台,构建里,将的 Pre-Export Method 输入框:CloudBuildProcessor.OnPreExport

苹果出口合规(Export Compliance)问题

解决 Build Number 后,上传成功。TestFlight 正常应该是会自动处理,发送邮件给测试人员。等了半天,没收到邮件,上去瞅一瞅,呦,出口合规问题又出来了。原先的操作也是在Info.plist里添加ITSAppUsesNonExemptEncryption。又回到了刚刚的问题,没有Info.plist啊。

UCB 自动注入

解决方法还是要靠UCB啊。Post-Export是在构建完成后执行。

using System;
using System.IO;
using UnityEditor;
using UnityEngine;

#if UNITY_IOS
using UnityEditor.iOS.Xcode;
#endif

/// <summary>
/// Unity Cloud Build (UCB) 自动化构建回调处理类
/// </summary>
public static class CloudBuildProcessor
{

    /// <summary>
    /// 【UCB 配置】请填入 UCB 后台的 Post-Export Method 输入框: CloudBuildProcessor.OnPostExport
    /// 执行时机:Unity 成功生成 Xcode 工程之后,开始编译 IPA 之前
    /// </summary>
    /// <param name="pathToBuiltProject">UCB 自动传入的 Xcode 工程根目录路径</param>
    public static void OnPostExport(string pathToBuiltProject)
    {
        FixIOSExportCompliance(pathToBuiltProject);
    }

    /// <summary>
    /// 内部功能:在生成的 Info.plist 中自动注入苹果出口合规证明 (ITSAppUsesNonExemptEncryption = false)
    /// </summary>
    private static void FixIOSExportCompliance(string pathToBuiltProject)
    {
        Debug.Log($"[UCB Post-Export] 正在开始修改 Info.plist,目标路径: {pathToBuiltProject}");

#if UNITY_IOS
        // 1. 获取并读取 Info.plist
        string plistPath = Path.Combine(pathToBuiltProject, "Info.plist");
        if (!File.Exists(plistPath))
        {
            Debug.LogError($"[UCB Post-Export] 未找到 Info.plist 文件: {plistPath}");
            return;
        }

        PlistDocument plist = new PlistDocument();
        plist.ReadFromFile(plistPath);

        // 2. 插入关闭出口合规提示的布尔值
        PlistElementDict rootDict = plist.root;
        rootDict.SetBoolean("ITSAppUsesNonExemptEncryption", false);

        // 3. 保存文件
        plist.WriteToFile(plistPath);
        Debug.Log("[UCB Post-Export] 成功向 Info.plist 写入 ITSAppUsesNonExemptEncryption = false");
#else
        Debug.LogWarning("[UCB Post-Export] 当前非 iOS 平台环境,跳过 Info.plist 修改。");
#endif
    }
}

在UCB 后台的 Post-Export Method 输入框: CloudBuildProcessor.OnPostExport

构建时自动完成。无需人工干预。

搞定,手工。

最终流程

最终形成的自动化发布链路:

Git Push
    ↓
Unity Cloud Build
    ↓
Pre-Export
    ├─ 自动生成 CFBundleVersion
    ↓
生成 Xcode 工程
    ↓
Post-Export
    ├─ 自动写入出口合规配置
    ↓
生成 IPA
    ↓
Shell Script
    ├─ 自动寻找 IPA
    ├─ 自动恢复 API Key
    ├─ altool 上传
    ↓
TestFlight

整个流程下来:

  • 不依赖 GitHub Actions 构建 Unity
  • 不依赖 Fastlane
  • 不依赖 App Uploader
  • 不需要手工修改 Build Number
  • 不需要每次填写出口合规信息

对于使用 Unity Personal License 的独立开发者来说,这套方案比 GitHub Actions 更加稳定。

  1. Unity – Manual: Introduction to manual license activation ↩︎
  2. Unity no longer supports manual activation of Personal licenses – Unity Engine – Unity Discussions ↩︎
  3. https://docs.unity.com/zh-cn/build-automation/advanced-build-configuration/run-custom-scripts-during-the-build-process ↩︎

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注