带有命名参数的自定义 jenkins 声明性管道 dsl

custom jenkins declarative pipeline dsl with named arguments

我已经尝试了一段时间,开始努力将我们的自由风格项目转移到管道中。为此,我觉得最好建立一个共享库,因为我们的大多数构建都是相同的。我通读了这个blog post from Jenkins。我想出了以下

// vars/buildGitWebProject.groovy
def call(body) {
    def args= [:]
    body.resolveStrategy = Closure.DELEGATE_FIRST
    body.delegate = args
    body()

    pipeline {
        agent {
            node {
                label 'master'
                customWorkspace "c:\jenkins_repos\${args.repositoryName}\${args.branchName}"
            }
        }
        environment {
            REPOSITORY_NAME = "${args.repositoryName}"
            BRANCH_NAME = "${args.branchName}"
            SOLUTION_NAME = "${args.solutionName}"
        }
        options {
            buildDiscarder(logRotator(numToKeepStr: '3'))
            skipStagesAfterUnstable()
            timestamps()
        }
        stages {
            stage("checkout") {
                steps {
                    script{
                        assert REPOSITORY_NAME != null : "repositoryName is null. Please include it in configuration."
                        assert BRANCH_NAME != null : "branchName is null. Please include it in configuration."
                        assert SOLUTION_NAME != null : "solutionName is null. Please include it in configuration."
                    }
                    echo "building with ${REPOSITORY_NAME}"
                    echo "building with ${BRANCH_NAME}"
                    echo "building with ${SOLUTION_NAME}"
                    checkoutFromGitWeb(args)
                }
            }
            stage('build and test') {
                steps {
                    executeRake(
                        "set_assembly_to_current_version",
                        "build_solution[$args.solutionName, Release, Any CPU]",
                        "copy_to_deployment_folder",
                        "execute_dev_dropkick"
                    )
                }
            }
        }
        post {
            always {
                sendEmail(args)
            }
        }
    }
}

在我的pipeline项目中我配置了Pipeline使用Pipeline脚本,脚本如下:

buildGitWebProject {
    repositoryName:'my-git-repo'
    branchName: 'qa'
    solutionName: 'my_csharp_solution.sln'
    emailTo='testuser@domain.com'
}

我尝试过使用和不使用环境块,但结果最终与每个参数的值为 'null' 相同。奇怪的是,代码的脚本部分也没有使构建失败......所以不确定那有什么问题。回声部分也显示为空。我做错了什么?

您的 Closure body 没有按照您 expect/believe 应有的方式行事。

在你的方法的开头你有:

def call(body) {
  def args= [:]
  body.resolveStrategy = Closure.DELEGATE_FIRST
  body.delegate = args
  body()

您的电话 body 是:

buildGitWebProject {
    repositoryName:'my-git-repo'
    branchName: 'qa'
    solutionName: 'my_csharp_solution.sln'
    emailTo='testuser@domain.com'
}

让我们尝试调试一下。

如果您在 call(body) 方法中的 body() 之后添加 println(args),您将看到如下内容:

[emailTo:testuser@domain.com]

但是,只设置了一个值。怎么回事?

这里有几点需要理解:

  1. 设置 delegateClosure 有什么作用?
  2. 为什么 repositoryName:'my-git-repo' 什么都不做?
  3. 为什么emailTo='testuser@domain.com'在地图中设置属性?

What does setting a delegate of a Closure do?

这一个大部分是直截了当的,但我认为它有助于理解。 Closure is powerful and is the Swiss Army knife of Groovy. The delegate essentially sets what the this is in the body of the Closure. You are also using the resolveStrategy of Closure.DELEGATE_FIRST, so methods and properties from the delegate are checked first, and then from the enclosing scope (owner) - see the Javadoc 以获得 in-depth 解释。如果调用 size()put(...)entrySet() 等方法,它们都会首先在 delegate 上调用。 属性访问也是如此。

Why does repositoryName:'my-git-repo' not do anything?

这似乎是 Groovy map literal, but it is not. These are actually labeled statements。如果您用 [repositoryName:'my-git-repo'] 之类的方括号将其括起来,那么这就是地图文字。但是,这就是您在那里所做的一切——创建地图文字。我们要确保这些 objects 在 Closure

中消耗

Why does emailTo='testuser@domain.com' set the property in the map?

这是使用 Groovy 的 map property notation 功能。如前所述,您将Closuredelegate设置为def args= [:],这是一个Map。您还设置了 Closure.DELEGATE_FIRSTresolveStrategy。这使您的 emailTo='testuser@domain.com' 决定在 args 上被调用,这就是 emailTo 键设置为该值的原因。这相当于调用 args.emailTo='testuser@domain.com'.

那么,你如何解决这个问题?

如果您想保留 Closure 语法方法,您可以将调用的 body 更改为任何实质上在委托 args 映射中存储值的内容:

buildGitWebProject {
  repositoryName = 'my-git-repo'
  branchName = 'qa'
  solutionName = 'my_csharp_solution.sln'
  emailTo = 'testuser@domain.com'
}

buildGitWebProject {
  put('repositoryName', 'my-git-repo')
  put('branchName', 'qa')
  put('solutionName', 'my_csharp_solution.sln')
  put('emailTo', 'testuser@domain.com')
}

buildGitWebProject {
  delegate.repositoryName = 'my-git-repo'
  delegate.branchName = 'qa'
  delegate.solutionName = 'my_csharp_solution.sln'
  delegate.emailTo = 'testuser@domain.com'
}

buildGitWebProject {
  // example of Map literal where the square brackets are not needed
  putAll(
      repositoryName:'my-git-repo',
      branchName: 'qa',
      solutionName: 'my_csharp_solution.sln',
      emailTo: 'testuser@domain.com'
  )
}

另一种方法是让您的 call 接受 Map 作为参数并删除您的 Closure.

def call(Map args) {
    // no more args and delegates needed right now
}
buildGitWebProject(
    repositoryName: 'my-git-repo',
    branchName: 'qa',
    solutionName: 'my_csharp_solution.sln',
    emailTo: 'testuser@domain.com'
)

还有一些其他方法可以为您的 API 建模,这取决于您要提供的用户体验。


关于共享库代码中声明性管道的旁注:

值得牢记共享库中声明性管道的局限性。看起来您已经在 vars 中这样做了,但为了完整起见,我只是将其添加到此处。在 documentation it is stated:

的最后

Only entire pipelines can be defined in shared libraries as of this time. This can only be done in vars/*.groovy, and only in a call method. Only one Declarative Pipeline can be executed in a single build, and if you attempt to execute a second one, your build will fail as a result.