22、CI/CD流水线:GitHub Actions配置、自动化测试、自动部署、版本管理

说实话,很多团队在开发初期都不太重视CI/CD。我见过太多项目,代码写完了,手动打包、手动上传、手动部署,每次发布都像在走钢丝。尤其是做Android后端API,一旦部署出错,客户端那边直接崩给你看。

这一章,我们就来聊聊怎么用GitHub Actions搭一套完整的CI/CD流水线。说白了,就是让机器替我们干那些重复的脏活累活。

为什么需要CI/CD?

你想想看,一个后端API从代码提交到上线,中间要经过多少步骤?编译、测试、打包、部署……每一步都可能出错。手动操作的话,漏掉一个测试用例,或者忘记更新版本号,都是家常便饭。

我个人习惯是:凡是能自动化的,绝不手动。CI/CD流水线就是帮你把这些步骤串起来,每次代码推送,自动触发整个流程。

核心目标:

  • 代码提交后自动运行测试
  • 测试通过后自动构建
  • 构建成功后自动部署到服务器
  • 每次发布自动打版本标签

GitHub Actions 基础配置

GitHub Actions 的配置文件放在 .github/workflows/ 目录下,YAML格式。我们先创建一个基础的流水线文件。

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      
      - name: Cache Gradle dependencies
        uses: actions/cache@v3
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      
      - name: Build and test
        run: ./gradlew clean build

嗯,这里要注意一点:缓存Gradle依赖这一步很关键。我第一次配置时没加缓存,每次构建都要下载所有依赖,一个项目跑下来要七八分钟。加了缓存后,直接缩短到两分钟以内。

自动化测试:不只是跑一下

很多人的自动化测试就是跑一遍 ./gradlew test,然后看绿了就完事。但我在项目中遇到过一个问题:测试通过了,但覆盖率太低,核心逻辑根本没测到。

所以,我建议在流水线中加入测试覆盖率检查代码质量检查

# 在 build.gradle.kts 中配置 JaCoCo
plugins {
    id("jacoco")
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required.set(true)
        html.required.set(true)
    }
}

tasks.check {
    dependsOn(tasks.jacocoTestReport)
}

然后在流水线中增加覆盖率门槛:

- name: Run tests with coverage
  run: ./gradlew check

- name: Upload coverage report
  uses: actions/upload-artifact@v3
  with:
    name: coverage-report
    path: build/reports/jacoco/

避坑指南:我曾经遇到过测试在本地全绿,但CI上一直失败的情况。后来发现是时区问题——测试里用了本地时间,而CI服务器是UTC。从那以后,我所有测试都强制使用UTC,或者用固定时间戳。

自动部署:从构建到上线

部署这块,不同团队用的方案不一样。有的用Docker,有的直接jar包扔服务器。我这里以Docker部署到云服务器为例,这是目前比较主流的做法。

deploy:
  needs: build-and-test
  runs-on: ubuntu-latest
  if: github.ref == 'refs/heads/main'
  
  steps:
    - uses: actions/checkout@v3
    
    - name: Build Docker image
      run: |
        docker build -t my-api:${{ github.sha }} .
        docker tag my-api:${{ github.sha }} my-api:latest
    
    - name: Push to Docker registry
      run: |
        docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
        docker push my-api:${{ github.sha }}
        docker push my-api:latest
    
    - name: Deploy to server
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SERVER_SSH_KEY }}
        script: |
          docker pull my-api:latest
          docker stop my-api || true
          docker rm my-api || true
          docker run -d --name my-api -p 8080:8080 my-api:latest

这里有个细节:github.sha 作为镜像标签。为什么?因为每个提交的SHA是唯一的,这样你可以随时回滚到任意版本。我见过有人只用 latest 标签,结果出问题时根本不知道线上跑的是哪个版本。

注意:SSH密钥和服务器密码等敏感信息,一定要存在GitHub的Secrets里,不要硬编码在YAML文件中。我曾经见过有人把密码直接写在配置文件里,然后不小心提交到了公开仓库……那场面,简直是灾难。

版本管理:自动打标签

版本管理这件事,说简单也简单,说复杂也复杂。我个人习惯用语义化版本(SemVer):主版本.次版本.修订号

在流水线中,我们可以根据分支和提交信息自动生成版本号:

- name: Generate version tag
  id: version
  run: |
    if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
      # 从Git标签获取最新版本
      LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
      # 解析版本号,递增修订号
      IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
      NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
    else
      # 开发分支使用提交SHA作为预发布版本
      NEW_VERSION="0.0.0-${GITHUB_SHA::7}"
    fi
    echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT

- name: Create Git tag
  run: |
    git config user.name "CI Bot"
    git config user.email "ci@example.com"
    git tag -a v${{ steps.version.outputs.version }} -m "Release v${{ steps.version.outputs.version }}"
    git push origin v${{ steps.version.outputs.version }}

版本策略总结:

分支 版本格式 示例
main 正式版本 v1.2.3
develop 预发布版本 v1.2.3-alpha.1
feature/* 不生成标签

完整的流水线流程图

下面这张图展示了我们整个CI/CD流水线的核心流程。从代码提交到最终上线,每一步都清晰可见。

CI/CD 流水线核心流程 代码提交 git push / PR 构建 & 测试 编译 → 单元测试 → 覆盖率检查 代码质量扫描 构建Docker镜像 docker build 打标签(SHA + latest) 推送镜像到仓库 docker push Docker Hub / 私有仓库 部署到服务器 SSH连接 → docker pull 停止旧容器 → 启动新容器 版本管理 自动生成版本号 创建Git标签 并行执行 测试失败 → 阻断流水线

环境变量与Secrets管理

流水线中免不了要使用各种环境变量。比如数据库连接串、API密钥、JWT密钥等等。这些东西绝对不能写在代码里。

GitHub提供了两种方式:

  • Repository Secrets:仓库级别的加密变量,适合单个项目
  • Environment Secrets:环境级别的变量,适合区分开发/测试/生产
# 在YAML中引用Secrets
- name: Deploy to production
  env:
    DB_URL: ${{ secrets.PROD_DB_URL }}
    DB_USER: ${{ secrets.PROD_DB_USER }}
    DB_PASS: ${{ secrets.PROD_DB_PASS }}
    JWT_SECRET: ${{ secrets.JWT_SECRET }}
  run: |
    echo "DB_URL=$DB_URL" >> .env
    echo "DB_USER=$DB_USER" >> .env
    # 启动应用时加载.env文件
    docker run -d --env-file .env my-api:latest

我的习惯:我会在仓库里放一个 .env.example 文件,列出所有需要的环境变量,但不填真实值。这样新成员加入时,直接复制这个文件,填上自己的值就能跑起来。既安全又方便。

流水线优化技巧

最后分享几个我在实际项目中用到的优化技巧:

  1. 并行化任务:测试和代码扫描可以并行跑,节省时间
  2. 条件触发:只有main分支才触发部署,feature分支只跑测试
  3. 通知机制:流水线失败时,自动发消息到Slack或钉钉
  4. 构建缓存:除了Gradle缓存,Docker层也可以缓存
# Docker层缓存示例
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2

- name: Build and push with cache
  uses: docker/build-push-action@v4
  with:
    context: .
    push: true
    tags: my-api:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

说实话,CI/CD流水线这东西,一开始配置起来确实有点繁琐。但一旦搭好了,后面就一劳永逸了。你只管写代码、提交,剩下的交给机器。这才是现代开发该有的样子。


公众号:蓝海资料掘金营,微信deep3321