Cancel previous Jenkins job builds only if it's the same branch - jenkins

I have the following code to cancel a previous job build if a new one is started:
def cancelPreviousBuilds() {
def jobName = env.JOB_NAME
def buildNumber = env.BUILD_NUMBER.toInteger() /* Get job name */ def currentJob =
Jenkins.instance.getItemByFullName(jobName)
for (def build : currentJob.builds) {
if (build.isBuilding() && build.number.toInteger() != buildNumber) {
build.doStop()
}
}
}
However, I would like this to not cancel previous job builds if it's a different branch. For instance, if a job build on develop were kicked off and then another one on master, it would not cancel any job builds.

You can access the build parameters of each build and compare the relevant values.
You can use build.getAction(hudson.model.ParametersAction) to get the ParametersAction object from where you can search for parameters in the build object using the getParameter function.
Something like:
#NonCPS
def cancelPreviousBuilds() {
def buildNumber = env.BUILD_NUMBER.toInteger()
def currentJob = Jenkins.instance.getItemByFullName(env.JOB_NAME)
def currentBranch = <BRANCH_PARAM_NAME> // Branch value of the current build
for (def build : currentJob.builds) {
def param = build.getAction(hudson.model.ParametersAction).getParameter('<BRANCH_PARAM_NAME>')
if (build.isBuilding() && build.number.toInteger() > buildNumber && currentBranch == param.value) {
build.doStop()
}
}
Another small thing, in the build number comparison it is better to use build.number.toInteger() > buildNumber then build.number.toInteger() != buildNumber to prevent the case in which old builds cancel new started builds, as you usually want each build to affect only previous ones.

Related

Groovy script to implement "How to get a parameter depend of other parameter in Hudson or Jenkins"?

Jenkins v2.289.3
I'm trying to implement the Active Choices plugin discussed in one of the answers in in How to get a parameter depend of other parameter in Hudson or Jenkins, but unsure how to implement my second script to get the value from the first parameter.
I have a MyFolder job folder with multi-pipeline job called Builders, with branches like master, release/*, feature/*. So the full name of the jobs in the folder will be MyFolder/Builders/release%2F1.0.0 for the release/1.0.0 job for example (%2F is the escape character for /).
I then created a second job in the same folder called DeployBranchVersion, whose goal is to execute deployment code that deploys a chosen branch and one its corresponding successful build numbers. I therefore need to pass 2 parameters to the deployment code, GIT_BRANCH and VERSION.
My first Active Choices parameter gets these branches using the following script, and assigns the choice to the GIT_BRANCH parameter.
Job name: MyFolder/DeployBranchVersion
Parameter name: GIT_BRANCH
Groovy script:
def gettags = "git ls-remote -h -t https://username:password#bitbucket.org/organization/myrepo.git".execute()
def branches = []
def t1 = []
gettags.text.eachLine {branches.add(it)}
for(i in branches)
t1.add(i.split()[1].replaceAll('\\^\\{\\}', '').replaceAll('refs/heads/', '').replaceAll('refs/tags/', ''))
t1 = t1.unique()
return t1
This returns a drop-down list of the branches in my repo, and the chosen one is assigned to the GIT_BRANCH parameter.
Now how do I setup the second Active Choices Reactive parameter to reference the above choice? I have the following Groovy code that works in a non-Active-Choice parameter setup. How can I modify it to work in this case? The BUILD_JOB_NAME needs to reference the GIT_BRANCH value from the first parameter?
import hudson.model.*
BUILD_JOB_NAME = "some_reference_to_GIT_BRANCH" // ??????????
def getJobs() {
def hi = Hudson.instance
return hi.getItems(Job)
}
def getBuildJob() {
def buildJob = null
def jobs = getJobs()
(jobs).each { job ->
if (job.fullName == BUILD_JOB_NAME) {
buildJob = job
}
}
return buildJob
}
def getAllBuildNumbers(Job job) {
def buildNumbers = []
(job.getBuilds()).each { build ->
def status = build.getBuildStatusSummary().message
if (status.contains("stable") || status.contains("normal")) {
buildNumbers.add("${build.displayName}")
}
}
return buildNumbers
}
def buildJob = getBuildJob()
return getAllBuildNumbers(buildJob)
I tried setting it this way to ho avail.
BUILD_JOB_NAME = "MyFolder/Builders/$GIT_BRANCH"
Turns out I was doing it correctly, I just had a buggy 2nd script. Here's the good one. I realized that GIT_BRANCH values had / in them so I had to replace them with the equivalent escape character %2F.
import hudson.model.*
BRANCH = GIT_BRANCH.replaceAll("/", "%2F")
BUILD_JOB_NAME = "MyFolder/Builders/$BRANCH"
def getJobs() {
def hi = Hudson.instance
return hi.getAllItems(Job.class)
}
def getBuildJob() {
def buildJob = null
def jobs = getJobs()
(jobs).each { job ->
if (job.fullName == BUILD_JOB_NAME) {
buildJob = job
}
}
return buildJob
}
def getAllBuildNumbers(Job job) {
def buildNumbers = []
(job.getBuilds()).each { build ->
def status = build.getBuildStatusSummary().message
if ((status.contains("stable") || status.contains("normal")) &&
build.displayName.contains("-")) {
buildNumbers.add(build.displayName)
}
}
return buildNumbers
}
def buildJob = getBuildJob()
return getAllBuildNumbers(buildJob)

Cannot invoke method toInteger() on null object

I have the following code in a separate groovy file from my Jenkinsfile. It's supposed to cancel old build jobs once a new one is fired off. It also checks for different branch names.
#NonCPS
def cancelPreviousBuilds() {
def buildNumber = env.BUILD_NUMBER.toInteger()
def currentJob = Jenkins.instance.getItemByFullName(env.JOB_NAME)
def currentBranch = env.BRANCH_NAME // Branch value of the current build
// Cancel old jobs that are from the same branch
for (def build : currentJob.builds) {
// parse out the branch name from each job
param = (build.getFullDisplayName().tokenize('ยป')[2]).tokenize(" ")[0]
if (build.isBuilding() && build.number < buildNumber && currentBranch == param) {
build.doStop()
}
}
}
However, my code is failing on
def buildNumber = env.BUILD_NUMBER.toInteger()
The error from Jenkins says:
java.lang.NullPointerException: Cannot invoke method toInteger() on null object
Can I not use toInteger() here? From echo'ing out buildNumber this is definitely pulling out the build number, so I am pretty sure it is not actually null.
You can convert it to integer using Integer.parseInt:
def buildNumber = Integer.parseInt(env.BUILD_NUMBER)
Adding snippet of code which I modified:
def currentJob = Jenkins.instance.getItemByFullName(env.JOB_NAME)
def buildNumber = Integer.parseInt(env.BUILD_NUMBER)
def jobBuilds = currentJob.getBuilds()
jobBuilds.each{ build ->
// Your condition goes here
if (build.number < buildNumber ){
build.doStop()
}
}

Jenkins API - Get current pipeline stage of the build

I'm trying to make my build pipeline more useful and I need a way to terminate previous builds if they are not finished yet.
I have the next Job definition:
pipeline {
stages {
stage('A'){...}
stage('B'){...}
stage('C'){...}
}
}
And I need to terminate all previous builds if they are not in stage'C'.
I use Jenkins API to get previous builds for a particular job:
#NonCPS
def cancelPreviousBuilds() {
def buildNumber = env.BUILD_NUMBER.toInteger()
def currentJob = Jenkins.getInstance().getItemByFullName(env.JOB_NAME)
currentJob.builds
.find{ build -> build.isBuilding() && build.number.toInteger() < buildNumber && currentStageName(build) != 'C' }
.each{ build -> build.doStop() }
}
So my current stopper is the implementation of currentStageName function.
I'm not able to get the name of the stage.
I've already found some code but it does not work well for me:
#NonCPS
def currentStageName(currentBuild) {
FlowGraphWalker walker = new FlowGraphWalker(currentBuild.getExecution())
for (FlowNode flowNode: walker) {
if(flowNode.isActive()) {
return flowNode.getDisplayName();
}
}
}
FlowNode object does not contain stage name it contains more narrow flow step inside the build.
So the question is:
How to get the current stage of previous build for particular Jenkins job?
Given a FlowNode, you can check if it is the start of a stage by checking if node instanceof StepEndNode. If it is, you can use its LabelAction class to get the name of the stage:
static String getLabel(FlowNode node) {
LabelAction labelAction = node.getAction(LabelAction.class);
if (labelAction != null) {
return labelAction.getDisplayName();
}
return null;
}
I don't think it's useful for your case, but you can also get it from the node that marks the end of a stage (a StepEndNode) by looking up the corresponding start node:
FlowNode startNode = ((StepEndNode) node).getStartNode();

Jenkins pipeline script - wait for running build

I have jenkins groovy pipeline which triggers other builds. It is done in following script:
for (int i = 0; i < projectsPath.size(); i++) {
stepsForParallel[jenkinsPath] = {
stage("build-${jenkinsPath}") {
def absoluteJenkinsPath = "/${jenkinsPath}/BUILD"
build job: absoluteJenkinsPath, parameters: [[$class: 'StringParameterValue', name: 'GIT_BRANCH', value: branch],
[$class: 'StringParameterValue', name: 'ROOT_EXECUTOR', value: rootExecutor]]
}
}
}
parallel stepsForParallel
The problem is that my jobs depend on other common job, i.e. job X triggers job Y and job Z triggers job Y. What I'd like to achieve is that the job X triggers job Y and job Z waits for result of Y triggered by X.
I suppose I need to iterate over all running builds and check if any build of the same type is running. If yes then wait for it. Following code could wait for build to be done:
def busyExecutors = Jenkins.instance.computers
.collect {
c -> c.executors.findAll { it.isBusy() }
}
.flatten()
busyExecutors.each { e ->
e.getCurrentWorkUnit().context.future.get()
}
My problem is that I need to tell which running job I need to wait. To do so I need to check:
build parameters
build environments variables
job name
How can i retreive this kind of data?
I know that jenkins have silent period feature but after period expires new job will be triggered.
EDIT1
Just to clarify why I need this function. I have jobs which builds applications and libs. Applications depend on libs and libs depend on other libs. When build is triggered then it triggers downstream jobs (libs on which it depends).
Sample dependency tree:
A -> B,C,D,E
B -> F
C -> F
D -> F
E -> F
So when I trigger A then B,C,D,E are triggered and F is also triggered (4 times). I'd like to trigger F only once.
I have beta/PoC solution (below) which almost work. Right now I have following problems with this code:
echo with text "found already running job" is not flushed to the screen until job.future.get() ends
I have this ugly "wait" (for(i = 0; i < 1000; ++i){}). It is because result field isn't set when get method returns
import hudson.model.*
def getMatchingJob(projectName, branchName, rootExecutor){
result = null
def busyExecutors = []
for(i = 0; i < Jenkins.instance.computers.size(); ++i){
def computer = Jenkins.instance.computers[i]
for(j = 0; j < computer.getExecutors().size(); ++j){
def executor = computer.executors[j]
if(executor.isBusy()){
busyExecutors.add(executor)
}
}
}
for(i = 0; i < busyExecutors.size(); ++i){
def workUnit = busyExecutors[i].getCurrentWorkUnit()
if(!projectName.equals(workUnit.work.context.executionRef.job)){
continue
}
def context = workUnit.context
context.future.waitForStart()
def parameters
def env
for(action in context.task.context.executionRef.run.getAllActions()){
if(action instanceof hudson.model.ParametersAction){
parameters = action
} else if(action instanceof org.jenkinsci.plugins.workflow.cps.EnvActionImpl){
env = action
}
}
def gitBranchParam = parameters.getParameter("GIT_BRANCH")
def rootExecutorParam = parameters.getParameter("ROOT_EXECUTOR")
gitBranchParam = gitBranchParam ? gitBranchParam.getValue() : null
rootExecutorParam = rootExecutorParam ? rootExecutorParam.getValue() : null
println rootExecutorParam
println gitBranchParam
if(
branchName.equals(gitBranchParam)
&& (rootExecutor == null || rootExecutor.equals(rootExecutorParam))
){
result = [
"future" : context.future,
"run" : context.task.context.executionRef.run,
"url" : busyExecutors[i].getCurrentExecutable().getUrl()
]
}
}
result
}
job = getMatchingJob('project/module/BUILD', 'branch', null)
if(job != null){
echo "found already running job"
println job
def done = job.future.get()
for(i = 0; i < 1000; ++i){}
result = done.getParent().context.executionRef.run.result
println done.toString()
if(!"SUCCESS".equals(result)){
error 'project/module/BUILD: ' + result
}
println job.run.result
}
I have a similar problem to solve. What I am doing, though, is iterating over the jobs (since an active job might not be executed on an executor yet).
The triggering works like this in my solution:
if a job has been triggered manually or by VCS, it triggers all its (recursive) downstream jobs
if a job has been triggered by another upstream job, it does not trigger anything
This way, the jobs are grouped by their trigger cause, which can be retrieved with
#NonCPS
def getTriggerBuild(currentBuild)
{
def triggerBuild = currentBuild.rawBuild.getCause(hudson.model.Cause$UpstreamCause)
if (triggerBuild) {
return [triggerBuild.getUpstreamProject(), triggerBuild.getUpstreamBuild()]
}
return null
}
I give each job the list of direct upstream jobs it has. The job can then check whether the upstream jobs have finished the build in the same group with
#NonCPS
def findBuildTriggeredBy(job, triggerJob, triggerBuild)
{
def jobBuilds = job.getBuilds()
for (buildIndex = 0; buildIndex < jobBuilds.size(); ++buildIndex)
{
def build = jobBuilds[buildIndex]
def buildCause = build.getCause(hudson.model.Cause$UpstreamCause)
if (buildCause)
{
def causeJob = buildCause.getUpstreamProject()
def causeBuild = buildCause.getUpstreamBuild()
if (causeJob == triggerJob && causeBuild == triggerBuild)
{
return build.getNumber()
}
}
}
return null
}
From there, once the list of upstream builds have been made, I wait on them.
def waitForUpstreamBuilds(upstreamBuilds)
{
// Iterate list -- NOTE: we cannot use groovy style or even modern java style iteration
for (upstreamBuildIndex = 0; upstreamBuildIndex < upstreamBuilds.size(); ++upstreamBuildIndex)
{
def entry = upstreamBuilds[upstreamBuildIndex]
def upstreamJobName = entry[0]
def upstreamBuildId = entry[1]
while (true)
{
def status = isUpstreamOK(upstreamJobName, upstreamBuildId)
if (status == 'OK')
{
break
}
else if (status == 'IN_PROGRESS')
{
echo "waiting for job ${upstreamJobName}#${upstreamBuildId} to finish"
sleep 10
}
else if (status == 'FAILED')
{
echo "${upstreamJobName}#${upstreamBuildId} did not finish successfully, aborting this build"
return false
}
}
}
return true
}
And abort the current build if one of the upstream builds failed (which nicely translates as a "aborted build" instead of a "failed build").
The full code is there: https://github.com/doudou/autoproj-jenkins/blob/use_autoproj_to_bootstrap_in_packages/lib/autoproj/jenkins/templates/library.pipeline.erb
The major downside of my solution is that the wait is expensive CPU-wise when there are a lot of builds waiting. There's the built-in waitUntil, but it led to deadlocks (I haven't tried on the last version of the pipeline plugins, might have been solved). I'm looking for ways to fix that right now - that's how I found your question.

Delete build history in Jenkins

I'm attempting to delete the build history from Jenkins using the instructions in this answer:
How do I clear my Jenkins/Hudson build history?
However, I'm getting the following error message:
groovy.lang.MissingMethodException: No signature of method:
jenkins.branch.OrganizationFolder.getBuilds() is applicable for
argument types: () values: [] Possible solutions: getViews(),
doBuild(jenkins.util.TimeDuration), getUrl(), getClass(),
getActions(), getApi() at
org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:58)
at
org.codehaus.groovy.runtime.callsite.PojoMetaClassSite.call(PojoMetaClassSite.java:49)
at
org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
at
org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
at
org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117)
If I run this script:
def jobName = "github-test"
def job = Jenkins.instance.getItem(jobName)
println(job)
I get the following output:
jenkins.branch.OrganizationFolder#134f3a3c[github-test]
I'm using version 2.32.2.
Here's the exact script that I ran:
def jobName = "github-test"
def job = Jenkins.instance.getItem(jobName)
job.getBuilds().each { it.delete() }
job.nextBuildNumber = 1
job.save()
The job that you are trying to delete, github-test, is an organization folder, e.g. you get jenkins.branch.OrganizationFolder. This means that it's not an actual job with actual builds, instead it's a folder which contains other jobs. Worth noting is that OrganizationFolder is of the type ComputedFolder which means that it is populated automatically by Jenkins when it finds new repositories and branches (on github).
So I'm guessing that you wan't to remove all builds from the sub-jobs in your github-test job/folder. I've modified the linked answer so that it accounts for folders:
import com.cloudbees.hudson.plugins.folder.AbstractFolder
import hudson.model.AbstractItem
// change this variable to match the name of the job whose builds you want to delete
def jobName = "test"
// Set to true in order to reset build number to 1
def resetBuildNumber = false
def removeBuilds(job, resetBuildNumber) {
if (job instanceof AbstractFolder) {
for (subJob in job.getItems()) {
removeBuilds(subJob, resetBuildNumber)
}
} else if (job instanceof AbstractItem) {
job.getBuilds().each { it.delete() }
if (resetBuildNumber) {
job.nextBuildNumber = 1
job.save()
}
} else {
throw new RuntimeException("Unsupported job type ${job.getClass().getName()}!")
}
}
removeBuilds(Jenkins.instance.getItem(jobName), resetBuildNumber)
You can use the script below to delete builds. You can specify how many latest builds to retain, just add a value of your choice to MAX_BUILDS.make it 0 to delete everything.
Assuming the builds you want to deleted belongs to "github-test". Replace jobName value with your choice
MAX_BUILDS = 5
def jobName = "github-test"
def job = Jenkins.instance.getItem(jobName)
println ""
println "selected Jenkins Job : "
println job.name
def recent = job.builds.limit(MAX_BUILDS)
println recent
for (build in job.builds) {
if (!recent.contains(build)) {
println ""
println "========================================================="
println "Preparing to delete: " + build
build.delete()
println ""
}
}

Resources