Fastlane 的自动化发布流程,Play Store 上传
Fastlane 的自动化发布流程,Play Store 上传
去年夏天,我在给一个新项目上传第一个内测版本。Play Console 的 Web 界面已经提示必须提交 Android App Bundle,但手动拖拽上传 AAB 之后,版本号对不上——CI 构建出来的 versionCode 是 42,而我在本地打包时忘了同步最新的增量。更麻烦的是,我需要同时上传脱糖后的 mapping.txt 和 12 个国家的商店列表文案。在浏览器里重复点击了四十分钟,最后因为少勾选了一个国家的价格层级,发布状态卡在了“审核中”无法撤回。那时候我决定重新把 Fastlane 捡起来,不是因为它完美,而是因为我受够了在 Web 界面里做机械劳动。
Fastlane 是一套用 Ruby 写的自动化工具链,最初由 Felix Krause 开发,现在属于 Google 的 Firebase 团队,代码开源在 GitHub 的 fastlane/fastlane 仓库上。它最出名的是 iOS 侧的签名和截图管理,但在 Android 这边,upload_to_play_store 这个 action,以前叫 supply,才是真正省时间的核心。本文只聊 Android 的上传流程,因为 iOS 的 deliver 和 Android 的 supply 在认证机制上完全是两套逻辑,混在一起讲只会让人更晕。
别直接 gem install,用 Bundler 锁死版本
很多人按照官网教程直接 sudo gem install fastlane,这在国内网络或者 M1/M2 Mac 上基本是自找麻烦。Ruby 的版本管理本身就是个历史包袱,系统自带的 Ruby 经常缺少头文件,安装 native extension 时编译失败是常态。我自己的做法是每一个 Android 项目根目录放一个 Gemfile,指定 fastlane 的精确版本,然后用 Bundler 管理。比如 Gemfile 里写 source rubygems.org,gem fastlane 约等于 2.219,然后跑 bundle install 生成 Gemfile.lock。
为什么要锁版本?因为 Fastlane 的迭代很快,2.214 版和 2.219 版在 Google Play API 的认证重试逻辑上有明显差异。2.215 之后修复了一个 Service Account 的 invalid_grant 错误重试问题。如果你用 Homebrew 装了一个全局版本,团队里五个人可能五个版本,CI 上跑的行为和本地完全不同。bundle exec fastlane 虽然打字多,但能避免“我本地能跑,CI 挂了”的经典困境。另外,2023 年 Fastlane 清理了一波老旧依赖,但 Ruby 的启动速度和内存占用在 CI 容器里依然比纯 Go 或 Rust 写的工具差一个数量级,这是它的原生局限,接受这个事实就好。
Service Account 和 JSON Key:最劝退的三十分钟
配置上传流程里最劝退的一步不是写 Fastfile,而是打通 Google API 的认证。早在 2021 年之后,Play Console 就完全强制要求 Service Account 加 JSON Key 的方式调用 Android Publisher API,账号密码那套已经彻底废弃。
具体路径是:先去 Google Cloud Console 创建项目,启用 Google Play Android Developer API,然后创建 Service Account。角色至少选 Editor,下载 JSON Key 文件。到了这一步,很多人会以为万事大吉,直接拿着 Key 去跑 Fastlane,然后报错:Google Api Error: Forbidden - Project is not associated with developer account。
这个错误在 GitHub issue 列表里被问了无数次。根本原因是 Play Console 和 Google Cloud Console 是两个独立的后台。你还需要去 Play Console 的“用户和权限”里,主动邀请那个 Service Account 的邮箱地址,类似 your-service@your-project.iam.gserviceaccount.com,给它“管理员”或者至少“发布”权限。缺少这一步,Fastlane 无论怎么换 Key 都过不了。Fastlane 文档里其实写得清楚,但步骤分散在两个不同控制台,很多人漏掉。
拿到 Key 之后,建议先跑 fastlane run validate_play_store_json_key,这个内置 action 会尝试用 Key 访问 API,如果权限正确会返回一个绿色提示。如果看到 invalid_grant 或者 unauthorized_client,先检查 Play Console 里的邀请是否接受,再检查 Cloud Console 里的 API 是否启用。国内网络环境下,这一步还可能出现连接超时,因为 oauth2.googleapis.com 的握手偶尔不稳定,挂代理或者换到海外 CI runner 能解决。
Fastfile 的核心:upload_to_play_store 的参数细节
认证通了之后,项目根目录跑 bundle exec fastlane init,选 android,它会问包名和 JSON Key 路径。生成的 fastlane/Appfile 里通常包含 json_key_file 和 package_name。真正的逻辑写在 Fastfile 里。
一个最基础的内部测试 lane 大概是:先调用 gradle action 跑 bundleRelease,然后 upload_to_play_store 指定 aab 路径为 app/build/outputs/bundle/release 下的 aab 文件,track 设为 internal,release_status 设为 completed,同时显式传入 mapping.txt 的路径。这里有几个参数必须手动核对,因为 Fastlane 的默认值并不总是符合预期。
track 参数的可选值包括 internal、alpha、beta、production。Play Console 的“Closed testing”对应 alpha 和 beta 两个 track,“Internal testing”就是 internal。如果你把应用推到了“Open testing”,Fastlane 里依然走 beta track,但 Play Console 侧需要额外配置。release_status 这个参数我强烈建议新手先用 draft,这样 Fastlane 只会在 Play Console 里创建一个草稿版本,不会直接发布到用户侧。等团队熟悉流程后,再改成 completed 或者 inProgress。
mapping 参数必须显式指向 app/build/outputs/mapping/release/mapping.txt。如果漏传,Play Console 的崩溃追踪里全是 R8 混淆后的短代码,定位问题非常痛苦。upload_to_play_store 不会自动推断 mapping 文件位置,这个坑踩过一次就不会忘。
还有一个细节是 user_fraction,用于 staged rollout。在 production track 下,你可以把 release_status 设为 inProgress,user_fraction 设为 0.1,表示先推送给 10% 的用户。Google Play 的 API 在 2023 年后对 staged rollout 做了调整,旧版 Fastlane 里的 rollout 参数已经逐步被 user_fraction 取代,如果你还在用老文档里的写法,会直接报错说参数不合法。
版本号与构建流程的衔接
手动改 build.gradle 里的 versionCode 在敏捷开发里很反人类。Fastlane 生态里有一些第三方插件声称能自动反写 versionCode,但我不太推荐,因为维护质量参差不齐,而且反写文件在 CI 上经常因为权限或 git 状态问题失败。更稳的做法是在 Gradle 里基于环境变量生成 versionCode。
比如让 versionCode 等于 CI 的 build number。在 GitHub Actions 里就是 GITHUB_RUN_NUMBER,在 GitLab CI 里是 CI_PIPELINE_ID。Gradle 脚本里读取 System.getenv,转成整数。Fastlane 侧只需要确保环境变量注入,不需要额外插件。这样本地打包时如果没有环境变量,默认回退到 1,不会干扰开发。
Fastlane 的 gradle action 在 2.210 版之后修复了 Gradle 8 下的 AAB 输出路径识别问题。老版本里,lane_context 拿不到正确的 GRADLE_AAB_OUTPUT_PATH,导致 upload_to_play_store 找不到文件。现在的做法是显式依赖 lane_context 里的输出路径,而不是硬编码 app/build/outputs/bundle/release/app-release.aab,这样即使 Gradle 调整输出目录,Fastlane 也能自动适配。
CI 实战:Secrets 管理与网络超时
GitHub Actions 的集成是最常见的场景。核心痛点是 JSON Key 怎么存。把 Key 文件内容放在 Repository secrets 里,命名 PLAY_STORE_JSON_KEY。Workflow 里需要一个步骤把 secret 内容还原成文件。这里有个 base64 编码的坑:有些人在 Mac 上生成 base64 字符串,粘贴到 GitHub secrets,然后在 Linux runner 上解码,因为换行符差异导致 JSON 格式损坏,Fastlane 跑时报 invalid_grant。建议统一用 base64 的默认参数,或者直接保存原始 JSON 内容,用引号小心转义,虽然长但避免了编码问题。
GitLab CI 类似,但 GitLab 的共享 runner 对 Ruby 环境不友好。我更喜欢自己打一个 Docker 镜像,基于 ruby:3.2 装上 Android SDK 和 Bundler,然后把 fastlane 和 gems 缓存到 CI cache。这样做的好处是构建环境完全可控,不会因为 runner 镜像升级导致 Ruby 补丁版本变化。
国内网络环境是另一个隐形杀手。Fastlane 的 supply 依赖 Google API 客户端库,上传 AAB 和 mapping 文件时要多次调用 androidpublisher.googleapis.com。如果 CI 跑在国内云服务器或者办公室内网,日志经常卡在 Uploading mapping file 或者 Updating track release,最后抛出 Connection reset by peer 或 Net::OpenTimeout。这不是 Fastlane 的 bug,纯粹是网络