首页
/ Immich iOS 的 fastlane 发布自动化:四个 Lane、多 Target 签名与 CI 集成详解

Immich iOS 的 fastlane 发布自动化:四个 Lane、多 Target 签名与 CI 集成详解

2026-09-06 21:12:07作者:何举烈Damon

本文基于 Immich 移动应用 iOS 端的 fastlane 文档 展开,完整讲解仓库中定义的四个 fastlane lane(开发/生产 TestFlight 构建、手动发布、仅构建验证)的用途与差异,并结合 Fastfile 源码剖析多 Target 手动签名、App Store Connect API Key 认证、版本号管理以及 GitHub Actions 工作流 中的证书与密钥管理。读完后,你可以理解 Immich iOS 从代码提交到 TestFlight 构建分发的完整发布链路,并在自己维护含 Share Extension、Widget Extension 的 iOS 应用时参考这套自动化方案。

一、fastlane 在 Immich iOS 工程中的位置

Immich 的移动应用(Flutter 项目,代码位于 mobile/ 目录)在 iOS 侧使用 fastlane 完成打包与 TestFlight 分发。相关配置集中在 mobile/ios/fastlane/ 目录:

  • README.md:fastlane 自动生成的文档,列出全部可用 lane 及其命令与描述,每次运行 fastlane 时会自动重新生成;
  • Fastfile:lane 定义与全部构建/签名逻辑的核心文件;
  • Appfile:fastlane 的应用级默认配置;
  • Gemfile:fastlane 的 Ruby 依赖声明。

Appfile 内容很简短,声明了默认应用标识与 Apple 开发者邮箱:

app_identifier "app.alextran.immich" # The bundle identifier of your app
apple_id "altran@futo.org" # Your Apple email address

fastlane 官方文档 开头给出的安装前提是安装 Xcode 命令行工具:

xcode-select --install

fastlane 本体则通过 Ruby 生态安装。Gemfile 声明了以下依赖:

source "https://rubygems.org"

gem "fastlane"
gem "cocoapods"
gem "abbrev" # Required for Ruby 3.4+
gem "multi_json"

其中 abbrev 是为兼容 Ruby 3.4+ 显式补充的依赖。在 mobile/ios/ 目录下执行 bundle install 即可完成安装(CI 中即通过 ruby/setup-rubybundler-cache: true 完成等价操作)。

二、运行 lane 需要哪些前置条件

Fastfile 的辅助方法可以看出,CI lane 依赖以下环境与文件:

依赖项 说明 源码依据
APP_STORE_CONNECT_API_KEY_ID 环境变量 App Store Connect API Key 的 Key ID Fastfile get_api_key
APP_STORE_CONNECT_API_KEY_ISSUER_ID 环境变量 API Key 的 Issuer ID 同上
~/.appstoreconnect/private_keys/AuthKey_<ID>.p8 文件 API 私钥文件,路径由 key_id 动态拼接 同上
FASTLANE_TEAM_ID 环境变量(可选) 覆盖默认的 TEAM_ID 常量 Fastfile configure_code_signing
已导入构建 Keychain 的 Distribution 证书 GHA 工作流负责导入,lane 内不再处理 build-mobile.yml

get_api_key 方法(Fastfile)的关键参数:

app_store_connect_api_key(
  key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
  issuer_id: ENV["APP_STORE_CONNECT_API_KEY_ISSUER_ID"],
  key_filepath: "#{Dir.home}/.appstoreconnect/private_keys/AuthKey_#{ENV['APP_STORE_CONNECT_API_KEY_ID']}.p8",
  duration: 1200,   # JWT 有效期 20 分钟
  in_house: false
)

duration: 1200 表示签发一次有效期 20 分钟的 JWT token 用于调用 App Store Connect API;in_house: false 表明走的是标准 App Store/TestFlight 通道而非企业内部分发。

Fastfile 顶部还定义了五个全局常量,贯穿所有 lane:

TEAM_ID = "2W7AC6T8T5"
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
BASE_BUNDLE_ID = "app.alextran.immich"
DEV_BUNDLE_ID  = "tech.futo.immich.testflight"
DEV_GROUP_ID   = "group.app.immich.share.testflight"

其中 BASE_BUNDLE_ID 是生产应用标识(与 Appfile 一致),DEV_BUNDLE_ID 是开发构建专用的独立 bundle ID,DEV_GROUP_ID 是开发构建专用的 App Group 标识。

三、核心辅助方法:签名、版本号与构建上传

Fastfile 把重复逻辑抽成了四个辅助方法,理解它们是理解各 lane 差异的基础。

3.1 build_xcargs:注入 CUSTOM_GROUP_ID 覆盖

build_xcargs 拼装 xcodebuild 参数:

def build_xcargs(group_id: nil)
  args = "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual"
  args += " CUSTOM_GROUP_ID='#{group_id}'" if group_id
  args
end

固定部分:-skipMacroValidation 跳过宏校验,并强制手动签名风格(CODE_SIGN_STYLE=Manual)与指定 Distribution 证书。可选部分:当传入 group_id 时注入 CUSTOM_GROUP_ID 构建参数——开发 lane 用它把 App Group ID 覆盖为 DEV_GROUP_ID,使开发构建与生产构建互不共享 App Group 数据(从源码结构看,该 ID 最终由 Xcode 工程在构建宏中消费)。

3.2 get_version_from_pubspec:版本号统一来自 pubspec.yaml

Immich 移动应用是 Flutter 项目,应用版本号统一维护在 mobile/pubspec.yamlversion 字段(格式为 x.y.z+build)。get_version_from_pubspec 负责解析:

def get_version_from_pubspec
  require 'yaml'

  pubspec_path = File.join(Dir.pwd, "../..", "pubspec.yaml")
  pubspec = YAML.load_file(pubspec_path)

  version_string = pubspec['version']
  version_string ? version_string.split('+').first.split('-').first : nil
end

注意路径 Dir.pwd/../..:fastlane 运行时的工作目录是 mobile/ios/,上溯两级正好是 mobile/pubspec.yaml。取值逻辑先按 + 截掉构建号(如 1.2.3+451.2.3),再按 - 截掉预发布后缀(如 1.2.3-rc11.2.3),保证写入 Info.plist 的版本号符合 App Store 的 x.y.z 规范。

3.3 configure_code_signing:三个 Target 的手动签名

Immich iOS 工程包含三个需要独立签名配置的 Target:主应用 Runner、分享扩展 ShareExtension、小组件扩展 WidgetExtension(对应 mobile/ios/Runner/ 下的目录结构)。configure_code_signing 对每个 Target 各调用一次 update_code_signing_settings

update_code_signing_settings(
  use_automatic_signing: false,
  path: "./Runner.xcodeproj",
  team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
  code_sign_identity: CODE_SIGN_IDENTITY,
  bundle_identifier: base_bundle_id,          # Runner
  profile_name: profile_name_main,
  targets: ["Runner"]
)
# ShareExtension:bundle id = "#{base_bundle_id}.ShareExtension",targets: ["ShareExtension"]
# WidgetExtension:bundle id = "#{base_bundle_id}.Widget",targets: ["WidgetExtension"]

要点:

  • 子扩展的 bundle ID 由基础标识派生:<base>.ShareExtension<base>.Widget,三个标识必须各自拥有独立的 Provisioning Profile;
  • profile_name 使用 sigh 刚下载安装的 Profile 名称(见下文各 lane),实现"下载即用"的 Profile 绑定;
  • team_id 支持通过 FASTLANE_TEAM_ID 环境变量覆盖默认团队,便于多团队环境。

3.4 build_and_upload:版本号、构建号、打包与上传

build_and_upload 是 dev 与 prod 两个 lane 共用的主干,参数为 api_keybase_bundle_idconfiguration(默认 "Release")、distribute_external(默认 true)、version_number、三个 Profile 名称及可选 group_id。执行顺序:

  1. 可选设置主版本号:若传入 version_number,调用 increment_version_number(version_number:) 显式覆盖(prod lane 传 pubspec 版本,dev lane 不传,沿用工程内版本);
  2. 递增构建号increment_build_number 的构建号取 latest_testflight_build_number(api_key:, app_identifier:) + 1,即通过 App Store Connect API 查询该 App 在 TestFlight 的最新构建号再 +1,避免构建号冲突;
  3. 构建build_app 使用 scheme: "Runner"workspace: "Runner.xcworkspace"export_method: "app-store"xcargs 来自 build_xcargsexport_options 中显式列出三个 bundle ID 与 Profile 的映射,signingStyle: "manual"signingCertificate 为 Distribution 证书;
  4. 上传upload_to_testflight(api_key:, skip_waiting_for_build_processing: true, distribute_external:),跳过等待 Apple 处理构建(处理过程可稍后在 App Store Connect 查看)。

四、四个 Lane 逐一解析

fastlane 文档 列出了四个可用命令,逐一对照 Fastfile 中的实现:

4.1 ios gha_testflight_dev — 开发构建到 TestFlight

[bundle exec] fastlane ios gha_testflight_dev

描述:iOS Development Build to TestFlight (requires separate bundle ID),对应 gha_testflight_dev lane。流程:

  1. get_api_key 获取 App Store Connect 凭据;
  2. DEV_BUNDLE_ID 及其 .ShareExtension.Widget 三个标识分别调用 sigh(api_key:, app_identifier:, force: true) 从 App Store Connect 下载/安装 Provisioning Profile,并从 lane_context[SharedValues::SIGH_NAME] 捕获每次 sigh 生成的 Profile 名称;
  3. configure_code_signing 用开发 bundle ID(tech.futo.immich.testflight)写入三个 Target 的签名配置;
  4. 调用 build_and_upload,注意三个特殊参数:
    • configuration: "Profile" —— 使用 Profile 配置而非 Release 配置编译;
    • distribute_external: false —— 上传后不自动分发外部测试员;
    • group_id: DEV_GROUP_ID —— 通过 CUSTOM_GROUP_ID 构建参数切换到开发专用 App Group。

独立的开发 bundle ID 意味着开发版 App 可与正式版共存于同一设备,且互不干扰共享数据。

4.2 ios gha_release_prod — 正式版到 TestFlight

[bundle exec] fastlane ios gha_release_prod

描述:iOS Release to TestFlight,对应 gha_release_prod lane。与 dev lane 的步骤完全同构,差异在于:

  • 三个 sigh 下载的是生产标识 app.alextran.immich 及其子扩展的 Profile;
  • configure_code_signing 使用 BASE_BUNDLE_ID
  • build_and_upload 传入 version_number: get_version_from_pubspec,即把 pubspec.yaml 中的主版本号显式写入 Info.plist,保证 TestFlight 上的版本与仓库声明版本严格一致;
  • configuration 保持默认的 "Release",不注入 group_id(沿用工程默认 App Group);
  • distribute_external: false,正式版上传同样不自动分发给外部测试员(由维护者在 App Store Connect 手动管理测试组)。

4.3 ios release_manual — 手动发布(本地自动签名)

[bundle exec] fastlane ios release_manual

描述:iOS Manual Release,对应 release_manual lane。这是面向本地开发者、不依赖 App Store Connect API Key 的 lane:

enable_automatic_code_signing(
  path: "./Runner.xcodeproj",
  targets: ["Runner", "ShareExtension", "WidgetExtension"]
)

increment_version_number(version_number: get_version_from_pubspec)
increment_build_number(build_number: latest_testflight_build_number + 1)

gym(
  scheme: "Runner",
  workspace: "Runner.xcworkspace",
  configuration: "Release",
  export_method: "app-store",
  skip_package_ipa: false,
  xcargs: "-skipMacroValidation -allowProvisioningUpdates",
  export_options: {
    method: "app-store",
    signingStyle: "automatic",
    uploadBitcode: false,
    uploadSymbols: true,
    compileBitcode: false
  }
)

upload_to_testflight(skip_waiting_for_build_processing: true)

与两个 gha lane 的关键差异:使用自动签名enable_automatic_code_signing + signingStyle: "automatic" + xcargs 中的 -allowProvisioningUpdates 允许构建时自动创建/更新 Profile),不传 API Key 直接基于本机凭据上传;gymskip_package_ipa: false 表示保留打包出的 .ipa 产物。注意该 lane 中 latest_testflight_build_number 未显式传递 api_key 参数,与 gha lane 中的调用方式(显式传入 api_key)不同,这是源码中的实际差异。

4.4 ios gha_build_only — 仅构建、不上传

[bundle exec] fastlane ios gha_build_only

描述:iOS Build Only (no TestFlight upload),对应 gha_build_only lane。源码注释说明其设计意图:"Use the same build process as the dev TestFlight lane, just skip the upload. This ensures PR builds validate the same way as dev TestFlight builds"(使用与 dev TestFlight lane 相同的构建流程,仅跳过上传,确保 PR 构建与 dev TestFlight 构建的验证方式一致)。实现上:

  • 复用 dev lane 的 sigh 三件套与 configure_code_signingDEV_BUNDLE_ID);
  • configuration: "Release"(dev lane 用的是 "Profile"),即 PR 构建按 Release 配置验证;
  • build_appskip_package_ipa: true,产出构建产物但不打包 ipa、不上传 TestFlight。

五、与 GitHub Actions 的集成

build-mobile.yml 中的 build-sign-ios job(L198 起)是这三个 gha lane 的实际调用方,运行于 macos-26 runner,在 main 分支、手动触发或非 fork PR 上执行。完整链路(L207-L314):

  1. 环境准备:选择 Xcode 26.2(sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer);用 ruby/setup-ruby(Ruby 3.3,bundler-cache: true,工作目录 ./mobile/ios)准备 Bundler 环境;通过 mise 执行 install:cicodegen,并用 flutter build ios --config-only --no-codesign 解析 Swift 包依赖;
  2. API Key 落地:从 secrets 读取 base64 编码的 APP_STORE_CONNECT_API_KEY,解码写入 ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8——这正是 Fastfilekey_filepath 拼接的路径;
  3. 证书导入:将 base64 编码的 IOS_CERTIFICATE_P12 解码为 certificate.p12,随后 security create-keychain 创建临时 build.keychain 并设为默认,导入证书后执行 security find-identity -v -p codesigning build.keychain 校验身份存在。这一步对应 Fastfile 中 "Certificate is imported by GHA workflow into build.keychain" 的注释——lane 内部假定证书已在 Keychain 中就位;
  4. 按环境分发 laneL296-L303):
if [[ "$DEPLOY" != 'true' ]]; then
  bundle exec fastlane gha_build_only
elif [[ "$ENVIRONMENT" == 'development' ]]; then
  bundle exec fastlane gha_testflight_dev
else
  bundle exec fastlane gha_release_prod
fi

即:非部署场景(PR 构建)走 gha_build_only 仅验证编译签名;部署且环境为 development 走 gha_testflight_dev;部署且为生产环境走 gha_release_prod。该 step 还显式传入 APP_STORE_CONNECT_API_KEY_IDAPP_STORE_CONNECT_API_KEY_ISSUER_IDFASTLANE_TEAM_ID 等环境变量,并设置 FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6 以放宽 xcodebuild settings 查询的超时与重试; 5. 收尾always() 条件下删除 build.keychainL305-L308),并把产物 mobile/ios/Runner.ipa 上传为 artifact ios-release-ipaL310-L314)。

六、关键要素速查

要素 值 / 位置 作用
生产 Bundle ID app.alextran.immichAppfile 正式 App 标识
开发 Bundle ID tech.futo.immich.testflightFastfile 开发 TestFlight 构建专用,可与正式版共存
开发 App Group group.app.immich.share.testflightFastfile CUSTOM_GROUP_ID 注入,隔离开发/生产共享数据
签名身份 Apple Distribution: FUTO Holdings, Inc. (2W7AC6T8T5)Fastfile CI 手动签名使用的 Distribution 证书
三个签名 Target Runner / ShareExtension / WidgetExtension(Fastfile 各自独立 bundle ID 与 Provisioning Profile
版本号来源 mobile/pubspec.yamlFastfile 统一 Flutter 应用的版本声明
构建号来源 TestFlight 最新构建号 +1(Fastfile 自动避免构建号冲突
环境密钥 APP_STORE_CONNECT_API_KEY_ID / APP_STORE_CONNECT_API_KEY_ISSUER_ID / IOS_CERTIFICATE_P12 / IOS_CERTIFICATE_PASSWORD / FASTLANE_TEAM_ID build-mobile.yml secrets 注入

七、小结

Immich iOS 的 fastlane 配置体现了一套典型的"多 bundle ID + 手动签名 + API Key 驱动"的移动发布方案:通过 gha_build_only / gha_testflight_dev / gha_release_prod 三个 lane 分别覆盖 PR 验证、开发分发与正式分发,release_manual 则保留了本地自动签名的手动发布通道;版本号统一收敛到 Flutter 的 pubspec.yaml,构建号由 TestFlight 远端状态自动推导,Provisioning Profile 由 sigh 按需下载并即时绑定到 Runner、ShareExtension、WidgetExtension 三个 Target。配合 build-mobile.yml 中 Keychain 生命周期管理(创建—导入—使用—删除)与 API Key 落地流程,整条链路可在全无状态 runner 上重复执行。对需要管理含扩展 Target 的 iOS 应用发布流程的团队,这套"常量集中定义、辅助方法复用、lane 按环境切分"的 Fastfile 组织方式是一个可直接参考的范式。

登录后查看全文
热门项目推荐
相关项目推荐