Checkout a specific git commit in a Jenkins job

As a good number of Jenkins related articles, this one is also fueled by past frustration. Trying to do the simplest of things often sets you off on a wild ride of trial and error (mostly error). At which point your Google results are painted purple. One such thing is checking out a specific git commit during a Jenkins job. On the surface it’s a trivial thing to do, and it is if you don’t wade of too far from the road.

Picture this: you want a Jenkins job which is set to run the master branch and you can specify any commit from the repository as a parameter, in which case it will check that commit out and carry on. Simple enough.

Setup

There is a commit_sha parameter which we will ignore if it’s empty but check it out otherwise:

param

The job is configured to use the Git plugin and the branch specifier is set to */master:

pipeline

Important: Lightweight checkout is not enabled.

(the repository name is a coincidence)

The Jenkinsfile (don’t copy this, the correct version is at the bottom):

node {
  stage("Hello") {
    print "Hi!"

    if (params.commit_sha?.trim()) {
      print "Checking out commit ${commit_sha}"

      scmVars = checkout([
        $class: 'GitSCM',
        branches: [[name: params.commit_sha]],
      ])
    } else {
      print "Checking out master"

      scmVars = checkout(scm)
    }
  }
}

Lastly, we work with two branches:

  • master
    8c9963d (HEAD -> master) Commit in 'master'
    ce3ed60 Another commit in master
    
  • feat-1
    9628c6f (HEAD -> feat-1) Commit in 'feat-1'
    

Verifying the setup

Running the job without the commit_sha parameter works as expected:

master-init

Running the job with ce3ed60 as the commit parameter also works as expected:

commit-init

Now let’s build the commit from a branch other than master, commit 9628c6f from feat-1.

commit-fails

ERROR: Couldn't find any revision to build. Verify the repository and branch configuration for this job.

The commit is obviously present but it still fails to check it out. What’s happening here? Ah right, the job is limited to the master branch only, so you remove that:

any-branch

Start the job with the commit, and:

ERROR: Couldn't find any revision to build. Verify the repository and branch configuration for this job.

What?!

Solution

The issue is twofold:

  1. The initial checkout (configured in the UI, not in the Jenkinsfile) is partial (sparse)
  2. The second checkout, defined in the if/else block, is working on a local copy of the repo which is non-existent

The first checkout, visible in the log, is defined in the Pipeline setup, and it’s concerned with checking out the Jenkinsfile only and only that. If you list the contents of the current directory before our own checkout call, you will find an empty directory.

Actually, the second run where we used the previous commit from master worked because the repository was fully checked out in the initial job, when commit_sha wasn’t defined and checkout(scm) ran. This made the state ‘dirty’ and the subsequent job knew about that commit.

The checkout in the if block, which tries to use the revision from the parameter, doesn’t have a repo to work with. Intuitively, you’d expect it to know about the repository configured in the settings, and it actually does in the specific case of checkout(scm). The scm variable comes from the job’s settings and equals to something like this:

checkout([
  $class: 'GitSCM',
  branches: scm.branches,
  extensions: scm.extensions,
  userRemoteConfigs: scm.userRemoteConfigs
])

But in our setup we have:

checkout([
  $class: 'GitSCM',
  branches: [[name: params.name]]
])

The key takeaway here is that userRemoteConfigs doesn’t default to scm.userRemoteConfigs. When that field is omitted, the checkout step works on what it has locally, which is nothing in this case, as the initial checkout is sparse. The error message does make sense after all.

The else block, doing the checkout(scm), worked because it does set that field based on scm (as seen above).

So the fix is:

checkout([
  $class: 'GitSCM',
  branches: [[name: params.name]],
  userRemoteConfigs: scm.userRemoteConfigs
])

(Accessing scm fields might require a permission in your setup.)

At this point, the branch specifier can be changed back to */master, so the Jenkinsfile will be checked out from there. Specifying a commit from a branch works now.

success

Summary

Like in this case, the root cause is usually the user, but the plugin did a really good job of not helping. This behavior is described as If the remote URL does NOT end with /.git, a bare repository is assumed. in the documentation. At the end, everything did exactly what it said it would:

  • the main SCM configuration checked out the Jenkinsfile only, thus: script-from-scm
  • the Git plugin worked on a local, bare repository as the omitted field doesn’t end with .git because it’s, well, empty

Things like this can be easily overlooked, especially because the code checkout part is usually done once and never touched again. The basics will always find a way to you, when you least expect them.