Salesforce DX – Setup Jenkins – Jenkinsfile

In Salesforce DX – Setup Jenkins we set up Jenkins to pull from our Github repository.  It really didn’t do anything else since there was no Jenkinsfile to explain to Jenkins what to do.  Lets provide a remedy for that now.  But first…

Jenkinsfile… wha?

When Jenkins 2.0 was released they introduced this concept of “Pipeline as Code“.  It was a new way to configure a build pipeline for Jenkins.  All described in a human-editable file called a Jenkinsfile.   The Jenkinsfile was defined in the repository that was to be built.  Since we used the default setup it was looking for a file at the root called Jenkinsfile and since we didn’t add one yet, SURPRISE! Nothing happened.

If you used Jenkins before version 2.0 was release build setup were configured manually.  If you had changes the only way to track them was if the user made notes.  Should people make notes when updating configurations?  Yes!  Do they?  Not always.   Pipeline as Code means all changes are now tracked by the same source control that tracks  code changes.

All that to say, We need a Jenkinsfile.

Jenkinsfile Setup

In our project let’s add a new file named Jenkinsfile at the root.  for our starter Jenkinsfile we are going to copy the one from /sfdx-dreamhouse and then adjust it to suit our needs to let’s go get the contents of it and add it to our file.   Now let’s look at what we have.

Import JsonSlurperClassic

#!groovy
import groovy.json.JsonSlurperClassic

For lines 1 and 2 we import JsonSlurperClassic so we can use it to parse json responses later on.

Open node

node {

Close node

}

At Line 3 we open a node statement that we close on line 64.  What the node statement does is create the work space and allocate the executor for the build to occur.   If we didn’t do this we couldn’t build anything.

Define Variables

def BUILD_NUMBER=env.BUILD_NUMBER
def RUN_ARTIFACT_DIR="tests/${BUILD_NUMBER}"
def SFDC_USERNAME

def HUB_ORG=env.HUB_ORG_DH
def SFDC_HOST = env.SFDC_HOST_DH
def JWT_KEY_CRED_ID = env.JWT_CRED_ID_DH
def CONNECTED_APP_CONSUMER_KEY=env.CONNECTED_APP_CONSUMER_KEY_DH

def toolbelt = tool 'toolbelt'

For lines 5 through 14 we define some variables to use later on.  Most of the values are set from the environment.  Line 14 we define the toolbelt this is the SFDX CLI .  Since we haven’t set up any of these environment variables or the access to the SFDX CLI we will have to circle back to Jenkins ot set these up next time.

Checkout Source

stage('checkout source') {
    // when running in multi-branch job, one must issue this command
    checkout scm
}

Line 16 we have a stage directive named “checkout source`.  During this stage we will get the code for the build.  Using the checkout keyword will checkout the code from the source control specified in the Jenkins setup.  The scm variable is that tells the checkout command what reversion triggered the build.

OpenwithCredentials

withCredentials([file(credentialsId: JWT_KEY_CRED_ID, variable: 'jwt_key_file')]) {

Close withCredentials

}

At line 21 we open a withCredentials snippet.  This is provided by the Credentials Binding Plugin.  The credentials we use are provided by a combination of the variable we defined earlier and the secret file we saved last time.  This will prevent any of our “secrets” from showing up in the building log.

Create Scratch Org

stage('Create Scratch Org') {

    rc = sh returnStatus: true, script: "${toolbelt}/sfdx force:auth:jwt:grant --clientid ${CONNECTED_APP_CONSUMER_KEY} --username ${HUB_ORG} --jwtkeyfile ${jwt_key_file} --setdefaultdevhubusername --instanceurl ${SFDC_HOST}"
    if (rc != 0) { error 'hub org authorization failed' }

    // need to pull out assigned username
    rmsg = sh returnStdout: true, script: "${toolbelt}/sfdx force:org:create --definitionfile config/workspace-scratch-def.json --json --setdefaultusername"
    printf rmsg
    def jsonSlurper = new JsonSlurperClassic()
    def robj = jsonSlurper.parseText(rmsg)
    if (robj.status != "ok") { error 'org creation failed: ' + robj.message }
    SFDC_USERNAME=robj.username
    robj = null

}

Here we are creating a scratch org.  At line 24 we run a shell command and get the return status of it.  As long as the return status is 0 everything is ok.  Line 28 we use a second shell command to call the same sfdx force:org:create that we have used in the past but with the added parameter of --json.  The --json parameter tells sfdx to format the results as json.  Those results are parse on line 31 with an instance of the JsonSlurperClassic we imported all the way back on line 2.  As long as the robj.status is ok on line 32 we save teh username that was returns in the variable that was defined on line 7 before nulling the robj out.

Push to Test Org

stage('Push To Test Org') {
    rc = sh returnStatus: true, script: "${toolbelt}/sfdx force:source:push --targetusername ${SFDC_USERNAME}"
    if (rc != 0) {
        error 'push failed'
    }
    // assign permset
    // rc = sh returnStatus: true, script: "${toolbelt}/sfdx force:user:permset:assign --targetusername ${SFDC_USERNAME} --permsetname WIPDeveloper"
    // if (rc != 0) {
    //     error 'permset:assign failed'
    // }
}

With the Push to Test Org stage we push the source to the scratch org we just created by specifying the --targetusername as what was saved on line 33.  Provided there are no errors we assign a permission set to the user that was created at line 33.

I don’t have any permission sets in my sample code yet so I have this commented out.

Run Tests

stage('Run Apex Test') {
    sh "mkdir -p ${RUN_ARTIFACT_DIR}"
    timeout(time: 120, unit: 'SECONDS') {
        rc = sh returnStatus: true, script: "${toolbelt}/sfdx force:apex:test:run --testlevel RunLocalTests --outputdir ${RUN_ARTIFACT_DIR} --resultformat tap --targetusername ${SFDC_USERNAME}"
        if (rc != 0) {
            error 'apex test run failed'
        }
    }
}

This may surprise you but with the Run Apex Test stage we will run the Apex Tests.  Surprise! More seriously though at line 51 we create a directory with the RUN_ARTIFACT_DIR that was defined and assigned at line 6.  At line 52 we use the timeout option to specify a timeout period of 120 seconds.  If it takes longer then the time specified Jenkins will abort the Pipeline.

Collect Results

stage('collect results') {
    junit keepLongStdio: true, testResults: 'tests/**/*-junit.xml'
}

With the Collect Results stage we use the junit plugin to collect the results.

Commit, Push and Nothing

With our Jenkins file defined let’s commit it to the repository and push it to the origin.  Don’t get too excited though as we haven’t set up those environment variables yet so your Jenkins may detect the change it will not build successfully yet.

Conclusion

We are almost ready for a successful build, one might way we are so close yet so var away.  Get it?  Let me know by leaving a comment below, emailing brett@wipdeveloper.com or following and yelling at me on Twitter/BrettMN.

Leave a Reply