Building an Azure DevOps Pipeline for Salesforce Scratch Org Automation: A Journey to Success

It's exciting to solve a challenging problem. I recently went through such a journey when tasked with automating Salesforce Scratch Org Automation workflows through Azure DevOps pipelines. I'm writing this blog to show how I solved the challenge, ensuring each pull request (PR) creation or merge triggered automation of scratch org creation, deployment, testing, and deletion. If you’re working with Salesforce and Azure DevOps, this Salesforce Scratch Org Automation guide might save you a lot of time and effort.

The requirement was simple in theory but diffucult in practice:

1. Whenever a pull request is created or merged into the master branch, an Azure Pipeline should:
- Create a Salesforce scratch org.
- Deploy the entire source code to the scratch org.
- Run all Apex tests.
- Validate that all tests pass.
2. After running the tests, delete the scratch org—whether the tests pass or fail.
3. If the pipeline fails at any step, an automatic email notification should be sent to the user who triggered the pipeline.

Though this seemed straightforward, it took significant research, experimentation, and debugging to perfect. Here’s how I tackled it.

Step 1: Setting Up the Pipeline Trigger

Azure DevOps provides flexible pipeline triggers. To meet the requirement, I configured triggers for:

Pull Request Creation: Ensures validation before merging into master.
Merge into Master: Validates the final state of the codebase.

Here’s the YAML snippet for the triggers:

trigger:
  branches:
    include:
      - master

pr:
  branches:
    include:
      - *

This configuration ensures the pipeline runs for both PRs and merges into `master`.

Step 2: JWT Authentication for Scratch Org Creation

A secure and scalable way to authenticate in CI/CD pipelines is using the JWT (JSON Web Token) flow. With JWT, the pipeline does not require interactive authentication and leverages a private key for secure communication.

Generate the Private Key and Self-Signed Certificate

Using OpenSSL, generate the private key and self-signed certificate:

# Create a directory for keys
mkdir ~/jwt_keys
cd ~/jwt_keys

# Generate private key
openssl genpkey -algorithm RSA -out server.key -pkeyopt rsa_keygen_bits:2048

# Create a certificate signing request (CSR)
openssl req -new -key server.key -out server.csr

# Generate self-signed certificate
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

Upload the `server.crt` to your Salesforce Connected App under "Use Digital Signatures."

Configure the Connected App in Salesforce

1. Navigate to  Setup > App Manager in Salesforce.
2. Create a new connected app with OAuth enabled.
3. Upload the `server.crt` and note the Consumer Key.
4. Add required OAuth Scopes such as:
- Access and manage your data (api)
- Perform requests on your behalf anytime (refresh_token, offline_access).

Authenticate via JWT in the Pipeline

In the pipeline, use the Salesforce CLI to authenticate via JWT:

- script: |
sf org login jwt --username your-username@example.com \
--jwt-key-file $(Agent.TempDirectory)/securedKeyForBuildValidation \
--client-id YOUR_CONSUMER_KEY \
--alias ci-scratch-org --set-default-dev-hub
displayName: 'Authenticate with JWT'

Ensure the private key is uploaded as a secure file in Azure DevOps (Go to Pipelines > Libraries > Add Secure file) and downloaded during the pipeline execution.

Step 3: Scratch Org Creation

Salesforce scratch orgs are temporary environments perfect for CI/CD workflows. Using the authenticated CLI session, I automated their creation:

- script: |
sf org create scratch --definition-file config/project-scratch-def.json --alias ci-scratch-org --duration-days 1
displayName: 'Create Scratch Org'

Step 4: Deploying the Code

With the scratch org ready, the next step was deploying the source code. I leveraged the force:source: deploy command:

- script: |
sf project deploy start --source-dir force-app --target-org ci-scratch-org --check-only
displayName: 'Deploy Code to Scratch Org'

This step ensures that the deployment is validated without committing changes.

Step 5: Running Apex Tests

Apex tests are critical for maintaining code quality. The following command executes all local tests, generating detailed reports:

- script: |
sf apex run test --result-format human --code-coverage --test-level RunLocalTests --target-org ci-scratch-org --wait 30
displayName: 'Run Apex Tests'
continueOnError: true

The `continueOnError` flag allows the pipeline to proceed to cleanup even if tests fail.

Step 6: Scratch Org Cleanup

To avoid clutter and maintain resource efficiency, the pipeline deletes the scratch org after tests are completed:

- script: |
sf org delete scratch --alias ci-scratch-org --no-prompt
displayName: 'Delete Scratch Org'

Step 7: Email Notifications for Failures

Azure Pipelines makes it easy to notify users about pipeline failures. I configured an email notification task to send alerts automatically. The pipeline captures the triggering user and includes them in the email:

- task: Email@1
inputs:
to: '$(Build.RequestedForEmail)'
subject: 'Pipeline Failure: $(Build.DefinitionName)'
body: |
The pipeline failed during execution. Please review the logs here:
$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)
condition: failed()

The Final YAML Script

Here’s the complete pipeline YAML:

trigger:
branches:
include:
- master

pr:
branches:
include:
- *

pool:
vmImage: 'windows-latest'

timeoutInMinutes: 60

steps:
- script: npm install @salesforce/cli --global
displayName: 'Install Salesforce CLI'

- task: DownloadSecureFile@1
inputs:
secureFile: 'securedKeyForBuildValidation'
displayName: 'Download Secure Key File'

- script: |
sf org login jwt --username your-username@example.com \
--jwt-key-file $(Agent.TempDirectory)/securedKeyForBuildValidation \
--client-id YOUR_CONSUMER_KEY \
--alias ci-scratch-org --set-default-dev-hub
displayName: 'Authenticate with JWT'

- script: |
sf org create scratch --definition-file config/project-scratch-def.json --alias ci-scratch-org --duration-days 1
displayName: 'Create Scratch Org'

- script: |
sf project deploy start --source-dir force-app --target-org ci-scratch-org --check-only
displayName: 'Deploy Code to Scratch Org'

- script: |
sf apex run test --result-format human --code-coverage --test-level RunLocalTests --target-org ci-scratch-org --wait 30
displayName: 'Run Apex Tests'
continueOnError: true

- script: |
sf org delete scratch --alias ci-scratch-org --no-prompt
displayName: 'Delete Scratch Org'

- task: Email@1
inputs:
to: '$(Build.RequestedForEmail)'
subject: 'Pipeline Failure: $(Build.DefinitionName)'
body: |
The pipeline failed during execution. Please review the logs here:
$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)
condition: failed()

There are other ways to authenticate with Salesforce but using JWT is the recommended approach for Salesforce CICD automation..

Please let me know if you have any questions.

Thanks!

Sharath Chandra Varkole

Responses

Popular Salesforce Blogs