Skip to content

CI/CD 集成实战

源:GitHub Actions | GitLab CI

在 CI/CD 环境中高效运行 Gradle 构建,实现自动化测试、构建和发布。

CI/CD 基础概念

什么是 CI/CD

Continuous Integration (CI)

  • 自动构建
  • 自动测试
  • 快速反馈

Continuous Deployment (CD)

  • 自动发布
  • 版本管理
  • 部署流程

GitHub Actions

基本构建

.github/workflows/build.yml

yaml
name: Build

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      
      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2
      
      - name: Build
        run: ./gradlew assembleDebug

优化构建速度

启用缓存

yaml
- name: Setup Gradle
  uses: gradle/gradle-build-action@v2
  with:
    cache-read-only: ${{ github.ref != 'refs/heads/main' }}

配置构建缓存

yaml
- name: Build with Cache
  run: ./gradlew build --build-cache --configuration-cache
  env:
    GRADLE_OPTS: -Dorg.gradle.caching=true

运行测试

yaml
- name: Run Unit Tests
  run: ./gradlew test
  
- name: Upload Test Reports
  if: always()
  uses: actions/upload-artifact@v3
  with:
    name: test-reports
    path: '**/build/reports/tests/**'

构建多个变体

yaml
strategy:
  matrix:
    variant: [debug, release]

steps:
  - name: Build ${{ matrix.variant }}
    run: ./gradlew assemble${{ matrix.variant }}

GitLab CI

基本配置

.gitlab-ci.yml

yaml
image: openjdk:17-jdk

variables:
  GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.caching=true"

before_script:
  - chmod +x gradlew

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - ./gradlew assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/
    expire_in: 1 week

test:
  stage: test
  script:
    - ./gradlew test
  artifacts:
    reports:
      junit: '**/build/test-results/test/TEST-*.xml'

缓存配置

yaml
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - .gradle/caches
    - .gradle/wrapper
    - build

构建优化策略

Gradle Daemon

禁用 Daemon(CI 推荐):

yaml
env:
  GRADLE_OPTS: -Dorg.gradle.daemon=false

并行构建

properties
# gradle.properties
org.gradle.parallel=true
org.gradle.workers.max=4

内存配置

yaml
env:
  GRADLE_OPTS: -Xmx4g -XX:+UseParallelGC

构建缓存

本地缓存

yaml
- name: Cache Gradle
  uses: actions/cache@v3
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      ${{ runner.os }}-gradle-

远程缓存

kotlin
// settings.gradle.kts
buildCache {
    remote<HttpBuildCache> {
        url = uri("https://cache.example.com/cache/")
        isPush = System.getenv("CI") == "true"
        
        credentials {
            username = System.getenv("CACHE_USER")
            password = System.getenv("CACHE_PASS")
        }
    }
}

自动化测试

单元测试

yaml
- name: Run Unit Tests
  run: ./gradlew test
  
- name: Publish Test Report
  uses: mikepenz/action-junit-report@v3
  if: always()
  with:
    report_paths: '**/build/test-results/test/TEST-*.xml'

UI 测试

yaml
- name: Run Instrumented Tests
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 33
    script: ./gradlew connectedCheck

代码覆盖率

yaml
- name: Generate Coverage Report
  run: ./gradlew jacocoTestReport
  
- name: Upload to Codecov
  uses: codecov/codecov-action@v3
  with:
    files: '**/build/reports/jacoco/test/jacocoTestReport.xml'

自动发布

版本管理

使用 Git Tag

yaml
on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Get Version
        id: version
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      - name: Build Release
        run: ./gradlew assembleRelease -Pversion=${{ steps.version.outputs.VERSION }}

发布到 GitHub Releases

yaml
- name: Create Release
  uses: softprops/action-gh-release@v1
  with:
    files: |
      app/build/outputs/apk/release/*.apk
      app/build/outputs/bundle/release/*.aab
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

发布到 Maven

yaml
- name: Publish to Maven Central
  run: ./gradlew publishReleasePublicationToOSSRHRepository
  env:
    OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
    OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
    SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
    SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}

实战案例

案例1:完整的 CI/CD 流程

yaml
name: CI/CD

on:
  push:
    branches: [ main, develop ]
    tags:
      - 'v*'
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      
      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2
      
      - name: Run Tests
        run: ./gradlew test
      
      - name: Build Debug APK
        run: ./gradlew assembleDebug
      
      - name: Upload APK
        uses: actions/upload-artifact@v3
        with:
          name: app-debug
          path: app/build/outputs/apk/debug/*.apk
  
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      
      - name: Run UI Tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          script: ./gradlew connectedCheck
  
  release:
    if: startsWith(github.ref, 'refs/tags/v')
    needs: [build, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      
      - name: Build Release
        run: ./gradlew assembleRelease bundleRelease
      
      - name: Sign APK
        uses: r0adkll/sign-android-release@v1
        with:
          releaseDirectory: app/build/outputs/apk/release
          signingKeyBase64: ${{ secrets.SIGNING_KEY }}
          alias: ${{ secrets.ALIAS }}
          keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
          keyPassword: ${{ secrets.KEY_PASSWORD }}
      
      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            app/build/outputs/apk/release/*.apk
            app/build/outputs/bundle/release/*.aab

案例2:多环境部署

yaml
jobs:
  deploy-dev:
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - name: Build Dev
        run: ./gradlew assembleDevDebug
      
      - name: Deploy to Firebase
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_APP_ID_DEV }}
          token: ${{ secrets.FIREBASE_TOKEN }}
          groups: dev-team
          file: app/build/outputs/apk/dev/debug/app-dev-debug.apk
  
  deploy-prod:
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - name: Build Prod
        run: ./gradlew bundleRelease
      
      - name: Deploy to Play Store
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
          packageName: com.example.app
          releaseFiles: app/build/outputs/bundle/release/app-release.aab
          track: production

最佳实践

缓存策略

  • 使用 Gradle Build Action
  • 配置远程构建缓存
  • 仅主分支推送缓存

并行执行

yaml
strategy:
  matrix:
    task: [assembleDebug, test, lint]

失败快速反馈

yaml
strategy:
  fail-fast: true

环境隔离

  • 使用 Secrets 管理凭证
  • 不同环境不同配置
  • 版本号自动化

构建优化

properties
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true

监控和通知

yaml
- name: Notify Slack
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}