持续集成详解
一、知识概述
持续集成(Continuous Integration,简称CI)是一种软件开发实践,要求开发人员频繁地将代码集成到主干分支,每次集成都通过自动化构建和测试来验证,从而尽早发现集成错误。
1.1 CI的核心价值
┌─────────────────────────────────────────────────────────────┐
│ 持续集成的核心价值 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 尽早发现问题 → 每次提交都验证,问题不会积累 │
│ │
│ 2. 减少重复劳动 → 自动化构建、测试、部署 │
│ │
│ 3. 提高软件质量 → 代码审查、自动化测试、静态分析 │
│ │
│ 4. 加快交付速度 → 自动化流水线,快速反馈 │
│ │
│ 5. 增强团队信心 → 可重复的构建,可靠的质量保障 │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 CI工作流程
kotlin
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 代码 │───▶│ 构建 │───▶│ 测试 │───▶│ 反馈 │
│ 提交 │ │ 编译 │ │ 验证 │ │ 通知 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
▼ ▼ ▼ ▼
Git Push Maven/Gradle Unit/Integration Email/Slack
Webhook 编译打包 Test 执行 状态通知
1.3 主流CI工具对比
| 工具 | 特点 | 适用场景 |
|---|---|---|
| Jenkins | 开源、插件丰富、高度可定制 | 企业级项目、复杂流水线 |
| GitLab CI | 与GitLab深度集成、配置简单 | GitLab托管项目 |
| GitHub Actions | GitHub原生、云端运行、生态丰富 | GitHub开源项目 |
| CircleCI | 云原生、快速启动、并行执行 | 云端项目 |
| Travis CI | 配置简单、开源项目免费 | 开源项目 |
二、Jenkins Pipeline详解
2.1 Jenkins架构
scss
┌─────────────────────────────────────────────────────────────┐
│ Jenkins 架构图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Master │────▶│ Agent │────▶│ Agent │ │
│ │ (调度) │ │ (执行) │ │ (执行) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Pipeline │ │ 构建任务 │ │ 构建任务 │ │
│ │ 定义 │ │ 执行器 │ │ 执行器 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2.2 Declarative Pipeline语法
groovy
// Jenkinsfile - 声明式Pipeline
pipeline {
agent any // 可在任何可用的agent上运行
// 环境变量定义
environment {
MAVEN_HOME = tool 'Maven-3.8.6'
JAVA_HOME = tool 'JDK-11'
PATH = "${MAVEN_HOME}/bin:${JAVA_HOME}/bin:${env.PATH}"
}
// 构建参数
parameters {
string(name: 'BRANCH_NAME', defaultValue: 'main', description: '分支名称')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试')
choice(name: 'DEPLOY_ENV', choices: ['dev', 'test', 'prod'], description: '部署环境')
}
// 触发器配置
triggers {
// 定时触发:每天凌晨2点
cron('0 2 * * *')
// 轮询SCM变更:每5分钟检查一次
pollSCM('H/5 * * * *')
}
// 构建工具配置
tools {
maven 'Maven-3.8.6'
jdk 'JDK-11'
}
// 选项配置
options {
timeout(time: 30, unit: 'MINUTES') // 超时时间
retry(3) // 重试次数
disableConcurrentBuilds() // 禁止并发构建
buildDiscarder(logRotator(numToKeepStr: '10')) // 保留构建历史
}
// 流水线阶段
stages {
stage('代码检出') {
steps {
echo '开始检出代码...'
checkout scm
sh 'git log -1 --pretty=format:"%h - %an, %ar : %s"'
}
}
stage('代码检查') {
parallel {
stage('静态代码分析') {
steps {
echo '执行静态代码分析...'
sh 'mvn checkstyle:check'
}
}
stage('依赖安全扫描') {
steps {
echo '执行依赖安全扫描...'
sh 'mvn dependency-check:check'
}
}
}
}
stage('编译构建') {
steps {
echo '开始编译构建...'
sh 'mvn clean compile -DskipTests=${SKIP_TESTS}'
}
post {
success {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
stage('单元测试') {
when {
expression { !params.SKIP_TESTS }
}
steps {
echo '执行单元测试...'
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
// 生成测试覆盖率报告
jacoco execPattern: 'target/jacoco.exec'
}
}
}
stage('集成测试') {
when {
expression { !params.SKIP_TESTS && params.DEPLOY_ENV == 'test' }
}
steps {
echo '执行集成测试...'
sh 'mvn verify -Pintegration-test'
}
}
stage('构建镜像') {
when {
anyOf {
branch 'main'
branch 'develop'
}
}
steps {
script {
// 获取版本号
def version = sh(script: 'mvn help:evaluate -Dexpression=project.version -q -DforceStdout', returnStdout: true).trim()
def imageName = "myapp:${version}-${BUILD_NUMBER}"
echo "构建Docker镜像: ${imageName}"
sh """
docker build -t ${imageName} .
docker tag ${imageName} myapp:latest
"""
}
}
}
stage('部署') {
when {
anyOf {
branch 'main'
branch 'develop'
}
}
steps {
echo "部署到 ${params.DEPLOY_ENV} 环境..."
script {
switch(params.DEPLOY_ENV) {
case 'dev':
sh 'kubectl apply -f k8s/dev/'
break
case 'test':
sh 'kubectl apply -f k8s/test/'
break
case 'prod':
input message: '确认部署到生产环境?', ok: '确认部署'
sh 'kubectl apply -f k8s/prod/'
break
}
}
}
}
}
// 构建后操作
post {
always {
echo '清理工作空间...'
cleanWs()
}
success {
echo '构建成功!'
// 发送成功通知
emailext(
subject: "构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
<h2>构建成功</h2>
<p>项目: ${env.JOB_NAME}</p>
<p>构建号: ${env.BUILD_NUMBER}</p>
<p>构建地址: ${env.BUILD_URL}</p>
""",
to: 'team@example.com'
)
}
failure {
echo '构建失败!'
// 发送失败通知
emailext(
subject: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
<h2>构建失败</h2>
<p>项目: ${env.JOB_NAME}</p>
<p>构建号: ${env.BUILD_NUMBER}</p>
<p>构建地址: ${env.BUILD_URL}</p>
<p>请检查构建日志!</p>
""",
to: 'team@example.com'
)
}
unstable {
echo '构建不稳定!'
}
}
}
2.3 Scripted Pipeline语法
groovy
// Scripted Pipeline - 更灵活的脚本式语法
node {
// 定义阶段标记
def stages = [:]
try {
stage('检出代码') {
checkout scm
env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
echo "当前提交: ${env.GIT_COMMIT_SHORT}"
}
stage('并行检查') {
// 并行执行多个检查任务
stages['代码规范'] = {
sh 'mvn checkstyle:check'
}
stages['代码重复'] = {
sh 'mvn pmd:check'
}
stages['安全扫描'] = {
sh 'mvn spotbugs:check'
}
parallel stages
}
stage('构建') {
def buildResult = sh(script: 'mvn clean package -DskipTests', returnStatus: true)
if (buildResult != 0) {
error "构建失败,返回码: ${buildResult}"
}
}
stage('测试') {
def testResult = sh(script: 'mvn test', returnStatus: true)
if (testResult != 0) {
unstable '测试失败'
}
junit 'target/surefire-reports/*.xml'
}
stage('部署') {
if (env.BRANCH_NAME == 'main') {
def userInput = input(
id: 'DeployConfirmation',
message: '确认部署到生产环境?',
parameters: [
booleanParam(defaultValue: true, description: '确认部署', name: 'confirm')
]
)
if (userInput) {
sh 'kubectl apply -f k8s/prod/'
}
}
}
} catch (Exception e) {
currentBuild.result = 'FAILURE'
throw e
} finally {
// 清理工作
cleanWs()
}
}
2.4 Shared Libraries
groovy
// vars/standardJavaPipeline.groovy - 共享库定义
def call(Map config) {
pipeline {
agent any
tools {
maven config.mavenVersion ?: 'Maven-3.8.6'
jdk config.jdkVersion ?: 'JDK-11'
}
stages {
stage('检出') {
steps {
checkout scm
}
}
stage('构建') {
steps {
sh "mvn clean package ${config.mavenOpts ?: ''}"
}
}
stage('测试') {
when {
expression { config.runTests ?: true }
}
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
stage('部署') {
when {
expression { config.deploy ?: false }
}
steps {
script {
deploy(config.deployEnv ?: 'dev')
}
}
}
}
post {
always {
notifyBuild(currentBuild.result, config.notifyEmail)
}
}
}
}
// vars/deploy.groovy
def call(String env) {
echo "部署到 ${env} 环境..."
sh "kubectl apply -f k8s/${env}/"
}
// vars/notifyBuild.groovy
def call(String buildResult, String email) {
if (buildResult == null) {
buildResult = 'SUCCESS'
}
def subject = "${buildResult}: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
def body = """
构建状态: ${buildResult}
项目: ${env.JOB_NAME}
构建号: ${env.BUILD_NUMBER}
构建地址: ${env.BUILD_URL}
"""
emailext subject: subject, body: body, to: email
}
使用共享库的Jenkinsfile:
groovy
// Jenkinsfile - 使用共享库
@Library('my-shared-lib@main') _
standardJavaPipeline(
mavenVersion: 'Maven-3.9.0',
jdkVersion: 'JDK-17',
mavenOpts: '-DskipTests=false',
runTests: true,
deploy: true,
deployEnv: 'test',
notifyEmail: 'dev-team@example.com'
)
三、GitLab CI详解
3.1 GitLab CI架构
scss
┌─────────────────────────────────────────────────────────────┐
│ GitLab CI 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ GitLab │────▶│ Runner │────▶│ Executor │ │
│ │ Server │ │ (执行器) │ │ (运行环境) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ .gitlab-ci.yml Shell/Docker Container │
│ Pipeline定义 /Kubernetes /VM/Shell │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 .gitlab-ci.yml完整配置
yaml
# .gitlab-ci.yml - GitLab CI配置文件
# 定义阶段
stages:
- build
- test
- security
- deploy
# 全局变量
variables:
MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
DOCKER_TLS_CERTDIR: ""
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
# 缓存配置
cache:
key: "${CI_JOB_NAME}"
paths:
- .m2/repository/
# 默认配置
default:
image: maven:3.8.6-openjdk-11
before_script:
- echo "开始执行 ${CI_JOB_NAME}..."
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
# 构建阶段
build:
stage: build
script:
- mvn $MAVEN_CLI_OPTS compile
artifacts:
name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}"
paths:
- target/
expire_in: 1 hour
only:
- merge_requests
- main
- develop
# 单元测试
unit-test:
stage: test
script:
- mvn $MAVEN_CLI_OPTS test
artifacts:
when: always
reports:
junit:
- target/surefire-reports/TEST-*.xml
coverage_report:
coverage_format: jacoco
path: target/site/jacoco/jacoco.xml
coverage: '/Total.*?([0-9]{1,3})%/'
only:
- merge_requests
- main
- develop
# 集成测试
integration-test:
stage: test
services:
- name: mysql:8.0
alias: mysql
- name: redis:7.0
alias: redis
variables:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: testdb
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/testdb
SPRING_REDIS_HOST: redis
script:
- mvn $MAVEN_CLI_OPTS verify -Pintegration-test
artifacts:
when: always
reports:
junit:
- target/failsafe-reports/TEST-*.xml
only:
- main
- develop
# 代码质量检查
sonarqube-check:
stage: test
image: maven:3.8.6-openjdk-11
variables:
SONAR_TOKEN: "${SONAR_TOKEN}"
SONAR_HOST_URL: "${SONAR_HOST_URL}"
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- mvn $MAVEN_CLI_OPTS verify sonar:sonar -Dsonar.projectKey=${CI_PROJECT_NAME}
allow_failure: true
only:
- merge_requests
- main
# 安全扫描
security-scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy fs --exit-code 0 --no-progress --severity HIGH,CRITICAL .
- trivy fs --exit-code 1 --no-progress --severity CRITICAL .
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
only:
- main
- develop
# 依赖检查
dependency-check:
stage: security
script:
- mvn $MAVEN_CLI_OPTS dependency-check:check
artifacts:
when: always
paths:
- target/dependency-check-report.html
allow_failure: true
# 构建Docker镜像
docker-build:
stage: deploy
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "${CI_REGISTRY_IMAGE}${tag}" .
- docker push "${CI_REGISTRY_IMAGE}${tag}"
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
docker tag "${CI_REGISTRY_IMAGE}${tag}" "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
fi
only:
- main
- develop
# 部署到开发环境
deploy-dev:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: development
url: https://dev.example.com
script:
- kubectl config use-context dev-cluster
- kubectl set image deployment/myapp myapp=${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} -n dev
- kubectl rollout status deployment/myapp -n dev
only:
- develop
# 部署到生产环境
deploy-prod:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: production
url: https://www.example.com
script:
- kubectl config use-context prod-cluster
- kubectl set image deployment/myapp myapp=${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} -n prod
- kubectl rollout status deployment/myapp -n prod
when: manual
only:
- main
# 创建Release
release:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- echo "Running release job"
release:
name: "Release $CI_COMMIT_TAG"
description: "Release created using the release-cli."
tag_name: "$CI_COMMIT_TAG"
ref: "$CI_COMMIT_TAG"
milestones:
- "v1.0"
assets:
links:
- name: "Download JAR"
url: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/target/myapp.jar"
3.3 GitLab Runner配置
toml
# /etc/gitlab-runner/config.toml - Runner配置文件
concurrent = 10
check_interval = 0
log_level = "info"
[[runners]]
name = "shared-runner"
url = "https://gitlab.example.com"
token = "xxxxxxxxxxxxx"
executor = "docker"
[runners.docker]
image = "maven:3.8.6-openjdk-11"
privileged = true
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
pull_policy = ["if-not-present"]
allowed_images = ["maven:*", "docker:*", "openjdk:*"]
[runners.cache]
Type = "s3"
Path = "runner-cache"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.amazonaws.com"
BucketName = "gitlab-runner-cache"
AccessKey = "xxxxxxxxxxxxx"
SecretKey = "xxxxxxxxxxxxx"
[[runners]]
name = "kubernetes-runner"
url = "https://gitlab.example.com"
token = "xxxxxxxxxxxxx"
executor = "kubernetes"
[runners.kubernetes]
namespace = "gitlab-runner"
poll_timeout = 600
service_account = "gitlab-runner"
privileged = true
[runners.kubernetes.node_selector]
"node.kubernetes.io/role" = "ci"
[[runners.kubernetes.volumes.host_path]]
name = "docker-socket"
mount_path = "/var/run/docker.sock"
host_path = "/var/run/docker.sock"
四、GitHub Actions详解
4.1 GitHub Actions架构
scss
┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ GitHub │────▶│ Runner │────▶│ Job │ │
│ │ Events │ │ (执行器) │ │ (任务) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ push/pull_ GitHub-hosted/ Steps(步骤) │
│ request/issues Self-hosted Actions(动作) │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 完整的Workflow配置
yaml
# .github/workflows/ci.yml - GitHub Actions工作流
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'dev'
type: choice
options:
- dev
- test
- prod
# 环境变量
env:
JAVA_VERSION: '11'
MAVEN_VERSION: '3.8.6'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 构建 job
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 设置 JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: 设置 Maven
uses: stCarolas/setup-maven@v4.5
with:
maven-version: ${{ env.MAVEN_VERSION }}
- name: 缓存 Maven 依赖
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: 构建项目
run: mvn clean compile -DskipTests
- name: 获取版本号
id: version
run: |
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: app-jar
path: target/*.jar
retention-days: 1
# 测试 job
test:
needs: build
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: 运行单元测试
run: mvn test
- name: 生成测试报告
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Unit Tests
path: target/surefire-reports/*.xml
reporter: java-junit
- name: 上传测试覆盖率
uses: codecov/codecov-action@v4
with:
file: ./target/site/jacoco/jacoco.xml
flags: unittests
fail_ci_if_error: true
# 集成测试 job
integration-test:
needs: build
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: testdb
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
redis:
image: redis:7.0
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: 运行集成测试
run: mvn verify -Pintegration-test
env:
SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/testdb
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root123
SPRING_REDIS_HOST: localhost
- name: 上传集成测试报告
uses: actions/upload-artifact@v4
if: failure()
with:
name: integration-test-reports
path: target/failsafe-reports/
# 代码质量检查 job
code-quality:
needs: build
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 设置 JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: SonarCloud 扫描
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SpotBugs 检查
run: mvn spotbugs:check
continue-on-error: true
# 安全扫描 job
security:
needs: build
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 运行 Trivy 漏洞扫描
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'
- name: 运行 OWASP 依赖检查
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'My Application'
path: '.'
format: 'HTML'
out: 'reports'
- name: 上传安全报告
uses: actions/upload-artifact@v4
with:
name: security-reports
path: reports/
# 构建 Docker 镜像 job
docker:
needs: [build, test]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 下载构建产物
uses: actions/download-artifact@v4
with:
name: app-jar
path: target/
- name: 登录 GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 提取 Docker 元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha,prefix=
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 构建并推送镜像
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# 部署到开发环境 job
deploy-dev:
needs: docker
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment:
name: development
url: https://dev.example.com
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 配置 kubectl
uses: azure/setup-kubectl@v3
- name: 设置 kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG_DEV }}" | base64 -d > ~/.kube/config
- name: 部署到开发环境
run: |
kubectl set image deployment/myapp \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n dev
kubectl rollout status deployment/myapp -n dev --timeout=300s
- name: 运行冒烟测试
run: |
sleep 30
curl -f https://dev.example.com/health || exit 1
# 部署到生产环境 job
deploy-prod:
needs: docker
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://www.example.com
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 配置 kubectl
uses: azure/setup-kubectl@v3
- name: 设置 kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG_PROD }}" | base64 -d > ~/.kube/config
- name: 部署到生产环境
run: |
kubectl set image deployment/myapp \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n prod
kubectl rollout status deployment/myapp -n prod --timeout=600s
- name: 运行健康检查
run: |
sleep 60
curl -f https://www.example.com/health || exit 1
# 通知 job
notify:
needs: [build, test, docker, deploy-dev, deploy-prod]
if: always()
runs-on: ubuntu-latest
steps:
- name: 发送 Slack 通知
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
if: always()
4.3 自定义Action开发
javascript
// action.yml - Action元数据
name: 'Maven Build Action'
description: 'Build Java project with Maven'
author: 'Your Name'
inputs:
maven-version:
description: 'Maven version'
required: false
default: '3.8.6'
java-version:
description: 'Java version'
required: false
default: '11'
goals:
description: 'Maven goals to execute'
required: false
default: 'clean package'
maven-opts:
description: 'Maven options'
required: false
default: ''
outputs:
build-status:
description: 'Build status'
value: ${{ steps.build.outputs.status }}
runs:
using: 'composite'
steps:
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: 'temurin'
cache: maven
- name: Build with Maven
id: build
shell: bash
run: |
mvn ${{ inputs.goals }} ${{ inputs.maven-opts }}
echo "status=success" >> $GITHUB_OUTPUT
javascript
// JavaScript Action示例
const core = require('@actions/core');
const exec = require('@actions/exec');
const github = require('@actions/github');
async function run() {
try {
// 获取输入参数
const mavenGoals = core.getInput('goals', { required: true });
const mavenOpts = core.getInput('maven-opts') || '';
core.info(`Running Maven goals: ${mavenGoals}`);
// 执行Maven命令
let output = '';
const options = {
listeners: {
stdout: (data) => {
output += data.toString();
}
}
};
const exitCode = await exec.exec('mvn', mavenGoals.split(' '), options);
if (exitCode !== 0) {
core.setFailed(`Maven build failed with exit code ${exitCode}`);
return;
}
// 设置输出
core.setOutput('build-output', output);
core.setOutput('status', 'success');
// 创建检查运行
const token = core.getInput('github-token');
if (token) {
const octokit = github.getOctokit(token);
await octokit.rest.checks.create({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
name: 'Maven Build',
head_sha: github.context.sha,
status: 'completed',
conclusion: 'success',
output: {
title: 'Maven Build Result',
summary: 'Build completed successfully'
}
});
}
} catch (error) {
core.setFailed(error.message);
}
}
run();
五、实战应用场景
5.1 场景一:多环境部署流水线
groovy
// Jenkins多环境部署示例
pipeline {
agent any
environment {
DEV_SERVER = 'dev.example.com'
TEST_SERVER = 'test.example.com'
PROD_SERVER = 'prod.example.com'
}
parameters {
choice(
name: 'ENVIRONMENT',
choices: ['dev', 'test', 'prod'],
description: '选择部署环境'
)
booleanParam(
name: 'SKIP_TESTS',
defaultValue: false,
description: '是否跳过测试'
)
}
stages {
stage('准备') {
steps {
script {
// 根据环境设置变量
switch(params.ENVIRONMENT) {
case 'dev':
env.DEPLOY_SERVER = env.DEV_SERVER
env.KUBE_NAMESPACE = 'dev'
break
case 'test':
env.DEPLOY_SERVER = env.TEST_SERVER
env.KUBE_NAMESPACE = 'test'
break
case 'prod':
env.DEPLOY_SERVER = env.PROD_SERVER
env.KUBE_NAMESPACE = 'prod'
break
}
echo "准备部署到 ${params.ENVIRONMENT} 环境"
}
}
}
stage('构建') {
steps {
sh 'mvn clean package -DskipTests=${SKIP_TESTS}'
}
}
stage('测试') {
when {
expression { !params.SKIP_TESTS }
}
steps {
sh 'mvn test'
junit 'target/surefire-reports/*.xml'
}
}
stage('构建镜像') {
steps {
script {
def imageTag = "${params.ENVIRONMENT}-${BUILD_NUMBER}"
sh """
docker build -t myapp:${imageTag} .
docker tag myapp:${imageTag} registry.example.com/myapp:${imageTag}
docker push registry.example.com/myapp:${imageTag}
"""
}
}
}
stage('部署') {
when {
expression {
if (params.ENVIRONMENT == 'prod') {
input message: '确认部署到生产环境?', ok: '确认'
return true
}
return true
}
}
steps {
script {
def imageTag = "${params.ENVIRONMENT}-${BUILD_NUMBER}"
sh """
kubectl set image deployment/myapp \
myapp=registry.example.com/myapp:${imageTag} \
-n ${env.KUBE_NAMESPACE}
kubectl rollout status deployment/myapp -n ${env.KUBE_NAMESPACE}
"""
}
}
}
stage('验证') {
steps {
script {
def healthUrl = "https://${env.DEPLOY_SERVER}/health"
retry(3) {
sleep time: 30, unit: 'SECONDS'
sh "curl -f ${healthUrl} || exit 1"
}
}
}
}
}
post {
success {
emailext(
subject: "部署成功: ${params.ENVIRONMENT}",
body: "部署到 ${params.ENVIRONMENT} 环境成功",
to: 'team@example.com'
)
}
failure {
emailext(
subject: "部署失败: ${params.ENVIRONMENT}",
body: "部署到 ${params.ENVIRONMENT} 环境失败,请检查",
to: 'team@example.com'
)
}
}
}
5.2 场景二:蓝绿部署
yaml
# GitHub Actions - 蓝绿部署
name: Blue-Green Deployment
on:
workflow_dispatch:
inputs:
strategy:
description: 'Deployment strategy'
required: true
default: 'blue-green'
type: choice
options:
- blue-green
- canary
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Determine active deployment
id: active
run: |
ACTIVE=$(kubectl get service myapp -n prod -o jsonpath='{.spec.selector.version}')
echo "active=${ACTIVE}" >> $GITHUB_OUTPUT
if [ "$ACTIVE" == "blue" ]; then
echo "target=green" >> $GITHUB_OUTPUT
else
echo "target=blue" >> $GITHUB_OUTPUT
fi
- name: Deploy to inactive environment
run: |
TARGET=${{ steps.active.outputs.target }}
kubectl apply -f k8s/${TARGET}/
kubectl rollout status deployment/myapp-${TARGET} -n prod
- name: Run smoke tests on new deployment
run: |
TARGET=${{ steps.active.outputs.target }}
kubectl port-forward svc/myapp-${TARGET} 8080:8080 -n prod &
sleep 10
curl -f http://localhost:8080/health || exit 1
curl -f http://localhost:8080/api/status || exit 1
- name: Switch traffic to new deployment
run: |
TARGET=${{ steps.active.outputs.target }}
kubectl patch service myapp -n prod -p '{"spec":{"selector":{"version":"${TARGET}"}}}'
- name: Monitor new deployment
run: |
for i in {1..10}; do
SUCCESS_RATE=$(curl -s https://www.example.com/metrics | grep success_rate | awk '{print $2}')
if (( $(echo "$SUCCESS_RATE < 0.95" | bc -l) )); then
echo "Low success rate detected: $SUCCESS_RATE"
exit 1
fi
sleep 30
done
- name: Rollback on failure
if: failure()
run: |
ACTIVE=${{ steps.active.outputs.active }}
kubectl patch service myapp -n prod -p '{"spec":{"selector":{"version":"${ACTIVE}"}}}'
echo "Rolled back to ${ACTIVE} deployment"
5.3 场景三:自动化测试集成
groovy
// Jenkins - 完整的测试流水线
pipeline {
agent any
stages {
stage('单元测试') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target/site/jacoco',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
stage('集成测试') {
steps {
sh 'mvn verify -Pintegration-test'
}
post {
always {
junit 'target/failsafe-reports/*.xml'
}
}
}
stage('性能测试') {
when {
branch 'main'
}
steps {
script {
// 启动应用
sh 'java -jar target/myapp.jar &'
sh 'sleep 30'
// 运行JMeter测试
sh 'jmeter -n -t tests/performance.jmx -l results.jtl'
// 发布性能测试报告
perfReport 'results.jtl'
}
}
}
stage('UI测试') {
when {
branch 'main'
}
steps {
script {
sh 'mvn test -Pui-test'
}
}
post {
always {
publishHTML([
allowMissing: true,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target/cucumber-reports',
reportFiles: 'index.html',
reportName: 'Cucumber Report'
])
}
}
}
stage('API测试') {
steps {
script {
// 使用Newman运行Postman集合
sh 'newman run tests/api-collection.json -r junit'
}
}
post {
always {
junit 'newman/*.xml'
}
}
}
}
}
六、最佳实践总结
6.1 流水线设计原则
┌─────────────────────────────────────────────────────────────┐
│ 流水线设计最佳实践 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 快速反馈 │
│ - 将快速检查放在前面(代码风格、静态分析) │
│ - 使用增量测试,只运行受影响的测试 │
│ - 并行执行独立的任务 │
│ │
│ 2. 幂等性 │
│ - 流水线可以重复执行,结果一致 │
│ - 清理工作空间,避免状态残留 │
│ - 使用版本化的依赖 │
│ │
│ 3. 可追溯性 │
│ - 保存构建日志和产物 │
│ - 记录每次构建的元数据 │
│ - 与版本控制关联 │
│ │
│ 4. 安全性 │
│ - 敏感信息使用Secret管理 │
│ - 最小权限原则 │
│ - 审计所有部署操作 │
│ │
│ 5. 可维护性 │
│ - 使用共享库减少重复 │
│ - 配置即代码,版本控制 │
│ - 清晰的阶段划分 │
│ │
└─────────────────────────────────────────────────────────────┘
6.2 常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 构建时间过长 | 串行执行、依赖下载慢 | 并行化、缓存依赖、增量构建 |
| 构建不稳定 | 环境不一致、网络问题 | 使用容器、重试机制、镜像仓库 |
| 测试失败难定位 | 测试日志不够详细 | 增强日志、保留测试报告 |
| 部署失败回滚困难 | 没有回滚机制 | 实现蓝绿部署、保留历史版本 |
| 敏感信息泄露 | 硬编码密码 | 使用Secret、Vault |
6.3 CI/CD成熟度模型
yaml
Level 1: 基础自动化
├── 手动触发构建
├── 基本的编译和打包
└── 简单的单元测试
Level 2: 持续集成
├── 自动触发构建
├── 完整的测试套件
├── 代码质量检查
└── 构建产物管理
Level 3: 持续交付
├── 自动化部署
├── 多环境管理
├── 自动化验收测试
└── 部署审批流程
Level 4: 持续部署
├── 生产环境自动部署
├── 蓝绿/金丝雀发布
├── 自动化回滚
└── 全链路监控
Level 5: 智能运维
├── AI辅助决策
├── 自适应测试
├── 混沌工程
└── 站点可靠性工程
八、思考与练习
思考题
- 基础题:持续集成的核心价值是什么?为什么说"每次提交都应该触发构建"?
- 进阶题:Jenkins的Declarative Pipeline和Scripted Pipeline有什么区别?各自适合什么场景?
- 实战题:蓝绿部署和金丝雀发布的区别是什么?如何选择合适的部署策略?
编程练习
练习:为一个Spring Boot项目设计完整的CI/CD流水线,包括:代码检出、编译构建、单元测试、集成测试、代码质量检查、Docker镜像构建、多环境部署(dev/test/prod),并实现生产环境的人工审批机制。
章节关联
- 前置章节:代码审查实践
- 后续章节:微服务架构设计
- 扩展阅读:《持续交付》、Jenkins官方文档、GitHub Actions官方文档
📝 下一章预告
至此,工程化实践系列文章全部完成。在后续的系列中,我们将深入探讨微服务架构、分布式系统设计等高级主题,帮助你在技术进阶之路上继续前行。
本章完 规模、项目特点选择合适的CI工具 2. 流水线设计 :遵循快速反馈、幂等性、可追溯性原则 3. 质量保障 :集成测试、代码检查、安全扫描 4. 部署策略 :蓝绿部署、金丝雀发布、自动化回滚 5. 持续改进:监控指标、优化构建时间、完善测试覆盖
通过本文的学习,读者应该能够:
- 理解持续集成的核心概念和价值
- 掌握Jenkins Pipeline、GitLab CI、GitHub Actions的使用
- 设计和实现完整的CI/CD流水线
- 应用最佳实践解决实际问题