现学现用之Jenkins--从泛泛了解到实操

402 阅读10分钟

前言

最近一周,我的工作任务都围绕着Jenkins展开,从泛泛了解到实际应用,中间的沟需要填平,坎需要铲平。这中间踩了一些坑,但也积累了一些实践经验。完成了从门外汉到入门的进阶,感觉学习Jenkins的一个障碍,就是权限不够的话,许多配置菜单看不到,看不到菜单的话就无从学习了解。这次有幸管理员给我充分赋权,让我实现了配置和调试的自由,所以学的比较快。今天就把这一周感觉有收获的知识点给各位掘友分享一下。

1.如何安装插件?

许多功能,都需要安装插件。可是邪门之处在于,一些插件,在插件管理-->可得到的插件面板搜不到,比如说http_request插件,

image.png

可是这个插件在Jenkins官方插件市场是存在的,这种情况经常发生,让新手感到很懵。 image.png

这时就需要手动下载,手动下载的链接在一个不起眼的地方,我第一次找了几分钟才找到。 image.png

切换到高级设置面板,进行部署。部署之后,需要重启才能生效。 image.png

手动安装后,重启才能生效。在浏览器端安全重启的方式是在浏览器中输入http://your-jenkins-domain/safeRestart

2.如何创建凭据?

所有的登录认证,都需要凭据。如登录Gitlab仓库,登录Docker镜像仓库,k8s部署登录等。新手创建凭据时。大概率会遇到的第一个障碍是,找不到创建凭据的菜单入口。因为创建凭据的菜单按钮不是一个很显眼的按钮。而是鼠标停留在全局链接时,会出现下拉箭头,点击下拉箭头之后,才会出现添加凭据菜单入口。

image.png

第二个障碍就是凭据是分类型的,经常出现的问题是你创建了凭据,使用的时候却在下拉列表不展示。如创建了一个jenkins-gitlab的凭据,

image.png

却在配置git仓库的凭据时找不到,这是因为创建的凭据类型不匹配。 image.png

另外就是创建凭据的时候, 对ID参数填什么感到很困惑。ID是个可选参数,可以不填。

image.png

3.如何在Jenkins流水线中进行登录认证?

这个功能需要安装Credentials PluginCredentials Binding Plugin插件。它们各自的用途是:

  • Credentials Plugin:管理凭据(REGISTRY 这个 credentialsId 就是保存到 Jenkins 凭据里的)
  • Credentials Binding Plugin:在 Pipeline 脚本中安全地把凭据绑定为环境变量(比如你的 REGISTRY_USERNAMEREGISTRY_PASSWORD

场景举例,比如说流水线构建有一个步骤是登录docker,执行镜像构建,再把构建之后的Docker镜像推送到远程仓库。就需要用到登录认证功能。

     steps {
                script {
                    try {
                        updateGitlabCommitStatus(name: '生成docker镜像', state: 'pending')
                        withCredentials([
                            usernamePassword(
                                credentialsId: 'REGISTRY',
                                usernameVariable: 'REGISTRY_USERNAME',
                                passwordVariable: 'REGISTRY_PASSWORD'
                            )
                        ]) {
                            sh """
                                echo "\$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" -u "\$REGISTRY_USERNAME" --password-stdin
                                docker build --build-arg TAR_FILE="$TAG_FILE_NAME" -t "$IMAGE_TAG" .
                                docker push "$IMAGE_TAG"
                            """
                        }

                        updateGitlabCommitStatus(name: '生成docker镜像', state: 'success')
                    } catch (Exception e) {
                        updateGitlabCommitStatus(name: '生成docker镜像', state: 'failed')
                        throw e
                    }
                }
            }

4.如何自定义Jenkins构建侧边栏展示信息?

Jenkins默认的构建侧边栏展示信息比较有限,只有启动者信息,如何自定义展示更多的信息呢。 image.png

答案是修改currentBuild.description的值,添加自定义内容。不过有长度约束,超过4行多余部分就会显示省略号。

stage('SetPanelInfo') {
    steps {

		def starter = env.gitlabUserName ?: env.BUILD_USER
		def shortSha = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
		def branch = "${env.gitlabBranch ?: params.BranchName}-${shortSha}"
		currentBuild.description = "自动部署: ${env.DEPLOY_ENV}, 分支: ${branch}, 启动者: ${starter}"

    }
}

这是定制之后的效果:

image.png

5.如何采集代码提交信息?

场景是这样的,需要采集手动构建时的代码提交消息,如Jenkins构建地址,提交地址,分支,提交ID,提交信息,提交作者,ESLint 总错误数,ESLint 总警告数。获取这些信息的方式参见下面的Groove脚本。有两处需要注意

  1. 跨stage使用的变量,要定义到环境变量中。
  2. 单个Stage执行时间过长的话,会引起Jenkins超时报错退出。对于比较耗时的操作,比如说ESLint全局校验,需要放在timeout(time: 10, unit: 'MINUTES') 中执行。
       stage('搜集上报信息') {

            when {
                expression {
                    return params.DEPLOY_ENV in ['test', 'canary']
                }
            }
            steps {
                script {
                    
                    env.branchName = sh(script: "git rev-parse --abbrev-ref HEAD", returnStdout: true).trim()
                    env.commitHash = sh(script: "git rev-parse HEAD", returnStdout: true).trim()
                    env.commitAuthorName = sh(script: "git show -s --format='%an' ${env.commitHash}", returnStdout: true).trim()
                    env.commitMessage = sh(script: "git log -1 --pretty=%B", returnStdout: true).trim()

                    // GitLab 项目基础地址
                    def gitlabProjectUrl = 'https://git.xxx.com/scenic/shop'

                    // commitHash 是上面已经拿到的
                    env.commitUrl = "${gitlabProjectUrl}/-/commit/${commitHash}"

                    

        
                    // 运行 ESLint 并生成 JSON 报告
                    timeout(time: 10, unit: 'MINUTES') {
                        sh '''
                            set -e
                            bash -c "
                                source ~/.bashrc
                                nvm use 18.20.6
                                npx eslint 'src/**/*.{ts,vue}' --cache  --format json --output-file eslint-report.json || true
                            "
                        '''
                        
                        // 读取 ESLint 报告文件
                        def reportFile = readFile file: 'eslint-report.json'
                        
                        // // 解析 JSON 报告
                        def report = readJSON text: reportFile
                        
                        // 初始化计数器
                        int totalErrors = 0
                        int totalWarnings = 0
                        
                        // 遍历报告中的每个文件
                        report.each { fileReport ->
                            fileReport.messages.each { message ->
                            if (message.severity == 2) {
                                totalErrors++
                            } else if (message.severity == 1) {
                                totalWarnings++
                            }
                            }
                        }
                        env.totalErrors = totalErrors
                        env.totalWarnings = totalWarnings
                        

                        // 输出统计结果
                        echo "Jenkins构建地址: ${env.BUILD_URL}"
                        echo "提交地址: ${env.commitUrl}"
                        echo "分支: ${env.branchName}"
                        echo "提交ID: ${env.commitHash}"
                        echo "提交信息: ${env.commitMessage}"
                        echo "提交作者: ${env.commitAuthorName}"
                        echo "ESLint 总错误数: ${env.totalErrors}"
                        echo "ESLint 总警告数: ${env.totalWarnings}"
                    }
                    
                }
            }
        }

6.如何发送邮件?

需要安装Email Extension Plugin插件,才能使用emailext发送邮件,emailext 支持 HTML格式(mimeType: 'text/html'),附件、条件发送、抄送/密送,非常灵活。比如说有这样一个场景,无论构建成功还是失败,要将每次构建的代码版本信息和质量检查统计结果邮件发送给产研负责人,限定只发送test和canary环境的构建操作。因为每个环境都发的话,收件箱会爆满。这个功能的实现脚本如下:

    post {

        always {
            script {
                if (params.DEPLOY_ENV in ['test', 'canary']) {
                    emailext (
                        subject: "项目:${env.JOB_NAME}--环境:${params.DEPLOY_ENV} -- 构建结果:${currentBuild.currentResult}",
                        body: """
                            <p>项目:${env.JOB_NAME}</p>
                            <p>状态:<strong>${currentBuild.currentResult}</strong></p>
                            <p>详情:<a href="${env.BUILD_URL}">${env.BUILD_URL}</a></p>
                            <p>提交地址: <a href="${env.commitUrl}">${env.commitUrl}</a></p>
                            <p>分支: ${env.branchName}</p>
                            <p>提交ID: ${env.commitHash}</p>
                            <p>提交作者: ${env.commitAuthorName}</p>
                            <p>提交信息: ${env.commitMessage}</p>
                            <p>ESLint 总错误数: ${env.totalErrors}</p>
                            <p>ESLint 总警告数: ${env.totalWarnings}</p>
                        """,
                        to: 'xxx1@xxx.com, xxx2@xxx.com',
                        from: 'opsnotice@xxx.com',
                        mimeType: 'text/html'
                    )
                } else {
                    echo "分支 ${env.branchName} 不在允许发送邮件的列表中,跳过发送邮件。"
                }
            }
        }
        success {
            // 构建成功后的操作,例如发送通知
            echo 'Build and package succeeded!'
        }
        failure {
            // 构建失败后的操作,例如发送通知
            echo 'Build or package failed!'
        }
    }

7.什么时候应该使用Pipeline Script?

流水线的获取方式有两种:

  • 一种是Pipeline script, 这种方式使用的是在线Jenkins脚本,多用于调试
  • Pipeline script from SCM是从指定的代码仓库与分支获取Jenkins脚本,适用于Jenkins脚本存档。

如果用的不恰当的话,会带来不少麻烦。比如说调试Jenkins脚本,使用 Pipeline script from SCM,每次改点东西都要推送到远程仓库才能生效,调试比较低效。而调试通过之后,不对脚本进行存档,可能会发生篡改或者无意清空在线Jenkins脚本的情形。

image.png

如果真的发生脚本被别人无意清空,运维给我传授了一个窍门,可以使用回放功能,找回丢失的构建脚本。

image.png

另外,配置页面的这两个按钮使用也有讲究。点击应用之后,新改的脚本会立即生效,但不会引起配置页面跳转。特别适合打开两个tab页,一个页面用于调试修改脚本,另外一个页面用于构建查看结果,很方便。

image.png

8.如何手动切换git仓库分支?

需要安装Git Plugin插件, 这个功能的用途是Jenkins使用的构建脚本在master分支,而要构建的业务功能是Gitlab Webhook推送过来的动态分支,这个时候,就需要手动切换到需要构建的动态分支。刚开始把脚本获取分支和构建分支分不清,发现手动构建时正常,自动构建时构建的产物内容不对,后来发现是这个问题引起的。

 stage('Checkout') {
            steps {
                script {
                    try {
                        sh 'env'
                        // 手动检出代码
                        updateGitlabCommitStatus(name: '拉取代码', state: 'pending')
                        echo "最新hash值 ${env.gitlabMergeRequestLastCommit} "
                        if (env.gitlabMergeRequestLastCommit || params.BranchName) {
                            checkout([
                                $class: 'GitSCM',
                                branches: [[name: "${env.gitlabMergeRequestLastCommit ?: 'dev'}"]],
                                // extensions: [
                                //     [$class: 'WipeWorkspace'],
                                //     [$class: 'CleanBeforeCheckout'],
                                //     [$class: 'PruneStaleBranch'],
                                //     [$class: 'CloneOption', noTags: false, shallow: false, depth: 0, reference: '', timeout: 10]
                                // ],
                                userRemoteConfigs: [[
                                    url: env.gitlabSourceRepoHttpUrl ,
                                    credentialsId: 'jenkins-robot'
                                ]]
                            ])
                        }
                        updateGitlabCommitStatus(name: '拉取代码', state: 'success')
                    } catch (Exception e) {
                        updateGitlabCommitStatus(name: '拉取代码', state: 'failed')
                        throw e
                    }
                }
            }
        }

9. 如何创建动态选项列表?

想要实现的业务功能是根据选择的部署环境和部署docker镜像,快速进行版本回退。要对docker镜像列表选项进行过滤,当选择的部署环境为dev时,只显示以dev开头的镜像tag选项,当选择的部署环境为test,canary,prod时,只显示以release开头的镜像tag选项。

进入你的 Jenkins Job 配置页面:

  • 点击「添加参数」 → 选择「Active Choices Reactive Parameter」
  • 配置如下:
字段内容
NameManualDockerTag
Script TypeGroovy Script
Script👇参考下面的脚本
Use Groovy Sandbox取消勾选,否则无权限访问网络)
import groovy.json.JsonSlurper
//try {
def registryHost = "https://reg.example.com:9088"
def namespace = "data"
def project = "ai-platform-frontend"
def registryUrl = "${registryHost}/api/v2.0/projects/${namespace}/repositories/${project}/artifacts?with_tag=true&with_scan_overview=true&with_label=true&with_accessory=false&page_size=50&page=1"

def username = System.getenv("REGISTRY_USERNAME") 
def password = System.getenv("REGISTRY_PASSWORD")

def deployEnv = binding.getVariable('ManualDeployEnv') ?: 'dev'
def auth = "${username}:${password}".bytes.encodeBase64().toString()
def conn = new URL(registryUrl).openConnection()
conn.setRequestProperty("Authorization", "Basic ${auth}")
conn.setRequestProperty("Accept", "application/json")

def jsonText = conn.inputStream.withReader { it.text }
def artifacts = new JsonSlurper().parseText(jsonText)

// 过滤并提取 tag 和 push_time
def filtered = artifacts.findAll { artifact ->
    def tags = artifact.tags?.collect { it.name } ?: []
    if (['test', 'dev'].contains(deployEnv)) {
        tags.any { it.startsWith('dev') }
    } else if (['prod', 'canary'].contains(deployEnv)) {
        tags.any { it.startsWith('release') }
    } else {
        tags // 保留所有
    }
}.collectMany { artifact ->
    // 可能一个 artifact 有多个 tag
    (artifact.tags ?: []).collect { tag ->
        [tag: tag.name, push_time: artifact.push_time]
    }
}

// 按 push_time 倒序排列
def sorted = filtered.sort { a, b -> b.push_time <=> a.push_time }

// 返回 tag 列表(或需要的结构)
return sorted.collect { it.tag }
//} catch (Exception e) {
//    def msg = "出错了: " + String.valueOf(e.getMessage())
//    return [msg]
//}

每次修改了上面的groove脚本后,要点击一下Approve script脚本才能生效。另外在「Active Choices Reactive Parameter」参数类型的脚步中,要去掉Use Groovy Sandbox选项,否则限制很大,如不能发起http请求,不能操作文件, 不能调用json转换API等等。

image.png

最终实现的效果如下:美中不足的是,如果按照时间倒序排列选项,因为docker提供的接口api查询出来的列表数据,没有tag创建时间信息。

image.png

10. 如何给gitlab合并通过的请求添加评论?

业务想实现的功能是合并请求通过后,在合并请求下方添加ESLint代码质量扫描结果,效果如下:

image.png

这个功能的实现,需要在GitLab中创建访问令牌,然后将访问令牌添加到Jenkins的全局访问凭证中。先调用gitlab提供的获取mergeId的api,再调用写评论信息的api,就能实现此效果。有个坑需要规避一下,就是在GitLab中创建访问令牌时,不要创建单个项目的,而要创建全局的,创建全局访问令牌的菜单入口是从gitlab左上角头像那里进入。

def getMergeRequestIid(api, projectId, sourceBranch, token) {
    def response = sh(
        script: """
            curl --silent --header "PRIVATE-TOKEN: ${token}" \
            "${api}/${projectId}/merge_requests?state=merged&source_branch=${sourceBranch}"
        """,
        returnStdout: true
    ).trim()

    def json = readJSON text: response
    return json[0]?.iid
}

stage('添加评论') {
            steps {
                script {
                    withCredentials([string(credentialsId: 'Merged-Notes', variable: 'MergedNotesToken')]) {
                        def GITLAB_API = 'https://git.example.com/api/v4/projects'
                        def PROJECT_ID = 11
                        def BRANCH_NAME = env.GIT_BRANCH.replace('origin/', '')
                        def MR_ID = getMergeRequestIid(GITLAB_API, PROJECT_ID, BRANCH_NAME, MergedNotesToken)

                        echo "Merge Request IID: ${MR_ID}"

                        def comment = """
                            - Jenkins+ESLint扫描结果
                            - Jenkins构建地址: ${env.BUILD_URL}
                            - ESLint 总错误数:${env.totalErrors ?: 0}
                            - ESLint 总警告数:${env.totalWarnings ?: 0}
                        """.stripIndent()

                        def jsonBody = groovy.json.JsonOutput.toJson([ body: comment ])

                        sh """
                            curl --request POST "${GITLAB_API}/${PROJECT_ID}/merge_requests/${MR_ID}/notes" \
                                --header "PRIVATE-TOKEN: ${MergedNotesToken}" \
                                --header "Content-Type: application/json" \
                                --data '${jsonBody}'
                        """
           }
        }
     }
}

11.如何将Jenkins构建信息推送给Gitlab?

在Jenkins==>系统菜单下,找到Gitlab项目,填写要接收Jenkins构建信息的Gitlab仓库地址,Token是在Gitlab个人信息偏好设置,访问凭据中设置好,然后添加到Jenkins凭据中,在下拉列表中就能看到。

image.png

配置完的效果是: 点击一下就能看到Jenkins构建步骤过程输出信息

image.png

12. 在Jenkins Pipeline中使用Docker基础镜像的方法

第一步:制作Docker Ruff基础镜像 Ruff是Python静态代码检查工具,为什么要把Ruff制作成镜像,是因为原来的写法,每次都会触发安装,在这个阶段会停留2分钟,比较耗时。

        stage('代码检查') {
            steps {
                sh '''
                curl -LsSf https://astral.sh/uv/install.sh | sh
                bash -c "source $HOME/.local/bin/env "
                bash -c "source ~/.bashrc &&  uvx ruff check --output-file ruff_report.json --output-format json || true"
                '''
            }
        }

在Jenkinsfile中定义制作Ruff镜像的构建步骤,先指定工作目录,登录Docker镜像,再创建和推送Ruff镜像。

pipeline {
  agent any

  environment {
    REGISTRY_HOST = 'reg.example.com:9088'
  }

  stages {
    stage('制作Python-Ruff镜像') {
      steps {
        script {
          try {
            updateGitlabCommitStatus(name: '生成Python-Ruff工具包', state: 'pending')
            withCredentials([
              usernamePassword(
                  credentialsId: 'REGISTRY',
                  usernameVariable: 'REGISTRY_USERNAME',
                  passwordVariable: 'REGISTRY_PASSWORD'
              )
            ]) {
              dir('common-tools/python-ruff') {
                sh """
                    echo "\$REGISTRY_PASSWORD" | docker login "https://$REGISTRY_HOST/" -u "\$REGISTRY_USERNAME" --password-stdin
                    docker build -t $REGISTRY_HOST/base-images/travel-ai-gpt:python-ruff .
                    docker push $REGISTRY_HOST/base-images/travel-ai-gpt:python-ruff
                """
              }
            }

            updateGitlabCommitStatus(name: '生成Python-Ruff工具包', state: 'success')
          } catch (Exception e) {
            updateGitlabCommitStatus(name: '生成Python-Ruff工具包', state: 'failed')
            throw e
          }
        }
      }
    }
  }
}

Dockerfile内容如下: 整理目标是在Docker镜像中安装ruff静态管理工具。在安装ruff之前,得安装uv/uvx,uv/uvx的安装又会用到curl+bash+git+ca-certificates,安装这些依赖的过程中会产生一些缓存文件,为了降低镜像包的体积,要清理缓存。ruff需要Python环境,所以这个Docker镜像得基于python:3.11-slim创建,python:3.11-slim 是一个小体积的 Python 官方镜像,适合生产环境构建。

# Dockerfile
FROM python:3.11-slim

# 安装系统依赖
RUN apt-get update && apt-get install -y curl bash git ca-certificates

# 安装 uvx
RUN curl -LsSf https://astral.sh/uv/install.sh | sh &&
  cp /root/.local/bin/uvx /usr/local/bin/uvx && chmod a+x /usr/local/bin/uvx &&
  cp /root/.local/bin/uv /usr/local/bin/uv && chmod a+x /usr/local/bin/uv

ENV PATH="/usr/local/bin:${PATH}"

# 清理缓存
RUN rm -rf /var/lib/apt/lists/*

# 验证 uvx 安装成功
RUN uvx --version

# 安装 ruff
RUN uv pip install ruff --system
# 验证 ruff 安装成功
RUN ruff --version

首先要安装Docker Pipeline插件,然后用 Ruff 的 Docker 镜像启动一个临时容器,把当前代码挂载进去,在容器里 /app 目录执行python代码静态检查命令,执行完后自动销毁容器

stage('代码检查') {
    steps {
        script {
            docker.image("${env.REGISTRY_HOST}/base-images/travel-ai-gpt:python-ruf").inside('-v $PWD:/app -w /app') {
                sh '''
                    echo "🔍 使用 Docker 运行 Ruff 代码检查..."
                    ruff check . --output-file ruff_report.json --output-format json || true
                '''
            }
        }
    }
}

13. 向特定分支提交代码时每次自动生成changelog

前提条件是在gitlab给项目配置好webhook,每天向特定分支提交代码时,触发Jenkins生成提交日志,因为是多个项目使用同样的脚本,所以每次要清理一下工作空间。changelog采用git-cliff生成,可以对生成的基础changelog进行修改,每次提交之前,判断有无内容更新,有更新才提交

pipeline {
    agent any
    environment {
        GIT_REPO_URL = "${env.gitlabSourceRepoHttpUrl}"
        GIT_BRANCH = "${env.gitlabTargetBranch}"
    }
    
    stages {
        stage('Clean Workspace') {
            steps {
                cleanWs()
            }
        }
        stage('Show Gitlab Info') {
            steps {
                script {
                    echo "GitLab Repo URL: ${GIT_REPO_URL}"
                    echo "GitLab Branch: ${GIT_BRANCH}"
                }
            }
        }
        stage('Checkout') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: [[name: env.GIT_BRANCH]],
                    userRemoteConfigs: [[
                        url: GIT_REPO_URL,
                        credentialsId: 'jenkins-robot'
                    ]]
                ])
            }
        }
        stage('Generate Changelog') {
            steps {
                script {
                    sh '''
                        python3 --version
                        pip3 --version
                        # 生成CHANGELOG.md
                        git-cliff --output CHANGELOG.md

                        # CHANGELOG.md内容去重
                        awk '
                            # 只处理以 "- " 开头的行(即实际的 changelog 条目)
                            /^- / {
                                full_line = $0
                                key       = full_line

                                # 再去掉末尾的一个或多个 "#数字"(可能有空格分隔),如 "#123""#123#456"" #789"sub(/([[:space:]]*#[0-9]+)+$/, "", key)

                                # 如果已经见过相同的 key(也就是相同的提交信息文本),跳过打印
                                if (seen[key]++) {
                                    next
                                }
                            }

                            # 非 "- " 开头的行(如版本标题、空行、描述等)直接打印
                            { print }
                        ' CHANGELOG.md > CHANGELOG.tmp
                        mv CHANGELOG.tmp CHANGELOG.md
                        # awk '!seen[$0]++' CHANGELOG.md > CHANGELOG.tmp && mv CHANGELOG.tmp CHANGELOG.md
                        '''
                }
            }
        }
        stage('Commit & Push Changes') {
            steps {
                script {
                    withCredentials([
                        string(
                            credentialsId: 'jenkins-ci', 
                            variable: 'GIT_TOKEN'
                        )
                    ]) {
                        sh '''
                            # 设置 Git 用户信息
                            git config user.name "Jenkins-ci"
                            git config user.email "jenkins-ci@hqshuke.com"

                            # 添加 changelog 文件
                            git add CHANGELOG.md

                            # 检查是否有变化
                            if git diff-index --quiet HEAD; then
                                echo "没有需要提交的内容."
                                exit 0
                            fi

                            # 提交 changelog
                            git commit -m "docs: Auto-generate changelog [ci-skip] #106"

                            # 推送至远程仓库
                            git push https://jenkins-ci:${GIT_TOKEN}@${GIT_REPO_URL#*//} HEAD:${GIT_BRANCH}
                        '''
                    }
                }
            }
        }
    }
}

14. 批量修改Job属性

下面这段脚本是用来批量修改Jenkins job配置参数的, jobName必须全局唯一,如果有所属组,要带上所属组,如"data/pay","prod-data/pay", 修改的job属性配置在jobParams中,原理是读取xml配置文件,进行属性的修改。

import javax.xml.transform.stream.StreamSource
import java.io.ByteArrayInputStream

def jobNames = [
  'job-0','job-1',
]

def jobParams = ['ManualBranchName', 'ManualDockerTag']

jobNames.each { jobName ->
  println "\n🛠️ 处理 Job: ${jobName}"
  def job = Jenkins.instance.getItemByFullName(jobName)

  if (job == null) {
    println "❌ 找不到 Job: ${jobName}"
    return
  }

  def configXml = job.getConfigFile().asString()
  println "📄 成功获取 config.xml 内容(前200字符预览):\n${configXml.take(200)}"

  def modified = false

  jobParams.each { paramName ->
    def regex = /<name>${paramName}<\/name>[\s\S]*?<choiceType>(.*?)<\/choiceType>/
    def matcher = configXml =~ regex
    if (matcher.find()) {
      def oldType = matcher[0][1]
      println "🔍 找到参数 ${paramName},当前 choiceType 是: ${oldType}"

      if (oldType != 'PT_RADIO') {
        def fullMatch = matcher[0][0]
        def replacedMatch = fullMatch.replaceFirst(/<choiceType>.*?<\/choiceType>/, '<choiceType>PT_RADIO</choiceType>')
        configXml = configXml.replace(fullMatch, replacedMatch)
        modified = true
        println "✅ ${jobName} - ${paramName} 的 choiceType 已修改为 RADIO"
      } else {
        println "ℹ️ ${jobName} - ${paramName} 已经是 RADIO"
      }
    } else {
      println "⚠️ ${jobName} - 没有找到参数 ${paramName}"
    }
  }

  if (modified) {
    def stream = new ByteArrayInputStream(configXml.getBytes('UTF-8'))
    job.updateByXml(new StreamSource(stream))
    job.save()
    println "💾 ${jobName} - 配置已更新并保存"
  } else {
    println "⏩ ${jobName} - 没有需要修改的内容"
  }
}

15. 给企微机器人推送消息

  1. 首先在企微群中创建机器人,获取消息推送地址
  2. 在Jenkins中, 将webhook配置为Generic Webhook Trigger类型, 这样可以获取Gitlab Webhook推送的原始数据

image.png

  1. 编写消息推送功能
class WeComNotifier implements Serializable {

  /**
   * 发送企业微信 Markdown 通知
   * @param script - Jenkins pipeline 的 script 作用域
   * @param status - 状态信息,例如 "✅ 成功" 或 "❌ 失败"
   */
  static void send(script, String status) {
    def jobName = script.env.JOB_BASE_NAME
    def deployEnv = script.env.WEBHOOK_JSON_labels_0_title ?: script.env.ManualDeployEnv
    def dockerTag = script.env.DockerImageTag
    def buildUrl = "${script.env.BUILD_URL}console"
    def commitAuth = script.env.WEBHOOK_JSON_user_name
    def List mentionMobiles = []
    def webhook = ''

    def message = """
    ${jobName} 项目 ${deployEnv} 环境使用 ${dockerTag} 分支, 刚刚进行了一次部署
    状态:${status}
    详情链接:${buildUrl}
    """

    if (jobName.contains('xxx项目')) {
      webhook = '企微群机器人推送地址'
      // 企微机器人测试链接
      if (jobName == 'travel-ai') {
        if (commitAuth == 'xxx') {
          mentionMobiles = ['项目开发者手机号']
        }
        // ...
      }
    }

    def payload = [
        msgtype: 'text',
        text: [
          content: message,
          mentioned_mobile_list: mentionMobiles
        ]
    ]

    def json = script.writeJSON returnText: true, json: payload

    script.withEnv([
      "WEBHOOK=${webhook}",
      "JSON=${json}"
    ]) {
      script.sh(
        script: '''
          set +x
          curl -s "$WEBHOOK" \
            -H "Content-Type: application/json" \
            -d "$JSON" > /dev/null 2>&1
        ''',
        returnStatus: true
      )
    }
  }
}

最后

这一周的工作经历,让人颇有获得感。现学现用的感觉真好。如果没有实践反馈,人不知道自己掌握的知识,能否经得起实践的考验。原因有三:

  1. 工具在不断迭代更新,你学到的可能是老版本的使用方法,而项目使用的却是工具的新版本。
  2. 看的文章作者可能省略了一些关键步骤,一步一步跟着做,跟到某一步会突然卡住跟不上
  3. 只有看到真实场景的反映,才了解一些专有名词所表达的实际含义,用陌生名词理解陌生名词,用生涩定义推理生涩定义,会让人云里雾里,大脑一片茫然。

本文所列举的知识点,都能经得起实践的检验,如你将来如遇类似问题,可直接套用现成方案。另外,手头还有两个Jenkins任务没做完,等做完后再添加上。