Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CI/CD 与发布体系

CI/CD 的目标是让每个变更可验证, 每个产物可追溯, 每次发布可控制风险. 门禁测试应全部通过;“80%” 若使用, 通常指经过讨论的覆盖率目标, 而不是测试通过率.

一, 流水线分层

  1. PR / 变更验证: 格式, 静态分析, 单元测试, 受影响模块构建, 安全检查.
  2. 主干验证: 完整测试矩阵, 制品构建, 集成 / 设备测试和性能回归.
  3. 候选发布: 签名, R8, mapping/native symbols, SBOM, 渠道配置和人工审批.
  4. 灰度发布: 按指标逐步放量, 可暂停, 降级和覆盖修复.
  5. 发布后监控: 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:

  1. 让 release-candidate 单独启动 (不依赖 verify 的 workspace), 检查 job 日志包含 checkout, Java 和 Android SDK 准备, 且 bundleProdRelease 找到所需 SDK; 预期证据为 job URL, 实际 Action commit SHA 和 Gradle 成功日志.
  2. 注入仅用于测试的 base64 keystore 及四个 signing secret, 检查 Gradle 收到四个 ORG_GRADLE_PROJECT_signing* 属性并产出已签名 AAB; 预期证据为签名验证输出, 制品 SHA-256 和不含 secret 值的日志.
  3. 故意令 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, 并保证归档不可被无审计覆盖.

易错点 / 追问

  • 不把覆盖率当测试通过率.
  • 不把固定灰度比例当通用规则.
  • 不承诺客户端可以让已安装用户直接回退版本.
  • 不把热修复作为绕过商店审核, 测试或安全审查的默认方案.

版本与参考资料