← 返回文章列表
开发笔记

用 Fastlane 发布 Flutter 应用:分步说明与 GitHub Actions 集成

Fastlane 是一套面向 iOS / Android 的自动化工具(Ruby),把打包、签名、上传商店、截图、元数据等重复操作写成可复用的 lane。Flutter 工程本身在仓库根目录用 flutter buildAPK/AAB/IPA,而 Fastlane 通常放在 android/fastlaneios/fastlane 里,负责调用 Gradle/Xcode或执行你已构建好的产物,并对接 Google PlayApp Store Connect

下面按顺序说明:本机准备 → Android → iOS → 与 Flutter 构建衔接 → GitHub Actions。商店政策与界面会变,实施时请以 Fastlane 文档Play ConsoleApp Store Connect 为准。

你将得到什么

  • 在仓库里用 fastlane <lane> 一键完成「构建 + 上传测试轨道 / TestFlight」类流程(具体由你写的 lane 决定)。
  • GitHub Actions 在推送或打 Tag 时自动跑同一套 lane,密钥走 Secrets,不进入 Git历史。

前置条件

  • 已安装 Flutter SDK,工程可在本机正常 flutter build
  • Ruby(建议用系统自带或 rbenv / asdf 固定版本),并习惯用 BundlerGemfile)锁定 fastlane 版本,避免「本机能跑、CI 不能跑」。
  • Androidandroid/Release 签名已配置(或 CI 注入);有 Play开发者账号,并能在 Play Console 里完成 API 访问(服务账号 JSON)。
  • iOSApple Developer Program;App 已在 App Store Connect 创建;本机曾成功归档/上传过更佳(便于先验证签名与描述文件)。
  • 若你同时做内购/订阅,商店侧配置可参考站内 Flutter 与 App Store IAPPlay 内购(与 Fastlane 正交,但上线前常一起做)。

第一步:用 Bundler 固定 Fastlane 版本(推荐)

android/ios/ 各放一份 Gemfile(内容相同即可),以便和下文 GitHub Actionsworking-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)

  1. 打开 Play Console → 设置 → API 访问,按向导在 Google Cloud 创建/关联项目并启用 Google Play Android Developer API
  2. 创建服务账号,下载 JSON 密钥;在 Play Console 里给该账号授权(至少能上传测试版本的角色,具体以控制台为准)。
  3. 不要把 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 若已在 Appfilesupply 默认推断中配置,可省略。
  • 若你坚持用 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 IDKey ID,下载 .p8(仅一次)。CI 中通常把 .p8 全文放进 Secret,运行时写入临时文件或直接用 app_store_connect_api_keykey_content

3.2 签名与导出 IPA

Flutter 侧常用:

flutter build ipa --release

这要求 Xcode 工程里 Release 签名Bundle IDProvisioning 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.yamlversion: 写入 Android versionCode / iOS CFBundleVersion(需与你当前工程如何管理版本一致);或
  • 使用 fastlane 插件或自定义 lane 调 agvtool / Gradle 属性。

此处不展开唯一方案,避免与你的 flutter_launcher_icons、flavor 配置冲突;原则是:单一真相源,避免手改三处。


第五步:接入 GitHub Actions

思路:Android 与 iOS 分 Job(iOS 必须 macOS runner),共用同一仓库;密钥全部来自 secrets

5.1 在仓库中配置 Secrets(示例命名)

Secret用途
PLAY_JSON_KEY_DATAPlay 服务账号 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_CONTENTApp Store Connect API
其他MATCH_PASSWORDGIT_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-rubyworking-directory 指向 androidios,以便使用各自目录下的 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 的 cacheBundler cache(如上)。
  • iOS 可考虑缓存 CocoaPods(若使用):在 iospod install 前恢复 PodsPodfile.lock 相关缓存,具体视项目是否用 Pods 而定。

排错与实务建议

  1. 先本机,后 CI:同一 lane 在开发者 Mac 上稳定后再搬到 GitHub,减少「黑盒失败」。
  2. 路径与工作目录:Flutter 在仓库根,fastlane 在子目录,sh("cd ../..")File.expand_path 极易写错;失败时先看日志里的当前目录
  3. 权限与审核:上传到 internal / TestFlight 不等于通过审核;正式发布仍要走商店审核流程。
  4. 双端并行:Android 可用 ubuntu,iOS 必须用 macos-latest(Apple 许可约束);注意 macOS runner 分钟配额与排队。

小结

步骤要点
依赖管理根目录或分目录 Gemfile + bundle exec fastlane
AndroidPlay API JSON + upload_to_play_store,先 flutter build appbundle
iOSApp Store Connect API(.p8)+ flutter build ipa + upload_to_testflight(或 deliver)
GitHub Actions分 Job、Secrets 注入、iOS 用 macOS

延伸阅读