Jenkins教程(八)实现 GitLab 触发 Jenkins 自动按模块发布前端

楔子

上篇文章解决了提交/合并请求自动触发的需求,但所有前端模块都在同一个代码仓库里,如何获取变更文件路径确定要发布哪个模块呢?本文将带你解决这个问题。

思路

分别解决 3 个问题:

  1. 获取变更的文件列表
  2. 根据文件列表判断所属模块
  3. 构建与发布脚本

过程

GitLab 事件触发 Jenkins 构建只是一个启动信号,获取变更文件列表需要知晓上一次构建时某个仓库的版本号,这里 Jenkins 的插件 git-plugin 已经帮我们实现了这部分工作。所以只需要通过 git-plugin 检出代码即可。

检出代码

    checkout([
        $class: 'GitSCM',
        branches: [[name: "*/$branchName"]],
        doGenerateSubmoduleConfigurations: false,
        extensions: [
            [$class: 'RelativeTargetDirectory',
            relativeTargetDir: "$relativeTarget"]
        ],
        submoduleCfg: [],
        userRemoteConfigs: [
            [credentialsId: "$credentialsId", url: "$gitUrl"]
        ]
    ])

请自行替换 $branchName 为分支名,$relativeTarget 为检出相对路径,$credentialsId 为用户凭据, $gitUrl 即 GIT仓库地址。

获取变更文件列表

//获取变更文件列表,返回HashSet,注意添加的影响文件路径不含仓库目录名
@NonCPS
def getChangeFilePathSet() {
    def changedFiles = new HashSet<String>();
    echo "开始获取变更的文件列表"
    for (int i = 0; i < currentBuild.changeSets.size(); i++) {
        def entries = currentBuild.changeSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            changedFiles.addAll(entry.getAffectedPaths());
        }
    }
    println '输出修改文件列表:' + changedFiles
    return changedFiles;
}

这个方法可以放到 pipeline 块外,直接在 script 块中引用。实现思路是访问 currentBuild.changeSets 获取所有本次构建相比上次构建的变更列表,返回的是 HashSet 是为了方便,用其他容器也是可以的。

注意:变更文件列表的各个文件是相对于它所在仓库的路径!

变更文件列表截字符串,获取模块列表并去重

//获取合并报表前端自动发布模块set集合。
//pathPrefix为模块路径前缀,如develop/@gc
@NonCPS
def getAutoPublishModuleSet(pathPrefix) {
    //使用Set容器去重,保证待发布模块只有一份
    def modulePaths = new HashSet<String>();
    for(def filePath in getChangeFilePathSet()){
        //忽略非前端模块的文件,比如 Jenkinsfile 等
        if(filePath.startsWith(pathPrefix)){
            //从超过模块前缀长度的下标开始,获取下一个/的位置。即分串位置
            int index = filePath.indexOf('/', pathPrefix.length()+1)
            //分串得到模块路径,比如 develop/@gc/test
            def modulePath = filePath.substring(0, index)
            println 'add module path: ' + modulePath
            modulePaths.add(modulePath)
        }
    }
    println '输出待发布模块列表:' + modulePaths
    return modulePaths;
}

写个构建发布 Shell 脚本

publish-web-module.sh

#!/bin/bash
#此脚本用于构建发布前端模块,@author: Hellxz
#$1:发布版本/$2:模块目录
set -eu

echo "------------开始发布$2模块------------>"
cd $2
echo "清理dist node_modules package-lock.json ……"
rm -rf dist node_modules package-lock.json
echo "正在安装依赖 ……"
npm i
echo "开始构建 ……"
npm run build:dev
echo "开始发布 ……"
npm --no-git-tag-version version $1
npm publish
echo "<------------发布$2模块完成------------"

cd ${WORKSPACE}/web; #回到前端源码目录
exit 0;

循环调用构建发布脚本

for(def modulePath in modulePaths){
    sh label: "构建发布前端模块 ${publishVersion} ${modulePath}", 
       script: "bash ${SHELL_PATH}/publish-web-module.sh ${publishVersion} ${modulePath}"
}

流水线示例

需将下列 Jenkinsfile 与 publish-web-module.sh 提交到同一仓库中

Jenkinsfile

pipeline{
    agent any;
    environment{
        gitUrl="http://xxxxxxxx/xxxx/web.git"
        branchName=dev
        relativeTarget="web"
        credentialsId=credentials('git-user')
        pathPrefix="develop/@gc"
        publishVersion="v1.0"
        npmRepo="http://xxxxxx/nexus/repository/npm-public/"
        npmToken=credentials('npm-token')
        shellPath="${WORKSPACE}/jenkins" //脚本与Jenkinsfile在同级目录中
    }
    stages{
        stage("检出代码"){
            steps{
                script {
                    cleanWs()
                    checkoutRepo("master", "jenkins", "${credentialsId}", "http://xxxxxxxx/xxxx/jenkins.git")
                    checkoutRepo("${branchName}", "${relativeTarget}", "${credentialsId}", "${gitUrl}")
                }
            }
        }
        stage("构建发布"){
            steps{
                script{
                    sh label: "设置npm仓库", script: "npm set registry ${npmRepo}"
                    sh label: "登录npm仓库", script: "npm config set //xxxxxx/nexus/repository/npm-public/:_authToken ${npmToken}"
                    def modulePaths = getAutoPublishModuleSet(env.pathPrefix)
                    for(def modulePath in modulePaths){
                        sh label: "构建发布前端模块 ${publishVersion} ${modulePath}", 
                           script: "bash ${shellPath}/publish-web-module.sh ${publishVersion} ${modulePath}"
                    }
                }
            }
            post{
                always{
                    script{
                        cleanWs()
                    }
                }
            }
        }
    }
}

//抽取检出代码方法
@NonCPS
def checkoutRepo(branchName, relativeTarget, credentialsId, gitUrl){
    checkout([
        $class: 'GitSCM',
        branches: [[name: "*/$branchName"]],
        doGenerateSubmoduleConfigurations: false,
        extensions: [
            [$class: 'RelativeTargetDirectory',
            relativeTargetDir: "$relativeTarget"]
        ],
        submoduleCfg: [],
        userRemoteConfigs: [
            [credentialsId: "$credentialsId", url: "$gitUrl"]
        ]
    ])
}

//获取变更文件列表,返回HashSet,注意添加的影响文件路径不含仓库目录名
@NonCPS
def getChangeFilePathSet() {
    def changedFiles = new HashSet<String>();
    echo "开始获取变更的文件列表"
    for (int i = 0; i < currentBuild.changeSets.size(); i++) {
        def entries = currentBuild.changeSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            changedFiles.addAll(entry.getAffectedPaths());
        }
    }
    println '输出修改文件列表:' + changedFiles
    return changedFiles;
}

//获取合并报表前端自动发布模块set集合。
@NonCPS
def getAutoPublishModuleSet(pathPrefix) {
    //使用Set容器去重,保证待发布模块只有一份
    def modulePaths = new HashSet<String>();
    for(def filePath in getChangeFilePathSet()){
        //忽略非前端模块的文件,比如 Jenkinsfile 等
        if(filePath.startsWith(pathPrefix)){
            //从超过模块前缀长度的下标开始,获取下一个/的位置。即分串位置
            int index = filePath.indexOf('/', pathPrefix.length()+1)
            //分串得到模块路径,比如 develop/@gc/test
            def modulePath = filePath.substring(0, index)
            println 'add module path: ' + modulePath
            modulePaths.add(modulePath)
        }
    }
    println '输出待发布模块列表:' + modulePaths
    return modulePaths;
}

仅供抛砖引玉,抽取出来的方法本人将它们放到共享库中,写脚本就更清晰简短了。

还有什么问题

  • 首次构建会识别不到提交记录,可能会漏发一次
  • 切到未构建过的分支,也会漏发一次
  • 限于文章篇幅,未添加手动传参指定模块发布的功能

对于多分支首次检出漏发的问题,这是因为没有上一个可供参考的相同分支提交ID作参考,本身不是技术问题,预先将所有前端发版分支提交点内容,只要构建触发了,后续就不会再漏发了。

最后

希望对您能有所启发,如果您有更优雅的实现方式 或者 文中有错误,希望您能不吝赐教评论指出,感谢。

本文同步发布于博客园(东北小狐狸 https://www.cnblogs.com/hellxz/)与CSDN(东北小狐狸-Hellxz https://blog.csdn.net/u012586326)禁止转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东北小狐狸-Hellxz

请作者喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值