Refined

Refined

从 CI 到 CD:一次 Spring Boot 项目自动化工作流实践

3
2025-07-06
从 CI 到 CD:一次 Spring Boot 项目自动化工作流实践

前言

在软件开发过程中,有很多重复性的工作,比如在提交代码前手动运行测试,或者在发布版本时手动打包、写更新记录、上传文件。这些操作不仅耗时,还容易出错。

GitHub Actions 是一个可以帮助我们自动完成这些任务的工具。这篇文章记录了我的一个 Spring Boot3 (w/ Kotlin) 项目的自动化流程配置实践。从持续集成 (CI) 开始,实现代码提交后自动进行测试;然后更进一步,实现持续交付 (CD),在项目源码有变动时每天自动发布新版本。

建立 CI 工作流

自动化的第一步是建立持续集成(CI)。它的主要作用是,当有新代码提交或合并时,系统能自动进行构建和测试。这可以很早地发现代码中的问题,保证项目代码的质量。

首先需要在项目里创建一个 CI 工作流。在项目根目录下,创建 .github/workflows/ci.yml 文件,并写入以下内容:

# .github/workflows/ci.yml
name: Spring Boot 3 Kotlin CI

# 触发条件:当有代码推送到 main 分支,或向 main 分支发起合并请求时
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # 第一步:获取代码
      - name: Checkout repository
        uses: actions/checkout@v4

      # 第二步:设置 JDK 环境
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      # 第三步:设置 Gradle (会自动处理依赖缓存)
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4
      
      # 第四步:执行构建和测试
      - name: Build and test with Gradle
        run: ./gradlew build

这个文件定义了一个基础的自动化流程。它会在指定的事件发生时,自动检出代码、准备好开发环境,并执行 build 命令来完成编译和测试。这样,所有代码变更都会经过一次自动检查。

实现自动发布

CI 保证了代码的质量,但软件的最终目的是交付给用户。在代码测试通过之后,发布版本是下一个关键步骤。手动发布版本涉及很多繁琐的操作,比如确定版本号、整理更新记录、打包、上传等,这些也很适合用程序来自动完成。

接下来,我们将 CI 的概念延伸到持续交付(CD),创建另一个工作流来专门负责版本的自动发布。

规划自动发布流程

在开始配置前,我们先明确一下这个自动发布流程需要做什么:

  1. 定时运行:每天在固定时间自动启动。

  2. 检查是否重复:如果当天的版本已经发布过,就停止运行。

  3. 检查是否有更新:如果上次发布后没有新的代码提交,也停止运行。

  4. 自动生成版本号:根据日期生成版本号,例如 2025.07.27

  5. 自动生成更新记录:从 Git 提交历史中提取两次发布之间的所有变更,形成更新记录。

  6. 自动打包和发布:将上面所有信息和打包好的文件整合,在 GitHub 上创建一个新的 Release。

解构自动发布工作流

根据上面的规划,我们创建第二个工作流文件 .github/workflows/release.yml。这个工作流由两个任务组成。

1. 检查任务

第一个任务的作用是进行前置检查,判断是否需要执行发布。它会先根据当前日期生成版本号,然后使用 GitHub 的命令行工具 gh 查询这个版本的 Release 是否已经存在。如果已存在,整个工作流就会停止,以避免重复发布。

2. 执行任务

只有在检查任务通过后,第二个任务才会开始执行。它首先会再次进行检查,通过 git log 命令查看是否有新的代码提交。如果没有,任务同样会停止,以避免发布一个内容没有任何变化的新版本。

如果确认有新的代码提交,任务就会继续执行以下步骤:运行 ./gradlew bootJar 命令打包项目,并把动态生成的版本号传递给构建过程。最后,它会使用一个社区提供的 Action,将版本号、自动生成的更新记录,以及打包好的 Jar 文件整合起来,在 GitHub 上创建一个标准的 Release。

整合与应用

现在,项目里有了两个工作流文件,它们有各自明确的分工。

文件一:.github/workflows/ci.yml (用于代码检查) 它的作用是在代码合并到主分支之前,对每次的提交和合并请求进行自动测试,确保代码质量。

文件二:.github/workflows/release.yml (用于版本发布) 它的作用是在代码合并到主分支之后,按照预定的时间(每日),自动执行版本发布流程。

# .github/workflows/release.yml
name: Daily Build and Release
on:
  schedule:
    - cron: '0 16 * * *' # 每天 UTC 16:00 (北京时间 00:00) 运行
  workflow_dispatch:
jobs:
  check:
    name: Check if release is needed
    runs-on: ubuntu-latest
    outputs:
      should_run: ${{ steps.check_release.outputs.should_run }}
      version: ${{ steps.generate_version.outputs.VERSION }}
    steps:
      - name: Generate Version
        id: generate_version
        run: |
          VERSION=$(date +'%Y.%m.%d')
          echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
      - name: Check if Release for today already exists
        id: check_release
        run: |
          if gh release view ${{ steps.generate_version.outputs.VERSION }} > /dev/null 2>&1; then
            echo "should_run=false" >> $GITHUB_OUTPUT
          else
            echo "should_run=true" >> $GITHUB_OUTPUT
          fi
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  build-and-release:
    name: Build and Create Release
    needs: check
    if: needs.check.outputs.should_run == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4
      - name: Generate Release Notes and Check for Changes
        id: generate_info
        shell: bash
        run: |
          if ! git describe --tags --abbrev=0 > /dev/null 2>&1; then
            CHANGELOG=$(git log --pretty=format:"* %s (%h) - %an")
          else
            LATEST_TAG=$(git describe --tags --abbrev=0)
            CHANGELOG=$(git log ${LATEST_TAG}..HEAD --pretty=format:"* %s (%h) - %an")
          fi
          if [[ -z "$CHANGELOG" ]]; then
            echo "has_changes=false" >> $GITHUB_OUTPUT
          else
            echo "has_changes=true" >> $GITHUB_OUTPUT
            {
              echo 'CHANGELOG<<EOF'
              echo "$CHANGELOG"
              echo 'EOF'
            } >> "$GITHUB_ENV"
          fi
      - name: Build with Gradle
        if: steps.generate_info.outputs.has_changes == 'true'
        run: ./gradlew bootJar -PnewVersion=${{ needs.check.outputs.version }} --build-cache
      - name: Create GitHub Release
        if: steps.generate_info.outputs.has_changes == 'true'
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.check.outputs.version }}
          name: Release ${{ needs.check.outputs.version }}
          body: ${{ env.CHANGELOG }}
          files: build/libs/*.jar

为了让 release.yml 能正确地设置 Jar 包的版本,还需要修改项目中的 Gradle 配置文件 build.gradle.kts,使其能接收命令行传来的版本号:

// build.gradle.kts
version = project.findProperty("newVersion")?.toString() ?: "0.0.1-SNAPSHOT"

下面是通过 Github Action 生成的 Release:

总结

通过以上配置,我们为项目建立了一套完整的自动化流程。CI 工作流保证了日常开发的质量,CD 工作流则处理了版本发布的繁琐工作。将这些重复性任务交给机器,可以让我们更专注于功能开发,同时也让整个开发和发布过程更加规范和高效。

果然,懒惰是第一生产力(。