CI/CD 实战——自动构建、签名、分发
系列:质量与交付篇(2/6)
标签建议:FlutterCI/CD自动化构建签名发布
很多 Flutter 团队都经历过同一种“发版焦虑”:
本地打包成功了,但换台机器就失败;安卓签名文件在群里传来传去;iOS 证书过期到最后一天才发现;测试包分发靠手动上传,版本号还会写错。
这篇文章聚焦一个目标:让“发版”从人工操作变成可重复、可追溯、可回滚的流水线。
1. 问题背景:业务场景 + 现象
在业务项目中,通常要同时支持:
- Android 多渠道/多环境(dev、staging、prod)
- iOS TestFlight + 正式商店
- 测试同学频繁要“最新可安装包”
- 紧急修复需要“半小时内可交付”
常见现象:
- 构建依赖人:只有某个同学机器能打包
- 签名高风险:keystore、p12、provisioning profile 管理混乱
- 版本不一致:Git tag、应用版本号、发版记录对不上
- 分发链路断裂:包打出来后还要手动上传、手动通知
- 失败不可定位:构建日志散落,本地复现成本高
2. 原因分析:核心原理 + 排查过程
核心原理
CI/CD 的本质不是“上一个平台”,而是把发布过程拆成三段:
- CI(持续集成):代码合并即触发验证(lint/test/build)
- CD(持续交付):产物自动归档并分发到测试渠道
- Release(持续发布):满足条件后自动/半自动上架
为什么团队会卡住
- 构建脚本写在个人本地,没进入仓库标准化
- 环境变量、签名文件、密钥没有统一密管策略
- “分支策略”和“发布策略”脱节(例如 main 能直接推生产)
- 缺少“失败即阻断”的质量门禁(测试失败也能打包)
快速排查清单
- 同一 commit 是否能在任意 Runner 复现产物?
- 签名资产是否都在密钥系统里,且有权限审计?
- 是否实现了“一键拿测试包链接”?
- 是否有从 tag 到安装包的全链路追踪?
3. 解决方案:方案对比 + 最终选择
方案对比
-
方案 A:纯手动脚本(本地执行)
优点:上手快;缺点:不可控、不可审计、不可规模化 -
方案 B:CI 只做测试,打包仍手动
优点:比 A 稳一点;缺点:最容易在“最后一公里”出事故 -
方案 C:全流程流水线(推荐)
- PR 阶段:静态检查 + 单测 + Widget 测试
- 合并阶段:自动构建 Android/iOS 候选包
- Tag 阶段:自动签名、归档、分发(Firebase/App Center/TestFlight)
- 生产阶段:审批后发布,失败可回滚
最终选择(中小团队可直接落地)
采用 “分层流水线 + 环境隔离 + 签名密管”:
- 分层流水线:
verify->build->sign->distribute->release - 环境隔离:dev/staging/prod 使用不同变量与密钥
- 签名密管:签名文件不入库,统一走 CI Secret + 临时文件注入
- 版本规范:Git tag 驱动版本(如
v2.3.1+231)
4. 关键代码:最小必要代码片段
以下示例为通用模板,平台可替换为 GitHub Actions / GitLab CI / Jenkins / Codemagic。
4.1 Flutter CI 基础流水线(校验 + 构建)
name: flutter-ci
on:
pull_request:
push:
branches: [main]
tags: ['v*']
jobs:
verify:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
- run: flutter pub get
- run: flutter analyze
- run: flutter test --coverage
build-android:
if: startsWith(github.ref, 'refs/tags/v')
needs: verify
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- run: flutter pub get
- run: flutter build apk --release --dart-define=ENV=prod
- uses: actions/upload-artifact@v4
with:
name: android-release-apk
path: build/app/outputs/flutter-apk/app-release.apk
4.2 Android 签名注入(避免 keystore 入库)
# CI 中通过 Secret 注入
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/app/release.keystore
cat > android/key.properties <<EOF
storePassword=$ANDROID_STORE_PASSWORD
keyPassword=$ANDROID_KEY_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
storeFile=release.keystore
EOF
4.3 iOS 签名与导出(Fastlane 思路)
lane :beta do
build_app(
workspace: "ios/Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store"
)
upload_to_testflight
end
4.4 自动分发到测试群(示例)
# 构建完成后上传 Firebase App Distribution
firebase appdistribution:distribute build/app/outputs/flutter-apk/app-release.apk \
--app "$FIREBASE_APP_ID_ANDROID" \
--groups "qa,product" \
--release-notes "build from ${GIT_TAG}"
5. 效果验证:数据/截图/日志
上线后建议跟踪这 4 组指标:
- 构建成功率:近 30 天主干构建成功率(目标 > 95%)
- 平均交付时长:从 merge 到测试可安装包(目标 < 15 分钟)
- 回归成本:每次发版人工步骤数(目标降到 3 步以内)
- 发布事故率:签名错误/版本错误/错包分发次数(目标趋近 0)
日志侧重点:
- 每个阶段有明确起止日志(verify/build/sign/distribute)
- 产物命名统一(
app-prod-v2.3.1+231.apk) - 每次发布关联 commit、tag、构建号、发布人
6. 可复用结论:通用经验 + 避坑清单
通用经验
- 先把“可重复”做对,再追求“全自动”
- 签名资产永远不进仓库,只走受控 Secret 注入
- Tag 驱动发布,不要靠手填版本号
- 失败即阻断:测试/校验不过,禁止进入签名与分发环节
- 发布结果可追溯:包名、日志、通知都能定位到 commit
避坑清单
-
--dart-define环境变量和后端环境是否一一对应 - Android/iOS 的 bundle id、包名、渠道名是否一致
- 签名证书有效期是否有提前告警(至少提前 30 天)
- PR 是否必须通过 CI 才能合并
- 是否保留最近 N 个可回滚产物
- 分发通知是否包含版本号、变更摘要、下载链接、回滚说明
结语
CI/CD 的价值不只是“省时间”,而是把发布从“个人经验”升级为“团队系统能力”。
当你的团队做到:任何人、任何时间、任意机器都能稳定产出同质量包,交付就真正可控了。