68-持续集成详解

持续集成详解

一、知识概述

持续集成(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辅助决策
├── 自适应测试
├── 混沌工程
└── 站点可靠性工程

八、思考与练习

思考题

  1. 基础题:持续集成的核心价值是什么?为什么说"每次提交都应该触发构建"?
  2. 进阶题:Jenkins的Declarative Pipeline和Scripted Pipeline有什么区别?各自适合什么场景?
  3. 实战题:蓝绿部署和金丝雀发布的区别是什么?如何选择合适的部署策略?

编程练习

练习:为一个Spring Boot项目设计完整的CI/CD流水线,包括:代码检出、编译构建、单元测试、集成测试、代码质量检查、Docker镜像构建、多环境部署(dev/test/prod),并实现生产环境的人工审批机制。

章节关联

  • 前置章节:代码审查实践
  • 后续章节:微服务架构设计
  • 扩展阅读:《持续交付》、Jenkins官方文档、GitHub Actions官方文档

📝 下一章预告

至此,工程化实践系列文章全部完成。在后续的系列中,我们将深入探讨微服务架构、分布式系统设计等高级主题,帮助你在技术进阶之路上继续前行。


本章完 规模、项目特点选择合适的CI工具 2. 流水线设计 :遵循快速反馈、幂等性、可追溯性原则 3. 质量保障 :集成测试、代码检查、安全扫描 4. 部署策略 :蓝绿部署、金丝雀发布、自动化回滚 5. 持续改进:监控指标、优化构建时间、完善测试覆盖

通过本文的学习,读者应该能够:

  • 理解持续集成的核心概念和价值
  • 掌握Jenkins Pipeline、GitLab CI、GitHub Actions的使用
  • 设计和实现完整的CI/CD流水线
  • 应用最佳实践解决实际问题
相关推荐
foggyprojects1 小时前
列表里要带子表统计值时,为什么需要 QM 聚合型 JOIN
后端
用户925807911481 小时前
redission原理
java·后端
小旭95271 小时前
Spring Cloud 集成分布式日志 ELK+Swagger 接口文档实战
java·分布式·后端·elk·spring cloud
属鼠哥1 小时前
HDFS 短路读取:mmap 与 Unix Domain Socket 铸就的零拷贝艺术
后端
好好风格1 小时前
Scrapling:现代 Web 抓取,正在从“写选择器”走向“自适应”
linux·后端
屋外雨大,惊蛰出没1 小时前
spring boot+mybatis开发基础复习
java·spring boot·后端
这个DBA有点耶1 小时前
死锁排查进阶:从日志到根因的完整分析链
java·开发语言·数据库·sql·运维开发·学习方法·dba
叫我少年1 小时前
C# 文件级 using(global using)
后端
郝学胜_神的一滴1 小时前
系统设计 014:缓存深度实战:如何用 Cache 优雅优化数据库读写?
前端·后端·面试