• 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)禁止转载。

  • 相关阅读:
    MySpace你居然报黄页?
    ExtAspNet应用技巧(十三) 后台主页面(IFrame框架)
    ExtAspNet应用技巧(十七) 新增菜单
    ExtAspNet应用技巧(十九) 日志管理
    ExtAspNet应用技巧(二十四) AppBox之Grid数据库分页排序与批量删除
    ExtAspNet应用技巧(二十三) Ext4JSLint之Grid的使用
    ExtAspNet应用技巧(二十一) Ext4JSLint之整体框架
    ExtAspNet应用技巧(十六) 菜单管理
    ExtAspNet应用技巧(二十二) Ext4JSLint之JSON文件创建树控件
    ImageList控件的问题
  • 原文地址:https://www.cnblogs.com/hellxz/p/15310001.html
Copyright © 2020-2023  润新知