Get Jenkins upstream jobs - groovy

I would like to get all the upstream jobs, just like in the console output:
Started by upstream project "allocate" build number 31
originally caused by:
Started by upstream project "start" build number 12
originally caused by:
I've tried groovy postbuild with the following:
def build = Thread.currentThread().executable
def causes= manager.build.getCauses()
for (cause in causes)
{
manager.listener.logger.println "upstream build: " + cause.getShortDescription()
}
but then I only get "allocate", not the "start" job.
I've also tried
def build = Thread.currentThread().executable
def test = build.getUpstreamBuilds()
for (up in test)
{
manager.listener.logger.println "test build project: " + up
}
but this is empty...
Any ideas?

You were close with your first solution.
Actually, what you need to do is iterate over the ancestor of this Cause depending on it's type.
Here is a sample snippet of code that could get you started :
def printCausesRecursively(cause) {
if (cause.class.toString().contains("UpstreamCause")) {
println "This job was caused by " + cause.toString()
for (upCause in cause.upstreamCauses) {
printCausesRecursively(upCause)
}
} else {
println "Root cause : " + cause.toString()
}
}
for (cause in manager.build.causes)
{
printCausesRecursively(cause)
}
You may want to refer to the documentation to handle all Cause types : http://javadoc.jenkins-ci.org/hudson/model/Cause.html
Hope it helps,
Best

Related

Jenkinsfile Pipeline DSL: How to Show Multi-Columns in Jobs dashboard GUI - For all Dynamically created stages - When within PIPELINE section

Jenkins 2.89.4 rolling
I saw almost all stackoverflow posts which show how we can successfully run parallel steps/stages (using list/maps etc) --OR hardcoding them directly --OR even create dynamic stages for Jenkinsfile (as seen in this post: Scripted jenkinsfile parallel stage)
My requirements are:
A pipeline which builds N. no of projects under "BUILD" steps i.e. parallel builds on each of those projects. i.e. it runs Gradle on all N projects. Here I have a Jenkinsfile which was created by a declarative JOB DSL Groovy. Here my Gradle projects are not set as multi-projects so I can't call the top level gradle and say, Gradle please do your parallel magic (within Gradle).
I want to run build of these N projects in their own separate parallel dynamically created stages (GUI Columns) as seen in jenkins job's dashboard.
I want to see the output of (Gradle build/console) each project's build separately i.e. I don't want to mix the console output of each projects build which are running in parallel in just ONE COLUMN (i.e. column named BUILD).
In this URL https://jenkins.io/blog/2017/09/25/declarative-1/ I see, how you can run parallel stages/steps but in doing so, either it's mixing those parallel step's output in just one column (I mean under BUILD column) --OR if you want it under separate stages/columns (i.e. the post says Test on Linux or Windows separately then you are still hard-coding all the stages/steps in Jenkinsfile early on (rather than using just a list or array hash which I'd prefer to update for adding more or less stages / parallel steps as in my case, they all follow the same standard). What I want is to just update in one place How many steps and what are all of the stages in just one place (list/array).
I'm not using Jenkins Blue Ocean for now.
Usually if you have parallel steps within a stage, their console std output for all steps gets mixed into one console output / stage/column when you click to see the console output for that given parallel step/stage; When you hover over the BUILD column (assuming there were parallel steps in BUILD stage) in job's dashboard (std output for all those steps is mixed up and very hard to see individual project step's console output just for a given step/stage).
If we want to create separate stages (dynamically) then Jenkins should be able to show console output of a given step/dynamic stage within the parallel section (i.e. each column should show their own project's build console output).
Using the above URL, I'm able to do the following after trying this script:
// main script block
// could use eg. params.parallel build parameter to choose parallel/serial
def runParallel = true
def buildStages
node('master') {
stage('Initializing Parallel Dynamic Stages') {
// Set up List<Map<String,Closure>> describing the builds
buildStages = prepareBuildStages()
println("Initialised pipeline.")
}
for (builds in buildStages) {
if (runParallel) {
parallel(builds)
} else {
// run serially (nb. Map is unordered! )
for (build in builds.values()) {
build.call()
}
}
}
stage('Done') {
println('The whole SHENZI is complete.')
}
}
// Create List of build stages to suit
def prepareBuildStages() {
def buildList = []
for (i=1; i<4; i++) {
def buildStages = [:]
for (name in [ 'Alpha', 'Tango', 'Chyarli' ] ) {
def n = "${name} ${i}"
buildStages.put(n, prepareOneBuildStage(n))
}
buildList.add(buildStages)
}
return buildList
}
def prepareOneBuildStage(String name) {
def proj_name = name.split(' ')[0]
def proj_parallel_sub_step = name.split(' ')[1]
//Return the whole chunkoni
return {
stage("Build\nProject-${proj_name}\nStep ${proj_parallel_sub_step}") {
println("Building ${proj_name} - ${proj_parallel_sub_step}")
sh(script:'sleep 15', returnStatus:true)
}
}
}
When I'm putting the above Groovy Script (which is creating DYNAMIC Stages) inside Pipeline Script or Pipeline Script from SCM (i.e. the same code available in a .groovy file) -- it runs successfully and creates dynamic stages under BUILD step for each of the 3 projects and runs 3 steps (Nth) for all 3 projects in parallel and then starts the next Nth step for all 3 projects and so on.
If you see below, we also got individual columns in Jenkins job dashboard for them.
Now, When I put the above script in Jenkinsfile (Pipeline DSL) where I have pipeline { .... } section, it's not working and giving me the following error.
Using my JOB DSL, I created a new Jenkins Pipeline job where Pipeline Script from SCM calls a groovy file (which now contains):
//----------------------------------------------------
// Both - Parallel Run and GUI View in JF Jenkins job.
//----------------------------------------------------
def runParallel = true
def buildStages
def wkspace = /var/lib/jenkins/workspaces/ignore_this_variale_or_its_value_for_now
// Create List of build stages to suit
def prepareBuildStages() {
def buildList = []
for (i=1; i<3; i++) {
def buildStages = [:]
for (name in [ 'Alpha', 'Tango', 'Chyarli' ] ) {
def n = "${name} ${i}"
buildStages.put(n, prepareOneBuildStage(n))
}
buildList.add(buildStages)
}
return buildList
}
//---
def prepareOneBuildStage(String name) {
def proj_name = name.split(' ')[0]
def proj_parallel_sub_step = name.split(' ')[1]
// return the whole chunkoni (i.e. for a given stage) - will be named dynamically.
return {
stage("Build\nProject-${proj_name}\nStep ${proj_parallel_sub_step}") {
println("Building ${proj_name} - ${proj_parallel_sub_step}")
sh(script:'sleep 15', returnStatus:true)
}
}
}
// Set up List<Map<String,Closure>> describing the builds
buildStages = prepareBuildStages()
//---------------------
String jenkinsBaselines
// SEE NOW --- we have this section called 'pipeline'
pipeline {
agent {
node {
label 'rhat6'
customWorkspace wkspace
}
}
options {
ansiColor('xterm')
timeout(time: 8, unit: 'HOURS')
skipDefaultCheckout()
timestamps()
}
environment {
someEnvVar = 'aValue'
}
//------------- Stages
stages {
stage('Initializing Parallel Dynamic Stages') {
// Set up List<Map<String,Closure>> describing the builds
println("Initialised pipeline.")
}
for (builds in buildStages) {
if (runParallel) {
parallel(builds)
} else {
// run serially (nb. Map is unordered! )
for (build in builds.values()) {
build.call()
}
}
}
stage('Done') {
println('The whole SHENZI is complete.')
}
}
//---------------------
}
Running the Jenkinsfile Jenkins job now gives me this error:
[BFA] Scanning build for known causes...
[BFA] No failure causes found
[BFA] Done. 1s
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
WorkflowScript: 69: Not a valid stage section definition: "buildStages = prepareBuildStages()". Some extra configuration is required. # line 69, column 5.
stage('Initializing Parallel Dynamic Stages') {
^
WorkflowScript: 69: Unknown stage section "println". Starting with version 0.5, steps in a stage must be in a steps block. # line 69, column 5.
stage('Initializing Parallel Dynamic Stages') {
^
WorkflowScript: 75: Expected a stage # line 75, column 5.
for (builds in buildStages) {
^
WorkflowScript: 86: Unknown stage section "println". Starting with version 0.5, steps in a stage must be in a steps block. # line 86, column 5.
stage('Done') {
^
WorkflowScript: 69: No "steps" or "parallel" to execute within stage "Initializing Parallel Dynamic Stages" # line 69, column 5.
stage('Initializing Parallel Dynamic Stages') {
^
WorkflowScript: 86: No "steps" or "parallel" to execute within stage "Done" # line 86, column 5.
stage('Done') {
^
6 errors
at org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:310)
at org.codehaus.groovy.control.CompilationUnit.applyToPrimaryClassNodes(CompilationUnit.java:1085)
at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:603)
at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:581)
at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:558)
at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:298)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:268)
at groovy.lang.GroovyShell.parseClass(GroovyShell.java:688)
at groovy.lang.GroovyShell.parse(GroovyShell.java:700)
at org.jenkinsci.plugins.workflow.cps.CpsGroovyShell.doParse(CpsGroovyShell.java:133)
at org.jenkinsci.plugins.workflow.cps.CpsGroovyShell.reparse(CpsGroovyShell.java:127)
at org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.parseScript(CpsFlowExecution.java:557)
at org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.start(CpsFlowExecution.java:518)
at org.jenkinsci.plugins.workflow.job.WorkflowRun.run(WorkflowRun.java:290)
at hudson.model.ResourceController.execute(ResourceController.java:97)
at hudson.model.Executor.run(Executor.java:429)
Finished: FAILURE
How can I get this working in Jenkinsfile pipeline section and still able to get individual columns per dynamically created stage for a given project N and Step M?
Tried the following way, still errors the say way.
//------------- Stages
stages {
stage('Initializing Parallel Dynamic Stages') {
// Set up List<Map<String,Closure>> describing the builds
buildStages = prepareBuildStages()
println("Initialised pipeline.")
// tried this way too. within a stage
buildStages.each { bld -->
parallel(bld)
}
}
stage('Done') {
println('The whole SHENZI is complete.')
}
}
//---------------------
So I did some poking into this and got it working.
The new code within Jenkinsfile (for STAGES) is now:
//------------- Stages
stages {
stage('Start Pipeline') {
steps {
script {
sh "echo HELLO moto razr!"
}
}
}
stage('Initializing Parallel Dynamic Stages'){
steps {
script {
// Run all Nth step for all Projects in Parallel.
buildStages.each { bs -> parallel(bs) }
// OR uncomment the following code (if conditional on boolean variable).
/*
for (builds in buildStages) {
if (runParallel) {
parallel(builds)
} else {
// run serially (nb. Map is unordered! )
for (build in builds.values()) {
build.call()
}
}
}
*/
}
}
}
stage('Done') {
println('The whole SHENZI is complete.')
}
}
//---------------------
That's all it took to work.
For clear messages / stage names, I tweaked the function as well and We won't set this variable buildStages within pipeline { ... }
//---
def prepareOneBuildStage(String name) {
def proj_name = name.split(' ')[0]
def proj_parallel_sub_step = name.split(' ')[1]
// return the whole chunkoni (i.e. for a given stage) - will be named dynamically.
return {
stage("BUILD Project-${proj_name} Parallel_Step_${proj_parallel_sub_step}") {
println("Parallel_Step # ${proj_parallel_sub_step} of Project => ${proj_name}")
sh(script:"echo \"Parallel_Step # ${proj_parallel_sub_step} of Project => ${proj_name}\" && sleep 20", returnStatus:true)
// -- OR -- you can call Gradle task i.e. rpm / any other / any other command here.
}
}
}
// Set up List<Map<String,Closure>> describing the builds. section now.
buildStages = prepareBuildStages()
//---------------------
This implementation is now creating N no. of parallel stages i.e. a separate column per project for a given Nth step (when looking at Jenkinsfile job's dashboard) for P no. of projects.
It will run All P projects for a given Nth step in parallel.
It will wait for Nth step for all Projects to complete first and then jump to the next Nth step.
What this means is, if Project ALPHA Step #1 is complete, it'll still wait for all Step #1 of other 2 projects and then launch Step #2 of all projects in parallel.
Challenge: How can we make ALPHA Project's Step #2 to start as soon as ALPHA project's Step #1 is complete i.e. it won't wait for Step 1 of other 2 projects to complete and could possibly run Step #2 of ALPHA project 1 in parallel with Step N(=1) or N+1 of other projects.
This assumes all projects are independent of each other and projects don't share contents generated by a given project/their stage/steps in any other project/stage/step.
Depending upon your own requirements, you may want to wait (i.e. don't run Step 2 of all projects until Step 1 of all projects are fully complete) --OR-- you may want to run Step 2 of ALPHA project with let's say - Step 2 of TANGO project while project CHYARLI's step 1 is still in progress.
As the main scope of this post was to get separate dynamically created columns/stages per project (running in parallel within pipeline { ... } section), I think, I got what I was looking for.
NOTE: Go easy with parallel if you want to run concurrent builds of a pipeline. For more info about issues related to running parallel build actions concurrently, see here: Jenkins - java.lang.IllegalArgumentException: Last unit does not have enough valid bits & Gradle error: Task 'null' not found in root project
You can also run the stages parallel as follow
pipeline {
agent { node { label 'master' } }
stages {
stage('Add regression tests') {
steps {
script {
def testList = ["a", "b", "c", "d"]
def branches = [:]
for (int i = 0; i < 4 ; i++) {
int index=i, branch = i+1
stage ("branch_${branch}"){
branches["branch_${branch}"] = {
node ('master'){
sh "echo 'node: ${NODE_NAME}, index: ${index}, i: ${i}, testListVal: " + testList[index] + "'"
}
}
}
}
parallel branches
}
}
}
}
}

How do I implement a retry option for failed stages in Jenkins pipelines?

I have a Jenkinsfile with multiple stages and one of them is in fact another job (the deploy one) which can fail in some cases.
I know that I can made prompts using Jenkinsfile but I don't really know how to implement a retry mechanism for this job.
I want to be able to click on the failed stage and choose to retry it.
You should be able to combine retry + input to do that
Something like that
stage('deploy-test') {
try {
build 'yourJob'
} catch(error) {
echo "First build failed, let's retry if accepted"
retry(2) {
input "Retry the job ?"
build 'yourJob'
}
}
}
you could also use timeout for the input if you want it to finish if nobody validates.
There is also waitUntil that might be useful but i haven't used it yet
Edit :
WaitUntil seems definitely the best, you should play with it a bit but something like that is cleaner :
stage('deploy-test') {
waitUntil {
try {
build 'yourJob'
} catch(error) {
input "Retry the job ?"
false
}
}
}
By the way, there is doc all of the steps here https://jenkins.io/doc/pipeline/steps
This one with a nice incremental wait
stage('deploy-test') {
def retryAttempt = 0
retry(2) {
if (retryAttempt > 0) {
sleep(1000 * 2 + 2000 * retryAttempt)
}
retryAttempt = retryAttempt + 1
input "Retry the job ?"
build 'yourJob'
}
}
This gist (not mine) was one of the better options that I found while trying to implement this functionality too. https://gist.github.com/beercan1989/b66b7643b48434f5bdf7e1c87094acb9
Changed it to a method in a shared library that just did retry or abort for my needs. Also added a max retries and made the timeout variable so that we could change it depending on the job or stage that needs it.
package com.foo.bar.jenkins
def class PipelineHelper {
def steps
PipelineHelper(steps) {
this.steps = steps
}
void retryOrAbort(final Closure<?> action, int maxAttempts, int timeoutSeconds, final int count = 0) {
steps.echo "Trying action, attempt count is: ${count}"
try {
action.call();
} catch (final exception) {
steps.echo "${exception.toString()}"
steps.timeout(time: timeoutSeconds, unit: 'SECONDS') {
def userChoice = false
try {
userChoice = steps.input(message: 'Retry?', ok: 'Ok', parameters: [
[$class: 'BooleanParameterDefinition', defaultValue: true, description: '', name: 'Check to retry from failed stage']])
} catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) {
userChoice = false
}
if (userChoice) {
if (count <= maxAttempts) {
steps.echo "Retrying from failed stage."
return retryOrAbort(action, maxAttempts, timeoutMinutes, count + 1)
} else {
steps.echo "Max attempts reached. Will not retry."
throw exception
}
} else {
steps.echo 'Aborting'
throw exception;
}
}
}
}
}
Example usage with a max of 2 retries that waits for 60s for input.
def pipelineHelper = new PipelineHelper(this)
stage ('Retry Example'){
pipelineHelper.retryOrAbort({
node{
echo 'Here is an example'
throw new RuntimeException('This example will fail.')
}
}, 2, 60)
}
Just remember to put nodes inside of the closure so that waiting for an input doesn't block an executor.
If you have the paid jenkins enterprise Cloudbees has a Checkpoint plugin that can better handle this, but it is not planned to be release for open source Jenkins (JENKINS-33846).

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)

Jenkins Groovy Script finding null testResultAction for a successful run

We have an email report writer for test suites on jenkins. It uses a groovy script to find the correct reports and then make an HTML report detailing the test status, last time ran, links etc.
hudson.model.Hudson.instance.getItems(hudson.model.FreeStyleProject).each { project ->
if(project.name.contains(searchCriteria)){
if(project.lastBuild.testResultAction == null){
tr(){
td(project.name)
td(){
b("No Results")
}
...
}
}
else{
if(project.lastBuild.testResultAction.failCount > 0){
tr(){
td(project.name)
td(){
b(style:'color:red', "FAIL")
}
...
}
}
else{
tr(){
td(project.name)
td(){
b(style:'color:red', "PASS")
}
...
}
}
}
}
}
Usually everything runs fine, but recently one or two specific builds have started to be returned consistently as "No results" i.e. their .testResultAction is null. I've checked the actual value for testResultAction, and it is indeed a null, despite them running a clean test that Jenkins itself recognises as such.
The tests have been re-ran, and the jenkins build deleted and remade; neither helped. This problem seems to be haunting certain, unrelated, builds. Is there a particular flaw in Jenkins here that I should know about that causes the testResultAction to default to null and not change? Otherwise, can anyone suggest what might be causing this to happen, or how I can stop it?
That method is deprecated and was giving me null too. I had more success with this:
project.lastBuild.getAction(hudson.tasks.test.AggregatedTestResultAction.class)
Though it can be null just because there are no tests in the project.
Anyway, here's a method for testing the results, which works for me.
def reportOnTestsForBuild(build) {
testResultAction = build.getAction(hudson.tasks.test.AggregatedTestResultAction.class);
if (testResultAction == null) {
println("No tests")
return
}
childReports = testResultAction.getChildReports();
if (childReports == null || childReports.size() == 0) {
println("No child reports")
return
}
def failures = [:]
childReports.each { report ->
def result = report.result;
if (result == null) {
println("null result from child report")
}
else if (result.failCount < 1) {
println("result has no failures")
}
else {
println("overall fail count: ${result.failCount}")
failedTests = result.getFailedTests();
failedTests.each { test ->
failures.put(test.fullDisplayName, test)
println("Failed test: ${test.fullDisplayName}\n" +
"name: ${test.name}\n" +
"age: ${test.age}\n" +
"failCount: ${test.failCount}\n" +
"failedSince: ${test.failedSince}\n" +
"errorDetails: ${test.errorDetails}\n")
}
}
}
}

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