jenkins Slave API setLabelString adds to User list - groovy

I have the following script under a JOB, which when run adds the userId to the label of the slave.
import jenkins.model.Jenkins
import hudson.model.User
import hudson.security.Permission
import hudson.EnvVars
EnvVars envVars = build.getEnvironment(listener);
def userId= envVars .get('BUILD_USER_ID')
def nodeName= envVars .get('NODE_NAME')
def nodeOffpool= envVars .get('NODE_GOING_OFFPOOL')
allUsers = User.getAll()
println allUsers
println ""
// add userid as a label if doesnot exist
for (slave in hudson.model.Hudson.instance.slaves) {
if( slave.nodeName.equals(nodeOffpool)) {
def labelList = (slave.getLabelString()).split()
println labelList
// check for user access to machine
for(label in labelList) {
println (User.get(label))
println (User.get(label) in allUsers)
if (User.get(label) in allUsers) {
if (label == userId) {
println ("This Node has already been assigned to you ($userId)")
} else {
println ("This Node($nodeOffpool) has already been assigned to someone($label) else, you cannot use it now")
println ("Please ask the user $label to release the Node($nodeOffpool) for you($label) to run")
}
return
}
};
println ("before: " + slave.getLabelString())
// setting the slave with new label
String newLabel = slave.getLabelString() + " " + userId
slave.setLabelString(newLabel)
println ("after: " + slave.getLabelString())
}
}
When i run for the first time output looks fine
[user1, user2, SYSTEM, unknown]
[TESTLAB3, TESTLAB4]
TESTLAB3
false
TESTLAB4
false
before: TESTLAB3 TESTLAB4
after: TESTLAB3 TESTLAB4 user1
When i run the second time
[user1, user2, SYSTEM, unknown, TESTLAB3, TESTLAB4]
[TESTLAB3, TESTLAB4, user1]
TESTLAB3
true
This Node(node1) has already been assigned to someone(TESTLAB3) else, you cannot use it now
Please ask the user TESTLAB3 to release the Node(node1) for you(TESTLAB3) to run
Finished: SUCCESS
Is this issue with Jenkins API. I am using Jenkins 1.573
related questions:
How to identify admins on Jenkins using groovy scripts
jenkins change label as requested
Updated:
I found out the answer by trail and error.
User.get(label)
adds the label as user if it doesnt exist by default. To prevent this addition, we have to use
User.get(label, false)

You are adding the username to the slave's label. Printing User.getAll() just prints the list of users for your Jenkins, not the slave's labels, which you modified with the user name.
Here is a script you can use to test if your slave machines have the new labels added to them:
for (slave in jenkins.model.Jenkins.instance.slaves) {
print "Slave: " + slave.getNodeName() + "\n";
print "Label: " + slave.getLabelString() + "\n\n";
}
Also, you might want to change the script so that it only sets the new label for the Slave if it doesn't already contain the user, because you will be adding it multiple times every time the script is run the way it's written now.

Related

Populate choice parameter in jenkinsfile with list of folders in SCM repository

I've got a Jenkinsfile that drives a pipeline which the user must select a specific folder in a bitbucket repo to target. I want that choice parameter dropdown to be dynamically populated.
Currently, I've got the choice param list hardcoded as per this generic example:
choice(name: 'IMAGE', choices: ['workload-x','workload-y','workload-z'])
I wondered if this is possible from within the jenkinsfile itself, or whether I'd have to create a specific groovy script for this then call it. Either way I'm a bit lost as I'm pretty new to jenkins and have only just started working with Jenkinsfiles.
Some trial and error googling has allowed me to create a groovy script which returns an array of folder names in the repository using json slurper:
import groovy.json.JsonSlurper
import jenkins.model.Jenkins
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.Credentials.class,
Jenkins.instance,
null,
null
);
def credential = creds.find {it.id == "MYBITBUCKETCRED"}
if (!credential) { return "Unable to pickup credential from Jenkins" }
username = credential.username
pass = credential.password.toString()
def urlStr = "https://bitbucket.mydomain.com/rest/api/1.0/projects/MYPROJECT/repos/MYREPO/browse/"
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection()
String encoded = Base64.getEncoder().encodeToString((username + ":" + pass).getBytes("UTF-8"));
conn.setRequestProperty("Authorization", "Basic " + encoded);
conn.connect();
def slurper = new JsonSlurper()
def browseList = slurper.parseText(conn.getInputStream().getText())
def dfList = browseList.children.values.path.name.findAll {it.contains('workload-')}
return dfList
This returns a result as follows:
Result: [workload-a,workload-b,workload-c,workload-x,workload-y,workload-z]
However I'm unsure how then to call this in my Jenkinsfile in order to populate the dropdown.
Any help would be greatly appreciated.
you can do it this way (follow my example below) or make use of Active Choice Jenkins Plugins - because It allows some groovy scripting to prepare your choice
Note- The Choice parameter will be available after a first run.
def choiceArray = []
node {
checkout scm
def folders = sh(returnStdout: true, script: "ls $WORKSPACE")
folders.split().each {
//condition to skip files if any
choiceArray << it
}
}
pipeline {
agent any;
parameters { choice(name: 'CHOICES', choices: choiceArray, description: 'Please Select One') }
stages {
stage('debug') {
steps {
echo "Selected choice is : ${params.CHOICES}"
}
}
}
}
I used the previous example to load the list in the same build
I send the command output to a file, to be consumed in the next step
Here the jenkinsfile:
def choiceArray = []
pipeline {
agent any
parameters {
string defaultValue: '', description: 'PATH_to_scripts', name: 'SCRIPTPATH', trim: false
string defaultValue: '', description: 'Optional parameters', name: 'MOREparams', trim: false
}
stages {
stage('show available date') {
steps {
sh '''
echo $JENKINS_HOME
cd $JENKINS_HOME/scripts
./0_db_show_available.sh $DBlike $AWS_PROFILE $DATEFILTER > $JENKINS_HOME/scripts/lista.txt
echo "Show available date"
cat $JENKINS_HOME/scripts/lista.txt
'''
}
}
stage('Restore') {
steps {
script {
def folders = sh(returnStdout: true, script: "cat $JENKINS_HOME/scripts/lista.txt")
//load the array using the file content (lista.txt)
folders.split().each {
choiceArray << it
}
// wait for user input
def INPUT_DATE = input message: 'Please select date', ok: 'Next',
//generate the list using the array content
parameters: [ choice(name: 'CHOICES', choices: choiceArray, description: 'Please Select One') ]
}
}
}
}
}
samit-kumar-patel, thanks for first example, it helps me

Groovy code not returning true for folders

groovy code for jenkinspipeline to check for directory or not, is giving false even if it is directoy.
def folderExists(folderName) {
if(fileExists(folderName))
{
def file = new File(folderName)
echo "file/folder exists: "+folderName
echo "Is directry "+ file.isDirectory().toString()
return file.isDirectory()
}
else{
echo "Not found: "+folderName
return false
}
}
Any suggestions??
As far as I remember, Java's File object either won't work in sandboxed mode, or will run only on master node. If you want to check for folder on a slave node, you need to invoke Pipeline methods, or pure shell.
The below code works for me. You had echo instead of println, I'm not au fait with Groovy in Jenkins but in pure Groovy there is no echo that I am aware of. Also, i moved the statement def file = new File(folderName) to before the if decision.
def folderExists(folderName) {
def file = new File(folderName)
if(file.exists())
{
println "file/folder exists: "+folderName
println "Is directry "+ file.isDirectory().toString()
return file.isDirectory()
}
else{
println "Not found: "+folderName
return false
}
}
println folderExists('C:\\temp')

How can I retrieve the build parameters from a queued job?

I would like to write a system groovy script which inspects the queued jobs in Jenkins, and extracts the build parameters (and build cause as a bonus) supplied as the job was scheduled. Ideas?
Specifically:
def q = Jenkins.instance.queue
q.items.each { println it.task.name }
retrieves the queued items. I can't for the life of me figure out where the build parameters live.
The closest I am getting is this:
def q = Jenkins.instance.queue
q.items.each {
println("${it.task.name}:")
it.task.properties.each { key, val ->
println(" ${key}=${val}")
}
}
This gets me this:
4.1.next-build-launcher:
com.sonyericsson.jenkins.plugins.bfa.model.ScannerJobProperty$ScannerJobPropertyDescriptor#b299407=com.sonyericsson.jenkins.plugins.bfa.model.ScannerJobProperty#5e04bfd7
com.chikli.hudson.plugin.naginator.NaginatorOptOutProperty$DescriptorImpl#40d04eaa=com.chikli.hudson.plugin.naginator.NaginatorOptOutProperty#16b308db
hudson.model.ParametersDefinitionProperty$DescriptorImpl#b744c43=hudson.mod el.ParametersDefinitionProperty#440a6d81
...
The params property of the queue element itself contains a string with the parameters in a property file format -- key=value with multiple parameters separated by newlines.
def q = Jenkins.instance.queue
q.items.each {
println("${it.task.name}:")
println("Parameters: ${it.params}")
}
yields:
dbacher params:
Parameters:
MyParameter=Hello world
BoolParameter=true
I'm no Groovy expert, but when exploring the Jenkins scripting interface, I've found the following functions to be very helpful:
def showProps(inst, prefix="Properties:") {
println prefix
for (prop in inst.properties) {
def pc = ""
if (prop.value != null) {
pc = prop.value.class
}
println(" $prop.key : $prop.value ($pc)")
}
}
def showMethods(inst, prefix="Methods:") {
println prefix
inst.metaClass.methods.name.unique().each {
println " $it"
}
}
The showProps function reveals that the queue element has another property named causes that you'll need to do some more decoding on:
causes : [hudson.model.Cause$UserIdCause#56af8f1c] (class java.util.Collections$UnmodifiableRandomAccessList)

"cancel" variable in groovy script not working in Jenkins Email-ext plugin

I'm having an issue where I want to cancel sending an email when a job gets into the FIXED state from the UNSTABLE state. So if a job was failing but got fixed, I want to send the message. But if a job is unstable (tests not passing) and gets fixed, don't send the email. It might be that a job is marked UNSTABLE after it has been in FAILURE state. When it goes to SUCCESS (i.e. it is fixed) I want an email in that case. You can see the cases in the code below.
My problem is that when I set the variable cancel to true by definition[1] it should cancel the email, but it doesn't. The email gets sent every time. Of course I'm using triggers for "Failure", "Still failing" and "Fixed".
Jenkins version: 1.533. Email-ext version: 2.37.2.2
// The goal of this script is to block sending the 'FIXED' message
// when the status goes from 'UNSTABLE' to 'SUCCESS'
//
// These are the cases (where F=FAILURE, U=UNSTABLE, S=SUCCESS)
// S - S : no msg (previous state: S, current state: S)
// F - S : msg
// S - U ... U - S : no msg <-- this is the one we need to avoid sending an email
// F - U ... U - S : msg
logger.println("Entering pre-send script")
// variable definitions
def keepGoing= true
def cancelEmail = false
// object to current job
job = hudson.model.Hudson.instance.getItem("incr-build-master")
// current build number
buildNumber = build.getNumber()
logger.println("Current build number: " + buildNumber)
// if the build failed or is unstable don't to anything,
// the specific triggers should take care of the messages
if (build.result.toString().equals("SUCCESS"))
{
logger.println("Build is successful. Procesing...")
while( keepGoing )
{
// get the number of the next past build
pastBuild = job.getBuildByNumber(--buildNumber)
buildResult = pastBuild.result.toString()
switch ( buildResult )
{
case "SUCCESS":
// if the previous non-unstable build was successful
// don't send a 'FIXED' message
cancelEmail = true
keepGoing = false
logger.println("Cancel sending email")
break
case "FAILURE":
// here we exit, but we will send the 'FIXED' message
keepGoing = false
logger.println("Send email")
break
case "UNSTABLE":
// let us keep looking until we find a previous build
// that is either 'SUCCESS' or 'FAILURE*
logger.println("Build " + buildNumber + " is unstable")
break
default:
logger.println("Error in script: result string is wrong - " + buildResult)
return
}
}
}
logger.println("Emailed canceled?: " + cancelEmail)
cancel=cancelEmail
logger.println("Exiting pre-send script")
[1] from the Help icon: "You may also cancel sending the email by setting the boolean variable "cancel" to true."
I encountered the same problem and found solution in a couple of days.
"cancel" only works when it is used in the last line of code.
This will cancel the build:
changed = false
files = 5
cancel = true
This will not:
changed = false
cancel = true
files = 5
And this will also do cancel:
changed = false
files = 5
if (files > 2) {
cancel = true
}
I hope this will save some time for somebody.
I am having a similar problem.
In the Pre-send script I put:
if ((build.getNumber() % 2) == 0) {
cancel=true;
} else {
cancel=false;
}
logger.println("cancel = " + cancel);
I get e-mail, with the build.log file attached, that shows the "cancel = true" and "cancel = false" cases.

Do not delete a Jenkins build if it's marked as "Keep this build forever" - Groovy script to delete Jenkins builds

I have the following Groovy script which deletes all builds of a given Jenkins job except one build number that user provides (i.e. wants to retain).
/*** BEGIN META {
"name" : "Bulk Delete Builds except the given build number",
"comment" : "For a given job and a given build number, delete all build except the user provided one.",
"parameters" : [ 'jobName', 'buildNumber' ],
"core": "1.409",
"authors" : [
{ name : "Arun Sangal" }
]
} END META **/
// NOTE: Uncomment parameters below if not using Scriptler >= 2.0, or if you're just pasting the script in manually.
// ----- Logic in this script takes 5000 as the infinite number, decrease / increase this value from your own experience.
// The name of the job.
//def jobName = "some-job"
// The range of build numbers to delete.
//def buildNumber = "5"
def lastBuildNumber = buildNumber.toInteger() - 1;
def nextBuildNumber = buildNumber.toInteger() + 1;
import jenkins.model.*;
import hudson.model.Fingerprint.RangeSet;
def jij = jenkins.model.Jenkins.instance.getItem(jobName);
println("Keeping Job_Name: ${jobName} and build Number: ${buildNumber}");
println ""
def setBuildRange = "1-${lastBuildNumber}"
def range = RangeSet.fromString(setBuildRange, true);
jij.getBuilds(range).each { it.delete() }
println("Builds have been deleted - Range: " + setBuildRange)
setBuildRange = "${nextBuildNumber}-5000"
range = RangeSet.fromString(setBuildRange, true);
jij.getBuilds(range).each { it.delete() }
println("Builds have been deleted - Range: " + setBuildRange)
This works well for any Jenkins job. For ex: If your Jenkins job name is "TestJob" and you have 15 builds i.e. build# 1 to build 15 in Jenkins, and you want to delete all but retain build# 13, then this script will delete the builds (build# 1-12 and 14-15 - even if you mark any build as "Keep this build forever") and only keep build#13.
Now, what I want is:
what should I change in this script to not delete a build - if a build is marked in Jenkins as "Keep this build forever". I tried the script and it deleted that keep forever build too.
Lets say, if I'm using "Build name setter plugin" in Jenkins, which can give me build names as what name I want i.e. instead of getting just build as build#1 or #2, or #15, I will get build as build# 2.75.0.1, 2.75.0.2, 2.75.0.3, ..... , 2.75.0.15 (as I would have set the build name/description as use some variable which contains 2.75.0 (as a release version value) and suffixed it with the actual Jenkins job's build number i.e. the last 4th digit - ex: set the name as:
${ENV,var="somepropertyvariable"}.${BUILD_NUMBER}
In this case, I'll start getting Jenkins builds as 2.75.0.1 to 2.75.0.x (where x is the last build# of that release (2.75.0)). Similarly, when I'll change the property release version to next i.e. 2.75.1 or 2.76.0, then the same Jenkins job will start giving me builds as 2.75.1.0, 2.75.1.1, ...., 2.75.1.x or 2.76.0.1, 2.76.0.2, ...., 2.76.0.x and so on. During the release version change, let say, our build will start from 1 again (as I mentioned above for 2.75.1 and 2.76.0 release versions).
In this case, if my Jenkins job's build history (shows all builds for 2.75.0.x, 2.75.1.x and 2.76.0.x), then what change should I make in this script to include a 3rd parameter/argument. This 3rd argument will take release /version value i.e. either 2.75.0 or 2.75.1 or 2.76.0 and then this script should delete build numbers on that release only (and should NOT delete other release's builds).
If you want to test whether a build has been marked permanent, use
if (!build.isKeepLog()) {
// Build can be deleted
} else {
// Build is marked permanent
}
I think you should be able to use the getName() method on each build to check whether you should delete a given build. The API JavaDoc can be fairly obscure, so I often go on GitHub and look through the code for a Jenkins plugin that's doing something similar to what I need. The public Scriptler repository can be useful, too.
Final Answer: This includes deleting the build artifacts from Artifactory as well using Artifactor's REST API call. This script will delete Jenkins/Artifactory builds/artifacts of a given Release/Version (as sometimes over the time - a given Jenkins job can create multiple release / version builds for ex: 2.75.0.1, 2.75.0.2, 2.75.0.3,....,2.75.0.54, 2.76.0.1, 2.76.0.2, ..., 2.76.0.16, 2.76.1.1, 2.76.1.2, ...., 2.76.1.5). In this case, for every new release of that job, we start the build# from 1 fresh. If you have to delete the all builds except one / even all (change the script a little bit for your own needs) and don't change older/other release builds, then use the following script.
Scriptler Catalog link: http://scriptlerweb.appspot.com/script/show/103001
Enjoy!
/*** BEGIN META {
"name" : "Bulk Delete Builds except the given build number",
"comment" : "For a given job and a given build numnber, delete all builds of a given release version (M.m.interim) only and except the user provided one. Sometimes a Jenkins job use Build Name setter plugin and same job generates 2.75.0.1 and 2.76.0.43",
"parameters" : [ 'jobName', 'releaseVersion', 'buildNumber' ],
"core": "1.409",
"authors" : [
{ name : "Arun Sangal - Maddys Version" }
]
} END META **/
import groovy.json.*
import jenkins.model.*;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.Job;
import hudson.model.Fingerprint;
//these should be passed in as arguments to the script
if(!artifactoryURL) throw new Exception("artifactoryURL not provided")
if(!artifactoryUser) throw new Exception("artifactoryUser not provided")
if(!artifactoryPassword) throw new Exception("artifactoryPassword not provided")
def authString = "${artifactoryUser}:${artifactoryPassword}".getBytes().encodeBase64().toString()
def artifactorySettings = [artifactoryURL: artifactoryURL, authString: authString]
if(!jobName) throw new Exception("jobName not provided")
if(!buildNumber) throw new Exception("buildNumber not provided")
def lastBuildNumber = buildNumber.toInteger() - 1;
def nextBuildNumber = buildNumber.toInteger() + 1;
def jij = jenkins.model.Jenkins.instance.getItem(jobName);
def promotedBuildRange = new Fingerprint.RangeSet()
promotedBuildRange.add(buildNumber.toInteger())
def promoteBuildsList = jij.getBuilds(promotedBuildRange)
assert promoteBuildsList.size() == 1
def promotedBuild = promoteBuildsList[0]
// The release / version of a Jenkins job - i.e. in case you use "Build name" setter plugin in Jenkins for getting builds like 2.75.0.1, 2.75.0.2, .. , 2.75.0.15 etc.
// and over the time, change the release/version value (2.75.0) to a newer value i.e. 2.75.1 or 2.76.0 and start builds of this new release/version from #1 onwards.
def releaseVersion = promotedBuild.getDisplayName().split("\\.")[0..2].join(".")
println ""
println("- Jenkins Job_Name: ${jobName} -- Version: ${releaseVersion} -- Keep Build Number: ${buildNumber}");
println ""
/** delete the indicated build and its artifacts from artifactory */
def deleteBuildFromArtifactory(String jobName, int deleteBuildNumber, Map<String, String> artifactorySettings){
println " ## Deleting >>>>>>>>>: - ${jobName}:${deleteBuildNumber} from artifactory"
def artifactSearchUri = "api/build/${jobName}?buildNumbers=${deleteBuildNumber}&artifacts=1"
def conn = "${artifactorySettings['artifactoryURL']}/${artifactSearchUri}".toURL().openConnection()
conn.setRequestProperty("Authorization", "Basic " + artifactorySettings['authString']);
conn.setRequestMethod("DELETE")
if( conn.responseCode != 200 ) {
println "Failed to delete the build artifacts from artifactory for ${jobName}/${deleteBuildNumber}: ${conn.responseCode} - ${conn.responseMessage}"
}
}
/** delete all builds in the indicated range that match the releaseVersion */
def deleteBuildsInRange(String buildRange, String releaseVersion, Job theJob, Map<String, String> artifactorySettings){
def range = RangeSet.fromString(buildRange, true);
theJob.getBuilds(range).each {
if ( it.getDisplayName().find(/${releaseVersion}.*/)) {
println " ## Deleting >>>>>>>>>: " + it.getDisplayName();
deleteBuildFromArtifactory(theJob.name, it.number, artifactorySettings)
it.delete();
}
}
}
//delete all the matching builds before the promoted build number
deleteBuildsInRange("1-${lastBuildNumber}", releaseVersion, jij, artifactorySettings)
//delete all the matching builds after the promoted build number
deleteBuildsInRange("${nextBuildNumber}-${jij.nextBuildNumber}", releaseVersion, jij, artifactorySettings)
println ""
println("- Builds have been successfully deleted for the above mentioned release: ${releaseVersion}")
println ""
OK - Solution for my question 2 is here: I'm still working on fixing question 1.
http://scriptlerweb.appspot.com/script/show/102001
bulkDeleteJenkinsBuildsExceptOne_OfAGivenRelease.groovy
/*** BEGIN META {
"name" : "Bulk Delete Builds except the given build number",
"comment" : "For a given job and a given build numnber, delete all builds of a given release version (M.m.interim) only and except the user provided one. Sometimes a Jenkins job use Build Name setter plugin and same job generates 2.75.0.1 and 2.76.0.43",
"parameters" : [ 'jobName', 'releaseVersion', 'buildNumber' ],
"core": "1.409",
"authors" : [
{ name : "Arun Sangal" }
]
} END META **/
// NOTE: Uncomment parameters below if not using Scriptler >= 2.0, or if you're just pasting the script in manually.
// ----- Logic in this script takes 5000 as the infinite number, decrease / increase this value from your own experience.
// The name of the job.
//def jobName = "some-job"
// The release / version of a Jenkins job - i.e. in case you use "Build name" setter plugin in Jenkins for getting builds like 2.75.0.1, 2.75.0.2, .. , 2.75.0.15 etc.
// and over the time, change the release/version value (2.75.0) to a newer value i.e. 2.75.1 or 2.76.0 and start builds of this new release/version from #1 onwards.
//def releaseVersion = "2.75.0"
// The range of build numbers to delete.
//def buildNumber = "5"
def lastBuildNumber = buildNumber.toInteger() - 1;
def nextBuildNumber = buildNumber.toInteger() + 1;
import jenkins.model.*;
import hudson.model.Fingerprint.RangeSet;
def jij = jenkins.model.Jenkins.instance.getItem(jobName);
//def build = jij.getLastBuild();
println ""
println("- Jenkins Job_Name: ${jobName} -- Version: ${releaseVersion} -- Keep Build Number: ${buildNumber}");
println ""
println " -- Range before given build number: ${buildNumber}"
println ""
def setBuildRange = "1-${lastBuildNumber}"
def range = RangeSet.fromString(setBuildRange, true);
jij.getBuilds(range).each {
if ( it.getDisplayName().find(/${releaseVersion}.*/)) {
println " ## Deleting >>>>>>>>>: " + it.getDisplayName();
// Trying to find - how to NOT delete a build in Jenkins if it's marked as "keep this build forever". If someone has an idea, please update this script with a newer version in GitHub.
//if ( !build.isKeepLog()) {
it.delete();
//} else {
// println "build -- can't be deleted as :" + build.getWhyKeepLog();
//}
}
}
println ""
println " -- Range after given build number: ${buildNumber}"
println ""
setBuildRange = "${nextBuildNumber}-5000"
range = RangeSet.fromString(setBuildRange, true);
jij.getBuilds(range).each {
if ( it.getDisplayName().find(/${releaseVersion}.*/)) {
println " ## Deleting >>>>>>>>>: " + it.getDisplayName();
it.delete();
}
}
println ""
println("- Builds have been successfully deleted for the above mentioned release: ${releaseVersion}")
println ""
One can also call this script via a Jenkins job (requires 3 parameters as mentioned in the scriptler script) -OR call it from browser as well: using the following link:
http ://YourJenkinsServerName:PORT/job/Some_Jenkins_Job_That_You_Will_Create/buildWithParameters?jobName=Test_AppSvc&releaseVersion=2.75.0&buildNumber=15

Resources