Cancelling as SUCCESS build when changeset is only certain file(s)? - jenkins

Following an old question being asked in HERE, I was wondering... Is it possible to retrieve changeset without being inside a stage?
I would like that condition to be triggered whenever only a certain file is modified. Once that file has been modified, we must skip the build and mark it as success so we don't waste resources.
pipeline {
agent { ... }
if () {
currentBuild.result = 'SUCCESS'
return
}
stages {
...
}

Related

How to write conditions in Jenkinsfile, if repo already cloned, skipped that stage

Or, is it possible to add manual stage, yes/no, that while the pipeline is running, I can decide to jump a current stage?
Something similar to this, but even if I choose skip, it continues to run.
stage('Approval') {
input {
message 'Should we continue?'
}
steps {
echo 'Deploying'
}
}

Trigger an action within Jenkins declarative pipeline right after a stage ends or just before a stage begins?

I have the following Jenkinsfile:
pipeline {
agent any
environment { }
stages {
stage('stageA') {
steps {
... Do something with arg1, arg2 or arg3
}
}
stage('stageB') {
steps {
... Do something with arg1, arg2 or arg3
}
}
...
}
}
Is there anywhere I can specify a universal "pre-stage" or "post-stage" set of actions to perform? A use-case would be sending logging information at the end of a stage to a log manager, but it would be preferable to not copy and paste those invocations at the end of each and every stage.
As far as I know there is no generic post- or pre-stage hook in Jenkins pipelines. You can define post steps in a post section but you need one per stage.
However, if you don't want to repeat yourself, you have some options.
Use a shared lib
The place to put repeating code to it a shared library. That way allows you to declare your own steps using Groovy.
You need another repository to define a shared lib, but apart from that it is a pretty strait forward way and you can reuse the code in all of your Jenkins' pipelines.
Use a function
If you declare a function outside of the pipeline, you can call it from any stage. This is not really documented and might be prevented in the future. As far as I understand it messes with the coordination between master and agents. However, it works:
pipeline {
agent any
stages {
stage ("First") {
steps {
writeFile file: "resultFirst.txt", text: "all good"
}
post {
always {
cleanup "first"
}
}
}
stage ("Second") {
steps {
writeFile file: "resultSecond.txt", text: "all good as well"
}
post {
always {
cleanup "second"
}
}
}
post {
always {
cleanup "global" // this is only triggered after all stages, not after every
}
}
}
}
void cleanup(String stage) {
echo "cleanup ${stage}"
archiveArtifacts artifacts: "result*"
}

How do I use `when` condition on a file's last modified time in Jenkins pipeline syntax

I am creating a Jenkins pipeline, I want certain stage to be triggered only when a particular log file's(log file is located in the server node where all the stages are going to run) last modified date is updated after the initiation of pipeline job, I understand we need to use "When" condition but not really sure how to implement it.
Tried referring some of the pipeline related portals but could not able to find an answer
Can some please help me through this?
Thanks in advance!
To get data about file is quite tricky in a Jenkins pipeline when using the Groovy sandbox since you're not allowed to do new File(...).lastModified. However there is the findFiles step, which basically returns a list of wrapped File objects with a getter for last modified time in millis, so we can use findFiles(glob: "...")[0].lastModified.
The returned array may be empty, so we should rather check on that (see full example below).
The current build start time in millis is accessible via currentBuild.currentBuild.startTimeInMillis.
Now that we git both, we can use them in an expression:
pipeline {
agent any
stages {
stage("create file") {
steps {
touch "testfile.log"
}
}
stage("when file") {
when {
expression {
def files = findFiles(glob: "testfile.log")
files && files[0].lastModified < currentBuild.startTimeInMillis
}
}
steps {
echo "i ran"
}
}
}
}

Can a Jenkins pipeline have an optional input step?

Is it possible to create a Jenkins pipeline with an optional input stage?
The below snippet doesn't achieve this goal.
Expected behaviour
The stage (and therefore the input prompt) should only run for specific branches.
Actual behaviour
This stage runs for all branches. The when filter is ignored when an input step is used.
stage('Approve') {
when {
expression { BRANCH_NAME ==~ /^qa[\w-_]*$/ }
}
input {
message "Approve release?"
ok "y"
submitter "admin"
parameters {
string(name: 'IS_APPROVED', defaultValue: 'y', description: 'Deploy to master?')
}
}
steps {
script {
if (IS_APPROVED != 'y') {
currentBuild.result = "ABORTED"
error "User cancelled"
}
}
}
}
The filter is not ignored, it is just evaluated after the input step. In your example, you would always be asked whether to deploy, and in the case that you are not on a QA branch, nothing would happen.
Now you could ask why Jenkins isn't evaluating the 'when' directive first. In that case, you could not use the input parameter in your when condition.
And having multiple when directives would be like scripting within the declarative pipeline.
However, there is an expression that allows you controlling when the 'when' directive is evaluated. This is beforeAgent. It allows you to evaluate the when statement before the agent is allocated. Similar to that, you would need something like beforeInput. You could create a feature request for that.
I stepped away from using the input directive and I use input within a scripting block now, because that provides much more flexibility, e.g. I am sending Slack notifications when somebody has to approve something, which is impossible with the declarative approach. You would need a notify directive for that. And if there was one, is that going to be evaluated before or after the input step?
You see, doing everything declarative is not always the best way. So my recommended approach is the following (disclaimer: this is untested!):
pipeline {
// We want to use agents per stage to avoid blocking our build agents
// while we are waiting for user input.
agent none
...
// The question mark naming convention is helpful to show you which
// approval stage belongs to which work stage.
stage('Release?') {
// Don't allocate an agent because we don't want to block our
// slaves while waiting for user input.
agent none
when {
// You forgot the 'env.' in your example above ;)
expression { env.BRANCH_NAME ==~ /^qa[\w-_]*$/ }
}
options {
// Optionally, let's add a timeout that we don't allow ancient
// builds to be released.
timeout time: 14, unit: 'DAYS'
}
steps {
// Optionally, send some notifications to the approver before
// asking for input. You can't do that with the input directive
// without using an extra stage.
slackSend ...
// The input statement has to go to a script block because we
// want to assign the result to an environment variable. As we
// want to stay as declarative as possible, we put noting but
// this into the script block.
script {
// Assign the 'DO_RELEASE' environment variable that is going
// to be used in the next stage.
env.DO_RELEASE = input ...
}
// In case you approved multiple pipeline runs in parallel, this
// milestone would kill the older runs and prevent deploying
// older releases over newer ones.
milestone 1
}
}
stage('Release') {
// We need a real agent, because we want to do some real work.
agent any
when {
// Evaluate the 'when' directive before allocating the agent.
beforeAgent true
// Only execute the step when the release has been approved.
environment name: 'DO_RELEASE', value: 'yes'
}
steps {
// Make sure that only one release can happen at a time.
lock('release') {
// As using the first milestone only would introduce a race
// condition (assume that the older build would enter the
// milestone first, but the lock second) and Jenkins does
// not support inter-stage locks yet, we need a second
// milestone to make sure that older builds don't overwrite
// newer ones.
milestone 2
// Now do the actual work here.
...
}
}
}
The correct syntax would be more like (completely untested):
stage('Approve') {
when {
expression { BRANCH_NAME ==~ /^qa[\w-_]*$/ }
}
steps {
script {
def IS_APPROVED = input(
message: "Approve release?"
ok: "y"
submitter: "admin"
parameters: [
string(name: 'IS_APPROVED', defaultValue: 'y', description: 'Deploy to master?')
]
)
if (IS_APPROVED != 'y') {
currentBuild.result = "ABORTED"
error "User cancelled"
}
}
}
}
So essentially, you're hitting the limits of declarative pipelines have to fall back to groovy scripting / scripted pipelines.

How to differentiate build triggers in Jenkins Pipeline

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:
changes to our git repo that are picked up on branch indexing
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?
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
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')) {
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.
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.
#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.)
}
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.
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.
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)
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}"
}
}
}
}

Resources