用 Fastlane 发布 Flutter 应用:分步说明与 GitHub Actions 集成
Fastlane 是一套面向 iOS / Android 的自动化工具(Ruby),把打包、签名、上传商店、截图、元数据等重复操作写成可复用的 lane。Flutter 工程本身在仓库根目录用 flutter build 出 APK/AAB/IPA,而 Fastlane 通常放在 android/fastlane 与 ios/fastlane 里,负责调用 Gradle/Xcode或执行你已构建好的产物,并对接 Google Play、App Store Connect。
下面按顺序说明:本机准备 → Android → iOS → 与 Flutter 构建衔接 → GitHub Actions。商店政策与界面会变,实施时请以 Fastlane 文档、Play Console、App Store Connect 为准。
你将得到什么
- 在仓库里用
fastlane <lane>一键完成「构建 + 上传测试轨道 / TestFlight」类流程(具体由你写的 lane 决定)。 - GitHub Actions 在推送或打 Tag 时自动跑同一套 lane,密钥走 Secrets,不进入 Git历史。
前置条件
- 已安装 Flutter SDK,工程可在本机正常
flutter build。 - Ruby(建议用系统自带或
rbenv/asdf固定版本),并习惯用 Bundler(Gemfile)锁定fastlane版本,避免「本机能跑、CI 不能跑」。 - Android:
android/下 Release 签名已配置(或 CI 注入);有 Play开发者账号,并能在 Play Console 里完成 API 访问(服务账号 JSON)。 - iOS:Apple Developer Program;App 已在 App Store Connect 创建;本机曾成功归档/上传过更佳(便于先验证签名与描述文件)。
- 若你同时做内购/订阅,商店侧配置可参考站内 Flutter 与 App Store IAP、Play 内购(与 Fastlane 正交,但上线前常一起做)。
第一步:用 Bundler 固定 Fastlane 版本(推荐)
在 android/ 与 ios/ 各放一份 Gemfile(内容相同即可),以便和下文 GitHub Actions 里 working-directory: android / ios 一致;若你只想在仓库根目录保留一份 Gemfile,请在 CI 里设置 BUNDLE_GEMFILE: ${{ github.workspace }}/Gemfile,并保证每个 step 里 bundle exec 都能读到该路径。
Gemfile 示例:
source "https://rubygems.org"
gem "fastlane", "~> 2.220"
在 android 目录执行:
cd android
bundle install
之后本机与 CI 都使用:
bundle exec fastlane <命令>
避免全局 gem install fastlane 与 CI 版本漂移。ios/ 下同样执行一次 bundle install。
第二步:初始化 Android 端 Fastlane
在仓库根目录:
cd android
bundle exec fastlane init
按提示选择用途(上传 Play 等)。初始化后会出现 android/fastlane/Fastfile、**Appfile**等。
2.1 准备 Google Play 服务账号(API)
- 打开 Play Console → 设置 → API 访问,按向导在 Google Cloud 创建/关联项目并启用 Google Play Android Developer API。
- 创建服务账号,下载 JSON 密钥;在 Play Console 里给该账号授权(至少能上传测试版本的角色,具体以控制台为准)。
- 不要把 JSON 提交到 Git。本机可放在
android/外某路径,通过环境变量指向;CI 用 GitHub Secrets(见后文)。
2.2 典型 lane:构建 AAB 并上传到内部测试轨道
Flutter 默认 Release AAB 路径形如:build/app/outputs/bundle/release/app-release.aab。
在 android/fastlane/Fastfile 中可编写类似逻辑(路径请按你工程实际调整):
default_platform(:android)
platform :android do
desc "Flutter build appbundle + upload internal track"
lane :deploy_internal do
Dir.chdir("../..") do
sh("flutter", "pub", "get")
sh("flutter", "build", "appbundle", "--release")
end
upload_to_play_store(
track: "internal",
aab: File.expand_path("../../build/app/outputs/bundle/release/app-release.aab", __dir__),
json_key: ENV["PLAY_JSON_KEY_PATH"],
package_name: ENV["ANDROID_PACKAGE_NAME"]
)
end
end
说明:
Dir.chdir("../..")从android/fastlane回到仓库根目录再跑flutter。json_key也可用json_key_data: ENV["PLAY_JSON_KEY_DATA"](CI 常把 JSON 全文塞进 Secret)。package_name若已在Appfile或supply默认推断中配置,可省略。- 若你坚持用 Gradle 打 bundle,也可改为
gradle(task: "bundleRelease"),但要与 Flutter 工程插件、签名配置一致。
本机试跑前导出环境变量后执行:
cd android
export PLAY_JSON_KEY_PATH="/绝对路径/play-api.json"
export ANDROID_PACKAGE_NAME="com.example.app"
bundle exec fastlane deploy_internal
第三步:初始化 iOS 端 Fastlane
cd ios
bundle exec fastlane init
会生成 ios/fastlane/Fastfile 等。iOS 自动化强烈建议使用 App Store Connect API 密钥(.p8),避免把 Apple ID 密码塞进 CI。
3.1 在 App Store Connect 创建 API 密钥
在 用户与访问 → 密钥 → App Store Connect API 创建密钥,记下 Issuer ID、Key ID,下载 .p8(仅一次)。CI 中通常把 .p8 全文放进 Secret,运行时写入临时文件或直接用 app_store_connect_api_key 的 key_content。
3.2 签名与导出 IPA
Flutter 侧常用:
flutter build ipa --release
这要求 Xcode 工程里 Release 签名、Bundle ID、Provisioning Profile 已正确;首次建议在 Xcode 里 Archive 成功一次,再搬到 Fastlane。
在 ios/fastlane/Fastfile 中可组合(示例骨架,需按你团队签名策略补全):
default_platform(:ios)
platform :ios do
desc "Build IPA with Flutter + upload TestFlight"
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_KEY_ID"],
issuer_id: ENV["APP_STORE_ISSUER_ID"],
key_content: ENV["APP_STORE_P8_CONTENT"],
is_key_content_base64: false,
duration: 1200,
in_house: false
)
Dir.chdir("../..") do
sh("flutter", "pub", "get")
sh("flutter", "build", "ipa", "--release", "--export-options-plist=ios/ExportOptions.plist")
end
ipa_path = File.expand_path("../../build/ios/ipa/*.ipa", __dir__)
upload_to_testflight(
api_key: api_key,
ipa: Dir.glob(ipa_path).first,
skip_waiting_for_build_processing: true
)
end
end
说明:
- **
ExportOptions.plist**需在 Xcode 或手工维护,声明 method(如app-store)、teamID、是否 manage version 等;路径也可换成你实际放置位置。 - 若使用 match 管理证书与描述文件,在 lane 开头调用
match(type: "appstore")等;团队若无统一证书策略,可先在本地固定好 Automatic signing 再在 CI 用同一 Team。 upload_to_testflight参数众多(changelog、groups等),见 pilot 文档。
本机试跑示例:
cd ios
export APP_STORE_KEY_ID="..."
export APP_STORE_ISSUER_ID="..."
export APP_STORE_P8_CONTENT="$(cat AuthKey_XXX.p8)"
bundle exec fastlane beta
第四步:与 Flutter 版本号对齐(建议)
商店上传常要求 version / build number 递增。可选做法:
- 在 CI 里用脚本读
pubspec.yaml的version:写入 AndroidversionCode/ iOSCFBundleVersion(需与你当前工程如何管理版本一致);或 - 使用
fastlane插件或自定义 lane 调agvtool/ Gradle 属性。
此处不展开唯一方案,避免与你的 flutter_launcher_icons、flavor 配置冲突;原则是:单一真相源,避免手改三处。
第五步:接入 GitHub Actions
思路:Android 与 iOS 分 Job(iOS 必须 macOS runner),共用同一仓库;密钥全部来自 secrets。
5.1 在仓库中配置 Secrets(示例命名)
| Secret | 用途 |
|---|---|
PLAY_JSON_KEY_DATA | Play 服务账号 JSON 全文(或 Base64,则 workflow 里先解码) |
ANDROID_KEYSTORE_B64 / KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD | 若 CI 内需要组装签名 keystore(你也可改用 Play App Signing 流程下的上传密钥策略) |
APP_STORE_KEY_ID / APP_STORE_ISSUER_ID / APP_STORE_P8_CONTENT | App Store Connect API |
| 其他 | MATCH_PASSWORD、GIT_AUTHORIZATION 等(若使用 match) |
切勿把 .p8、keystore、Play JSON 提交进仓库。
5.2 Workflow 示例(拆分 Android / iOS)
在 .github/workflows/mobile-deploy.yml:
name: Mobile deploy
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.24.0"
channel: "stable"
cache: true
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
working-directory: android
- name: Decode Play service account
env:
DATA: ${{ secrets.PLAY_JSON_KEY_DATA }}
run: |
echo "$DATA" > "${{ runner.temp }}/play.json"
- name: Fastlane Android internal
env:
PLAY_JSON_KEY_PATH: ${{ runner.temp }}/play.json
ANDROID_PACKAGE_NAME: com.example.app
run: |
cd android
bundle install
bundle exec fastlane deploy_internal
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.24.0"
channel: "stable"
cache: true
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
working-directory: ios
- name: Fastlane iOS beta
env:
APP_STORE_KEY_ID: ${{ secrets.APP_STORE_KEY_ID }}
APP_STORE_ISSUER_ID: ${{ secrets.APP_STORE_ISSUER_ID }}
APP_STORE_P8_CONTENT: ${{ secrets.APP_STORE_P8_CONTENT }}
run: |
cd ios
bundle install
bundle exec fastlane beta
说明与可调点:
- Flutter 版本请与团队统一;也可用
fvm在 CI 读.fvmrc。 ruby/setup-ruby的working-directory指向android或ios,以便使用各自目录下的Gemfile.lock;若只在根目录放一个Gemfile,把bundle install改在根目录执行,并在 Fastlane 里保持路径一致即可。- iOS Job 若需 钥匙串、描述文件,可在 Step 里用
import-certificate类 Action 或 match;未配置签名时flutter build ipa会在 CI 失败——需先按 Xcode 要求把签名链路跑通。 - 首次接入建议:仅
workflow_dispatch手动触发,成功后再放开push: tags。
5.3 缓存与提速
- 开启
subosito/flutter-action的 cache、Bundler cache(如上)。 - iOS 可考虑缓存 CocoaPods(若使用):在
ios下pod install前恢复Pods与Podfile.lock相关缓存,具体视项目是否用 Pods 而定。
排错与实务建议
- 先本机,后 CI:同一 lane 在开发者 Mac 上稳定后再搬到 GitHub,减少「黑盒失败」。
- 路径与工作目录:Flutter 在仓库根,
fastlane在子目录,sh("cd ../..")与File.expand_path极易写错;失败时先看日志里的当前目录。 - 权限与审核:上传到 internal / TestFlight 不等于通过审核;正式发布仍要走商店审核流程。
- 双端并行:Android 可用
ubuntu,iOS 必须用macos-latest(Apple 许可约束);注意 macOS runner 分钟配额与排队。
小结
| 步骤 | 要点 |
|---|---|
| 依赖管理 | 根目录或分目录 Gemfile + bundle exec fastlane |
| Android | Play API JSON + upload_to_play_store,先 flutter build appbundle |
| iOS | App Store Connect API(.p8)+ flutter build ipa + upload_to_testflight(或 deliver) |
| GitHub Actions | 分 Job、Secrets 注入、iOS 用 macOS |