CI/CD 与发布体系
CI/CD 的目标是让每个变更可验证, 每个产物可追溯, 每次发布可控制风险. 门禁测试应全部通过;“80%” 若使用, 通常指经过讨论的覆盖率目标, 而不是测试通过率.
一, 流水线分层
- PR / 变更验证: 格式, 静态分析, 单元测试, 受影响模块构建, 安全检查.
- 主干验证: 完整测试矩阵, 制品构建, 集成 / 设备测试和性能回归.
- 候选发布: 签名, R8, mapping/native symbols, SBOM, 渠道配置和人工审批.
- 灰度发布: 按指标逐步放量, 可暂停, 降级和覆盖修复.
- 发布后监控: crash-free, ANR, 启动, 核心业务漏斗和用户反馈.
二, 正确的质量门禁
- 被纳入门禁的测试必须 100% 通过; flaky test 应隔离, 跟踪和修复, 不能用 “80% 通过率” 放行.
- 代码覆盖率只是风险信号, 可按模块设置目标, 但不能替代断言质量, 边界测试和集成测试.
- 静态分析, 安全扫描和许可证策略应定义严重级别, 豁免责任人和到期时间.
- 性能门禁使用稳定设备, 足够迭代和统计阈值, 避免单次波动阻塞所有发布.
最小 CI 骨架
以下为流程片段, 以 GitHub Actions 为例; Action SHA, secret 名称, Android SDK 组件, 变体和任务名必须按项目实际情况填充和核验. 因为仍使用占位符且未列出项目的 SDK/签名校验细节, 它不是可直接生成候选制品的工作流. 所有密钥均为占位符, 必须在 CI secret/受控身份中替换, 不能提交到仓库或回显到日志.
name: android-verify
on:
pull_request:
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
permissions: { contents: read }
steps:
- uses: actions/checkout@<PINNED_ACTION_COMMIT>
- uses: actions/setup-java@<PINNED_ACTION_COMMIT>
with: { distribution: temurin, java-version: "17" }
- run: ./gradlew :app:testDemoDebugUnitTest :app:assembleDemoDebug --no-daemon
release-candidate:
if: github.ref == 'refs/heads/main'
needs: verify
runs-on: ubuntu-latest
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@<PINNED_ACTION_COMMIT>
- uses: actions/setup-java@<PINNED_ACTION_COMMIT>
with: { distribution: temurin, java-version: "17" }
- uses: android-actions/setup-android@<PINNED_ACTION_COMMIT>
- name: Install required Android SDK components
shell: bash
run: sdkmanager "platforms;android-<COMPILE_SDK>" "build-tools;<BUILD_TOOLS>"
- name: Materialize signing key
shell: bash
run: |
set -euo pipefail
key_store="$RUNNER_TEMP/release-signing.jks"
printf '%s' '${{ secrets.REPLACE_WITH_KEYSTORE_BASE64 }}' | base64 --decode > "$key_store"
test -s "$key_store"
chmod 600 "$key_store"
printf 'ORG_GRADLE_PROJECT_signingStoreFile=%s\n' "$key_store" >> "$GITHUB_ENV"
- name: Build signed bundle
shell: bash
run: ./gradlew :app:bundleProdRelease --no-daemon
env:
ORG_GRADLE_PROJECT_signingStorePassword: ${{ secrets.REPLACE_WITH_KEYSTORE_PASSWORD }}
ORG_GRADLE_PROJECT_signingKeyAlias: ${{ secrets.REPLACE_WITH_KEY_ALIAS }}
ORG_GRADLE_PROJECT_signingKeyPassword: ${{ secrets.REPLACE_WITH_KEY_PASSWORD }}
- uses: actions/upload-artifact@<PINNED_ACTION_COMMIT>
with: { name: release-evidence, path: "app/build/outputs" }
- name: Remove temporary signing key
if: ${{ always() }}
shell: bash
run: rm -f "$RUNNER_TEMP/release-signing.jks"
该 job 独立完成 checkout, JDK 与必要 Android SDK 准备; 将 base64 keystore 临时写到 runner 临时目录, 并映射 storeFile, storePassword, keyAlias, keyPassword 至 ORG_GRADLE_PROJECT_signingStoreFile, ORG_GRADLE_PROJECT_signingStorePassword, ORG_GRADLE_PROJECT_signingKeyAlias, ORG_GRADLE_PROJECT_signingKeyPassword. 构建失败时 shell 的非零退出会停止后续普通步骤; 清理步骤以 always() 执行. Gradle 配置也必须读取这四个同名 properties, 例如 providers.gradleProperty("signingStoreFile"), 否则此片段不能签名.
预期: PR 只生成可验证的 debug 产物; 主干在门禁通过后才进入签名流程. 该片段不声称已运行. 实际流水线还必须限制谁能触发签名, 验证 secret 映射和归档 mapping/native symbols/SBOM, 且将 Action 固定为真实不可变 commit SHA.
流水线自测与预期证据
在隔离的测试仓库或受控分支执行以下检查, 不在 PR 日志中输出 secret:
- 让
release-candidate单独启动 (不依赖verify的 workspace), 检查 job 日志包含 checkout, Java 和 Android SDK 准备, 且bundleProdRelease找到所需 SDK; 预期证据为 job URL, 实际 Action commit SHA 和 Gradle 成功日志. - 注入仅用于测试的 base64 keystore 及四个 signing secret, 检查 Gradle 收到四个
ORG_GRADLE_PROJECT_signing*属性并产出已签名 AAB; 预期证据为签名验证输出, 制品 SHA-256 和不含 secret 值的日志. - 故意令 bundle 任务失败, 检查上传步骤不执行而
Remove temporary signing key仍执行; 预期证据为失败 job 记录及 runner 临时目录清理审计 (仅记录文件名 / 退出状态).
版本与发布决策
versionCode 是 Android 安装 / 升级比较用的单调递增整数; versionName 可采用语义化版本如 2.4.0 供人识别, 但不会替代 versionCode. 每次候选制品把二者, commit, CI build ID 和制品 SHA-256 绑定. 语义化版本对 Android 客户端是团队约定, 不应虚构为系统强制规则.
| 信号 | 决策 | 操作 | 验证 |
|---|---|---|---|
| crash-free/ANR 在预设观察窗内正常 | 扩大灰度 | 进入下一人群或渠道 | 分层看版本, 机型, 网络和核心漏斗 |
| 新版本严重崩溃或数据风险 | 立即暂停 | 停止放量, 关闭风险功能, 评估服务端兼容 | 确认新安装不再获取问题版本 |
| 可由开关规避且数据兼容 | 降级后观察 | 保留审计记录并限时修复 | 观察指标恢复且修复版回归 |
| 已安装包必须修复 | 覆盖发布 | 发布更高 versionCode 的修复版本 | 核对 mapping, 签名, 版本追溯 |
三, APK, AAB 与分发渠道
AAB 是 Google Play 新应用和其动态交付体系的主要发布格式, Play 根据设备配置生成 APK. 它不是所有应用商店和企业分发场景的统一格式; 国内商店, 企业 MDM 或侧载可能仍需要 APK / 渠道包.
发布系统应根据渠道生成, 签名和归档对应制品, 不把 “AAB 必须用于所有 Android 发布” 写成平台绝对规则.
四, 签名与供应链安全
- 上传密钥, 应用签名密钥和 CI 身份分离, 使用最小权限与审计.
- keystore, 密码和 API token 不进入代码库, 构建日志或普通制品缓存.
- 固定 Gradle Wrapper, JDK, AGP 和依赖锁, 记录构建环境.
- 生成并归档 APK/AAB 校验值, mapping, native symbols/build-id, SBOM, 依赖清单和签名信息.
- 第三方 Action / 插件固定版本或 commit, 评估供应链和权限边界.
五, 灰度, 降级与回滚
灰度比例不是固定 1% → 10% → 50% 模板, 应由样本量, 风险, 渠道和指标决定. 每阶段定义 crash-free, ANR, 启动, 关键业务漏斗, 分层维度, 暂停条件, 责任人和观察窗口.
客户端通常无法把已安装用户直接回退到旧包. 可用手段包括暂停放量, 下架问题渠道包, 服务端兼容, 功能开关降级和发布更高版本覆盖. 热修复受技术风险和商店政策约束, 不是所有项目 “必须有” 的能力, 也不能替代正常发布与回滚设计.
六, 制品追溯
每个制品至少绑定: git commit, versionCode/versionName, CI build ID, 渠道, ABI, 构建变体, 依赖锁, mapping, native symbols/build-id, 签名身份, 审批记录, 灰度配置和测试报告.
线上事件携带同一组版本标识, 才能把崩溃, ANR 和性能回归映射回唯一源码与制品.
高频面试题
Q1: 单元测试通过率 80% 能否作为门禁?
不能. 纳入门禁的测试应全部通过; 80% 如果出现, 通常是覆盖率目标, 而且仍需解释统计范围和风险例外.
Q2: AAB 是否是所有 Android 渠道的强制格式?
不是. 它主要适用于 Google Play 的发布和动态交付要求, 其他商店或企业分发可能使用 APK.
Q3: 线上严重崩溃如何处置?
先暂停放量和入口, 评估服务端兼容 / 功能开关降级, 必要时发布覆盖版本; 只有在合规, 可验证且风险可控时才考虑热修复.
Q4: 如何保证发布可追溯?
将源码, 环境, 依赖, 签名, 制品, 符号表, 测试和灰度记录绑定到同一 build ID, 并保证归档不可被无审计覆盖.
易错点 / 追问
- 不把覆盖率当测试通过率.
- 不把固定灰度比例当通用规则.
- 不承诺客户端可以让已安装用户直接回退版本.
- 不把热修复作为绕过商店审核, 测试或安全审查的默认方案.
版本与参考资料
- 最后核验: 2026-08-07
- Android App Bundle
- Play App Signing
- GitHub Actions security hardening