Android 项目如何创建 CI/CD 工作流

高桥凉介
分享互动规则

在原生 Android 项目里,CI/CD 的目标很简单:

  • 每次提交代码后,自动检查项目能不能正常构建。
  • 每次创建 Pull Request 时,自动验证代码有没有破坏构建。
  • 推送到 main 分支后,自动生成可发布的 APK 和 AAB。
  • 不把 keystore.jks、密码等敏感信息提交到 GitHub 仓库。

这篇文章以一个典型的原生 Android 项目为例,说明如何创建一个 GitHub Actions 工作流文件:

.github/workflows/android-ci-cd.yml

一、先理解 CI/CD 是什么

在 Android 项目里,可以这样理解:

flowchart LR
    A["开发者提交代码"] --> B["GitHub Actions 自动运行"]
    B --> C["下载代码"]
    C --> D["配置 JDK 和 Gradle"]
    D --> E["构建 Debug APK"]
    E --> F["构建 Release APK"]
    F --> G["构建 Release AAB"]
    G --> H{"是否是 main 分支或手动触发?"}
    H -->|"否,比如 PR"| I["只做构建验证"]
    H -->|"是"| J["解码 keystore"]
    J --> K["签名 APK 和 AAB"]
    K --> L["上传 artifact"]

简单说:

  • CI:Continuous Integration,持续集成。主要负责自动构建、测试、检查代码。
  • CD:Continuous Delivery/Deployment,持续交付/部署。主要负责自动生成可发布产物,比如 signed APK、signed AAB。

对于 Android 项目来说,常见流程就是:

代码提交 -> GitHub Actions 构建 -> 签名 -> 上传构建产物

二、项目需要具备哪些条件

一个标准原生 Android 项目通常会有这些文件:

.
├── app/
│   ├── build.gradle.kts
│   └── keystore.jks
├── gradlew
├── settings.gradle.kts
└── .github/
    └── workflows/
        └── android-ci-cd.yml

其中:

  • gradlew 是 Gradle Wrapper。
  • app/build.gradle.kts 是 app 模块的 Gradle 配置。
  • app/keystore.jks 是本地签名文件,但不要提交到 GitHub
  • .github/workflows/android-ci-cd.yml 是 GitHub Actions 工作流配置。

三、新建 GitHub Actions 工作流文件

在项目根目录创建文件:

.github/workflows/android-ci-cd.yml

完整配置如下:

name: Android CI/CD

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: android-ci-cd-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    name: Build Android app
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Set up JDK 17
        uses: actions/setup-java@v5
        with:
          distribution: temurin
          java-version: "17"

      - name: Set up Gradle
        uses: gradle/actions/setup-gradle@v4

      - name: Make Gradle wrapper executable
        run: chmod +x ./gradlew

      - name: Build debug APK, release APK, and release AAB
        run: ./gradlew --no-daemon :app:assembleDebug :app:assembleRelease :app:bundleRelease

      - name: Validate signing secrets
        if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
        env:
          ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
        run: |
          missing=0

          if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
            echo "::error::Missing required secret: ANDROID_KEYSTORE_BASE64"
            missing=1
          fi

          if [ -z "$ANDROID_KEYSTORE_PASSWORD" ]; then
            echo "::error::Missing required secret: ANDROID_KEYSTORE_PASSWORD"
            missing=1
          fi

          if [ -z "$ANDROID_KEY_ALIAS" ]; then
            echo "::error::Missing required secret: ANDROID_KEY_ALIAS"
            missing=1
          fi

          if [ -z "$ANDROID_KEY_PASSWORD" ]; then
            echo "::error::Missing required secret: ANDROID_KEY_PASSWORD"
            missing=1
          fi

          if [ "$missing" -ne 0 ]; then
            exit 1
          fi

      - name: Sign release APK and AAB
        if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
        env:
          ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
        run: |
          set -euo pipefail

          SIGNED_DIR="app/build/signed"
          KEYSTORE_FILE="$RUNNER_TEMP/release-keystore.jks"
          UNSIGNED_APK="app/build/outputs/apk/release/app-release-unsigned.apk"
          ALIGNED_APK="$RUNNER_TEMP/app-release-aligned.apk"
          SIGNED_APK="$SIGNED_DIR/app-release-signed.apk"
          UNSIGNED_AAB="app/build/outputs/bundle/release/app-release.aab"
          SIGNED_AAB="$SIGNED_DIR/app-release-signed.aab"

          mkdir -p "$SIGNED_DIR"
          printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$KEYSTORE_FILE"

          BUILD_TOOLS_DIR="$(find "$ANDROID_HOME/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)"
          ZIPALIGN="$BUILD_TOOLS_DIR/zipalign"
          APKSIGNER="$BUILD_TOOLS_DIR/apksigner"

          "$ZIPALIGN" -f -p 4 "$UNSIGNED_APK" "$ALIGNED_APK"

          "$APKSIGNER" sign \
            --ks "$KEYSTORE_FILE" \
            --ks-pass "pass:$ANDROID_KEYSTORE_PASSWORD" \
            --ks-key-alias "$ANDROID_KEY_ALIAS" \
            --key-pass "pass:$ANDROID_KEY_PASSWORD" \
            --out "$SIGNED_APK" \
            "$ALIGNED_APK"

          "$APKSIGNER" verify --verbose "$SIGNED_APK"

          jarsigner \
            -keystore "$KEYSTORE_FILE" \
            -storepass "$ANDROID_KEYSTORE_PASSWORD" \
            -keypass "$ANDROID_KEY_PASSWORD" \
            -signedjar "$SIGNED_AAB" \
            "$UNSIGNED_AAB" \
            "$ANDROID_KEY_ALIAS"

          jarsigner -verify -verbose "$SIGNED_AAB"

      - name: Upload signed release artifacts
        if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
        uses: actions/upload-artifact@v4
        with:
          name: android-signed-release
          path: app/build/signed/*
          if-no-files-found: error
          retention-days: 30

四、触发条件说明

这一段决定什么时候运行工作流:

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
  workflow_dispatch:

含义是:

  • 推送到 main 分支时运行。
  • main 分支发起 Pull Request 时运行。
  • 允许在 GitHub 页面手动点击运行。

也就是:

flowchart TD
    A["触发 GitHub Actions"] --> B{"触发类型"}
    B --> C["push 到 main"]
    B --> D["pull_request 到 main"]
    B --> E["workflow_dispatch 手动运行"]
    C --> F["构建 + 签名 + 上传产物"]
    D --> G["只构建,不签名"]
    E --> F

这里特别重要的一点是:

PR 只做构建验证,不执行签名。

原因是签名需要读取 GitHub Secrets,而外部 PR 不应该接触敏感信息。


五、CI 阶段:自动构建 Android 项目

CI 阶段的核心命令是:

./gradlew --no-daemon :app:assembleDebug :app:assembleRelease :app:bundleRelease

它会生成:

app/build/outputs/apk/debug/app-debug.apk
app/build/outputs/apk/release/app-release-unsigned.apk
app/build/outputs/bundle/release/app-release.aab

这里要注意:

  • assembleDebug 生成 Debug APK。
  • assembleRelease 生成 Release APK,但默认可能是 unsigned APK。
  • bundleRelease 生成 Release AAB,用于 Google Play 发布。

GitHub Actions 里使用 JDK 17:

- name: Set up JDK 17
  uses: actions/setup-java@v5
  with:
    distribution: temurin
    java-version: "17"

如果你的 Android Gradle Plugin 或项目配置要求 Java 17,这一步必须设置正确。


六、CD 阶段:为什么不能直接提交 keystore

Android Release 包需要签名。

但是下面这些东西不能提交到仓库:

app/keystore.jks
keystore password
key alias
key password

原因很简单:

一旦泄露,别人就可能用你的签名身份发布伪造 APK。

所以正确做法是:

本地 keystore.jks
        |
        | base64 编码
        v
GitHub Secrets
        |
        | GitHub Actions 运行时解码
        v
临时 keystore 文件
        |
        v
签名 APK / AAB

流程图如下:

sequenceDiagram
    participant Dev as 开发者本地
    participant GitHub as GitHub Secrets
    participant Actions as GitHub Actions Runner
    participant Artifact as GitHub Artifact

    Dev->>Dev: base64 编码 keystore.jks
    Dev->>GitHub: 保存 ANDROID_KEYSTORE_BASE64
    Dev->>GitHub: 保存密码和 alias
    Actions->>GitHub: 运行时读取 Secrets
    Actions->>Actions: 解码 keystore 到临时目录
    Actions->>Actions: 使用 apksigner 签名 APK
    Actions->>Actions: 使用 jarsigner 签名 AAB
    Actions->>Artifact: 上传 signed APK 和 signed AAB

七、需要配置的 GitHub Secrets

需要在 GitHub 仓库里添加 4 个 Secrets:

ANDROID_KEYSTORE_BASE64
ANDROID_KEYSTORE_PASSWORD
ANDROID_KEY_ALIAS
ANDROID_KEY_PASSWORD

进入路径:

GitHub 仓库
-> Settings
-> Secrets and variables
-> Actions
-> New repository secret

1. 获取 ANDROID_KEYSTORE_BASE64

假设你的 keystore 文件在:

app/keystore.jks

在项目根目录运行:

base64 -i app/keystore.jks | pbcopy

这条命令会把 base64 内容复制到剪贴板。

然后在 GitHub Secrets 新建:

Name: ANDROID_KEYSTORE_BASE64
Value: 粘贴剪贴板内容

注意:不要把 base64 内容打印到聊天、文档或提交到仓库里。


2. 获取 ANDROID_KEY_ALIAS

运行:

keytool -list -v -keystore app/keystore.jks

输入 keystore 密码后,找到这一行:

Alias name: your_alias_name

这个 your_alias_name 就是:

ANDROID_KEY_ALIAS

3. 获取 ANDROID_KEYSTORE_PASSWORD

这是创建 keystore 时设置的密码。

它不能从 .jks 文件里反推出明文。

如果你忘记了,只能尝试你当时设置过的密码,或者重新生成 keystore。


4. 获取 ANDROID_KEY_PASSWORD

这是 key alias 对应 key 的密码。

如果你创建 keystore 时没有单独设置 key password,很多情况下它和 keystore password 是同一个。

也就是说,常见配置是:

ANDROID_KEYSTORE_PASSWORD = keystore 的密码
ANDROID_KEY_PASSWORD = key 的密码,可能和 keystore 密码相同

八、签名 APK 的过程

在 workflow 里,APK 签名分两步:

unsigned APK -> zipalign -> apksigner -> signed APK

对应命令:

"$ZIPALIGN" -f -p 4 "$UNSIGNED_APK" "$ALIGNED_APK"

"$APKSIGNER" sign \
  --ks "$KEYSTORE_FILE" \
  --ks-pass "pass:$ANDROID_KEYSTORE_PASSWORD" \
  --ks-key-alias "$ANDROID_KEY_ALIAS" \
  --key-pass "pass:$ANDROID_KEY_PASSWORD" \
  --out "$SIGNED_APK" \
  "$ALIGNED_APK"

为什么要先 zipalign

因为 Android 官方推荐 Release APK 在签名前先做 zipalign,它可以优化 APK 内部资源对齐方式。

签名完成后再验证:

"$APKSIGNER" verify --verbose "$SIGNED_APK"

如果验证失败,GitHub Actions 会直接失败,不会上传错误产物。


九、签名 AAB 的过程

AAB 使用 jarsigner 签名:

jarsigner \
  -keystore "$KEYSTORE_FILE" \
  -storepass "$ANDROID_KEYSTORE_PASSWORD" \
  -keypass "$ANDROID_KEY_PASSWORD" \
  -signedjar "$SIGNED_AAB" \
  "$UNSIGNED_AAB" \
  "$ANDROID_KEY_ALIAS"

然后验证:

jarsigner -verify -verbose "$SIGNED_AAB"

最终会得到:

app/build/signed/app-release-signed.apk
app/build/signed/app-release-signed.aab

十、上传构建产物

最后一步使用:

- name: Upload signed release artifacts
  uses: actions/upload-artifact@v4
  with:
    name: android-signed-release
    path: app/build/signed/*
    if-no-files-found: error
    retention-days: 30

上传完成后,可以在 GitHub Actions 的运行记录页面下载:

android-signed-release

里面包含:

app-release-signed.apk
app-release-signed.aab

十一、本地验证方式

在提交 workflow 前,建议先本地跑一次构建:

./gradlew --no-daemon :app:assembleDebug :app:assembleRelease :app:bundleRelease

如果本地都构建失败,GitHub Actions 上也大概率会失败。

构建成功后,可以检查这些文件是否存在:

app/build/outputs/apk/debug/app-debug.apk
app/build/outputs/apk/release/app-release-unsigned.apk
app/build/outputs/bundle/release/app-release.aab

下载 GitHub Actions artifact 后,可以验证签名:

apksigner verify --verbose app-release-signed.apk

验证 AAB:

jarsigner -verify -verbose app-release-signed.aab

十二、常见问题

1. PR 为什么不签名?

因为 PR 可能来自外部贡献者。

如果 PR 阶段允许读取 Secrets,就有泄露签名信息的风险。

所以推荐策略是:

PR:只构建
main push:构建 + 签名 + 上传
workflow_dispatch:构建 + 签名 + 上传

2. Secrets 缺失会怎样?

workflow 里有专门的检查:

echo "::error::Missing required secret: ANDROID_KEYSTORE_BASE64"

如果缺少任何一个 Secret,CD 阶段会失败。

这样比静默失败更好,因为你能直接看到缺少哪个配置。


3. keystore.jks 要不要提交?

不要。

应该加入 .gitignore

*.jks
*.keystore

如果已经提交过 keystore,需要尽快从仓库历史里清理,并考虑更换签名文件。


总结

原生 Android 项目接入 GitHub Actions CI/CD 的核心思路是:

用 GitHub Actions 自动构建
用 GitHub Secrets 保存签名信息
用 apksigner 签 APK
用 jarsigner 签 AAB
用 upload-artifact 上传产物

最终效果是:

  • 提 PR 时,自动检查项目是否能构建。
  • 推送到 main 后,自动生成 signed APK 和 signed AAB。
  • keystore 和密码不进入代码仓库。
  • 构建失败、Secrets 缺失、签名失败都会明确报错。

这样,一个原生 Android 项目的基础 CI/CD 流程就搭好了。

评论 0

支持 @用户名 提醒对方(需为站内已注册用户名);回复仅支持一层楼中楼。

登录后发表评论、回复与 @ 提及。

举报

举报会匿名发送给管理员审核。

  • 暂无评论,来发表第一条。

码谱 · The Digital Atelier · 技术内容社区

下载 Android 版

下载完成后,点击通知栏中的安装包完成安装