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流水线的核心流程。从代码提交到最终上线,每一步都清晰可见。
环境变量与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 文件,列出所有需要的环境变量,但不填真实值。这样新成员加入时,直接复制这个文件,填上自己的值就能跑起来。既安全又方便。
流水线优化技巧
最后分享几个我在实际项目中用到的优化技巧:
- 并行化任务:测试和代码扫描可以并行跑,节省时间
- 条件触发:只有main分支才触发部署,feature分支只跑测试
- 通知机制:流水线失败时,自动发消息到Slack或钉钉
- 构建缓存:除了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