This blog post is about the talk I gave at one of the London Salesforce Developers’ Meetup on the Salesforce CLI. 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.