Newer Version Available
Jenkinsfile Walkthrough
This walkthrough relies on the sfdx-jenkins-package Jenkinsfile. We assume that you are familiar with the structure of the Jenkinsfile, Jenkins Pipeline DSL, and the Groovy programming language. This walkthrough demonstrates implementing a Jenkins pipeline using Salesforce CLI and scratch orgs. See the CLI Command Reference regarding the commands used.
This workflow most closely corresponds to Jenkinsfile stages.
- Define Variables
- Check Out the Source Code
- Wrap All Stages in a withCredentials Command
- Wrap All Stages in a withEnv Command
- Authorize Your Dev Hub Org and Create a Scratch Org
- Push Source and Assign a Permission Set
- Run Apex Tests
- Delete the Scratch Org
- Create a Package
- Create a Scratch Org and Display Info
- Install Package, Run Unit Tests, and Delete Scratch Org
Define Variables
Use the def keyword to define the variables required by Salesforce CLI commands. Assign each variable the corresponding environment variable that you previously set in your Jenkins environment.
1def SF_CONSUMER_KEY=env.SF_CONSUMER_KEY
2def SERVER_KEY_CREDENTALS_ID=env.SERVER_KEY_CREDENTALS_ID
3def TEST_LEVEL='RunLocalTests'
4def PACKAGE_NAME='0Ho1U000000CaUzSAK'
5def PACKAGE_VERSION
6def SF_INSTANCE_URL = env.SF_INSTANCE_URL ?: "https://login.salesforce.com"Define the SF_USERNAME variable, but don’t set its value. You do that later.
1def SF_USERNAMEAlthough not required, we assume that you used the Jenkins Global Tool Configuration to create the toolbelt custom tool that points to the CLI installation directory. In your Jenkinsfile, use the tool command to set the value of the toolbelt variable to this custom tool.
1def toolbelt = tool 'toolbelt'You can now reference the Salesforce CLI executable in the Jenkinsfile using ${toolbelt}/sfdx.
Check Out the Source Code
Before testing your code, get the appropriate version or branch from your version control system (VCS) repository. In this example, we use the checkout scm Jenkins command. We assume that the Jenkins administrator has already configured the environment to access the correct VCS repository and check out the correct branch.
1stage('checkout source') {
2 // when running in multi-branch job, one must issue this command
3 checkout scm
4 }Wrap All Stages in a withCredentials Command
You previously stored the JWT private key file as a Jenkins Secret File using the Credentials interface. Therefore, you must use the withCredentials command in the body of the Jenkinsfile to access the secret file. The withCredentials command lets you name a credential entry, which is then extracted from the credential store and provided to the enclosed code through a variable. When using withCredentials, put all stages within its code block.
This example stores the credential ID for the JWT key file in the variable SERVER_KEY_CREDENTALS_ID. You defined the SERVER_KEY_CREDENTALS_ID earlier and set it to its corresponding environment variable. The withCredentials command fetches the contents of the secret file from the credential store and places the contents in a temporary location. The location is stored in the variable server_key_file. You use the server_key_file variable with the auth:jwt command to specify the private key securely.
1withCredentials([file(credentialsId: SERVER_KEY_CREDENTALS_ID, variable: 'server_key_file')])
2 # all stages will go here
3}Wrap All Stages in a withEnv Command
When running Jenkins jobs, it’s helpful to understand where files are being stored. There are two main directories to be mindful of: the workspace directory and the home directory. The workspace directory is unique to each job while the home directory is the same for all jobs.
The withCredentials command stores the JWT key file in the Jenkins workspace during the job. However, Salesforce CLI auth commands store authentication files in the home directory; these authentication files persist outside of the duration of the job.
This setup is not a problem when you run a single job but can cause problems when you run multiple jobs. So, what happens if you run multiple jobs using the same Dev Hub or other Salesforce user? When the CLI tries to connect to the Dev Hub as the user you authenticated, it fails to refresh the token. Why? The CLI tries to use a JWT key file that no longer exists in the other workspace, regardless of the withCredentials for the current job.
If you set the home directory to match the workspace directory using withEnv, the authentication files are unique for each job. Creating unique auth files per job is also more secure because each job has access only to the auth files it creates.
When using withEnv, put all stages within its code block,
1withEnv(["HOME=${env.WORKSPACE}"]) {
2 # all stages will go here
3}Authorize Your Dev Hub Org and Create a Scratch Org
This sfdx-jenkins-package example uses two stages: one stage to authorize the Dev Hub org and another stage to create a scratch org.
1// -------------------------------------------------------------------------
2// Authorize the Dev Hub org with JWT key and give it an alias.
3// -------------------------------------------------------------------------
4
5stage('Authorize DevHub') {
6 rc = command "${toolbelt}/sfdx auth:jwt:grant --instanceurl ${SF_INSTANCE_URL} --clientid ${SF_CONSUMER_KEY} --username ${SF_USERNAME} --jwtkeyfile ${server_key_file} --setdefaultdevhubusername --setalias HubOrg"
7 if (rc != 0) {
8 error 'Salesforce dev hub org authorization failed.'
9 }
10}
11
12
13// -------------------------------------------------------------------------
14// Create new scratch org to test your code.
15// -------------------------------------------------------------------------
16
17stage('Create Test Scratch Org') {
18 rc = command "${toolbelt}/sfdx force:org:create --targetdevhubusername HubOrg --setdefaultusername --definitionfile config/project-scratch-def.json --setalias ciorg --wait 10 --durationdays 1"
19 if (rc != 0) {
20 error 'Salesforce test scratch org creation failed.'
21 }
22}Use auth:jwt:grant to authorize your Dev Hub org.
You are required to run this step only once, but we suggest you add it to your Jenkinsfile and authorize each time you run the Jenkins job. This way you’re always sure that the Jenkins job is not aborted due to lack of authorization. There is typically little harm in authorizing multiple times, but keep in mind that the API call limit for your scratch org’s edition still applies.
Use the parameters of the auth:jwt:grant command to provide information about the Dev Hub org that you’re authorizing. The values for the --clientid, --username, and --instanceurl parameters are the SF_CONSUMER_KEY, HubOrg, and SF_INSTANCE_URL environment variables you previously defined, respectively. The value of the --jwtkeyfile parameter is the server_key_file variable that you set in the previous section using the withCredentials command. The --setdefaultdevhubusername parameter specifies that this HubOrg is the default Dev Hub org for creating scratch orgs.
Use the force:org:create CLI command to create a scratch org. In the example, the CLI command uses the config/project-scratch-def.json file (relative to the project directory) to create the scratch org. The --json parameter specifies the output as JSON format. The --setdefaultusername parameter sets the new scratch org as the default.
The Groovy code that parses the JSON output of the force:org:create command extracts the username that was auto-generated as part of the org creation. This username, stored in the SF_USERNAME variable, is used with the CLI commands that push source, assign a permission set, and so on.
Push Source and Assign a Permission Set
Let’s populate your new scratch org with metadata. This example uses the force:source:push command to upload your source to the org. The source includes all the pieces that make up your Salesforce application: Apex classes and test classes, permission sets, layouts, triggers, custom objects, and so on.
1// -------------------------------------------------------------------------
2// Push source to test scratch org.
3// -------------------------------------------------------------------------
4
5stage('Push To Test Scratch Org') {
6 rc = command "${toolbelt}/sfdx force:source:push --targetusername ciorg"
7 if (rc != 0) {
8 error 'Salesforce push to test scratch org failed.'
9 }
10}Recall the SF_USERNAME variable that contains the auto-generated username that was output by the force:org:create command in an earlier stage. The code uses this variable as the argument to the --targetusername parameter to specify the username for the new scratch org.
The force:source:push command pushes all the Salesforce-related files that it finds in your project. Add a .forceignore file to your repository to list the files that you don’t want pushed to the org.
Run Apex Tests
Now that your source code and test source are pushed to the scratch org, run the force:apex:test:run command to run Apex tests.
1// -------------------------------------------------------------------------
2// Run unit tests in test scratch org.
3// -------------------------------------------------------------------------
4
5stage('Run Tests In Test Scratch Org') {
6 rc = command "${toolbelt}/sfdx force:apex:test:run --targetusername ciorg --wait 10 --resultformat tap --codecoverage --testlevel ${TEST_LEVEL}"
7 if (rc != 0) {
8 error 'Salesforce unit test run in test scratch org failed.'
9 }
10}You can specify various parameters to the force:apex:test:run CLI command. In the example:
- The --testlevel ${TEST_LEVEL} option runs all tests in the scratch org, except tests that originate from installed managed packages. You can also specify RunLocalTests to run only local tests, RunSpecifiedTests to run only certain Apex tests or suites or RunAllTestsInOrg to run all tests in the org.
- The --resultformat tap option specifies that the command output is in Test Anything Protocol (TAP) format. The test results that are written to a file are still in JUnit and JSON formats.
- The --targetusername ciorg option specifies the username for accessing the scratch org (the value in SF_USERNAME).
The force:apex:test:run command writes its test results in JUnit format.
Delete the Scratch Org
Salesforce reserves the right to delete a scratch org a specified number of days after it was created. You can also create a stage in your pipeline that uses force:org:delete to explicitly delete your scratch org when the tests complete. This cleanup ensures better management of your resources.
1// -------------------------------------------------------------------------
2// Delete package install scratch org.
3// -------------------------------------------------------------------------
4
5stage('Delete Package Install Scratch Org') {
6 rc = command "${toolbelt}/sfdx force:org:delete --targetusername installorg --noprompt"
7 if (rc != 0) {
8 error 'Salesforce package install scratch org deletion failed.'
9 }
10}Create a Package
Now, let’s create a package. If you’re new to packaging, you can think about a package as a container that you fill with metadata. It contains a set of related features, customizations, and schema. You use packages to move metadata from one Salesforce org to another. After you create a package, add metadata and create a new package version.
1// -------------------------------------------------------------------------
2// Create package version.
3// -------------------------------------------------------------------------
4
5stage('Create Package Version') {
6 if (isUnix()) {
7 output = sh returnStdout: true, script: "${toolbelt}/sfdx force:package:version:create --package ${PACKAGE_NAME} --installationkeybypass --wait 10 --json --targetdevhubusername HubOrg"
8 } else {
9 output = bat(returnStdout: true, script: "${toolbelt}/sfdx force:package:version:create --package ${PACKAGE_NAME} --installationkeybypass --wait 10 --json --targetdevhubusername HubOrg").trim()
10 output = output.readLines().drop(1).join(" ")
11}
12
13 // Wait 5 minutes for package replication.
14 sleep 300
15
16 def jsonSlurper = new JsonSlurperClassic()
17 def response = jsonSlurper.parseText(output)
18
19 PACKAGE_VERSION = response.result.SubscriberPackageVersionId
20
21 response = null
22
23 echo ${PACKAGE_VERSION}
24}Create a Scratch Org and Display Info
Remember when you created a scratch org earlier? Now let’s create a new scratch org to install your package into, and display info about that scratch org.
1// -------------------------------------------------------------------------
2// Create new scratch org to install package to.
3// -------------------------------------------------------------------------
4
5stage('Create Package Install Scratch Org') {
6 rc = command "${toolbelt}/sfdx force:org:create --targetdevhubusername HubOrg --setdefaultusername --definitionfile config/project-scratch-def.json --setalias installorg --wait 10 --durationdays 1"
7 if (rc != 0) {
8 error 'Salesforce package install scratch org creation failed.'
9 }
10}
11
12
13// -------------------------------------------------------------------------
14// Display install scratch org info.
15// -------------------------------------------------------------------------
16
17stage('Display Install Scratch Org') {
18 rc = command "${toolbelt}/sfdx force:org:display --targetusername installorg"
19 if (rc != 0) {
20 error 'Salesforce install scratch org display failed.'
21 }
22}Install Package, Run Unit Tests, and Delete Scratch Org
To finish up, install your package in your scratch org, run unit tests, then delete the scratch org. That’s it!
1// -------------------------------------------------------------------------
2// Install package in scratch org.
3// -------------------------------------------------------------------------
4
5stage('Install Package In Scratch Org') {
6 rc = command "${toolbelt}/sfdx force:package:install --package ${PACKAGE_VERSION} --targetusername installorg --wait 10"
7 if (rc != 0) {
8 error 'Salesforce package install failed.'
9 }
10}
11
12
13// -------------------------------------------------------------------------
14// Run unit tests in package install scratch org.
15// -------------------------------------------------------------------------
16
17stage('Run Tests In Package Install Scratch Org') {
18 rc = command "${toolbelt}/sfdx force:apex:test:run --targetusername installorg --resultformat tap --codecoverage --testlevel ${TEST_LEVEL} --wait 10"
19 if (rc != 0) {
20 error 'Salesforce unit test run in pacakge install scratch org failed.'
21 }
22}
23
24
25// -------------------------------------------------------------------------
26// Delete package install scratch org.
27// -------------------------------------------------------------------------
28
29stage('Delete Package Install Scratch Org') {
30 rc = command "${toolbelt}/sfdx force:org:delete --targetusername installorg --noprompt"
31 if (rc != 0) {
32 error 'Salesforce package install scratch org deletion failed.'
33 }
34}