48

I'm hoping to add a conditional stage to my Jenkinsfile that runs depending on how the build was triggered. Currently we are set up such that builds are either triggered by:

  1. changes to our git repo that are picked up on branch indexing
  2. a user manually triggering the build using the 'build now' button in the UI.

Is there any way to run different pipeline steps depending on which of these actions triggered the build?

old_qwerty_bastard
  • 491
  • 1
  • 4
  • 5
  • Does this answer your question? [Jenkins - How to get and use upstream info in downstream](https://stackoverflow.com/questions/39207924/jenkins-how-to-get-and-use-upstream-info-in-downstream) – sschuberth Apr 17 '20 at 12:27

9 Answers9

34

In Jenkins Pipeline without currentBuild.rawBuild access the build causes could be retrieved in the following way:

// started by commit
currentBuild.getBuildCauses('jenkins.branch.BranchEventCause')
// started by timer
currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause')
// started by user
currentBuild.getBuildCauses('hudson.model.Cause$UserIdCause')

You can get a boolean value with:

isTriggeredByTimer = !currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause').isEmpty()

Or, as getBuildCauses() returns an array, the array's size will work correctly with Groovy truthy semantics:

if (currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause')) {
Carl Walsh
  • 6,100
  • 2
  • 46
  • 50
  • 2
    To complement, to access internal information of causes use: `def name = currentBuild.getBuildCauses('hudson.model.Cause$UserIdCause').userName` More information in: https://github.com/jenkinsci/workflow-support-plugin/blob/c019079f526f627665c812fe86dd72419075df62/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/RunWrapper.java#L165 – João Pedro Schmitt Aug 27 '19 at 13:51
  • The Groovy truthy statement gave unexpected results when checking if false combined with other checks. I ended up explicitly checking the array size: `if (currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause').size().equals(0)) {`. – Craigo Jul 22 '22 at 03:50
33

The following code should works to determine if a user has started the pipeline or a timer/other trigger:

def isStartedByUser = currentBuild.rawBuild.getCause(hudson.model.Cause$UserIdCause) != null
Philip
  • 2,959
  • 1
  • 17
  • 22
  • 1
    Can you also show, how to differentiate between trigger by a scheduled cron from trigger by webhook? – badera Oct 17 '18 at 16:03
  • 5
    The different Cause subclasses can be found here: https://javadoc.jenkins-ci.org/hudson/model/Cause.html. I guess (untested) the Causes you are looking for are: hudson.triggers.TimerTrigger$TimerTriggerCause and hudson.model.Cause$RemoteCause. – Philip Oct 18 '18 at 07:07
  • 1
    No, this only tell if the pipeline was not started by a user. This could be a timer as well as a plugin or a build on push event. – TommyD Feb 12 '19 at 19:27
  • 4
    What about declarative pipeline? – Dan M. Feb 21 '19 at 17:43
  • 2
    I just want to add for people coming across this answer now: accessing `rawBuild` is a potential security risk (and needs to be approved by an admin), but as [this answer](https://stackoverflow.com/a/53342374/11744714) mentions, since version 2.22 you can use the `currentBuild.getBuildCauses()` method instead. – AdHorger Oct 04 '22 at 17:00
28

The ability to get causes for a workflow run was released in version 2.22 (2018 Nov 02) to the Pipeline Supporting APIs Plugin. The feature was requested in JENKINS-41272.

A couple methods were added to the currentBuild global variable with that release:

getBuildCauses

  • Returns a JSON array of build causes for the current build

EXPERIMENTAL - MAY CHANGE getBuildCauses(String causeClass)

  • Takes a string representing the fully qualified Cause class and returns a JSON array of build causes filtered by that type for the current build, or an empty JSON array if no causes of the specified type apply to the current build

And an example from me submitting:

echo "${currentBuild.buildCauses}" // same as currentBuild.getBuildCauses()
echo "${currentBuild.getBuildCauses('hudson.model.Cause$UserCause')}"
echo "${currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause')}"

And the output:

[Pipeline] echo
[[_class:hudson.model.Cause$UserIdCause, shortDescription:Started by user anonymous, userId:null, userName:anonymous], [_class:org.jenkinsci.plugins.workflow.cps.replay.ReplayCause, shortDescription:Replayed #12]]
[Pipeline] echo
[]
[Pipeline] echo
[]
[Pipeline] End of Pipeline
Finished: SUCCESS

NOTE

There appears to be an issue with the currentBuild.getBuildCauses(type) when the type is a type of Cause contributed by a plugin. For example, currentBuild.getBuildCauses('org.jenkinsci.plugins.workflow.cps.replay.ReplayCause') fails with a java.lang.ClassNotFoundException. This was reported in JENKINS-54673 for the 2.22 version of the Pipeline: Supporting APIs (workflow-support) plugin. It is reportedly fixed in the 2.24 version.

mkobit
  • 43,979
  • 12
  • 156
  • 150
  • Thanks! That bug would affect builds triggered by Git hooks, right? That's unfortunate. – Raphael Nov 17 '18 at 13:06
  • Build causes are only available if you disable script security or the sandbox. Otherwise org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException – andrew lorien Apr 16 '19 at 01:31
  • @andrewlorien you will have to provide a reproducible case in a new question. The methods are [`@Whitelisted`](https://github.com/jenkinsci/workflow-support-plugin/blob/d0898011f1f2fd9344f0622f6d21e677b97c12e0/src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/RunWrapper.java#L139-L140) so you might be doing something different. – mkobit Apr 16 '19 at 14:23
  • Thanks @mkobit . https://stackoverflow.com/questions/55720853/jenkins-rejectedaccessexception-in-whitelisted-method – andrew lorien Apr 17 '19 at 06:01
11

I might be missing something, but you can achieve what you want easily by making use of the when directive:

pipeline {
  agent any

  stages {
    stage('Always') {
      steps {
        echo "I am always executed"
      }
    }

    stage('ManualTimed') {
      steps {
        echo "I am only executed when triggered manually or timed"
      }

      when {
        beforeAgent true
        anyOf {
          triggeredBy 'TimerTrigger'
          triggeredBy cause: 'UserIdCause'
        }
      }
    }
    
    stage('GitLabWebHookCause') {
      steps {
        echo "I am only executed when triggered by SCM push"
      }

      when {
        beforeAgent true
        triggeredBy 'GitLabWebHookCause'
      }
    }
  }
}

You will find many similar useful examples for various use cases in the documentation of the when directive.

Edit: thanks to Jean-Francois Larvoire's answer, I was able to figure out 'my trigger' GitLabWebHookCause I required for my use case.

Ildrial
  • 344
  • 3
  • 10
  • Also, for `parameterizedCron` use `when … triggeredBy 'ParameterizedTimerTriggerCause'` (see https://stackoverflow.com/a/62772852/5287257). – Alexey Vazhnov Dec 30 '21 at 09:53
10

@vitalii-blagodir:
Your answer works for detecting builds triggered by users and timers, but not by commits.
Instead, I found this to work in my case:

def isTriggeredByIndexing = currentBuild.getBuildCauses('jenkins.branch.BranchIndexingCause').size()  
def isTriggeredByCommit = currentBuild.getBuildCauses('com.cloudbees.jenkins.GitHubPushCause').size()  
def isTriggeredByUser = currentBuild.getBuildCauses('hudson.model.Cause$UserIdCause').size()  
def isTriggeredByTimer = currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause').size()  

The .size() suffix returns 0 if the object is missing, or 1 if it's present. This makes the result usable as a boolean.

For finding the object name to use, I found it convenient to display this in the log:

echo "# Build causes"
def buildCauses = currentBuild.buildCauses
def numCause = 0
for (cause in buildCauses) {
  echo "${numCause++}: ${cause.shortDescription}" // Display a human-readable index and description
  echo "${cause}" // Display the object class name. This allows knowing what names to use in getBuildCauses(name) calls below.
}

Finally, if the goal is to abort a pipeline build in specific cases, then the test must be done before the beginning of the pipeline.
For example, we had a problem with the branch indexing triggering extra useless builds. This was fixed by adding this before the pipeline:

// Avoid useless buils: The branch indexing should only trigger the initial build of a new branch.
def isTriggeredByBranchIndexing = currentBuild.getBuildCauses('jenkins.branch.BranchIndexingCause').size()
if (isTriggeredByBranchIndexing && currentBuild.previousBuild) { // Then it's not the initial build.
  echo "# Reindexing a branch already built. It is useless to rebuild it now. Aborting."
  currentBuild.result = 'SUCCESS' // Make sure the build is not displayed in red in the Jenkins UI.
  return // Abort before the pipeline even starts. (Inside the pipeline, this would only abort one stage.)
}
8

I think that the answers here are incomplete and do not provide an actual ready to use answer. Here's my code to get it working:

import com.cloudbees.groovy.cps.NonCPS

@NonCPS
def isStartedByTimer() {
    def buildCauses = currentBuild.rawBuild.getCauses()
    echo buildCauses

    boolean isStartedByTimer = false
    for (buildCause in buildCauses) {
        if ("${buildCause}".contains("hudson.triggers.TimerTrigger\$TimerTriggerCause")) {
            isStartedByTimer = true
        }
    }

    echo isStartedByTimer
    return isStartedByTimer
}

// [...]
// Other pipeline stuff

script {
    isStartedByTimer()
}

When started by user:

00:00:01.353 [hudson.model.Cause$UserIdCause@fa5cb22a]
[Pipeline] echo
00:00:01.358 false

When started by timer:

00:00:01.585 [hudson.triggers.TimerTrigger$TimerTriggerCause@5]
[Pipeline] echo
00:00:01.590 true

Note: the NonCPS decorator is needed because otherwise the next non-script step will throw.

Geoffrey Booth
  • 7,168
  • 5
  • 35
  • 42
TommyD
  • 731
  • 8
  • 14
4

Assuming the two different build causes are "timer" and "push" (to a git repo), you can add the following stage to your Jenkinsfile (in a declarative Jenkins pipeline) to make use of getBuildCauses():

pipeline {

  stages {

    stage('preparation') {

      steps {

        script {

          // get build cause (time triggered vs. SCM change)
          def buildCause = currentBuild.getBuildCauses()[0].shortDescription
          echo "Current build was caused by: ${buildCause}\n"

          // e.g. "Current build was caused by: Started by GitHub push by mirekphd"
          // vs. "Started by timer"

        }
      }
    }
  }
}

Then I can decide whether to perform certain stages conditionally (depending on the build cause). For example, pulling a docker base image and inspecting for changes in system libraries (likely security updates) should be done periodically, regardless of whether there was a source code change or not.

mirekphd
  • 4,799
  • 3
  • 38
  • 59
  • 1
    How can a build have multiple causes? A build will be triggered on some single event right? Am I missing something? – Prasad Shinde May 15 '20 at 11:26
  • There can be multiple build causes, available in ${BUILD_CAUSE} environment variable. The most frequent is "Started by timer", but if you click the 'Rerun' button, the cause would be "Started by user ", and there is also the git push ("Started by an SCM change"). – mirekphd May 15 '20 at 11:47
3

We can use "BUILD_CAUSE" variable for getting the information about who initiated the run

for [jenkins-pipeline] you may use

currentBuild.rawBuild.getCauses()

(see github.com/jenkinsci/pipeline-examples/blob/master/… for more details)

Subrata Fouzdar
  • 724
  • 5
  • 17
  • 3
    Rawbuild is only available if you disable script security or the sandbox. Otherwise org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException – andrew lorien Apr 16 '19 at 01:32
0

There was a similar requirement, where user detail who triggered the build should be there in success / failure notification. The job was already had time based triggered, hence could not use wrap([$class: 'BuildUser']) directly.

I used below step, which print username if the job is triggered manually or timer triggered. So, I used this:

pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                script{ 
                    env.buildCauses = currentBuild.rawBuild.getCauses()
                    if (buildCauses.contains("hudson.triggers.TimerTrigger")){
                        env.builduser = "TimerTrigger"
                    } else {
                        wrap([$class: 'BuildUser']) {
                            env.builduser = "${BUILD_USER}"
                        }
                    }
                }
                echo "Initiated by: ${env.builduser}"
            }
        }
    }   
}
Prosenjit Sen
  • 326
  • 4
  • 7