It's a common pattern to write Jenkins pipeline code like this:
def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
}
I'm not sure how to word this simply, but this closure get implicitly imported and can be called by the file name of the .groovy file it lives in.
I call it like:
MyClosure { myarg = 'sdfsdf' }
I not entirely sure what this is doing. It's doing call(body) and then assigning body as the delegate. so that means the closure I pass it is the delegate, but isn't that just the owner? Wah? me confused.
When this runs, it is creating an empty map (config). Then it is telling the closure (body) to look at the delegate first to find properties by setting its resolveStrategy to be the constant Closure.DELEGATE_FIRST. Then it assigns the config map as the delegate for the body object.
Now when you execute the body() closure, the variables are scoped to the config map, so now config.myarg = 'sdfsdf'.
Now later in the code you can have easy access to the map of values in config.
body is the owner, and by default is the delegate. But when you switch the delegate to be config, and tell it to use the delegate first, you get the variables config's scope.
Related
I have a properties file where certain string shoudl be replaced with Jenkins parameter. I ahve tried using the variable directly in the Properties file which did not work.
properties file
DOCKER_TAG_SUFFIX=-REPLACE_RELEASE_VERSION
PROPERTY_FILE_PATH=someproperty
Jenkinsfile snippet
def jboss_parameters = readProperties file: jboss_propfile
jboss_parameters .replaceAll("RELEASE_VERSION",params.RELEASE_VERSION) # try1
jboss_parameters = readFile(jboss_propfile).replaceAll("REPLACE_RELEASE_VERSION",params.RELEASE_VERSION) # try2
# try 3
jboss_parameters.each{k,v ->
if (v == "REPLACE_RELEASE_VERSION" )
jboss_parameters.setProperty($k,params.RELEASE_VERSION)
}
# try 4
def jboss_source_file = new File(jboss_propfile)
def jboss_parameters = jboss_source_file.text.replace("REPLACE_RELEASE_VERSION",params.RELEASE_VERSION)
I am not able to find another way that works for me.
println jboss_parameters output
{DOCKER_TAG_SUFFIX=-REPLACE_RELEASE_VERSION, PROPERTY_FILE_PATH=someproperty}
The readProperties step returns a dictionary (map), not a string, that is crated from the properties file.
Your first attempt (# try1) fails because maps in groovy do not have a replaceAll function like strings have and therefore you will get an error.
Your third attempt (# try3) is failing because you are comparing the map values to REPLACE_RELEASE_VERSION without the - character and therefore the comparison always fails and no values are changed.
I tested the second attempt (# try 2) and it seems to be working, so i am not sure what is your issue, but it is easier to handle properties as a map instead of a string that is retuned from the readFile method.
So if you have only specific properties that need to be updated you can update them directly:
def jboss_parameters = readProperties file: jboss_propfile
jboss_parameters.DOCKER_TAG_SUFFIX = params.RELEASE_VERSION // update relevant property
Or if you have multiple properties that should be modified you can iterate and update each value using the collectEntries method. Something like:
def jboss_parameters = readProperties file: jboss_propfile
updated_parameters = jboss_parameters.collectEntries { key, value ->
[key, value.replaceAll("REPLACE_RELEASE_VERSION",params.RELEASE_VERSION)]
}
I'm trying to write a Jenkins plugin that provides Step myStep which expects a block with a single parameter per below
myStep { someParameter -> <user code> }
I've found that BodyInvoker ( retrieved from StepContext.newBodyInvoker() ) provides no facilities to invoke the user provided block with parameters.
Expanding the environment would not be ideal, even though the type of the parameter is serializable ( to/from String ), i'd have to provide additional helpers to carry out this serialization, e.g
myStep { deserialize "${env.value}" <user code> }
do i have any other option to pass a non-string type in to the provided block? would type information of the parameter survive even if i did?
nb: i understand you can return a value from your Execution.run() which will be the return value of the step in the pipeline. It's just that in a related shared pipeline library i'm already heavily leaning in to this pattern of:
withFoo { computedFoo ->
# something with computedFoo
withBar computedFoo { computedBar ->
}
}
i prefer this over
computedFoo = withFoo
# something with computedFoo
withBar(computedFoo)
..then again, i couldn't find any plugins pulling this off.
no matter how close i look at workflow-step-api-plugin this doesn't seem possible today. The options are:
expand the environment context with a string value
add a custom object to the context ( requires access to step context in pipeline )
use a return value
I've been trying for a while now to start working towards moving our free style projects over to pipeline. To do so I feel like it would be best to build up a shared library since most of our builds are the same. I read through this blog post from Jenkins. I came up with the following
// 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)
}
}
}
}
in my pipeline project I configured the Pipeline to use Pipeline script and the script is as follows:
buildGitWebProject {
repositoryName:'my-git-repo'
branchName: 'qa'
solutionName: 'my_csharp_solution.sln'
emailTo='testuser#domain.com'
}
I've tried with and without the environment block but the result ends up being the same that the value is 'null' for each of those arguments. Oddly enough the script portion of the code doesn't make the build fail either... so not sure what's wrong with that. Also the echo parts show null as well. What am I doing wrong?
Your Closure body is not behaving the way you expect/believe it should.
At the beginning of your method you have:
def call(body) {
def args= [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = args
body()
Your call body is:
buildGitWebProject {
repositoryName:'my-git-repo'
branchName: 'qa'
solutionName: 'my_csharp_solution.sln'
emailTo='testuser#domain.com'
}
Let's take a stab at debugging this.
If you add a println(args) after the body() in your call(body) method you will see something like this:
[emailTo:testuser#domain.com]
But, only one of the values got set. What is going on?
There are a few things to understand here:
What does setting a delegate of a Closure do?
Why does repositoryName:'my-git-repo' not do anything?
Why does emailTo='testuser#domain.com' set the property in the map?
What does setting a delegate of a Closure do?
This one is mostly straightforward, but I think it helps to understand. 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 for an in-depth explanation. If you call methods like size(), put(...), entrySet(), etc., they are all first called on the delegate. The same is true for property access.
Why does repositoryName:'my-git-repo' not do anything?
This may appear to be a Groovy map literal, but it is not. These are actually labeled statements. If you surround it instead with square brackets like [repositoryName:'my-git-repo'] then that would be a map literal. But, that is all you would be doing there - is creating a map literal. We want to make sure that these objects are consumed in the Closure
Why does emailTo='testuser#domain.com' set the property in the map?
This is using the map property notation feature of Groovy. As mentioned earlier, you have set the delegate of the Closure to def args= [:], which is a Map. You also set the resolveStrategy of Closure.DELEGATE_FIRST. This makes your emailTo='testuser#domain.com' resolve to being called on args, which is why the emailTo key is set to the value. This is equivalent to calling args.emailTo='testuser#domain.com'.
So, how do you fix this?
If you want to keep your Closure syntax approach, you could change the body of your call to anything that essentially stores values in the delegated args map:
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'
)
}
Another way would be to have your call take in the Map as an argument and remove your 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'
)
There are also some other ways you could model your API, it will depend on the UX you want to provide.
Side note around declarative pipelines in shared library code:
It's worth keeping in mind the limitations of declarative pipelines in shared libraries. It looks like you are already doing it in vars, but I'm just adding it here for completeness. At the very end of the 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.
I am calling an environment variable outside handler and initializing a variable to be available in a global scope so multiple methods can reuse them.my handler is triggered by s3 event hence environment variable that i set outside handler become useless, can anyone let me know if its possible to have a variable initialized outside handler
Thanks
masterkey = environ['accesstoken']
`print("environment variable: " + environ['accesstoken'])
def lambda_handler(event, context):
........
........
I have code based on "structured DSL" concept.
// vars/buildStuff.groovy
def call(body) {
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
body()
node {
assert env
assert params
doStuff()
}
}
In this code I can access env and params directly, as expected.
However in the top level Jenkinsfile:
buildStuff {
someParam=params.SOME_PARAM
buildId=env.BUILD_ID
}
Causes java.lang.NullPointerException: Cannot get property 'SOME_PARAM' on null object. I have to work around that by writing this as:
buildStuff {
someParam=this.params.SOME_PARAM
buildId=this.env.BUILD_ID
}
Why is that the case? According to all examples in Pipelines documentation I should be able to access env and params directly.
What am I doing wrong?
It's an issue with the resolveStrategy.
def config = [:]
body.resolveStrategy = Closure.DELEGATE_FIRST
body.delegate = config
The config you provide resolves any property to its value or null, thus the owner is not queried for it. In you example the owner is just this. That's why it works.
Depending on what you're actually trying to achieve, OWNER_FIRST might be a better strategy. If you cannot change this, better use a data structure without defaults for properties.