第87章:CI/CD:GitHub Actions、Jenkins Pipeline、自动化测试与部署

说实话,很多团队做 Kotlin 项目,代码写得挺漂亮,一到上线就手忙脚乱。我见过太多人,每次发版都是「本地 build → 压缩 → 上传服务器 → 手动重启」。这种操作,偶尔一次还行,要是每周来一次,迟早出事故。

CI/CD 说白了,就是把「构建、测试、部署」这些重复劳动交给机器。你只管写代码,剩下的流水线帮你搞定。这一章,我就带你看看 Kotlin 项目里怎么搭这套东西。

为什么 Kotlin 项目需要 CI/CD?

Kotlin 项目有个特点:它既能跑在 JVM 上,也能编译成 JS 或 Native。这意味着你的构建流程可能比纯 Java 项目更复杂。举个例子,我有个项目同时要出 Android APK 和后端 JAR,手动打包简直要命。

CI/CD 能帮你解决三个核心问题:

  • 一致性:每次构建环境都一样,不会出现「我本地能跑啊」的尴尬
  • 自动化:代码一推送,自动跑测试、自动打包、自动部署
  • 可追溯:每次构建都有日志,出了问题能快速定位

核心观点:CI/CD 不是锦上添花,而是工程化的底线。项目超过 3 个人参与,就应该上 CI/CD。

GitHub Actions:轻量级 CI/CD 首选

如果你的代码托管在 GitHub 上,GitHub Actions 是最省事的选择。它不需要额外搭建服务器,直接在仓库里写 YAML 配置就行。

我个人习惯把工作流文件放在 .github/workflows/ 目录下。每个文件对应一个流水线。

一个基础的 Kotlin 构建工作流

先看一个最简单的例子:每次推送代码,自动编译并跑单元测试。

# .github/workflows/build.yml
name: Kotlin Build

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

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

      - name: Build and Test
        run: ./gradlew clean build

      - name: Upload Test Reports
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-reports
          path: build/reports/tests/

这里有个细节:if: always() 的意思是,就算测试失败了,也要上传测试报告。我曾经踩过这个坑,测试失败后看不到报告,排查问题全靠猜。

小技巧:Gradle 缓存能大幅提速。第一次构建可能要 3 分钟,缓存命中后能压缩到 40 秒。

多模块项目的并行构建

Kotlin 项目经常拆成多个模块。比如一个后端项目,可能有 apicoreinfrastructure 三个模块。你可以让它们并行构建,节省时间。

jobs:
  build-api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build API Module
        run: ./gradlew :api:build

  build-core:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Core Module
        run: ./gradlew :core:build

  build-infra:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Infrastructure Module
        run: ./gradlew :infrastructure:build

嗯,这里要注意:并行构建虽然快,但如果模块之间有依赖关系,你得用 needs 关键字控制顺序。

Jenkins Pipeline:企业级的选择

GitHub Actions 虽好,但有些公司要求代码必须放在内网 GitLab 上。这时候 Jenkins 就是主力了。

Jenkins Pipeline 有两种写法:Declarative(声明式)和 Scripted(脚本式)。我推荐用 Declarative,结构清晰,容易维护。

一个 Jenkinsfile 示例

pipeline {
    agent any

    tools {
        jdk 'JDK17'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                sh './gradlew clean build'
            }
            post {
                success {
                    archiveArtifacts artifacts: 'build/libs/*.jar'
                }
            }
        }

        stage('Test') {
            steps {
                sh './gradlew test'
            }
            post {
                always {
                    junit 'build/test-results/**/*.xml'
                }
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh './deploy-staging.sh'
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                input message: '确认部署到生产环境?', ok: '确认'
                sh './deploy-production.sh'
            }
        }
    }

    post {
        failure {
            emailext(
                subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                to: 'team@example.com',
                body: "请查看构建日志: ${env.BUILD_URL}"
            )
        }
    }
}

你看,Jenkins 的 Pipeline 比 GitHub Actions 更灵活。比如 input 步骤,可以在部署前让人工确认。这个功能我在生产环境用过很多次,能有效防止手滑。

注意:Jenkins 的 input 步骤会阻塞流水线。如果长时间没人确认,Pipeline 会超时。建议设置 timeout 参数。

自动化测试:CI/CD 的核心价值

CI/CD 如果没有测试,那只是个自动打包工具。真正的价值在于「每次提交都跑测试,确保代码质量不滑坡」。

Kotlin 项目里,我一般会跑三类测试:

测试类型 工具 运行时机 说明
单元测试 JUnit 5 + MockK 每次提交 测试单个类或方法,速度快
集成测试 Testcontainers PR 合并前 测试与数据库、Redis 等外部组件的交互
端到端测试 Ktor Client + 自定义脚本 部署前 模拟真实用户请求,验证整个链路

为什么要把集成测试放在 PR 合并前?因为集成测试通常要启动 Docker 容器,跑一次要 5-10 分钟。如果每次提交都跑,开发效率会受影响。

在 CI 中运行集成测试

用 Testcontainers 做集成测试时,CI 环境需要支持 Docker。GitHub Actions 默认就有 Docker,Jenkins 需要额外配置。

// 一个使用 Testcontainers 的集成测试示例
class UserRepositoryIntegrationTest {

    companion object {
        private val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply {
            withDatabaseName("testdb")
            withUsername("test")
            withPassword("test")
        }

        @JvmStatic
        @BeforeAll
        fun setup() {
            postgres.start()
            // 配置数据源指向 postgres.getJdbcUrl()
        }

        @JvmStatic
        @AfterAll
        fun cleanup() {
            postgres.stop()
        }
    }

    @Test
    fun `should save and find user`() {
        val repo = UserRepository()
        val user = User("test@example.com", "Test User")
        repo.save(user)
        val found = repo.findById(user.id)
        assertEquals(user.email, found?.email)
    }
}

在 CI 配置里,你需要确保 Docker 服务在运行。GitHub Actions 的 ubuntu-latest 默认就有,但如果你用自托管 Runner,记得安装 Docker。

避坑指南:我曾经在 Jenkins 上跑 Testcontainers,结果容器一直起不来。查了半天,发现是 Jenkins 用户没有 Docker 权限。解决方案是把 Jenkins 用户加到 docker 用户组里。

自动化部署:从构建到上线

测试通过后,下一步就是部署。部署策略有很多,我挑两个常用的说说。

直接部署到服务器

适合小项目或内部工具。流程很简单:构建 JAR → SCP 到服务器 → 重启服务。

# deploy-staging.sh
#!/bin/bash
SERVER="staging.example.com"
JAR_FILE="build/libs/app.jar"

echo "上传 JAR 到服务器..."
scp $JAR_FILE deployer@$SERVER:/opt/app/app.jar

echo "重启服务..."
ssh deployer@$SERVER "sudo systemctl restart app"

echo "检查服务状态..."
ssh deployer@$SERVER "sudo systemctl status app"

这种方式的缺点很明显:如果部署过程中出问题,回滚很麻烦。我建议至少保留上一个版本的 JAR 文件。

Docker 容器化部署

更现代的做法是用 Docker。每次构建生成镜像,推送到镜像仓库,然后在服务器上拉取新镜像并重启容器。

# Dockerfile
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY build/libs/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

在 CI 中构建并推送镜像:

# GitHub Actions 中构建 Docker 镜像
- name: Build Docker Image
  run: |
    docker build -t myregistry.com/app:${{ github.sha }} .
    docker push myregistry.com/app:${{ github.sha }}

服务器上只需要一个 docker-compose.yml,更新时改一下镜像版本号,然后 docker-compose up -d 就行。

我的建议:不管用哪种方式,一定要在 CI 脚本里加入健康检查。比如部署后等 30 秒,然后 curl 一下 /health 端点。如果返回 200,才算部署成功。

知识体系总览

下面这张图,把 CI/CD 的核心流程和工具串起来了。你可以把它当作一个检查清单,看看自己的项目还缺哪一块。

Kotlin CI/CD 知识体系 代码提交 触发方式:Push / PR / 定时 CI 工具:GitHub Actions / Jenkins Pipeline 构建 (Build) 测试 (Test) 部署 (Deploy) 通知 (Notify) 单元测试 | 集成测试 | 端到端测试

你看,整个流程其实不复杂。核心就是:代码提交 → 自动触发 → 构建测试 → 部署上线。每个环节都有对应的工具和最佳实践。

最后说一句:CI/CD 不是一次性配置完就完事了。你需要根据项目的发展不断调整。比如项目初期,测试阶段可能只需要跑单元测试;等业务复杂了,再慢慢加入集成测试和端到端测试。

嗯,这一章的内容就到这里。希望你能从这些经验里找到适合自己的 CI/CD 方案。


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