10.1 共享库概述

什么是共享库

共享库定义:

Jenkins共享库(Shared Libraries)是一种将Pipeline代码模块化和重用的机制。
它允许团队创建可重用的Pipeline代码,并在多个项目中共享。

核心特性:
- 代码重用:避免重复编写相同的Pipeline逻辑
- 标准化:确保所有项目使用一致的构建流程
- 维护性:集中管理和更新Pipeline代码
- 版本控制:支持库的版本管理和回滚
- 安全性:提供受信任的代码执行环境

共享库优势:

传统Pipeline vs 共享库:

传统Pipeline:
- 每个项目重复编写相似代码
- 难以维护和更新
- 不一致的实现方式
- 缺乏标准化流程

共享库:
- 代码重用和标准化
- 集中维护和更新
- 版本化管理
- 更好的测试覆盖
- 团队协作和知识共享

使用场景:

适用场景:

1. 多项目环境
   - 多个项目使用相似的构建流程
   - 需要标准化CI/CD流程
   - 团队协作开发

2. 复杂Pipeline逻辑
   - 复杂的部署策略
   - 多环境管理
   - 高级错误处理

3. 企业级应用
   - 合规性要求
   - 安全策略实施
   - 审计和监控

4. DevOps最佳实践
   - 基础设施即代码
   - 持续改进
   - 知识管理

共享库架构

目录结构:

shared-library/
├── src/                    # Groovy源代码
│   └── com/
│       └── company/
│           └── jenkins/
│               ├── Build.groovy
│               ├── Deploy.groovy
│               └── Utils.groovy
├── vars/                   # 全局变量和函数
│   ├── buildApp.groovy
│   ├── deployApp.groovy
│   ├── notifyTeam.groovy
│   └── runTests.groovy
├── resources/              # 资源文件
│   ├── scripts/
│   │   ├── deploy.sh
│   │   └── test.sh
│   ├── templates/
│   │   ├── Dockerfile.template
│   │   └── k8s-deployment.yaml
│   └── config/
│       ├── sonar.properties
│       └── checkstyle.xml
└── test/                   # 测试代码
    ├── groovy/
    │   └── com/
    │       └── company/
    │           └── jenkins/
    │               ├── BuildTest.groovy
    │               └── DeployTest.groovy
    └── resources/
        └── test-data/

组件说明:

核心组件:

1. src/ 目录
   - 包含Groovy类和方法
   - 支持面向对象编程
   - 可以导入和使用Java库
   - 适合复杂的业务逻辑

2. vars/ 目录
   - 包含全局变量和函数
   - 可以直接在Pipeline中调用
   - 支持参数化调用
   - 适合简单的工具函数

3. resources/ 目录
   - 存储静态资源文件
   - 脚本、模板、配置文件
   - 可以在Pipeline中读取
   - 支持文件模板化

4. test/ 目录
   - 单元测试和集成测试
   - 确保库的质量和稳定性
   - 支持TDD开发模式
   - 自动化测试执行

10.2 创建共享库

基础设置

创建库仓库:

# 1. 创建Git仓库
mkdir jenkins-shared-library
cd jenkins-shared-library
git init

# 2. 创建基本目录结构
mkdir -p src/com/company/jenkins
mkdir -p vars
mkdir -p resources/{scripts,templates,config}
mkdir -p test/groovy/com/company/jenkins

# 3. 创建README文件
cat > README.md << 'EOF'
# Jenkins Shared Library

这是公司的Jenkins共享库,包含标准化的CI/CD流程和工具函数。

## 使用方法

```groovy
@Library('company-shared-library') _

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                buildApp()
            }
        }
    }
}

函数列表

  • buildApp(): 构建应用
  • deployApp(): 部署应用
  • runTests(): 运行测试
  • notifyTeam(): 发送通知

EOF

4. 提交初始代码

git add . git commit -m “Initial shared library structure” git remote add origin https://github.com/company/jenkins-shared-library.git git push -u origin main


**Jenkins配置:**
```groovy
// 在Jenkins中配置共享库
// 管理Jenkins -> 系统配置 -> Global Pipeline Libraries

// 库配置示例:
Name: company-shared-library
Default version: main
Retrieval method: Modern SCM
Source Code Management: Git
Repository URL: https://github.com/company/jenkins-shared-library.git
Credentials: github-credentials
Behaviors:
  - Discover branches
  - Discover pull requests from origin
  - Discover pull requests from forks

vars目录函数

buildApp.groovy:

#!/usr/bin/env groovy

/**
 * 构建应用程序
 * @param config 构建配置
 */
def call(Map config = [:]) {
    // 默认配置
    def defaultConfig = [
        buildTool: 'maven',
        javaVersion: '11',
        skipTests: false,
        profile: 'default'
    ]
    
    // 合并配置
    config = defaultConfig + config
    
    echo "开始构建应用,配置: ${config}"
    
    // 设置Java环境
    def javaHome = tool name: "JDK-${config.javaVersion}", type: 'jdk'
    env.JAVA_HOME = javaHome
    env.PATH = "${javaHome}/bin:${env.PATH}"
    
    // 根据构建工具执行构建
    switch (config.buildTool.toLowerCase()) {
        case 'maven':
            buildWithMaven(config)
            break
        case 'gradle':
            buildWithGradle(config)
            break
        case 'npm':
            buildWithNpm(config)
            break
        default:
            error "不支持的构建工具: ${config.buildTool}"
    }
    
    echo '应用构建完成'
}

/**
 * Maven构建
 */
private def buildWithMaven(config) {
    def mvnHome = tool name: 'Maven-3.8.1', type: 'maven'
    def mvnCmd = "${mvnHome}/bin/mvn"
    
    // 构建命令
    def goals = ['clean', 'compile']
    if (!config.skipTests) {
        goals.add('test')
    }
    goals.add('package')
    
    def profiles = config.profile != 'default' ? "-P${config.profile}" : ''
    def command = "${mvnCmd} ${goals.join(' ')} ${profiles}"
    
    echo "执行Maven命令: ${command}"
    sh command
    
    // 发布测试结果
    if (!config.skipTests) {
        publishTestResults testResultsPattern: '**/target/surefire-reports/*.xml'
    }
    
    // 归档构建产物
    archiveArtifacts artifacts: '**/target/*.jar,**/target/*.war', fingerprint: true
}

/**
 * Gradle构建
 */
private def buildWithGradle(config) {
    def gradleHome = tool name: 'Gradle-7.0', type: 'gradle'
    def gradleCmd = "${gradleHome}/bin/gradle"
    
    def tasks = ['clean', 'build']
    if (config.skipTests) {
        tasks.add('-x test')
    }
    
    def command = "${gradleCmd} ${tasks.join(' ')}"
    
    echo "执行Gradle命令: ${command}"
    sh command
    
    // 发布测试结果
    if (!config.skipTests) {
        publishTestResults testResultsPattern: '**/build/test-results/test/*.xml'
    }
    
    // 归档构建产物
    archiveArtifacts artifacts: '**/build/libs/*.jar', fingerprint: true
}

/**
 * NPM构建
 */
private def buildWithNpm(config) {
    def nodeHome = tool name: 'NodeJS-16', type: 'nodejs'
    env.PATH = "${nodeHome}/bin:${env.PATH}"
    
    echo '安装依赖'
    sh 'npm ci'
    
    if (!config.skipTests) {
        echo '运行测试'
        sh 'npm test'
        
        // 发布测试结果
        publishTestResults testResultsPattern: 'test-results.xml'
    }
    
    echo '构建应用'
    sh 'npm run build'
    
    // 归档构建产物
    archiveArtifacts artifacts: 'dist/**', fingerprint: true
}

deployApp.groovy:

#!/usr/bin/env groovy

/**
 * 部署应用程序
 * @param config 部署配置
 */
def call(Map config = [:]) {
    // 默认配置
    def defaultConfig = [
        environment: 'staging',
        strategy: 'rolling',
        replicas: 3,
        healthCheck: true,
        rollbackOnFailure: true,
        timeout: 600
    ]
    
    // 合并配置
    config = defaultConfig + config
    
    echo "开始部署应用到 ${config.environment} 环境"
    echo "部署配置: ${config}"
    
    // 验证配置
    validateDeployConfig(config)
    
    try {
        // 根据环境选择部署策略
        switch (config.environment.toLowerCase()) {
            case 'development':
            case 'dev':
                deployToDev(config)
                break
            case 'staging':
            case 'test':
                deployToStaging(config)
                break
            case 'production':
            case 'prod':
                deployToProduction(config)
                break
            default:
                error "不支持的部署环境: ${config.environment}"
        }
        
        // 健康检查
        if (config.healthCheck) {
            performHealthCheck(config)
        }
        
        echo "应用成功部署到 ${config.environment} 环境"
        
    } catch (Exception e) {
        echo "部署失败: ${e.getMessage()}"
        
        // 自动回滚
        if (config.rollbackOnFailure) {
            echo '执行自动回滚'
            rollbackDeployment(config)
        }
        
        throw e
    }
}

/**
 * 验证部署配置
 */
private def validateDeployConfig(config) {
    def requiredFields = ['environment']
    
    requiredFields.each { field ->
        if (!config.containsKey(field) || !config[field]) {
            error "缺少必需的配置字段: ${field}"
        }
    }
    
    // 验证副本数
    if (config.replicas < 1) {
        error "副本数必须大于0: ${config.replicas}"
    }
    
    // 验证超时时间
    if (config.timeout < 60) {
        error "超时时间必须至少60秒: ${config.timeout}"
    }
}

/**
 * 部署到开发环境
 */
private def deployToDev(config) {
    echo '部署到开发环境'
    
    // 简单的Docker部署
    sh """
        docker stop myapp-dev || true
        docker rm myapp-dev || true
        docker run -d --name myapp-dev -p 8080:8080 \
            myapp:${env.BUILD_NUMBER}
    """
}

/**
 * 部署到测试环境
 */
private def deployToStaging(config) {
    echo '部署到测试环境'
    
    // Kubernetes部署
    def deploymentYaml = libraryResource('templates/k8s-deployment.yaml')
    
    // 替换模板变量
    deploymentYaml = deploymentYaml
        .replace('{{APP_NAME}}', 'myapp')
        .replace('{{IMAGE_TAG}}', env.BUILD_NUMBER)
        .replace('{{ENVIRONMENT}}', 'staging')
        .replace('{{REPLICAS}}', config.replicas.toString())
    
    // 写入临时文件
    writeFile file: 'deployment.yaml', text: deploymentYaml
    
    // 应用部署
    sh 'kubectl apply -f deployment.yaml'
    
    // 等待部署完成
    sh "kubectl rollout status deployment/myapp-staging --timeout=${config.timeout}s"
}

/**
 * 部署到生产环境
 */
private def deployToProduction(config) {
    echo '部署到生产环境'
    
    // 生产环境需要额外的安全检查
    if (!env.BRANCH_NAME?.equals('main')) {
        error '只能从main分支部署到生产环境'
    }
    
    // 蓝绿部署或滚动更新
    switch (config.strategy.toLowerCase()) {
        case 'blue-green':
            deployBlueGreen(config)
            break
        case 'rolling':
            deployRolling(config)
            break
        case 'canary':
            deployCanary(config)
            break
        default:
            error "不支持的部署策略: ${config.strategy}"
    }
}

/**
 * 蓝绿部署
 */
private def deployBlueGreen(config) {
    echo '执行蓝绿部署'
    
    // 获取当前活跃环境
    def currentEnv = sh(
        script: 'kubectl get service myapp-prod -o jsonpath="{.spec.selector.version}"',
        returnStdout: true
    ).trim()
    
    def newEnv = currentEnv == 'blue' ? 'green' : 'blue'
    
    echo "当前环境: ${currentEnv}, 新环境: ${newEnv}"
    
    // 部署到新环境
    sh """
        kubectl set image deployment/myapp-prod-${newEnv} \
            myapp=myapp:${env.BUILD_NUMBER}
        kubectl rollout status deployment/myapp-prod-${newEnv} --timeout=${config.timeout}s
    """
    
    // 切换流量
    sh "kubectl patch service myapp-prod -p '{\"spec\":{\"selector\":{\"version\":\"${newEnv}\"}}}'"
    
    echo "流量已切换到 ${newEnv} 环境"
}

/**
 * 滚动更新
 */
private def deployRolling(config) {
    echo '执行滚动更新'
    
    sh """
        kubectl set image deployment/myapp-prod \
            myapp=myapp:${env.BUILD_NUMBER}
        kubectl rollout status deployment/myapp-prod --timeout=${config.timeout}s
    """
}

/**
 * 金丝雀部署
 */
private def deployCanary(config) {
    echo '执行金丝雀部署'
    
    // 部署金丝雀版本(10%流量)
    sh """
        kubectl set image deployment/myapp-canary \
            myapp=myapp:${env.BUILD_NUMBER}
        kubectl rollout status deployment/myapp-canary --timeout=${config.timeout}s
    """
    
    // 等待监控数据
    echo '等待金丝雀版本监控数据...'
    sleep(time: 300, unit: 'SECONDS')
    
    // 检查金丝雀版本健康状态
    def canaryHealthy = checkCanaryHealth()
    
    if (canaryHealthy) {
        echo '金丝雀版本健康,执行全量部署'
        deployRolling(config)
    } else {
        error '金丝雀版本不健康,停止部署'
    }
}

/**
 * 健康检查
 */
private def performHealthCheck(config) {
    echo '执行健康检查'
    
    def maxRetries = 10
    def retryInterval = 30
    
    for (int i = 0; i < maxRetries; i++) {
        try {
            def healthUrl = getHealthCheckUrl(config.environment)
            def response = sh(
                script: "curl -f ${healthUrl}",
                returnStatus: true
            )
            
            if (response == 0) {
                echo '健康检查通过'
                return
            }
            
        } catch (Exception e) {
            echo "健康检查失败 (${i + 1}/${maxRetries}): ${e.getMessage()}"
        }
        
        if (i < maxRetries - 1) {
            echo "等待 ${retryInterval} 秒后重试"
            sleep(time: retryInterval, unit: 'SECONDS')
        }
    }
    
    error '健康检查失败,应用可能未正常启动'
}

/**
 * 获取健康检查URL
 */
private def getHealthCheckUrl(environment) {
    def urls = [
        'development': 'http://localhost:8080/health',
        'staging': 'http://staging.company.com/health',
        'production': 'http://api.company.com/health'
    ]
    
    return urls[environment.toLowerCase()] ?: 'http://localhost:8080/health'
}

/**
 * 检查金丝雀版本健康状态
 */
private def checkCanaryHealth() {
    // 这里可以集成监控系统API
    // 检查错误率、响应时间等指标
    
    echo '检查金丝雀版本指标'
    
    // 模拟检查逻辑
    def errorRate = sh(
        script: 'curl -s http://monitoring.company.com/api/error-rate/canary',
        returnStdout: true
    ).trim().toDouble()
    
    def responseTime = sh(
        script: 'curl -s http://monitoring.company.com/api/response-time/canary',
        returnStdout: true
    ).trim().toDouble()
    
    echo "金丝雀版本错误率: ${errorRate}%"
    echo "金丝雀版本响应时间: ${responseTime}ms"
    
    // 健康标准
    return errorRate < 1.0 && responseTime < 500
}

/**
 * 回滚部署
 */
private def rollbackDeployment(config) {
    echo "回滚 ${config.environment} 环境的部署"
    
    switch (config.environment.toLowerCase()) {
        case 'development':
        case 'dev':
            sh 'docker stop myapp-dev && docker start myapp-dev-backup'
            break
        case 'staging':
        case 'test':
            sh 'kubectl rollout undo deployment/myapp-staging'
            break
        case 'production':
        case 'prod':
            sh 'kubectl rollout undo deployment/myapp-prod'
            break
    }
    
    echo '回滚完成'
}

runTests.groovy:

#!/usr/bin/env groovy

/**
 * 运行测试套件
 * @param config 测试配置
 */
def call(Map config = [:]) {
    // 默认配置
    def defaultConfig = [
        types: ['unit', 'integration'],
        parallel: true,
        coverage: true,
        quality: true,
        security: false,
        performance: false,
        failFast: false
    ]
    
    // 合并配置
    config = defaultConfig + config
    
    echo "开始运行测试,配置: ${config}"
    
    try {
        if (config.parallel) {
            runTestsParallel(config)
        } else {
            runTestsSequential(config)
        }
        
        // 生成测试报告
        generateTestReports(config)
        
        echo '所有测试完成'
        
    } catch (Exception e) {
        echo "测试失败: ${e.getMessage()}"
        
        if (config.failFast) {
            throw e
        } else {
            currentBuild.result = 'UNSTABLE'
        }
    }
}

/**
 * 并行运行测试
 */
private def runTestsParallel(config) {
    def parallelTests = [:]
    
    config.types.each { testType ->
        parallelTests["${testType} Tests"] = {
            runTestType(testType, config)
        }
    }
    
    // 添加其他测试类型
    if (config.coverage) {
        parallelTests['Coverage Analysis'] = {
            runCoverageAnalysis(config)
        }
    }
    
    if (config.quality) {
        parallelTests['Code Quality'] = {
            runQualityAnalysis(config)
        }
    }
    
    if (config.security) {
        parallelTests['Security Scan'] = {
            runSecurityScan(config)
        }
    }
    
    if (config.performance) {
        parallelTests['Performance Test'] = {
            runPerformanceTest(config)
        }
    }
    
    parallel parallelTests
}

/**
 * 顺序运行测试
 */
private def runTestsSequential(config) {
    config.types.each { testType ->
        runTestType(testType, config)
    }
    
    if (config.coverage) {
        runCoverageAnalysis(config)
    }
    
    if (config.quality) {
        runQualityAnalysis(config)
    }
    
    if (config.security) {
        runSecurityScan(config)
    }
    
    if (config.performance) {
        runPerformanceTest(config)
    }
}

/**
 * 运行特定类型的测试
 */
private def runTestType(testType, config) {
    echo "运行 ${testType} 测试"
    
    switch (testType.toLowerCase()) {
        case 'unit':
            runUnitTests(config)
            break
        case 'integration':
            runIntegrationTests(config)
            break
        case 'e2e':
        case 'end-to-end':
            runE2ETests(config)
            break
        case 'api':
            runApiTests(config)
            break
        case 'ui':
            runUITests(config)
            break
        default:
            echo "未知测试类型: ${testType}"
    }
}

/**
 * 运行单元测试
 */
private def runUnitTests(config) {
    echo '执行单元测试'
    
    // 根据项目类型选择测试命令
    if (fileExists('pom.xml')) {
        sh 'mvn test'
        publishTestResults testResultsPattern: '**/target/surefire-reports/*.xml'
    } else if (fileExists('build.gradle')) {
        sh 'gradle test'
        publishTestResults testResultsPattern: '**/build/test-results/test/*.xml'
    } else if (fileExists('package.json')) {
        sh 'npm test'
        publishTestResults testResultsPattern: 'test-results.xml'
    } else {
        echo '未找到支持的构建文件,跳过单元测试'
    }
}

/**
 * 运行集成测试
 */
private def runIntegrationTests(config) {
    echo '执行集成测试'
    
    // 启动测试数据库
    sh 'docker run -d --name test-db -e POSTGRES_DB=testdb -e POSTGRES_PASSWORD=test postgres:13'
    
    try {
        // 等待数据库启动
        sleep(time: 30, unit: 'SECONDS')
        
        if (fileExists('pom.xml')) {
            sh 'mvn verify -P integration-tests'
            publishTestResults testResultsPattern: '**/target/failsafe-reports/*.xml'
        } else if (fileExists('build.gradle')) {
            sh 'gradle integrationTest'
            publishTestResults testResultsPattern: '**/build/test-results/integrationTest/*.xml'
        } else {
            echo '未找到支持的构建文件,跳过集成测试'
        }
        
    } finally {
        // 清理测试数据库
        sh 'docker stop test-db && docker rm test-db'
    }
}

/**
 * 运行端到端测试
 */
private def runE2ETests(config) {
    echo '执行端到端测试'
    
    // 启动应用
    sh 'docker run -d --name app-under-test -p 8080:8080 myapp:latest'
    
    try {
        // 等待应用启动
        sleep(time: 60, unit: 'SECONDS')
        
        // 运行E2E测试
        if (fileExists('cypress.json')) {
            sh 'npx cypress run --record --key $CYPRESS_RECORD_KEY'
        } else if (fileExists('selenium-tests/')) {
            sh 'mvn test -P e2e-tests'
        } else {
            echo '未找到E2E测试配置,跳过端到端测试'
        }
        
    } finally {
        // 清理应用
        sh 'docker stop app-under-test && docker rm app-under-test'
    }
}

/**
 * 运行API测试
 */
private def runApiTests(config) {
    echo '执行API测试'
    
    if (fileExists('postman/')) {
        sh 'newman run postman/collection.json -e postman/environment.json --reporters cli,junit'
        publishTestResults testResultsPattern: 'newman-results.xml'
    } else if (fileExists('rest-assured-tests/')) {
        sh 'mvn test -P api-tests'
        publishTestResults testResultsPattern: '**/target/surefire-reports/*.xml'
    } else {
        echo '未找到API测试配置,跳过API测试'
    }
}

/**
 * 运行UI测试
 */
private def runUITests(config) {
    echo '执行UI测试'
    
    if (fileExists('selenium-tests/')) {
        sh 'mvn test -P ui-tests'
        publishTestResults testResultsPattern: '**/target/surefire-reports/*.xml'
    } else if (fileExists('playwright.config.js')) {
        sh 'npx playwright test'
        publishTestResults testResultsPattern: 'test-results.xml'
    } else {
        echo '未找到UI测试配置,跳过UI测试'
    }
}

/**
 * 运行覆盖率分析
 */
private def runCoverageAnalysis(config) {
    echo '执行代码覆盖率分析'
    
    if (fileExists('pom.xml')) {
        sh 'mvn jacoco:report'
        publishHTML([
            allowMissing: false,
            alwaysLinkToLastBuild: true,
            keepAll: true,
            reportDir: 'target/site/jacoco',
            reportFiles: 'index.html',
            reportName: 'Coverage Report'
        ])
    } else if (fileExists('package.json')) {
        sh 'npm run coverage'
        publishHTML([
            allowMissing: false,
            alwaysLinkToLastBuild: true,
            keepAll: true,
            reportDir: 'coverage',
            reportFiles: 'index.html',
            reportName: 'Coverage Report'
        ])
    }
}

/**
 * 运行代码质量分析
 */
private def runQualityAnalysis(config) {
    echo '执行代码质量分析'
    
    if (fileExists('pom.xml')) {
        sh 'mvn sonar:sonar'
    } else if (fileExists('sonar-project.properties')) {
        sh 'sonar-scanner'
    } else {
        echo '未找到SonarQube配置,跳过代码质量分析'
    }
}

/**
 * 运行安全扫描
 */
private def runSecurityScan(config) {
    echo '执行安全扫描'
    
    // 依赖安全扫描
    if (fileExists('pom.xml')) {
        sh 'mvn dependency-check:check'
        publishHTML([
            allowMissing: false,
            alwaysLinkToLastBuild: true,
            keepAll: true,
            reportDir: 'target/dependency-check-report',
            reportFiles: 'dependency-check-report.html',
            reportName: 'Security Report'
        ])
    } else if (fileExists('package.json')) {
        sh 'npm audit --audit-level moderate'
    }
    
    // 代码安全扫描
    if (fileExists('.bandit')) {
        sh 'bandit -r . -f json -o bandit-report.json'
    }
}

/**
 * 运行性能测试
 */
private def runPerformanceTest(config) {
    echo '执行性能测试'
    
    if (fileExists('jmeter-tests/')) {
        sh 'jmeter -n -t jmeter-tests/test-plan.jmx -l results.jtl'
        perfReport sourceDataFiles: 'results.jtl'
    } else if (fileExists('k6-tests/')) {
        sh 'k6 run k6-tests/load-test.js'
    } else {
        echo '未找到性能测试配置,跳过性能测试'
    }
}

/**
 * 生成测试报告
 */
private def generateTestReports(config) {
    echo '生成测试报告'
    
    // 聚合测试结果
    def testResults = [
        total: 0,
        passed: 0,
        failed: 0,
        skipped: 0
    ]
    
    // 这里可以解析各种测试结果文件
    // 生成统一的测试报告
    
    echo "测试结果汇总: ${testResults}"
    
    // 发送测试报告
    if (config.notify) {
        notifyTestResults(testResults)
    }
}

/**
 * 发送测试结果通知
 */
private def notifyTestResults(results) {
    def message = """
测试执行完成
总计: ${results.total}
通过: ${results.passed}
失败: ${results.failed}
跳过: ${results.skipped}
"""
    
    slackSend(
        channel: '#ci-cd',
        color: results.failed > 0 ? 'warning' : 'good',
        message: message
    )
}

notifyTeam.groovy:

#!/usr/bin/env groovy

/**
 * 发送团队通知
 * @param config 通知配置
 */
def call(Map config = [:]) {
    // 默认配置
    def defaultConfig = [
        channels: ['slack'],
        status: currentBuild.currentResult ?: 'SUCCESS',
        includeChanges: true,
        includeTests: true,
        includeCoverage: false
    ]
    
    // 合并配置
    config = defaultConfig + config
    
    echo "发送团队通知,配置: ${config}"
    
    // 构建通知消息
    def message = buildNotificationMessage(config)
    
    // 发送到各个渠道
    config.channels.each { channel ->
        sendToChannel(channel, message, config)
    }
}

/**
 * 构建通知消息
 */
private def buildNotificationMessage(config) {
    def status = config.status
    def emoji = getStatusEmoji(status)
    def color = getStatusColor(status)
    
    def message = [
        title: "${emoji} 构建 ${status}",
        fields: []
    ]
    
    // 基本信息
    message.fields.add([
        title: '项目',
        value: env.JOB_NAME,
        short: true
    ])
    
    message.fields.add([
        title: '构建号',
        value: "#${env.BUILD_NUMBER}",
        short: true
    ])
    
    message.fields.add([
        title: '分支',
        value: env.BRANCH_NAME ?: 'unknown',
        short: true
    ])
    
    message.fields.add([
        title: '持续时间',
        value: currentBuild.durationString,
        short: true
    ])
    
    // 变更信息
    if (config.includeChanges) {
        def changes = getChangeInfo()
        if (changes) {
            message.fields.add([
                title: '变更',
                value: changes,
                short: false
            ])
        }
    }
    
    // 测试信息
    if (config.includeTests) {
        def testInfo = getTestInfo()
        if (testInfo) {
            message.fields.add([
                title: '测试结果',
                value: testInfo,
                short: false
            ])
        }
    }
    
    // 覆盖率信息
    if (config.includeCoverage) {
        def coverageInfo = getCoverageInfo()
        if (coverageInfo) {
            message.fields.add([
                title: '代码覆盖率',
                value: coverageInfo,
                short: true
            ])
        }
    }
    
    // 构建链接
    message.fields.add([
        title: '构建链接',
        value: "<${env.BUILD_URL}|查看详情>",
        short: false
    ])
    
    message.color = color
    return message
}

/**
 * 获取状态表情符号
 */
private def getStatusEmoji(status) {
    def emojis = [
        'SUCCESS': '✅',
        'FAILURE': '❌',
        'UNSTABLE': '⚠️',
        'ABORTED': '⏹️',
        'NOT_BUILT': '⚪'
    ]
    
    return emojis[status] ?: '❓'
}

/**
 * 获取状态颜色
 */
private def getStatusColor(status) {
    def colors = [
        'SUCCESS': 'good',
        'FAILURE': 'danger',
        'UNSTABLE': 'warning',
        'ABORTED': '#808080',
        'NOT_BUILT': '#808080'
    ]
    
    return colors[status] ?: '#808080'
}

/**
 * 获取变更信息
 */
private def getChangeInfo() {
    def changes = currentBuild.changeSets
    if (!changes || changes.isEmpty()) {
        return null
    }
    
    def changeList = []
    changes.each { changeSet ->
        changeSet.each { change ->
            def author = change.author.displayName
            def message = change.msg.take(50)
            changeList.add("• ${author}: ${message}")
        }
    }
    
    return changeList.take(5).join('\n')
}

/**
 * 获取测试信息
 */
private def getTestInfo() {
    try {
        def testResult = currentBuild.rawBuild.getAction(hudson.tasks.test.AbstractTestResultAction.class)
        if (testResult) {
            def total = testResult.totalCount
            def failed = testResult.failCount
            def skipped = testResult.skipCount
            def passed = total - failed - skipped
            
            return "总计: ${total}, 通过: ${passed}, 失败: ${failed}, 跳过: ${skipped}"
        }
    } catch (Exception e) {
        echo "获取测试信息失败: ${e.getMessage()}"
    }
    
    return null
}

/**
 * 获取覆盖率信息
 */
private def getCoverageInfo() {
    try {
        // 这里可以集成JaCoCo或其他覆盖率工具的API
        // 返回覆盖率百分比
        return "85%"
    } catch (Exception e) {
        echo "获取覆盖率信息失败: ${e.getMessage()}"
    }
    
    return null
}

/**
 * 发送到指定渠道
 */
private def sendToChannel(channel, message, config) {
    switch (channel.toLowerCase()) {
        case 'slack':
            sendToSlack(message, config)
            break
        case 'email':
            sendToEmail(message, config)
            break
        case 'teams':
            sendToTeams(message, config)
            break
        case 'webhook':
            sendToWebhook(message, config)
            break
        default:
            echo "不支持的通知渠道: ${channel}"
    }
}

/**
 * 发送到Slack
 */
private def sendToSlack(message, config) {
    def slackChannel = config.slackChannel ?: '#ci-cd'
    
    slackSend(
        channel: slackChannel,
        color: message.color,
        message: formatSlackMessage(message)
    )
}

/**
 * 格式化Slack消息
 */
private def formatSlackMessage(message) {
    def text = message.title + '\n'
    
    message.fields.each { field ->
        text += "*${field.title}:* ${field.value}\n"
    }
    
    return text
}

/**
 * 发送邮件
 */
private def sendToEmail(message, config) {
    def recipients = config.emailRecipients ?: 'team@company.com'
    def subject = "${message.title} - ${env.JOB_NAME} #${env.BUILD_NUMBER}"
    
    def body = formatEmailMessage(message)
    
    emailext(
        subject: subject,
        body: body,
        to: recipients,
        mimeType: 'text/html'
    )
}

/**
 * 格式化邮件消息
 */
private def formatEmailMessage(message) {
    def html = """
<html>
<body>
<h2>${message.title}</h2>
<table border="1" cellpadding="5" cellspacing="0">
"""
    
    message.fields.each { field ->
        html += "<tr><td><strong>${field.title}</strong></td><td>${field.value}</td></tr>"
    }
    
    html += """
</table>
</body>
</html>
"""
    
    return html
}

/**
 * 发送到Teams
 */
private def sendToTeams(message, config) {
    def webhookUrl = config.teamsWebhook
    if (!webhookUrl) {
        echo 'Teams webhook URL未配置'
        return
    }
    
    def payload = [
        '@type': 'MessageCard',
        '@context': 'http://schema.org/extensions',
        'themeColor': message.color == 'good' ? '00FF00' : (message.color == 'danger' ? 'FF0000' : 'FFFF00'),
        'summary': message.title,
        'sections': [[
            'activityTitle': message.title,
            'facts': message.fields.collect { field ->
                ['name': field.title, 'value': field.value]
            }
        ]]
    ]
    
    httpRequest(
        httpMode: 'POST',
        url: webhookUrl,
        contentType: 'APPLICATION_JSON',
        requestBody: groovy.json.JsonBuilder(payload).toString()
    )
}

/**
 * 发送到Webhook
 */
private def sendToWebhook(message, config) {
    def webhookUrl = config.webhookUrl
    if (!webhookUrl) {
        echo 'Webhook URL未配置'
        return
    }
    
    def payload = [
        'build': [
            'number': env.BUILD_NUMBER,
            'status': config.status,
            'url': env.BUILD_URL,
            'project': env.JOB_NAME,
            'branch': env.BRANCH_NAME
        ],
        'message': message
    ]
    
    httpRequest(
        httpMode: 'POST',
        url: webhookUrl,
        contentType: 'APPLICATION_JSON',
        requestBody: groovy.json.JsonBuilder(payload).toString()
    )
}

本章小结

本章介绍了Jenkins共享库的基础知识,包括:

  1. 共享库概述:了解共享库的概念、优势和架构
  2. 创建共享库:学习如何创建和配置共享库
  3. vars目录函数:掌握全局函数的编写和使用

共享库是实现Pipeline代码重用和标准化的重要工具,能够显著提高团队的开发效率和代码质量。

下一章预告

下一章我们将继续学习共享库的高级特性,包括src目录类、资源文件使用和库的测试与发布。

练习与思考

理论练习

  1. 共享库设计

    • 分析团队的Pipeline需求
    • 设计共享库的结构和功能
    • 考虑版本管理策略
  2. 函数抽象

    • 识别可重用的Pipeline逻辑
    • 设计通用的函数接口
    • 考虑配置的灵活性

实践练习

  1. 创建基础共享库

    • 搭建共享库项目结构
    • 实现基本的构建和部署函数
    • 配置Jenkins使用共享库
  2. 函数开发

    • 开发通知函数
    • 实现测试执行函数
    • 添加错误处理和重试逻辑

思考题

  1. 最佳实践

    • 如何设计易于维护的共享库?
    • 如何平衡通用性和特定需求?
    • 如何确保共享库的向后兼容性?
  2. 团队协作

    • 如何管理共享库的开发和发布?
    • 如何处理不同团队的需求差异?
    • 如何推广共享库的使用?