CI/CD:自动构建与分发
移动端的 CI 比其他项目多两道坎:构建环境要装 Flutter 与 Android SDK,iOS 还必须有 macOS 机器与签名证书。把这两件事解决后,CI 的价值立刻体现出来——每次合并自动跑测试、出包、分发到内测渠道,发版从“提心吊胆两小时”变成“点一下等十分钟”。本章以 GitHub Actions 为主线,给出可用的工作流模板与常见坑。
CI/CD 要解决的四件事
| 环节 | 目标 | 落地手段 |
|---|---|---|
| 质量门禁 | 提交即验证,坏代码不合并 | flutter analyze + flutter test |
| 构建 | 产出可安装包 | flutter build apk/appbundle/ipa |
| 分发 | 让测试同学拿到包 | Firebase App Distribution、TestFlight、内网下载页 |
| 通知 | 结果可达 | 群机器人 webhook、CI 状态徽章 |
发布到应用商店不是必须自动化的目标:Google Play 有 API(fastlane supply、Gradle Play Publisher)可以自动提审,App Store 也支持 fastlane deliver,但首次上架与隐私问卷仍需人工处理。
GitHub Actions 工作流
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0' # 锁定版本,避免上游升级导致构建突然失败
channel: stable
cache: true # 缓存 Flutter SDK 与 pub 依赖
- run: flutter pub get
- run: flutter analyze --fatal-infos
- run: flutter test --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/lcov.info
build-android:
runs-on: ubuntu-latest
needs: test # 测试不过就不出包
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
cache: true
- run: flutter pub get
# 签名材料由 Secrets 注入,绝不能提交到仓库
- run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/upload-keystore.jks
cat > android/key.properties <<EOF
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
keyAlias=upload
storeFile=../upload-keystore.jks
EOF
- run: flutter build appbundle --release --dart-define=ENV=prod
- uses: actions/upload-artifact@v4
with:
name: android-release
path: build/app/outputs/bundle/release/app-release.aab
要点:subosito/flutter-action 负责装 SDK,cache: true 能省下大半时间;needs: test 让构建依赖测试通过;产物用 upload-artifact 留档,配合内测分发步骤上传。
iOS 构建的额外要求
| 项目 | 说明 |
|---|---|
| Runner 类型 | 必须 macos-latest,Linux 无法编译 iOS |
| 证书与描述文件 | 导出 .p12 与 .mobileprovision,Base64 存 Secrets 后在 CI 里还原到 ~/Library/MobileDevice/Provisioning Profiles |
| CocoaPods | macOS Runner 自带,必要时显式 pod install |
| 出包命令 | flutter build ipa --release --export-options-plist=ExportOptions.plist |
| 上传 | xcrun altool(旧)或 xcrun notarytool / Transporter,也可用 fastlane pilot 传 TestFlight |
Codemagic 对 Flutter 更友好:内置 Flutter 镜像、有可视化的签名配置与 TestFlight 分发步骤,适合不想手写签名脚本的团队;代价是免费额度有限、依赖第三方平台。
版本号自动递增
# 用构建号覆盖 pubspec 里的 +N,保证每次上传的 versionCode/buildNumber 都递增
BUILD_NUMBER=${GITHUB_RUN_NUMBER}
flutter build appbundle --release --build-number=$BUILD_NUMBER
flutter build ipa --release --build-number=$BUILD_NUMBER
--build-number 会覆盖 pubspec.yaml 中 version: 1.0.0+1 的 + 部分,语义化版本号仍由人工在发版时提升。iOS 的构建号必须严格递增且不能重复上传,用 CI 的运行序号最稳妥。
内测分发与通知
| 渠道 | 适用 | 说明 |
|---|---|---|
| Firebase App Distribution | Android + iOS 内测 | 有 Gradle/CLI 插件,可指定测试者邮箱组 |
| TestFlight | iOS 内测与灰度 | 需上传 App Store Connect,外部测试要审核 |
| 蒲公英、fir.im 类平台 | 国内 Android 分发 | 上传即有二维码,适合快速发给测试同学 |
| 企业内网下载页 | 自有渠道 | 配合 Jenkins/CI 产物目录,需自己做权限 |
分发完成后用群机器人(企业微信、钉钉、飞书)发 webhook,消息里带上版本号、变更摘要与下载链接,比让人去 CI 页面找包高效得多。
CI 常见问题
| 问题 | 原因 | 处理 |
|---|---|---|
| 构建超时 | 未缓存 SDK 与 Gradle 依赖 | 开 cache: true,缓存 ~/.gradle,Android 用 --split-per-abi 减少产物 |
| 缓存失效频繁 | 缓存 key 未绑定 Flutter 版本 | key 里带上 Flutter 版本号 |
| 本地能过 CI 失败 | Dart/SDK/JDK 版本不一致 | 用 .fvmrc 或固定 flutter-version,JDK 固定 17 |
| 签名报错 | Secrets 未配置或 Base64 还原错误 | 用 base64 -d 校验,临时打印文件大小 |
| 密钥泄露 | 把 keystore 提交进仓库 | 全部走 Secrets,仓库加 .gitignore 与扫描 |
| iOS 只在发版时才发现问题 | iOS 构建未进 PR 流程 | 至少每日跑一次 macOS 构建 |
常见坑
| 坑 | 现象 | 正确做法 |
|---|---|---|
| 不锁 Flutter 版本 | 上游升级后构建突然失败 | flutter-version 固定,本地用 FVM 对齐 |
| 测试与构建放同一 Job | 一个失败全流程重跑 | 拆 Job 并用 needs 串联 |
| 直接把密钥写进 yaml | 泄露即被冒名发版 | 使用 Secrets,日志里也不要打印 |
| 只发 APK 不做渠道标记 | 出问题无法回溯版本 | 产物名带版本号与 commit 短哈希 |
| 忽略构建耗时 | 每次提交等十几分钟 | 缓存 + 并行 + 只在 main 分支出包 |
小结:CI 的最小闭环是“flutter analyze 与 flutter test 做门禁 → 按环境注入 --dart-define → 出 AAB/IPA → 上传内测渠道 → 群机器人通知”;GitHub Actions 用 subosito/flutter-action 加 cache: true 起步,iOS 必须用 macOS Runner 并把证书、描述文件、密钥全部托管为 Secrets;版本号用 --build-number 由 CI 注入保证递增,商店提审保留人工环节,缓存与版本锁定是让流水线稳定的两个关键。