This blog post is about the talk I gave at the London Salesforce Developers' March Meetup - Click here to view the video or the associated slide deck.

The Salesforce CLI is a command line tool that came out of the SalesforceDX initiative, hence it is named 'sfdx'. It's a standalone tool that must be installed on your local machine, but it doesn't require you to use other aspects of SalesforceDX such as scratch orgs - all of the examples in this post are based on using a regular developer edition.

The SalesforceCLI is fast becoming the only tool that you need to manage your Salesforce org metadata. It also has capabilities around data management, but if you are working with large amounts of data I'd definitely advocate sticking with your current tooling (e.g. Jitterbit, MuleSoft, Apex Data Loader). In the following sections I'll show how the SalesforceCLI can be used in various scenarios - these all assume that you have been through the installation instructions and have a working sfdx command.

 

Authorising the SalesforceCLI for a Salesforce instance

In order to authorise the SalesforceCLI for a Salesforce instance you need to login to the org and approve the oauth access. Execute:

sfdx force:auth:web:login

which opens the Salesforce UI at the login page. Enter the credentials and click the 'Login' button, then click the 'Allow' button on the resulting page. You'll see the following output at the command line, indicating that you have successfully authorised sfdx to access your Salesforce instance:

Successfully authorized <username> with org ID <id>
​You may now close the browser

In order to verify this, execute the following to open the org in your browser:

sfdx force:org:open -u <username>

which takes you straight to the home page without the need to authenticate. While at first glance this looks pretty cool, and is a lot of the time, it's also a security risk. If you leave your local machine unlocked and unattended, anyone can see which orgs you have authorised by running:

sfdx force:org:list

and can then open these orgs at will. This isn't a big deal if, as in these examples, you are targeting a developer edition, but it is if you have logged into a production org with real data, you'll want to logout again as soon as you've completed your work in that org:

sfdx force:auth:logout -u <username>

this will generate a warning that you only need to care about for scratch orgs, and you'll need to confirm you really want to logout. If you then attempt to open the org again, you'll get an error that no org configuration was found for the username, which is exactly what you want.

 

Deploying metadata to a Salesforce instance

Much like existing tools such as the Force.com IDE and Migration Tool (ant), you'll need a structured source directory (src) and a manifest file (package.xml) in order to deploy metadata. All examples from this point on are based on the Bob Buzzard Training System codebase, which is structured appropriately and handily includes a package.xml manifest file. If you want to follow along, clone the repository to your local machine and change into the newly created directory containing the clone).

When deploying, you can choose to wait until the deployment has completed (or until the specified number of minutes has expired) or to return straight away and poll to see the status. To wait for the deployment to complete for 2 minutes, execute:

sfdx force:mdapi:deploy -d src -w 2 -u <username>

Which produces output similar to the following:

=== Status
​Status:  InProgress

jobid:  0Af1r00000Vq2uHCAR

  ...

Deployment finished in 7000ms

=== Result
Status:  Succeeded
jobid:  0Af1r00000Vq2uHCAR
Completed:  2018-04-16T10:31:39.000Z
Component errors:  0
Components deployed:  40
Components total:  40
Tests errors:  0
Tests completed:  0
Tests total:  0
Check only: false

To deploy and then poll, execute:

sfdx force:mdapi:deploy -d src -u <username>

This immediately returns with the following output:

=== Status
Status:  Queued
jobid:  0Af1r00000Vq2yxCAB

The deploy request did not complete within the specified wait time [0 minutes].
To check the status of this deployment, run "sfdx force:mdapi:deploy:report"

You can then poll for the status of the deployment:

sfdx force:mdapi:deploy:report -u <username> -i 0Af1r00000Vq2yxCAB

Note that the -i switch value is the jobid from the output of the previous command:

Deployment finished in 9000ms

=== Result

Status:  Succeeded
jobid:  0Af1r00000Vq2yxCAB
Completed:  2018-04-16T10:33:20.000Z
Component errors:  0
Components deployed:  40
Components total:  40
Tests errors:  0
Tests completed:  0
Tests total:  0
Check only: false

 

Executing Unit Tests

Much like deployment, there are two flavours of test execution - waiting for the tests to complete or polling for the status. However, there are also variants that describe which test you want to run.  For example, to run all tests in the local namespace and wait up to 2 minutes for the results, execute the following command:

sfdx force:apex:test:run -l RunLocalTests -u <username> -w 2

which generates the following results:

=== Test Results
TEST NAME                                            OUTCOME  MESSAGE  RUNTIME (MS)
───────────────────────────────────────────────────  ───────  ───────  ────────────
ConfigAccessorTest.GetAllEndpointsTest               Pass              54
ConfigAccessorTest.GetEndpointTest                   Pass              9
ConfigAccessorTest.GetTrainingServiceFromConfigTest  Pass              21
TrainingServiceRemoteImpl_Test.FailStepAndWait       Pass              47
TrainingServiceRemoteImpl_Test.GetUserInfo           Pass              16
TrainingServiceRemoteImpl_Test.PassStep              Pass              9
TrainingServiceRemoteImpl_Test.TestGetAllPaths       Pass              12
TrainingServiceRemoteImpl_Test.TestGetPath           Pass              10
TrainingServiceRemoteImpl_Test.TestGetStep           Pass              12

=== Test Summary
NAME                 VALUE
───────────────────  ────────────────────────────
Outcome              Passed
Tests Ran            9
Passing              9
Failing              0
Skipped              0
Pass Rate            100%
Fail Rate            0%
Test Start Time      Apr 16, 2018 12:07 PM
Test Execution Time  190 ms
Test Total Time      190 ms
Command Time         11012 ms
Test Run Id          7071r00005m5k5x

If I've only changed one class and just want to execute the tests on that, I can specify the tests to run via the -t switch. To execute just the tests in the ConfigAccessorTest class and poll for the results, I execute:

sfdx force:apex:test:run -l RunSpecifiedTests -t ConfigAccessorTest -u <username>

In this case, the Salesforce CLI tells me not only the command to run, but also the parameters - why this is different to deployment I'm afraid I don't know:

Run "sfdx force:apex:test:report -i 7071r00005m5kEJ -u <username>" to retrieve test results.

Executing the command shows me that just the tests I requested were run:

=== Test Results
​TEST NAME                                            OUTCOME  MESSAGE  RUNTIME (MS)
───────────────────────────────────────────────────  ───────  ───────  ────────────
ConfigAccessorTest.GetAllEndpointsTest               Pass              35
ConfigAccessorTest.GetEndpointTest                   Pass              9
ConfigAccessorTest.GetTrainingServiceFromConfigTest  Pass              10

=== Test Summary
NAME                 VALUE
───────────────────  ────────────────────────────
Outcome              Passed
Tests Ran            3
Passing              3
Failing              0
Skipped              0
Pass Rate            100%
Fail Rate            0%
Test Start Time      Apr 16, 2018 12:09 PM
Test Execution Time  54 ms
Test Total Time      54 ms
Command Time         676 ms
Test Run Id          7071r00005m5kEJ

 

Retrieving Metadata

In order to retrieve metadata a manifest file (package.xml) describing the metadata to be retrieved is required. This is one area where I think the old Force CLI has better functionality than the Salesforce CLI, as it was able to extract all metadata of a specific type or types without needing a manifest file. I've manually created a package.xml in a new subdirectory named retrieve:

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
   <members>*</members>
   <name>FlexiPage</name>
</types>
<types>
   <members>Training_Admin</members>
   <name>PermissionSet</name>
</types>
<version>39.0</version>
​</Package>

This will retrieve all Lightning page (FlexiPage) metadata components and a single permission set (PermissionSet) named 'Training_Admin'

As before, I can either wait for the metadata to be retrieved:

sfdx force:mdapi:retrieve -k retrieve/package.xml -r . -u <username>

Note that this time if I don't specify a -w switch the command waits for the retrieve to complete. Again I don't know why this isn't consistent with the other commands.

Retrieving source...

=== Status
Status:  Pending
jobid:  09S1r000001zs3hEAA

=== Result
Status:  Succeeded
jobid:  09S1r000001zs3hEAA

Wrote retrieve zip to /Users/kbowden/SalesforceCLI/Meetup/unpackaged.zip

When the command completes, the retrieved metadata is stored in a zip file in the local directory. 

If I choose to poll for the results:

sfdx force:mdapi:retrieve -k retrieve/package.xml -r . -u <username> -w 0

I get the output varianty identifying the command but without the parameters:

Retrieving source...

=== Status
Status:  Queued
jobid:  09S1r000001zs3rEAA

The retrieve request did not complete within the specified wait time [0 minutes].
​To check the status of this retrieve, run "sfdx force:mdapi:retrieve:report"

Executing the report command completes the retrieve:

sfdx force:mdapi:retrieve:report -r . -u <username> -i 09S1r000001zs3rEAA

And generates the output:

=== Result
Status:  Succeeded
jobid:  09S1r000001zs3rEAA

​Wrote retrieve zip to /Users/kbowden/SalesforceCLI/Meetup/unpackaged.zip.

When executing tests or deploying metadata, I can run the report commands as often as I like after the event and, assuming the details haven't expired, I get the results. This is not the case for metadata retrieval - once the zip file has been written attempting to report again gives an error:

ERROR:  INVALID_LOCATOR: Retrieve result has been deleted.

 

Querying Salesforce Data

As I mentioned earlier, the Salesforce CLI isn't limited to metadata, it can also access your Salesforce data, which is another very good reason to make sure you don't inadvertently leave yourself logged into your production org.

To query the contacts in my org, I can simply execute a SOQL query:

sfdx force:data:soql:query -q 'select id, FirstName, LastName from Contact' -u <username>

Which retrieves all of those in my dev org:

ID                  FIRSTNAME  LASTNAME
──────────────────  ─────────  ─────────
0031r000024AyurAAC  Rose       Gonzalez
0031r000024AyusAAC  Sean       Forbes
0031r000024AyutAAC  Jack       Rogers
0031r000024AyuuAAC  Pat        Stumuller
0031r000024AyuvAAC  Andy       Young
0031r000024AyuwAAC  Tim        Barr
0031r000024AyuxAAC  John       Bond
0031r000024AyuyAAC  Stella     Pavlova
0031r000024AyuzAAC  Lauren     Boyle
0031r000024Ayv0AAC  Babara     Levy
0031r000024Ayv1AAC  Josh       Davis
0031r000024Ayv2AAC  Jane       Grey
0031r000024Ayv3AAC  Arthur     Song
0031r000024Ayv4AAC  Ashley     James
0031r000024Ayv5AAC  Tom        Ripley
0031r000024Ayv6AAC  Liz        D'Cruz
0031r000024Ayv7AAC  Edna       Frank
0031r000024Ayv8AAC  Avi        Green
0031r000024Ayv9AAC  Siddartha  Nedaerk
0031r000024AyvAAAS  Jake       Llorrac
Total number of records retrieved: 20.

But I'm not limited to querying - I can also change the data directly from the command line, supplying the ID of the record to update and the collection of changed fields:

sfdx force:data:record:update -s Contact -i 0031r000024AyurAAC -v 'FirstName=Harry LastName=Potter' -u <username>

If I then rerun my query, I can see that Rose Gonzalez (record id 0031r000024AyurAAC) has been renamed Harry Potter :

ID                  FIRSTNAME  LASTNAME
──────────────────  ─────────  ─────────
0031r000024AyurAAC  Harry      Potter

 

Creating Metadata

As well as creating data, I can create metadata - if you are using the VSCode plugin this is how new components are created All of the required files are created along with the actual metadata file, so the meta-xml for the likes of Apex classes and Visualforce pages, and a whole bunch of files for Lightning Components.

Apex Class:

sfdx force:apex:class:create -n NewClass -d newmd/classes

target dir = /Users/kbowden/SalesforceCLI/Meetup/newmd/classes
  create NewClass.cls
  create NewClass.cls-meta.xml

Lightning Component:

sfdx force:lightning:component:create -n NewComp -d newmd/aura

target dir = /Users/kbowden/SalesforceCLI/Meetup/newmd/aura
  create NewComp/NewComp.cmp
  create NewComp/NewComp.cmp-meta.xml
  create NewComp/NewCompController.js
  create NewComp/NewCompHelper.js
  create NewComp/NewComp.css
  create NewComp/NewCompRenderer.js
  create NewComp/NewComp.svg
  create NewComp/NewComp.auradoc
  create NewComp/NewComp.design

Visualforce:

sfdx force:visualforce:page:create -n NewPage -l NewPage -d newmd/pages

​target dir = /Users/kbowden/SalesforceCLI/Meetup/newmd/pages
  create NewPage.page
  create NewPage.page-meta.xml

Note that the metadata files are created on the local machine and not deployed to any server - I have to carry out a deployment if I want to add them to a specific Salesforce instance - so I don't specify the -u switch in any of the commands.

 

Chaining Commands

Executing commands manually is one way to work, things get more interesting once I wreao the Salesforce CLI in a script. The following script deploys the codebase to my dev org and executes the unit tests:

#!/bin/bash
sfdx force:mdapi:deploy -d src -u <username> -w 2
​sfdx force:apex:test:run -l RunLocalTests -u <username> -w 2

Executing this gives the following results:

Deploying /var/folders/tn/q5mzq6n53blbszymdmtqkflc0000gs/T/src.zip...

Deployment finished in 8000ms

=== Result
Status:  Succeeded
jobid:  0Af1r00000VqFkWCAV
Completed:  2018-04-16T11:42:11.000Z
Component errors:  0
Components deployed:  40
Components total:  40
Tests errors:  0
Tests completed:  0
Tests total:  0
Check only: false

=== Test Results
TEST NAME                                            OUTCOME  MESSAGE  RUNTIME (MS)
───────────────────────────────────────────────────  ───────  ───────  ────────────
ConfigAccessorTest.GetAllEndpointsTest               Pass              41
ConfigAccessorTest.GetEndpointTest                   Pass              7
ConfigAccessorTest.GetTrainingServiceFromConfigTest  Pass              18
TrainingServiceRemoteImpl_Test.FailStepAndWait       Pass              59
TrainingServiceRemoteImpl_Test.GetUserInfo           Pass              16
TrainingServiceRemoteImpl_Test.PassStep              Pass              9
TrainingServiceRemoteImpl_Test.TestGetAllPaths       Pass              12
TrainingServiceRemoteImpl_Test.TestGetPath           Pass              11
TrainingServiceRemoteImpl_Test.TestGetStep           Pass              11

=== Test Summary
NAME                 VALUE
───────────────────  ────────────────────────────
Outcome              Passed
Tests Ran            9
Passing              9
Failing              0
Skipped              0
Pass Rate            100%
Fail Rate            0%
Test Start Time      Apr 16, 2018 12:42 PM
Test Execution Time  184 ms
Test Total Time      184 ms
Command Time         10089 ms
Test Run Id          7071r00005m5m6x

 

Processing Output

Scripting isn't just about back to back commands - it can also capture the output of commands and process it. The output from the deployment is quite wordy when things haven't completed, so I've created a Node JS script to capture the report output in JSON format and create a single line of output. The script is a bit long for this blog but you can find it at this gist.

Executing the script:

node 1deploy.js

Results in much reduced output:

Deployment Queued
Deployment Pending (0/0)
Deployment Pending (0/0)
Deployment Pending (0/0)
Deployment Pending (0/0)
Deployment InProgress (0/16)
Deployment InProgress (0/16)
Deployment Succeeded

I can also take action based on the output - this gist is another Node JS script, based on the first, that generates a MacOS notification when the deployment finishes.

 

Orchestration

Orchestration, where scripts include business logic to take decisions is where the real power of the Salesforce CLI becomes evident. This gist is yet another Node JS script to carry out deployment, but with one key difference - it returns an appropriate exit value, 0 for success, 1 for failure.

I then have a script that orchestrates a deploy/test cycle :

  • Clones the Bob Buzzard Training System from Github
  • Deploys to my dev org
  • Updates a custom setting with the current Git commit id, so that I can easily get at the code that I deployed.

The last step is only carried out if the deployment is successful.

#!/bin/bash

git clone https://github.com/keirbowden/bbtrn.git Orchestrate

node deploy.js

if [ $? -eq 0 ]
then
       cd Orchestrate
       COMMIT_ID=`git rev-parse HEAD`
       sfdx force:data:record:update -s Version__c -i a001r00000oZi8w -v "Version__c=$COMMIT_ID" -u <username>
else
       echo -e "\n\nDeployment failed - not updating commit id"
fi

Executing this with no issues in the source code completes all steps:

Cloning into 'Orchestrate'...
remote: Counting objects: 396, done.
remote: Compressing objects: 100% (192/192), done.
remote: Total 396 (delta 200), reused 348 (delta 172), pack-reused 0
Receiving objects: 100% (396/396), 89.79 KiB | 446.00 KiB/s, done.
Resolving deltas: 100% (200/200), done.

Deployment Queued
Deployment InProgress (0/41)
Deployment InProgress (23/41)
Deployment InProgress (40/41)
Deployment Succeeded

Successfully updated record: a001r00000oZi8w.

However, if I introduce a problem to the source code:

Cloning into 'Orchestrate'...
remote: Counting objects: 396, done.
remote: Compressing objects: 100% (192/192), done.
remote: Total 396 (delta 200), reused 348 (delta 172), pack-reused 0
Receiving objects: 100% (396/396), 89.78 KiB | 437.00 KiB/s, done.
Resolving deltas: 100% (200/200), done.

Deployment Queued
Deployment InProgress (0/41)
Deployment InProgress (23/41)
Deployment InProgress (39/41)
Deployment Failed
Error: src/classes/ConfigAccessor.cls: Line 1, col 1 : Unexpected token 'aksjfnskacn'.

Deployment failed - not updating commit id

 

More Information

This post has just scratched the surface of both the Salesforce CLI commands and how they can be used to orchestrate deployment processes. For details of the other commands available, see the Salesforce CLI Command Reference.

Keir Bowden is CTO of BrightGen, a Certified Technical Architect and multi-time Salesforce MVP - you can find him on twitter @bob_buzzard.