freeCodeCamp.org

How to Automate Flutter Releases with Fastlane and GitHub Actions for Firebase App Distribution, Google Play, TestFlight, and App Store Connect

8.5内容质量
How to Automate Flutter Releases with Fastlane and GitHub Actions for Firebase App Distribution, Google Play, TestFlight, and App Store Connect

TL;DR · AI 摘要

通过GitHub Actions与Fastlane集成,实现Flutter应用向Firebase、TestFlight、Google Play和App Store的自动化发布,节省人工操作时间。

核心要点

  • 使用GitHub Actions触发CI/CD流程,自动构建Flutter应用
  • Fastlane处理Android签名、IPA生成及多平台分发
  • 配置Android Keystore和Apple API Key实现安全凭证管理

结构提纲

按章节快速跳转。

  1. 揭示手动发布流程的低效性,引出自动化解决方案

  2. GitHub Actions提供云算力,Fastlane处理构建与分发逻辑

  3. 加密存储Android Keystore和Apple API Key等敏感信息

  4. 分Android/iOS配置Gemfile、Appfile等核心文件

  5. 编写Android/iOS专属GitHub Actions工作流YAML文件

  6. 建议设置构建号策略、分支保护规则等安全措施

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Flutter自动化发布
    • 工具链
      • GitHub Actions
      • Fastlane
    • 流程步骤
      • 凭证加密
      • 构建编译
      • 多平台分发
    • 安全实践
      • Secrets管理
      • 分支保护

金句 / Highlights

值得收藏与分享的关键句。

#Flutter#CI/CD#Fastlane#GitHub Actions#Firebase
打开原文

如何使用 Fastlane 和 GitHub Actions 自动化 Flutter 发布流程(适用于 Firebase App Distribution、Google Play、TestFlight 和 App Store Connect)

2026年8月11日

/

#Flutter

Atuoha Anthony

想象一下:周五下午4点,团队刚刚合并了冲刺周期的最后一个功能。但产品经理要求在当天结束前将新版本上传到 TestFlight,以便客户周末审核。

你打开 Xcode,等待归档完成,却遇到昨天没有的代码签名错误,修复后重新归档、等待、上传,再等待 App Store Connect 处理。接着你重复同样的流程处理 Android 版本,通过 Android Studio 签名 APK,登录 Firebase App Distribution 上传文件,添加测试人员,撰写发布说明,点击发送。

现在是晚上6:45。过去两个小时你没有编写任何产品代码。每次发布周期都会这样。

再想象另一种场景:你将代码推送到 dev 分支。GitHub 服务器立即接管。几分钟内,隔离的云环境会检出你的代码,安装 Flutter,从加密密钥中解码签名凭证,构建 APK 和 IPA,并同时将两者分发到 Firebase App Distribution(Android 测试人员)和 TestFlight(iOS 测试人员)。你已经到家了,测试人员会自动收到通知。

这正是本手册要构建的发布流程。

当你读完本指南时,推送代码到 dev 分支将自动将构建分发到 Firebase App Distribution 和 TestFlight。推送代码到 prod 分支将自动分发到 Google Play Store 和 Apple App Store。你将永远不再需要手动导出 IPA 或上传 APK。

实现这一切的工具是 GitHub Actions(提供运行自动化任务的云计算机)和 Fastlane(处理构建、签名和分发逻辑)。本手册将两者视为与应用程序本身同等重要的生产基础设施,值得同样细致的维护和文档说明。

目录

  • 前提条件
  • 什么是 CI/CD 以及为什么你的 Flutter 应用需要它 概念 手动部署存在的问题
  • 架构:所有组件如何连接
  • 生成你的凭证和密钥 Firebase 凭证 Apple App Store Connect API 密钥 Google Play Store 服务账户 Fastlane Match 证书仓库
  • 背景密码学:将文件转换为密钥 生成 Android 密钥库 编码 Apple API 密钥 编码 GitHub 凭证用于 Match 编码环境文件
  • 配置 GitHub Actions 密钥
  • 为 Android 配置 Fastlane Gemfile Gradle Properties 文件 Android Appfile Android Pluginfile Android Fastfile
  • 为 iOS 配置 Fastlane iOS Gemfile iOS Appfile Matchfile iOS Pluginfile iOS Fastfile
  • 编写 GitHub Actions 工作流 Android 工作流 iOS 工作流
  • 截图
  • 完整部署的端到端流程
  • 最佳实践 保持证书仓库私有并受访问控制 设置最低构建号策略 添加分支保护规则 监控工作流运行时间和成本 将发布说明存储在文件中而非仅作为输入
  • 使用 Xcode 项目而非工作区的常见错误 未为 iOS 设置 setupci 为新项目在只读模式下运行 Match 忘记递增构建版本号 用带有尾随换行符的文件进行编码 为 Firebase 使用错误的分发类型 为 Google Play 服务账号授予的权限不足
  • 结论
  • 参考资料 GitHub Actions Fastlane Apple Google Flutter

前置条件

开始之前,请确保以下内容已准备就绪。跳过任何步骤都可能导致难以诊断的失败。

  • 一个已有 GitHub 仓库的 Flutter 项目:该项目应已能本地构建。如果在你的机器上 flutter build apk --release 和 flutter build ios --release --no-codesign 都能成功执行,说明你已准备就绪。
  • 具有管理员或账户持有者角色的 Apple 开发者账号:你需要这个账号来创建 App Store Connect API 密钥。开发者角色的账号不足以完成此操作。
  • 一个 Google Play Console 账号,且应用至少处于草稿状态:Google Play API 无法向从未上传过任何版本的应用推送内容。如果你的应用是全新的,需要先进行一次手动上传以创建应用列表,之后才能由自动化流程接管。
  • 一个已启用 Android 和 iOS Firebase 应用分发功能的 Firebase 项目。
  • 开发机上已安装 Ruby:Fastlane 是一个 Ruby gem。运行 ruby -v 命令进行检查。macOS 自带 Ruby 但版本通常较旧。可通过 Homebrew 安装最新版本:brew install ruby。
  • 本地已安装 Fastlane:通过 gem install fastlane 命令进行安装。在 CI 服务器接管之前,你将在终端中使用它进行设置。
  • macOS 上已安装 Homebrew:用于本地安装依赖项。
  • 一个你熟悉的终端:本指南的每一步都需要运行命令。其中大部分操作没有图形界面替代方案。

自动化解决了这三个问题。步骤被记录在版本控制的文件中。由于每次运行都是由这些文件构建的全新云环境,因此每次运行的环境都完全一致。并且该过程在后台运行,不会影响你开发下一个功能。

架构:所有组件如何连接

在接触任何配置文件之前,先理解整个系统以及每个组件如何协同工作。在没有完整系统认知的情况下进行构建,会导致在不知道从哪里查找时就陷入调试困境。

GitHub Actions 提供了称为 runners(运行器)的基于云的虚拟机。每次你向配置的分支推送代码时,GitHub 会启动一个全新的 runner(Android 使用 Ubuntu,iOS 使用 macOS),执行工作流文件中的步骤,完成后销毁该机器。每次运行时机器都会从零开始全新构建。

Fastlane 是一个用于自动化移动应用构建和部署任务的开源工具。它在 GitHub Actions 的 runner 中运行,处理平台特定的步骤:构建应用包、管理 iOS 代码签名、将二进制文件上传到分发平台。你编写 Fastlane 的 "lanes"(命名的步骤序列),由 GitHub Actions 调用执行。

Fastlane Match 是 Fastlane 中用于 iOS 代码签名的子系统。iOS 应用需要在构建它们的机器上安装证书和配置文件。Match 将这些文件存储在加密的私有 GitHub 仓库中,并在构建前将它们下载到 CI runner 上。这消除了在多台机器上手动管理证书的噩梦。

Firebase App Distribution 接收你构建的 APK 和 IPA 文件用于开发环境,并自动通知测试人员。

App Store Connect 和 Google Play Console 接收你用于生产环境的构建版本。

证书仓库是一个独立的私有 GitHub 仓库,Fastlane Match 会从中读取和写入数据。它保存着使用你独知的密码加密的 iOS 签名材料。

生成你的凭证和密钥

本节需要在多个第三方仪表板之间导航,以收集 CI 流水线所需的凭证。

Firebase 凭证

Firebase App Distribution 需要两部分信息:你的应用 ID 和一个授权 CI 服务器上传构建的 service account(服务账户)。

前往 Firebase 控制台并打开你的项目。

进入项目设置(左侧边栏中“项目概览”旁边的齿轮图标)。

向下滚动到“你的应用”部分。你会看到已注册的 Android 和 iOS 应用列表。找到并复制每个应用的 App ID。Android App ID 的格式类似 1:1234567890:android:abc123def456,iOS App ID 的格式类似 1:1234567890:ios:abc123def456。

保持在项目设置中,点击“服务账户”标签页。

点击“生成新私钥”并确认对话框。一个 .json 文件将下载到你的机器上。这个文件就是服务账户凭证。请妥善保管,不要提交到任何仓库。

Apple App Store Connect API 密钥

Apple 用 API 密钥取代了基于密码的 API 访问方式。你需要一个密钥,让 Fastlane 在不使用你的 Apple ID 凭证的情况下与 App Store Connect 通信。

前往 App Store Connect,导航到顶部导航栏的“用户和访问”部分。

点击“集成”标签页,然后在左侧边栏选择“App Store Connect API”。

点击“+”按钮生成新密钥。将其命名为清晰的名称,例如“GitHub Actions CI”。将访问权限设置为“App Manager”。

创建密钥后,请记录页面顶部显示的 Issuer ID 和密钥行中显示的 Key ID。点击 Download API Key 保存 .p8 文件。该文件只能下载一次。如果丢失,必须创建新密钥。

Google Play Store 服务账号

Google Play API 使用服务账号(Google Cloud 中的机器身份)来验证上传。

打开 Google Cloud Console 并确保您已进入与 Play Console 关联的项目。

在左侧边栏导航到 IAM 和管理,然后点击 服务账号。

点击 创建服务账号。为其命名,例如 github-actions-play-store。分配角色 Service Account User。完成创建。

在列表中点击新创建的服务账号。进入 密钥 选项卡。点击 添加密钥 然后 创建新密钥。选择 JSON 格式。将下载 .json 文件。

现在将此服务账号与您的 Play Console 关联。进入 Google Play Console,打开您的应用,导航到 设置 然后 API 访问。授予服务账号至少具有发布管理员权限的访问权限。

Fastlane Match 证书仓库

Fastlane Match 将您的 iOS 签名材料存储在专用的私有 GitHub 仓库中。现在创建一个全新、完全空的私有仓库。将其命名为类似 your-app-certificates 的名称。不要使用任何文件初始化它。

接下来创建一个个人访问令牌,使 Fastlane 能够从 CI runner 读取和写入此仓库。进入 GitHub 账户的 设置,滚动到底部并点击 开发者设置,然后点击 个人访问令牌,再点击 令牌(经典)。

生成一个新的经典令牌。为其命名,例如 fastlane-match-ci。在 选择范围 中勾选 repo 范围(授予完整仓库访问权限)。如果安全策略允许,将过期时间设置为至少一年或无过期时间。生成令牌并立即复制。GitHub 不会再次显示该令牌。

新生成的令牌:

背景密码学:将文件转换为秘密

GitHub Actions Secrets 仅接受纯文本字符串。您的签名凭证是二进制文件:Android 的 .jks 密钥库、Apple 的 .p8 密钥文件和 Firebase 的 .json 服务账号。要将二进制文件作为秘密存储,需要将其转换为 Base64,这是一种将任何二进制数据表示为可打印 ASCII 字符串的方式。

本节中的每个命令都在您的终端中运行。运行每个命令后,打开生成的 .txt 文件,复制其中的全部内容,并将其保存在安全的位置(密码管理器是一个不错的选择)。复制完成后,删除 .txt 文件。

生成 Android 密钥库

Android 密钥库是您在 Play 商店上的应用的密码学身份。一旦使用特定密钥库发布应用,您必须永远使用该密钥库进行所有更新。丢失它意味着您无法向现有应用推送更新。生成它并安全备份。

code
keytool -genkey -v \
  -keystore release-keystore.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias YOUR_KEY_ALIAS \
  -dname "CN=Your Name, OU=App, O=Your Company, L=Your City, ST=Your State, C=US" \
  -storepass "YOUR_SECURE_PASSWORD" \
  -keypass "YOUR_SECURE_PASSWORD"

keytool 是 Java 开发工具包的一部分,是管理 Java 加密密钥库的标准工具。-keystore release-keystore.jks 指定输出文件名称。-keyalg RSA 和 -keysize 2048 分别指定加密算法和密钥长度,这是 Android 签名的推荐配置。

-validity 10000 将证书有效期设置为约 27 年,这是 Google Play 商店密钥的通用推荐值。-alias YOUR_KEY_ALIAS 是你在密钥库中引用该密钥时使用的名称。请将其替换为有意义的名称,例如你的应用名称。-dname 是可分辨名称(Distinguished Name),用于标识证书所有者。请将所有值替换为你的个人信息。

-storepass 和 -keypass 分别是保护密钥库文件和密钥本身的密码。这两个密码可以设置为相同值,这可以简化 GitHub Secrets 的配置。

现在将密钥库文件转换为 GitHub Secrets 可存储的 Base64 字符串:

code
base64 -i release-keystore.jks > release-keystore-base64.txt

base64 -i release-keystore.jks 读取二进制 .jks 文件并将其编码为 Base64 字符串。> 操作符将输出重定向到 release-keystore-base64.txt 文件,而不是打印到终端。打开该文件,复制整个字符串(它会很长),将其保存到密码管理器中并标记为 ANDROID_KEYSTORE_BASE64,然后删除 .txt 文件。

编码 Apple API 密钥

code
base64 -i AuthKey_YOUR_KEY_ID.p8 > authkey-base64.txt

将 AuthKey_YOUR_KEY_ID.p8 替换为你从 App Store Connect 下载的 .p8 文件的确切文件名。密钥 ID 在文件名中。该命令将二进制密钥文件编码为 Base64 字符串。打开 authkey-base64.txt,复制内容,保存为 APPSTORE_API_PRIVATE_KEY_BASE64,然后删除文件。

为 Fastlane Match 编码 GitHub 凭据

Fastlane Match 使用 HTTP 基本认证连接到你的证书仓库,需要将用户名和令牌编码为 Base64 字符串。这是 HTTP 基本认证的标准格式。

code
echo -n "YOUR_GITHUB_USERNAME:YOUR_PERSONAL_ACCESS_TOKEN" | base64

echo -n 输出字符串时不带换行符。-n 参数至关重要:如果包含换行符,会导致 Base64 编码结果损坏。| base64 将输出直接传递给 Base64 编码器,无需创建中间文件。编码结果会直接打印到终端。复制该结果并保存为 MATCH_GIT_BASIC_AUTHORIZATION。

编码环境配置文件

如果你的 Flutter 应用使用 .env 文件存储敏感配置(如 API 密钥,此类内容绝不能提交到 Git),需要将其编码,以便 CI 构建前能还原:

code
base64 -i .env > env-base64.txt

该命令从项目根目录读取 .env 文件并进行 Base64 编码。打开 env-base64.txt,复制内容,保存为 ENV_FILE_BASE64,然后删除文件。如果项目未使用 .env 文件,请跳过此步骤,并在后续删除 GitHub Actions 工作流文件中对应的步骤。

在左侧边栏中,点击 Secrets and variables(密钥和变量),然后点击 Actions(操作)。

为以下每个密钥点击 New repository secret(新建仓库密钥)。名称必须与原文完全一致,因为工作流文件会直接引用这些名称。

依次添加以下密钥:

环境和配置:

  • ENV_FILE_BASE64:通过编码你的 .env 文件得到的 Base64 字符串。

Firebase 和 Google Play:

  • FIREBASE_APP_ID_ANDROID:从 Firebase 控制台复制的 Android 应用 ID(格式:1:xxx:android:xxx)。
  • FIREBASE_APP_ID_IOS:从 Firebase 控制台复制的 iOS 应用 ID。
  • FIREBASE_SERVICE_ACCOUNT_JSON:直接粘贴 Firebase 服务账户 .json 文件的原始内容。不要对这个文件进行编码:工作流会直接将其写入文件。
  • GOOGLE_PLAY_JSON:直接粘贴 Google Play 服务账户 .json 文件的原始内容。

Android 签名:

  • ANDROID_KEYSTORE_BASE64:通过编码 .jks 密钥库文件得到的 Base64 字符串。
  • ANDROID_KEY_ALIAS:生成密钥库时使用的别名(例如:your-app-key)。
  • ANDROID_KEY_PASSWORD:生成密钥库时设置的密钥密码。
  • ANDROID_STORE_PASSWORD:生成密钥库时设置的存储密码。

Apple App Store:

  • APPSTORE_ISSUER_ID:来自 App Store Connect API 密钥页面的 Issuer ID。
  • APPSTORE_API_KEY_ID:来自 App Store Connect API 密钥页面的 Key ID。
  • APPSTORE_API_PRIVATE_KEY_BASE64:通过编码 .p8 文件得到的 Base64 字符串。

Fastlane Match:

  • MATCH_GIT_BASIC_AUTHORIZATION:username:token 的 Base64 字符串。
  • MATCH_PASSWORD:你自己创建的强密码。这个密码用于加密 Match 仓库中的证书。建议使用密码管理器生成强密码。请妥善保管,因为无法恢复:如果丢失,必须重新创建证书仓库。

为 Android 设置 Fastlane

Android 的 Fastlane 位于 Flutter 项目的 android/ 目录中。创建以下文件。

Gemfile

code
# android/Gemfile

source "https://rubygems.org"
gem "fastlane"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

source "https://rubygems.org" 告诉 Bundler(Ruby 的包管理器)从哪里获取 gem。gem "fastlane" 声明 Fastlane 为一个依赖项。

plugins_path 的代码行会在存在 Pluginfile 时加载额外的插件声明。这种结构允许主 Gemfile 和插件列表分开维护,这是 Fastlane 项目遵循的惯例。

始终使用 Bundler(bundle exec fastlane)而不是直接调用 fastlane,因为 Bundler 会确保使用 Gemfile.lock 中声明的确切 gem 版本,从而使构建过程在不同机器上可复现。

Gradle 属性文件

code
# android/gradle.properties

org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=1G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError

org.gradle.jvmargs 配置 Gradle 构建过程的 Java 虚拟机参数。-Xmx4G 将最大堆内存设置为 4GB。-XX:MaxMetaspaceSize=1G 将元空间(类元数据)限制为 1GB。-XX:ReservedCodeCacheSize=512m 为编译代码缓存预留 512MB。-XX:+HeapDumpOnOutOfMemoryError 会在 JVM 内存不足时生成堆转储文件,这有助于事后调试。

如果没有此配置,GitHub Actions runner 在 Gradle 构建期间经常会因超出标准 GitHub 主机 runner 的 7 GB 内存限制而失败,表现为退出码 137 或 143。

Android Appfile

code
# android/fastlane/Appfile

json_key_file(ENV["FIREBASE_SERVICE_ACCOUNT_JSON_PATH"])
package_name("com.yourcompany.app")

json_key_file(...) 告诉 Fastlane 去哪里查找授予 Google Play 访问权限的 Google 服务账户 JSON 文件。它从 FIREBASE_SERVICE_ACCOUNT_JSON_PATH 环境变量读取,该变量由 GitHub Actions 工作流步骤设置。package_name(...) 声明应用的包标识符。请将 com.yourcompany.app 替换为 AndroidManifest.xml 中定义的实际应用包名。

Android Pluginfile

code
# android/fastlane/Pluginfile

gem 'fastlane-plugin-firebase_app_distribution'

这声明了 Firebase App Distribution 插件作为依赖项。Fastlane 的核心安装不包含平台特定插件。fastlane-plugin-firebase_app_distribution gem 添加了 firebase_app_distribution 动作,该动作被 firebase 路线使用以上传构建并通知测试人员。如果没有这一行,当 firebase 路线尝试调用 firebase_app_distribution 时会因 "undefined method" 错误而失败。

Android Fastfile

code
# android/fastlane/Fastfile

default_platform(:android)

platform :android do
  desc "Submit a new Beta Build to Firebase App Distribution"
  lane :firebase do
    notes = ENV["RELEASE_NOTES"]
    if notes.nil? || notes.strip.empty?
      file_path = File.join(Dir.pwd, "..", "release_notes.txt")
      if File.exist?(file_path) && !File.read(file_path).strip.empty?
        notes = File.read(file_path)
      else
        notes = "New build uploaded by CI"
      end
    end

    firebase_app_distribution(
      app: ENV["FIREBASE_APP_ID_ANDROID"],
      apk_path: "../build/app/outputs/flutter-apk/app-release.apk",
      groups: "testers",
      release_notes: notes,
      service_credentials_file: ENV["FIREBASE_SERVICE_ACCOUNT_JSON_PATH"]
    )
  end

  desc "Deploy to Google Play Store"
  lane :prod do
    upload_to_play_store(
      track: 'production',
      aab: '../build/app/outputs/bundle/release/app-release.aab',
      json_key: 'play-store-service-account.json',
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

default_platform(:android) 设置默认上下文,让 Fastlane 知道它正在处理 Android 项目。lane :firebase do 定义了一个名为 firebase 的步骤序列。

顶部的 notes 逻辑尝试按优先级顺序从三个来源获取发布说明:首先从 RELEASE_NOTES 环境变量(当 GitHub Actions 使用 notes 输入手动触发工作流时设置),然后从项目根目录的 release_notes.txt 文件,最后使用默认的备用字符串。firebase_app_distribution(...) 是插件提供的操作。

app: ENV["FIREBASE_APP_ID_ANDROID"] 用于指定要上传到的 Firebase 应用,该值从工作流中设置的环境变量读取。apk_path 指向 Flutter 输出编译后 APK 的路径。groups: "testers" 指向 Firebase App Distribution 中一个命名的测试者组。请将其替换为实际的组名。对于 prod 环境,upload_to_play_store(...) 是 Fastlane 内置的操作。track: 'production' 用于上传到生产轨道。skip_upload_metadata: true、skip_upload_images: true 和 skip_upload_screenshots: true 会阻止 Fastlane 尝试管理商店列表内容,这部分不属于当前流程的职责范围。

为 iOS 配置 Fastlane

与 Android 相比,iOS 的配置更复杂,因为涉及代码签名。ios/ 目录需要自己的 Fastlane 配置。

iOS 的 Gemfile

code
# ios/Gemfile

source "https://rubygems.org"
gem "fastlane"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

这与 Android 的 Gemfile 结构完全相同。由于 iOS 和 Android 分别位于不同目录,可能需要不同的 gem 版本或插件,因此它们维护独立的 Bundler 环境。在 ios/ 目录内运行 bundle install 会独立于 android/ 目录内的安装过程安装 gem。

iOS 的 Appfile

code
# ios/fastlane/Appfile

app_identifier("com.yourcompany.app")

app_identifier(...) 声明 iOS 的 bundle 标识符。这必须与 Xcode 中设置的 bundle 标识符完全一致(在 Runner 目标 General 标签页下可见)。请将 com.yourcompany.app 替换为实际的 bundle ID。Fastlane Match 会使用此标识符来命名存储在证书仓库中的证书和配置文件。

Matchfile

code
# ios/fastlane/Matchfile

git_url(ENV["MATCH_GIT_URL"] || "https://github.com/YOUR_GITHUB_USERNAME/your-certificates-repo")
storage_mode("git")
type("appstore")

git_url(...) 告诉 Match 私有证书仓库的位置。在 GitHub Actions 工作流中,MATCH_GIT_URL 环境变量会被设置为包含嵌入 URL 中的个人访问令牌,使 Match 能够认证到私有仓库。当本地运行 Match 时,|| "https://github.com/..." 的备用地址会使用交互式凭证提示。storage_mode("git") 告诉 Match 使用 Git 作为存储后端(而非 S3 或 Google Cloud Storage)。type("appstore") 设置默认证书类型,但每个环境可以覆盖此设置。

iOS 的 Pluginfile

code
# ios/fastlane/Pluginfile

gem 'fastlane-plugin-firebase_app_distribution'

上传到 Firebase 的 ad-hoc IPA 需要 iOS 环境中声明 Firebase App Distribution 插件。iOS 和 Android 的 Pluginfile 是独立的,都需要此声明。

iOS 的 Fastfile

code
# ios/fastlane/Fastfile

default_platform(:ios)

before_all do
  setup_ci
end

platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: ENV["APP_STORE_CONNECT_API_KEY_KEY_ID"],
      issuer_id: ENV["APP_STORE_CONNECT_API_KEY_ISSUER_ID"],
      key_filepath: ENV["APP_STORE_CONNECT_API_KEY_KEY_FILEPATH"],
      in_house: false
    )

    match(
      type: "appstore",
      readonly: false,
      app_identifier: "com.YOUR-APP.app",
      api_key: api_key
    )
ruby
update_code_signing_settings(
  path: "Runner.xcodeproj",
  use_automatic_signing: false,
  team_id: "GL369K3W98",
  code_sign_identity: "Apple Distribution",
  profile_name: "match AppStore com.YOUR-APP.app",
  targets: ["Runner"]
)

build_app(
  workspace: "Runner.xcworkspace",
  scheme: "Runner",
  export_method: "app-store"
)

notes = ENV["RELEASE_NOTES"]
if notes.nil? || notes.strip.empty?
  file_path = File.join(Dir.pwd, "..", "release_notes.txt")
  if File.exist?(file_path) && !File.read(file_path).strip.empty?
    notes = File.read(file_path)
  else
    notes = "New build uploaded by CI"
  end
end

upload_to_testflight(
  skip_waiting_for_build_processing: true,
  changelog: notes
)
end

desc "部署到 Apple App Store"
lane :prod do
  api_key = app_store_connect_api_key(
    key_id: ENV["APP_STORE_CONNECT_API_KEY_KEY_ID"],
    issuer_id: ENV["APP_STORE_CONNECT_API_KEY_ISSUER_ID"],
    key_filepath: ENV["APP_STORE_CONNECT_API_KEY_KEY_FILEPATH"],
    in_house: false
  )

  match(
    type: "appstore",
    readonly: false,
    app_identifier: "com.YOUR-APP.app",
    api_key: api_key
  )

  update_code_signing_settings(
    path: "Runner.xcodeproj",
    use_automatic_signing: false,
    team_id: "GL369K3W98",
    code_sign_identity: "Apple Distribution",
    profile_name: "match AppStore com.YOUR-APP.app",
    targets: ["Runner"]
  )

  build_app(
    workspace: "Runner.xcworkspace",
    scheme: "Runner",
    export_method: "app-store"
  )

  upload_to_app_store(
    force: true, # 跳过 HTML 报告
    submit_for_review: false, # 上传到 App Store Connect 但不自动提交审核
    automatic_release: false
  )
end

desc "将新测试版构建推送至 Firebase App Distribution"
lane :firebase do
  api_key = app_store_connect_api_key(
    key_id: ENV["APP_STORE_CONNECT_API_KEY_KEY_ID"],
    issuer_id: ENV["APP_STORE_CONNECT_API_KEY_ISSUER_ID"],
    key_filepath: ENV["APP_STORE_CONNECT_API_KEY_KEY_FILEPATH"],
    in_house: false
  )

  match(
    type: "adhoc",
    readonly: false,
    app_identifier: "com.YOUR-APP.app",
    api_key: api_key
  )

  update_code_signing_settings(
    path: "Runner.xcodeproj",
    use_automatic_signing: false,
    team_id: "GL369K3W98",
    code_sign_identity: "Apple Distribution",
    profile_name: "match AdHoc com.YOUR-APP.app",
    targets: ["Runner"]
  )

  build_app(
    workspace: "Runner.xcworkspace",
    scheme: "Runner",
    export_method: "ad-hoc"
  )

  notes = ENV["RELEASE_NOTES"]
  if notes.nil? || notes.strip.empty?
    file_path = File.join(Dir.pwd, "..", "release_notes.txt")
    if File.exist?(file_path) && !File.read(file_path).strip.empty?
      notes = File.read(file_path)
    else
      notes = "New build uploaded by CI"
    end
  end

  firebase_app_distribution(
    app: ENV["FIREBASE_APP_ID_IOS"],
    groups: "testers",
    release_notes: notes,
    service_credentials_file: ENV["FIREBASE_SERVICE_ACCOUNT_JSON_PATH"]
  )
end

before_all do setup_ci end 在每条 lane 执行前运行。setup_ci 是 Fastlane 内置的一个操作,用于配置 CI 环境:它会创建临时密钥链(以便在不提示密码的情况下安装证书)、禁用代码签名弹窗,并配置其他 CI 特定设置。缺少这个步骤时,证书安装会卡在等待用户点击永远不会出现的确认对话框。

app_store_connect_api_key(...) 读取 App Store Connect API 密钥并创建后续操作用于 App Store 认证的 API 密钥对象。key_id、issuer_id 和 key_filepath 都来自工作流设置的环境变量。in_house: false 表示这是一个标准开发者账号(非 Apple 企业计划账号,后者有不同分发规则)。

match(type: "appstore", ...) 连接到证书仓库,下载 AppStore 分发证书和配置文件,并安装到 macOS 密钥链中。

readonly: false 允许 Match 在证书不存在时创建新证书。对于新项目的首次运行,Match 会生成证书并推送到仓库。后续运行只需下载现有证书。对于 Firebase lane,使用 type: "adhoc" 是因为 Firebase App Distribution 需要临时分发证书而非 App Store 证书。

update_code_signing_settings(...) 修改 Xcode 项目文件以使用 Match 刚下载的特定证书和配置文件。

use_automatic_signing: false 至关重要:自动签名会导致 Xcode 自行管理证书,这在无头 CI 环境中会失败。team_id: "YOUR_TEAM_ID" 是你的 Apple 开发者团队 ID,在 Apple 开发者门户的会员信息部分可见。profile_name: "match AppStore com.yourcompany.app" 匹配 Match 创建配置文件时使用的命名规则。

build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store") 调用 xcodebuild 来归档并导出应用。Runner.xcworkspace 是 Flutter 生成的 Xcode 工作区。当存在 CocoaPods 依赖时,必须使用工作区而非项目文件。export_method: "app-store" 告诉 Xcode 使用哪种导出选项生成最终 IPA。对于 Firebase lane,此处为 "ad-hoc"。

upload_to_testflight(skip_waiting_for_build_processing: true) 将 IPA 上传到 App Store Connect。skip_waiting_for_build_processing: true 告诉 Fastlane 不等待 Apple 完成构建处理(这可能需要 15 到 30 分钟)。上传完成后工作流结束。Apple 完成处理后,构建包会出现在 TestFlight 中。

upload_to_app_store(force: true, submit_for_review: false, automatic_release: false) 上传到 App Store Connect 用于生产分发。force: true 跳过 Fastlane 的 HTML 总结报告(在 CI 中无用)。submit_for_review: false 上传构建包但不自动提交审核,给你机会手动审核提交。automatic_release: false 防止审核通过后自动发布。

编写 GitHub Actions 工作流

工作流是 YAML 文件,放置在仓库根目录的 .github/workflows/ 路径下。每个文件定义一个工作流,包含名称、触发事件和执行步骤序列。

Android 工作流

code
# .github/workflows/android_distribution.yml

name: Android Firebase 应用分发 on: push: branches:

  • dev
  • prod

workflow_dispatch: inputs: release_notes: description: 'Release Notes' required: false default: 'Manual trigger from GitHub Actions'

jobs: distribute_android: runs-on: ubuntu-latest steps:

  • uses: actions/checkout@v4
  • uses: actions/setup-java@v3

with: distribution: 'zulu' java-version: '17'

  • uses: subosito/flutter-action@v2

with: channel: 'stable' cache: true

  • run: flutter pub get
  • uses: ruby/setup-ruby@v1

with: ruby-version: '3.2' bundler-cache: true working-directory: android

  • name: 解码密钥库

env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} run: | echo $ANDROID_KEYSTORE_BASE64 | base64 --decode > android/app/upload-keystore.jks echo "storeFile=upload-keystore.jks" > android/key.properties echo "storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}" >> android/key.properties echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >> android/key.properties echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" >> android/key.properties

  • name: 创建 .env 文件

env: ENV_FILE_BASE64: ${{ secrets.ENV_FILE_BASE64 }} run: echo $ENV_FILE_BASE64 | base64 --decode > .env

  • name: 构建 Android 发布版本

run: | if [ "${{ github.ref_name }}" == "prod" ]; then flutter build appbundle --release else flutter build apk --release fi

  • name: 创建 Firebase 服务账户 JSON

if: ${{ github.ref_name == 'dev' }} env: FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} run: echo $FIREBASE_SERVICE_ACCOUNT_JSON > android/firebase-service-account.json

  • name: 发布到 Firebase 应用分发(Dev)

if: ${{ github.ref_name == 'dev' }} env: FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_ID_ANDROID }} FIREBASE_SERVICE_ACCOUNT_JSON_PATH: "firebase-service-account.json" RELEASE_NOTES: ${{ github.event.inputs.release_notes }} run: bundle exec fastlane firebase working-directory: android

  • name: 发布到 Google Play 商店(Prod)

if: ${{ github.ref_name == 'prod' }} env: GOOGLE_PLAY_JSON: ${{ secrets.GOOGLE_PLAY_JSON }} run: | echo $GOOGLE_PLAY_JSON > play-store-service-account.json bundle exec fastlane prod working-directory: android

code

名称:Android Firebase 应用分发是您仓库中 GitHub Actions 标签中可见的显示名称。

on: push: branches: [dev, prod] 配置了触发条件。每当向 dev 或 prod 分支推送提交时,此工作流都会运行。它不会在其他分支(包括 main 和 develop)上运行,这些分支保持为未修改的预发布分支。

workflow_dispatch: inputs: release_notes 添加了手动触发功能。在 GitHub Actions 标签中,您可以点击“运行工作流”,并可选地输入将传递给 Fastlane 的发布说明。这对于测试和临时发布非常有用。

runs-on: ubuntu-latest 指定了虚拟机。Android 使用 Ubuntu 是因为 Android 构建工具链在 Linux 上运行,且 Ubuntu 运行器的费用低于 macOS 运行器。

actions/checkout@v4 会将你的代码库克隆到运行器的工作目录中。如果没有这一步,后续步骤将无法访问你的代码。

actions/setup-java@v3 使用 Zulu 发行版安装 Java 17。由于当前 Flutter 项目使用 Gradle 8,而 Java 17 是 Gradle 8 的兼容要求,因此需要安装 Java 17。如果 Java 版本不正确,Gradle 会立即失败。

subosito/flutter-action@v2 安装 Flutter SDK。channel: 'stable' 使用稳定版本通道,这符合生产构建的要求。cache: true 会在工作流运行之间缓存 Flutter SDK 下载,显著减少后续运行的设置时间。

ruby/setup-ruby@v1 安装 Ruby 3.2,并在设置 bundler-cache: true 时自动在 android/ 目录中运行 bundle install。bundler-cache 选项还会在运行之间缓存已安装的 gems,每次工作流执行可节省 2-3 分钟。

解码密钥库步骤是 Android 安全设置的核心。echo $ANDROID_KEYSTORE_BASE64 | base64 --decode > android/app/upload-keystore.jks 会将 Base64 编码还原为预期路径下的二进制 .jks 文件。后续的 echo 命令会写入 Android Gradle 构建读取的 key.properties 文件,用于定位密钥库及其密码。该文件每次运行都会直接从 secrets 中生成,因此永远不会被永久存储。

if [ "${{ github.ref_name }}" == "prod" ] 是一个 bash 条件判断。github.ref_name 是触发推送的分支名称。如果分支是 prod,工作流将构建 App Bundle(.aab,Play Store 所需格式)。否则(对于 dev 分支),将构建 APK(.apk,更简单快速,适合 Firebase App Distribution)。通过这个单一条件判断,同一工作流文件可同时处理这两个分支。

if: ${{ github.ref_name == 'dev' }} 是一个步骤级条件判断。带有此条件的步骤仅在触发分支为 dev 时运行。在 prod 分支推送时,Firebase 分发步骤将完全跳过;在 dev 分支推送时,Play Store 步骤将完全跳过。

iOS 工作流

code
# .github/workflows/ios_distribution.yml

name: iOS TestFlight 和 Firebase 分发
on:
  push:
    branches:
      - dev
      - prod
  workflow_dispatch:
    inputs:
      release_notes:
        description: '发布说明'
        required: false
        default: '来自 GitHub Actions 的手动触发'

jobs:
  distribute_ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: '17'

      - uses: subosito/flutter-action@v2
        with:
          channel: 'stable'
          cache: true

      - run: flutter pub get

      - name: 创建 .env 文件
        env:
          ENV_FILE_BASE64: ${{ secrets.ENV_FILE_BASE64 }}
        run: echo $ENV_FILE_BASE64 | base64 --decode > .env

      - name: 构建 Flutter iOS(不签名)
        run: flutter build ios --release --no-codesign

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
          working-directory: ios
markdown
      - name: 配置 Fastlane Match
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
        run: |
          echo "MATCH_PASSWORD=${MATCH_PASSWORD}" >> $GITHUB_ENV
          AUTH=$(echo "$MATCH_GIT_BASIC_AUTHORIZATION" | base64 --decode)
          echo "MATCH_GIT_URL=https://[email protected]/YOUR_GITHUB_USERNAME/your-certificates-repo" >> $GITHUB_ENV

      - name: 为 App Store Connect 创建认证密钥
        env:
          APPSTORE_API_PRIVATE_KEY_BASE64: ${{ secrets.APPSTORE_API_PRIVATE_KEY_BASE64 }}
          APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
        run: |
          mkdir -p ~/.appstoreconnect/private_keys/
          echo $APPSTORE_API_PRIVATE_KEY_BASE64 | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8

      - name: 创建 Firebase 服务账户 JSON
        if: ${{ github.ref_name == 'dev' }}
        env:
          FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
        run: echo $FIREBASE_SERVICE_ACCOUNT_JSON > ios/firebase-service-account.json

      - name: 分发到 Firebase App Distribution (Dev)
        if: ${{ github.ref_name == 'dev' }}
        env:
          FIREBASE_APP_ID_IOS: ${{ secrets.FIREBASE_APP_ID_IOS }}
          FIREBASE_SERVICE_ACCOUNT_JSON_PATH: "firebase-service-account.json"
          RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
          APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
          APP_STORE_CONNECT_API_KEY_KEY_FILEPATH: ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8
        run: bundle exec fastlane firebase
        working-directory: ios

      - name: 分发到 TestFlight (Dev)
        if: ${{ github.ref_name == 'dev' }}
        env:
          APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
          APP_STORE_CONNECT_API_KEY_KEY_FILEPATH: ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8
        run: bundle exec fastlane beta
        working-directory: ios

      - name: 分发到 Apple App Store (Prod)
        if: ${{ github.ref_name == 'prod' }}
        env:
          APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
          APP_STORE_CONNECT_API_KEY_KEY_FILEPATH: ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8
        run: bundle exec fastlane prod
        working-directory: ios

runs-on: macos-latest 对 iOS 构建来说是强制要求的。Xcode 仅能在 macOS 上运行,而 xcodebuild(Fastlane 在底层使用的工具)也仅在 macOS 上可用。与 Ubuntu runner 相比,macOS runner 每分钟的成本大约高十倍,这也是 Android 使用 Ubuntu 的原因。对于 iOS 构建来说,没有其他替代方案。

flutter build ios --release --no-codesign 会将 Flutter Dart 代码和原生 iOS 框架代码编译为发布版本,但不会应用任何代码签名。--no-codesign 标志在此处至关重要:Flutter 的构建步骤不应尝试签名,因为签名证书尚未安装。Fastlane Match 会在后续的 Fastlane 流程中处理签名,此时它已下载并安装了正确的证书。

配置 Fastlane Match 步骤执行了关键操作。AUTH=$(echo "$MATCH_GIT_BASIC_AUTHORIZATION" | base64 --decode) 将 Base64 编码的 username:token 字符串解码为明文。echo "MATCH_GIT_URL=https://[email protected]/..." >> $GITHUB_ENV 将完整的认证 URL(包含 token)写入 $GITHUB_ENV 文件,GitHub Actions 会读取该文件将环境变量传递给后续步骤。认证 URL 格式 https://username:[email protected]/... 是 HTTP 基本认证格式,这是 Git 在非交互环境中传递凭证的标准方式。

创建认证密钥步骤会从 Base64 编码中还原 .p8 文件。mkdir -p ~/.appstoreconnect/private_keys/ 创建 Fastlane 预期存放密钥的目录。echo $APPSTORE_API_PRIVATE_KEY_BASE64 | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8 将解码后的密钥写入 app_store_connect_api_key 预期的精确文件名模式。

iOS 工作流为 dev 分支运行两个并行的分发步骤:firebase 流程(构建 ad-hoc IPA 并上传至 Firebase App Distribution)和 beta 流程(构建 App Store IPA 并上传至 TestFlight)。这两个流程在共享设置步骤后按顺序执行。这意味着对 dev 分支的一次推送会自动将构建结果同时发送到两个分发渠道。

截图:

Android 和 iOS 工作流运行中:

已完成的 Android 工作流:

已完成的 iOS 工作流:

Android 和 iOS 完成的工作流:

Firebase App Distribution – Android:

Firebase App Distribution – iOS:

TestFlight iOS 构建:

完整部署的端到端流程

当所有配置就绪后,从 dev 分支推送的完整事件序列如下:

两个 runner 并行执行,因此总耗时大致等于耗时较长的平台(通常为 iOS,由于 Xcode 编译时间)

对于 prod 推送,流程结构相同,但最终分发步骤分别针对 Google Play Store(Android)和 App Store Connect(iOS)。

最佳实践

保持证书仓库私有并受访问控制

证书仓库中存储的 iOS 签名材料使用 Match 密码加密。即使文件已加密,也应将该仓库的访问权限视为与生产数据库相当。及时撤销不再需要的个人访问令牌。不要在任何地方以明文形式共享 Match 密码。

设置最小构建号策略

自动化 CI 构建需要每个上传都有唯一构建号。App Store Connect 和 Google Play 都会拒绝重复构建号的上传。实施无需人工干预的版本策略。一种可靠方法是使用 GitHub Actions 的 GITHUB_RUN_NUMBER,这是一个随每次工作流运行递增的整数:

code
- name: 设置构建编号
  run: |
    BUILD_NUMBER=${{ github.run_number }}
    # 对于 Flutter,更新 pubspec.yaml 中的构建编号
    sed -i '' "s/version: .*/version: 1.0.0+${BUILD_NUMBER}/" pubspec.yaml

github.run_number 是 GitHub 提供的环境变量,首次运行时从 1 开始,后续每次运行递增 1。这确保了所有运行中构建编号的唯一性和单调递增性。sed 命令会将 pubspec.yaml 中的版本行替换为运行编号作为构建编号。

添加分支保护规则

在启用自动化后,保护分支免受意外的直接推送。在仓库的 Settings 中,进入 Branches 并为 main、develop、dev 和 prod 分支添加保护规则。

对于 prod 分支,建议要求至少一个拉取请求审批后再合并,这在生产部署触发器执行前创建了人工审核关卡。

监控工作流运行时间和成本

GitHub Actions 按照运行器分钟数计费。macOS 分钟数的成本是 Linux 的十倍。前往 GitHub 组织的 Settings,然后进入 Billing 查看当前使用情况。

缓存(Flutter 的 cache: true 和 Ruby 的 bundler-cache: true)是最有效的优化手段。首次运行后,后续命中缓存的运行将完全跳过下载和解压步骤。

将发布说明存储在文件中,而不仅仅是作为输入

Fastfile 中的 release_notes.txt 备用方案意味着你可以将发布说明作为拉取请求的一部分提交,它们会自动出现在 Firebase 和 TestFlight 分发通知中。在项目根目录创建此文件并在每个发布分支中更新它。这将发布说明与描述它的代码一起保留在版本历史中。

常见错误

在 Fastlane 中使用 Xcode 项目而非工作区

Flutter iOS 项目始终使用工作区(Runner.xcworkspace)而非项目文件(Runner.xcodeproj),因为 CocoaPods 依赖项在工作区层级进行集成。将 Runner.xcodeproj 传递给 build_app 会导致缺少依赖项的错误。始终使用 workspace: "Runner.xcworkspace"。

未为 iOS 设置 setup_ci

省略 before_all 块中的 setup_ci 会导致工作流无限挂起,因为 macOS 会等待永远不会出现的密钥链访问权限批准。这看起来像超时,错误信息会指向其他位置。任何在 CI 中使用的 iOS Fastfile 都必须包含 before_all do setup_ci end。

在新项目中以只读模式运行 Match

Match 首次针对新应用标识符运行时需要创建证书和配置文件。如果设置了 readonly: true,Match 无法创建它们并会报错 "No certificates found"。应使用 readonly: false。在生产环境中,一些团队在初始设置后切换为 readonly: true 以防止意外重新生成证书,但此设置中应使用 false。

忘记递增构建编号

苹果和谷歌都会拒绝与之前上传构建具有相同版本号的构建。如果在不递增构建编号的情况下两次推送至 dev,第二次上传会失败。最佳实践中描述的 GITHUB_RUN_NUMBER 策略可自动防止此问题。

文件编码包含尾随换行符

code

使用 echo "content" | base64 而非 echo -n "content" | base64 会在编码前向字符串添加尾随换行符。当在 CI runner 上解码时,生成的文件会包含原始内容中没有的尾随换行符。对于 MATCH_GIT_BASIC_AUTHORIZATION 中的 username:token 字符串,尾随换行符会破坏凭证并导致看似权限错误的认证失败。对非文件内容进行编码时,始终使用 echo -n。

### 为 Firebase 选择错误的分发类型

Firebase App Distribution for iOS 需要使用 ad-hoc 分发证书,而非 App Store 证书。上传 App Store 签名的 IPA 到 Firebase 会失败,因为 ad-hoc 构建专门用于 App Store 外的设备直接分发。iOS Fastfile 中的 firebase lane 明确使用 type: "adhoc" 和 export_method: "ad-hoc" 正是出于这个原因。beta lane 使用 type: "appstore" 是因为 TestFlight 需要 App Store 证书。

### 为 Google Play 服务账户授予权限不足

最常见的 Play Store 上传失败是来自 API 的权限错误。服务账户必须通过至少发布管理员权限与你的 Play Console 应用关联。在 Google Cloud 中创建服务账户只是设置的一半:你还需要在 Play Console 的 API 访问部分为其授予访问权限。如果遗漏了 Play Console 的步骤,Fastlane 上传操作会返回 403 Forbidden 错误。

## 结论

你在这里构建的是能产生复利效应的基础设施。第一次将代码推送到 dev 环境,看到 GitHub Actions 标签同时显示 Android 和 iOS 构建完成而无需你的干预时,这套配置的价值会立即以具象化的方式显现。第四次、第十次、第五十次时,这种价值会悄无声息地持续增长,因为你永远意识不到部署正在发生。它只是自然而然地发生了。

本指南的架构覆盖了常见路径,但底层工具(GitHub Actions、Fastlane、Match)的灵活性足以适应几乎所有工作流。团队可以添加构建前的自动化测试步骤,在构建完成或失败时发送 Slack 通知,通过 Git 标签驱动版本号管理,以及支持超出 dev 和 prod 之外的多个目标环境。你在此建立的基础架构支持所有这些扩展。

需要特别强调的实践是:请像对待生产代码一样谨慎对待 CI 配置文件。在 Pull Request 中审查工作流文件的修改。为非显而易见的步骤添加注释。将密钥保留在 Secrets 保险库中,而不是工作流文件里。流水线失败的原因与生产代码失败的原因相同:未经审查的变更、缺失的上下文和未记录的假设。

有了这套流水线,你的团队可以更快、更有信心地交付代码,因为将代码交给测试人员手中的过程不再是手动且容易出错的仪式。它只是提交代码时的附带结果,这正是它应有的样子。

## 参考资料

### GitHub Actions

- GitHub Actions 文档 完整的 workflow 语法、上下文、密钥管理和 runner 规格参考。
- actions/checkout 工作流中检出仓库的官方操作。
- subosito/flutter-action 社区维护的 GitHub Actions 安装 Flutter SDK 的操作。
- ruby/setup-ruby 官方 Ruby 操作,用于安装指定 Ruby 版本并可选运行 Bundler。

- GitHub Actions 计费文档参考,涵盖 runner 分钟数、计费规则以及 macOS 和 Windows runner 的成本倍数计算。

### Fastlane

- Fastlane 文档 完整参考所有 Fastlane 操作,包括 upload_to_testflight、upload_to_play_store、match 和 build_app。

- Fastlane Match 文档 代码签名管理系统详细文档,包含初始设置和证书轮换说明。

- firebase_app_distribution Fastlane 插件 文档说明该插件如何为 Fastlane 车道添加 firebase_app_distribution 操作。

### Apple

- App Store Connect API 文档 参考 App Store Connect API 密钥、所需角色以及 .p8 文件格式说明。

- Apple 代码签名指南 Apple 官方对证书和配置文件的官方解释。

- TestFlight 文档 测试者限制、构建过期时间和上传至可用状态之间的处理时间参考。

### Google

- Google Play 开发者 API 文档 Fastlane 上传至 Play Store 所使用的 API 参考,包含轨道名称和所需权限说明。

- Firebase 应用分发文档 测试者组管理、版本说明和 CI/CD 集成的完整参考。

- Google Cloud 服务账号文档 创建和管理服务账号以及 IAM 角色分配的官方指南。

### Flutter

- Flutter 构建文档 flutter build apk、flutter build appbundle 和 flutter build ios 命令及其参数的官方参考。

- Android 应用签名 Flutter 官方指南 创建密钥库和配置 Gradle 用于发布构建的官方说明。

- Flutter iOS 部署 Flutter 官方指南说明如何部署到 App Store 和 TestFlight。

Atuoha Anthony 是一位资深移动软件工程师,拥有跨 Android、iOS、Web 等多平台构建可扩展高性能应用的丰富经验,主要使用 Flutter,同时结合 Kotlin 和 Swift,并利用人工智能技术。

如果你读到这里,请感谢作者以表达你的支持。说声谢谢

免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发者。立即开始

ADVERTISEMENT