Jenkins CI Trick to Prevent Task from Triggering Itself on SCM Commit

Posted by & filed under , .

jenkins_logoI’ve come across this recently working on a project in Netflix and thought worth sharing.

The project I was dealing with packages some JavaScript code which, before deployment, needed to be minified. We have set up our Jenkins task such that it polls Git for changes in the master branch and when the changes are found it starts the Jenkins pipeline. This is based on the fact that we all work in a development branch and deploy our stuff in test environments — and finally when all the testing is good we merge into master and from there deploy in production. By the time we deploy to production though, we have to minify the JavaScript source code (to lesser the pressure on bandwidth and increase the speed of our application.)

As such, at this point, we want to “process” the JavaScript resources and generate minified versions of them — which then get pushed into our production environment.

As a side note, since we use Gradle, we decided to go with Eric Wendelin‘s gradle plugin gradle-js-plugin. This allows us to take our original source file abc.js and generate an abc.min.js in the same directory. Now, we want to keep a copy of the minified version in Git so we can investigate if any issues come up. As such, after we run the minification we need to commit back abc.min.js (which should be the only file changed at this stage!) into git — and of course push the commit set back to git.

So our gradle build file quickly got the following tasks added:

def gitPath = "/usr/bin"
task gitAdd(type: Exec) {
 workingDir project.projectDir
 commandLine "$gitPath/git", "add", "--all"
}
gitAdd.dependsOn minifyJs
 
task gitCommit(type: Exec) {
 workingDir project.projectDir
 commandLine "$gitPath/git", "commit", "-m", "[automatic-minification]"
}
gitCommit.dependsOn gitAdd
 
task gitPush(type: Exec) {
 workingDir project.projectDir
 commandLine "$gitPath/git", "push"
}
gitPush.dependsOn gitCommit

And since we want to run this task during the resource processing — together with all the other JS/HTML/CSS/etc processing — we set it to run then by doing:

processResources.dependsOn gitPush

So that’s all set, now when we do a gradle build, Gradle will minify the JavaScript file abc.js into abc.min.js and commit back into git.

… and since there is a change in SCM, the same very task will kick in again — and this time will fail because well there is nothing to commit! (The minification will actually run again but the result will be the same as the version of abc.min.js already in git, so git commit will fail.)

OK so then how do we prevent the build from triggering its own start since it is in fact doing a commit into git — which is the very thing it is listening for itself?

It turns out that Jenkins offers a plugin for excluding commits from certain users — Source Code Management –> Additional Behaviours –> Add :

jenkins_ignore_commit_from_users

As per Jenkins’ docco:

If set, and Jenkins is set to poll for changes, Jenkins will ignore any revisions committed by users in this list when determining if a build needs to be triggered. This can be used to exclude commits done by the build itself from triggering another build, assuming the build server commits the change with a distinct SCM user.

Each exclusion uses literal pattern matching, and must be separated by a new line.

auto_build_user

The example above illustrates that if only revisions by “auto_build_user” have been committed to the SCM a build will not occur.

So if you have your Jenkins configured to store its own credentials for git (which you should!) then add this to your Jenkins task configuration. Then configure the git username Jenkins uses when dealing with Git (and in the above case when it’s committing changes to git):

jenkins_setup_ignore_from_userNow, when your Jenkins job starts first time because you made a change yourself to the git repo, this plugin will work as expected: code is being checked out and build kicks in. Part of the build, as I mentioned, is to minify the JavaScript file and commit it back. However, since the commit is being performed using the Jenkins git user, when Jenkins check for changes in the repo it will exclude those and as such not end up with this task being started in the very next moment after you’ve committed the minify version!

As a side note, if you don’t have a specific set of git credentials just for Jenkins, there is another way around it: “Ignore commits with certain messages” (in the same Additional Behaviours category):

jenkins_ignore_commit_with_msgThe idea this time is that rather than monitoring the git username which made the change we look at the comment/message associated with that commit (the message you specify in the command line when you do a git commit -m "message") and match it against a certain pattern (it seems the plugin actually does a regex matching not just a precise match, which is quite powerful). So all you have to do in this case is decide on a pattern to be excluded — for instance in the above case something like .*\[automatic-minification\].* and exclude the commits generated by running the gradle build.

 

Bonus Point : Atlassian Stash and Jenkins Integration

However, there is one small (yet so important!) detail here: all of the above only applies to Jenkins tasks triggered by Jenkins polling the code repository! You might think this is not that important — if you have your own git repo and have Jenkins poll it every minute or so then you are fine with the above. However, if you are using the Atlassian suite of products and your code is stored in Stash then you probably have set up a Stash Webhook to Jenkins such that when users commit changes to your Stash repo it notifies Jenkins to start any jobs that are using the source from that particular repo. (This sort of hook/notification is very much preferrable to Jenkins polling your code repo, as polling is an expensive operation — especially if you do it every few seconds! — and it doesn’t really kick off in real time the build process, as changes are being committed.) Thing is, if you use Stash’s webhook to Jenkins, you will notice that the Jenkins job starts regardless of your settings above to ignore certain users. That is because, as I just mentioned : the above settings only applies to Jenkins tasks triggered by Jenkins polling the code repository — and hopefully now this phrase makes sense 🙂 Since your Jenkins task is triggered by a commit to Stash then these settings will be ignored!

Luckily though, the webhook itself offers the possibility to ignore commits from users, such that it won’t trigger a hook/notification to Jenkins for commits from say your “build” user. To configure this simply:

  1. Go to the “Stash Webook to Jenkins” module in your Stash repo settings
  2. Enable it
  3. Add the username you want to exclude (or usernames if you want more) in the Advanced Configuration –> Committers to Ignore field:

stash_webhook_jenkins.ping

Save the settings and now you are finally good to go! If you have Jenkins also poll your repo say monthly or so, it will ignore commits from the Jenkins users — and also, any commits made by this user will not trigger now builds in Jenkins.

Note: There is a lot to be said about user interface here: the Jenkins settings regarding polling and ignoring messages/users are labelled in a misleading way, as at no point they make it clear that these settings only apply to builds triggered from polling. Hopefully they will review those soon to make the UI a bit more intuitive as I have found myself losing lots of time on this 🙁

4 Responses to “Jenkins CI Trick to Prevent Task from Triggering Itself on SCM Commit”

  1. Derek

    Saved my day.. thanks!!

  2. praveen

    Thanks. Saved my day as well.

  3. Ralph Azucena

    Is this still working?? I can’t seem to get it working using a standalone pipeline

  4. Scott Lavender

    I can’t get the users to ignore part to work. My job keeps re-triggering…

    I’ve tried to add the Bit plugin options in Jenkins, and using the Stash triggered approach. In both cases my job keeps re-triggering after running, since it does a commit and a tag, creating a new commit. I’m in an endless loop.