A Guide to Publishing Salesforce Second Generation Managed Packages using Azure CI/CD

12.18.24 By

In the modern era of software development, the ability to deliver high-quality applications swiftly and reliably is more important than ever. As businesses increasingly embrace cloud technologies and agile methodologies, the need for robust deployment processes has become a top priority. Continuous Integration and Continuous Deployment (CI/CD) pipelines are central to this transformation, enabling development teams to automate their build, test, and deployment processes efficiently.

Azure DevOps, with its comprehensive suite of tools, stands out as one of the leading solutions for managing these pipelines effectively. When it comes to Salesforce development, particularly with the adoption of second-generation managed packages (2GP), the benefits of a well-designed CI/CD pipeline become even more apparent. By implementing a CI/CD pipeline using Azure DevOps, you can streamline your development processes, reduce manual errors, and ensure a smoother deployment experience.

So, are you ready to streamline the process of publishing second-generation managed packages on Salesforce, achieve greater efficiency, reduce manual errors, and deliver higher-quality applications with confidence? Let’s embark on a journey to unlock the powerful capabilities of Azure DevOps and second-generation managed packages for your development needs.

Read our blog to learn how to set up an Azure CI/CD pipeline specifically for publishing second-generation managed packages in Salesforce. We will delve into the steps required to integrate Azure DevOps into your development workflow, showcasing how to automate the various stages of your Salesforce application lifecycle—from initial code commits to final production releases.

Prerequisites

Before we dive into the details of setting up an Azure CI/CD pipeline for Salesforce second-generation managed packages, there are a few prerequisites that need to be in place. Here’s what you’ll need to get started:

  1. Salesforce CLI: Ensure that the Salesforce CLI is installed on your local machine for seamless interaction with Salesforce. You can install the Salesforce CLI by following the instructions at here.
  2. VS Code Editor: Use Visual Studio Code as your primary development environment for writing and editing code.
  3. Salesforce Extension Pack: Install the Salesforce Extension Pack for enhanced functionality in VS Code, including tools specifically designed for Salesforce development.
  4. Azure DevOps Account: Set up an Azure DevOps account to manage your code repository and implement CI/CD pipelines effectively. If you don’t have an account, you can sign up for free at https://dev.azure.com/.
  5. Dev Hub Org: This will be your central management hub for overseeing and accessing all your managed packages.
  6. Development Org: This will serves as your primary workspace for building, customizing, and testing Salesforce applications prior to deployment.
  7. Namespace Org: A Namespace Org is essential for ensuring that your custom components and applications have a unique global identity. This environment allows you to register a namespace, preventing potential conflicts with other Salesforce components.
    • If you do not currently have an organization with a registered namespace, create a Developer Edition org that is distinct from your Dev Hub or scratch orgs. If you already possess an org with a registered namespace, you’re all set!
    • Within the Developer Edition org, follow the steps to create and register your unique namespace.

Before you can create second-generation managed packages in Salesforce, you need to link a namespace to your Dev Hub org. A namespace is a unique identifier that distinguishes your package from others in the Salesforce ecosystem. By linking a namespace to your Dev Hub org, you establish a naming convention for your packages and components, ensuring that they are uniquely identified and can be distributed across different orgs seamlessly.

  1. Log in to your Dev Hub org as the System Administrator or as a user with the Salesforce DX Namespace Registry permissions.
  2. From the App Launcher menu, select Namespace Registries.
  3. Namespace Registries

  4. Click Link Namespace.
  5. Link Namespace

  6. In the window that pops up, log in to the Developer Edition org in which your namespace is registered using the org’s System Administrator’s credentials.
  7. To view all the namespaces linked to the Namespace Registry, select the All Namespace Registries list view.
  8. All Namespace

Create Salesforce DX Project

A Salesforce DX project has a specific structure and a configuration file (sfdx-project.json) that identifies the directory as a Salesforce DX project.

This command generates the necessary configuration files and directories to get you started.

sf project generate –name mywork –default-package-dir force-app–manifest
  1. Open sfdx-project.json file.
  2. Add your registered namespace.
  3. Namespace Registries

Authorize Dev Hub and Dev Org

  1. Authorize Dev Hub: Run the following command to authorize your Dev Hub:
  2. sf org login web –set-default-dev-hub –alias DevHub

    Follow the instructions to log in and authorize Dev Hub.

  3. Authorize Dev Org: Use the following command to authorize your Development Org:
  4. sf org login web –alias DevOrg

    Follow the prompts to log in and authorize the Development Org.

Configure Scratch Org Definition

Before creating your scratch org, you might need to set up some pre-configurations. This includes defining features like namespaces, enabling specific Salesforce features, and setting custom fields. Update your project-scratch-def.json file with these configurations.

  1. Open project-scratch-def.json: This file is located in the config directory of your Salesforce DX project.
  2. Add Necessary Configurations:
  3. Adjust these settings based on your project requirements. Refer to Salesforce documentation for additional options you might need.

Add Metadata to Manifest

Before committing your code to the Azure DevOps repository, you need to ensure that all required metadata is included in your manifest/package.xml file. You can do this manually or use the Salesforce Package.xml Generator Extension for VS Code.

  1. Using Salesforce Package.xml Generator Extension:
    • Install the Salesforce Package.xml Generator extension from the VS Code marketplace.
    • Open the Command Palette (Ctrl+Shift+P) and search for “SFDX Package.xml Generator: Choose Metadata components”.
    • Follow the prompts to generate a package.xml file that includes all the necessary metadata for your project. Package XML Extension
  2. Manually Adding Metadata:
    • Open the manifest/package.xml file in your project directory.
    • Add all relevant metadata components that you want to include in your deployment. For example:

Ensure that you include all the metadata components required for your managed package.

Retrieve Source from Org

To ensure your local project is in sync with the Salesforce org, retrieve the source metadata using Salesforce CLI.

  1. Retrieve Metadata:
    • Use the Salesforce CLI command to retrieve the metadata specified in your manifest.xml file:
    • sf project retrieve start –manifest manifest/package.xml –target-org DevOrg

      This command pulls the metadata from your Salesforce org into your local project directory.

Generate OpenSSL Certificate

This step is needed to generate a private key (server.key) and a self-signed certificate (server.crt) that will be used for authentication in the CI/CD pipeline.

Follow steps mentioned in the Salesforce documentation to Create a Private Key and Self-Signed Digital Certificate.

Create Connected App in Salesforce

  1. In your Salesforce org, create a connected app to establish a secure connection between the CI/CD pipeline and Salesforce.
  2. Configure the connected app with the following settings:
    • Connected App Name: Azure CI/CD Pipeline
    • API (Enable OAuth Settings): Enable OAuth Settings
    • Callback URL: https://login.salesforce.com
    • Selected OAuth Scopes: Full access (full)
    • Require Secret for Web Server Flow: Checked
    • Require Secret for Refresh Token Flow: Checked
  3. After creating the connected app, note down the Consumer Key and Consumer Secret, which will be used in the CI/CD pipeline for authentication.

Follow this guide for detailed steps to create a connected app in salesforce.

Upload Private Key in Azure Secure Files

  1. In Azure DevOps, navigate to your project and select the Library tab.
  2. Create a Secure File named server.key and upload the private key generated earlier.
  3. This secure file will be used in the CI/CD pipeline for authentication purposes.

Upload Private key

Creating Azure CI/CD Pipeline

  1. Create an azure-pipelines.yml file at the root location of project directory.
  2. Update this file with the code below. A sample configuration might look like this
  3.       
          trigger:
          - main
          
          pool:
            vmImage: 'ubuntu-latest'
          
          variables:
            - template: .\variables\variables.yaml
          
          jobs:
          - job: SalesforcePackageCreation
            displayName: 'Salesforce Managed Package Creation
            steps:
            - checkout: self
          
            - task: DownloadSecureFile@1
              name: jwtKey
              inputs:
                secureFile: 'server.key'
          
            - task: UseNode@1
              inputs:
                version: '20.x'
              displayName: 'Install Node.js'
          
            - bash: npm install @salesforce/cli --global
              displayName: 'Install Salesforce CLI'
          
            - script: |
                echo "Installing jq"
                sudo apt-get update
                sudo apt-get install -y jq
              displayName: 'Install jq'
          
            - script: |
                echo "Authenticating to Dev Hub"
                sf org login jwt \
                --username DEV_HUB_Username \
                --jwt-key-file $(jwtKey.secureFilePath) \
                --client-id $(CONSUMER_KEY) \
                --alias $(DEV_HUB_ORG_ALIAS) \
                --set-default-dev-hub
              displayName: 'Authenticate to Dev Hub'
          
            - script: |
                echo "Creating Scratch Org"
                sf org create scratch \
                  --set-default \
                  --target-dev-hub $(DEV_HUB_ORG_ALIAS) \
                  --definition-file config/project-scratch-def.json \
                  --alias $(SCRATCH_ORG_ALIAS) \
                  --duration-days 3
                echo "Scratch Org created"
              displayName: 'Create Scratch Org'
          
            - script: |
                echo "Pushing Source to Scratch Org"
                sf project deploy start --source-dir force-app --target-org $(SCRATCH_ORG_ALIAS)
              displayName: 'Push Source to Scratch Org'
          
            - script: |
                echo "Running Tests on Scratch Org"
                sf apex run test \
                  --target-org $(SCRATCH_ORG_ALIAS) \
                  --synchronous \
                  --code-coverage \
                  --detailed-coverage \
                  --result-format human > result.txt
              displayName: 'Run Apex Tests on Scratch Org'
          
            - script: |
                echo "Deleting Scratch Org"
                sf org delete scratch \
                  --target-org $(SCRATCH_ORG_ALIAS) \
                  --no-prompt
              displayName: 'Delete Scratch Org'
              condition: always()
          
            - script: |
                echo "Creating 2nd Generation Package"
                PACKAGE_LIST=$(sf package list --target-dev-hub $(DEV_HUB_ORG_ALIAS))
                echo "Available packages: $PACKAGE_LIST"
                if echo "$PACKAGE_LIST" | grep -q "$(PACKAGE_NAME)"; then
                  echo "Package $(PACKAGE_NAME) exists. No action required."
                else
                  echo "$(PACKAGE_NAME) does not exist, running sf package create"
                  sf package create \
                    --name $(PACKAGE_NAME) \
                    --path force-app \
                    --package-type Managed \
                    --target-dev-hub $(DEV_HUB_ORG_ALIAS)
                fi
              displayName: 'Create 2nd Generation Package'
          
            - script: |
                echo "Creating package version"
                OUTPUT=$(sf package version create \
                  --package $(PACKAGE_NAME) \
                  --installation-key-bypass \
                  --wait 10 \
                  --target-dev-hub $(DEV_HUB_ORG_ALIAS) \
                  --code-coverage \
                  --definition-file config/project-scratch-def.json \
                  --version-number $(VERSION_NUMBER) \
                  --version-name "$(VERSION_NAME)" \
                  --version-description "$(VERSION_DESCRIPTION)" \
                  --tag "$(TAG)" \
                  --verbose \
                  --json)
          
                if [ $? -ne 0 ]; then
                  echo "Failed to create package version. Command output:"
                  echo "$OUTPUT"
                  exit 1
                fi
                
                PACKAGE_VERSION_ID=$(echo "$OUTPUT" | jq -r '.result.SubscriberPackageVersionId')
                
                if [ -z "$PACKAGE_VERSION_ID" ]; then
                  echo "Package Version ID is null or empty. Command output:"
                  echo "$OUTPUT"
                  exit 1
                fi
                
                echo "Package Version ID: $PACKAGE_VERSION_ID"
                echo "##vso[task.setvariable variable=PACKAGE_VERSION_ID]$PACKAGE_VERSION_ID"
              displayName: 'Create Package Version'
              condition: succeeded()
          
            - script: |
                if [ "$PROMOTE_PACKAGE" = "true" ]; then
                  echo "Promoting Package Version"
                  echo "Publishing Package with Version ID: $(PACKAGE_VERSION_ID)"
                  sf package version promote \
                    --package "$(PACKAGE_VERSION_ID)" \
                    --target-dev-hub $(DEV_HUB_ORG_ALIAS) \
                    --no-prompt
                fi
              displayName: 'Promote Package Version'
              condition: and(succeeded(), eq(variables.PROMOTE_PACKAGE, 'true'))
          
            - task: PublishPipelineArtifact@1
              condition: always()'
              inputs:
                targetPath: $(System.DefaultWorkingDirectory)/result.txt'
                artifactName: TestResults'
                publishLocation: pipeline'
          
            - task: PublishBuildArtifacts@1
              condition: succeeded()'
              inputs:
                pathtoPublish: $(Build.ArtifactStagingDirectory)'
                artifactName: deploy-artifacts'
          
                  

Step-by-Step Explanation of the YAML Pipeline

In this section, we’ll break down each component of the YAML pipeline and explain the purpose of every step involved.

  1. Trigger

    This defines which branch initiates the pipeline. In our case, the pipeline is triggered by any changes made to the main branch.

  2. Pool

    Here, we specify the virtual machine image that will run the pipeline. For this example, we’re utilizing the latest version of Ubuntu to ensure compatibility and access to updated tools.

  3. Variables

    This section is crucial for managing configuration settings. You can store sensitive information such as API keys and connection strings here, keeping them secure while allowing easy access during pipeline execution.

  4. Jobs

    Jobs are the core components of the pipeline, outlining the sequence of tasks to be executed. In this example, we have a single job called SalesforcePackageCreation.

  5. Steps

    Each step represents an individual action within the job, executing specific tasks like checking out code, running scripts, or deploying to a Salesforce org. Here’s a detailed look at each step:

    • Checkout: This step retrieves the code from the repository to the agent, ensuring the latest version is available for processing.
    • Secure File Download: In this step, we download a secure file containing the private key used for authentication, ensuring a safe connection.
    • Node.js Installation: This step installs Node.js on the agent machine, preparing the environment for subsequent tasks.
    • Salesforce CLI Installation: Here, we install the Salesforce CLI globally on the agent, enabling Salesforce-specific commands.
    • jq Installation: This step installs the jq utility, which is essential for parsing JSON output generated by various scripts.
    • Dev Hub Authentication: We authenticate to the Dev Hub org using the JWT flow, allowing the pipeline to interact securely with Salesforce resources.
    • Scratch Org Creation: This step creates a scratch org based on the specifications provided in the project-scratch-def.json file, providing an isolated environment for development and testing.
    • Source Code Push: The pipeline pushes the source code to the newly created scratch org, ensuring that the latest changes are reflected.
    • Apex Tests Execution: This step runs Apex tests on the scratch org to verify the integrity of the code and ensure everything is functioning as expected.
    • Scratch Org Deletion: After testing, we delete the scratch org to clean up resources and avoid unnecessary charges.
    • Second-Generation Package Creation: This step checks if a second-generation package exists and creates one if it doesn’t, facilitating streamlined package management.
    • Package Version Creation: Here, we create a new version of the package based on specified parameters, making it ready for deployment.
    • Package Promotion: If the PROMOTE_PACKAGE variable is set to true, this step promotes the package version, making it available for broader use.
    • Publishing Test Results: This step publishes the test results as a pipeline artifact, allowing for easy review and tracking of the testing process.
    • Publishing Deployment Artifacts: Finally, we publish the deployment artifacts as a build artifact, ensuring that all necessary files are available for future reference.

By following this structured approach, we ensure a clear understanding of the pipeline’s operation, making it easier for you to adapt and implement in your own projects.

Commit Code to Azure DevOps Repo

  1. Create a Repository: In Azure DevOps, create a new Git repository to host your Salesforce DX project.
  2. Add Remote and Push Code:
    • Navigate to your local Salesforce DX project directory.
    • Initialize a Git repository if you haven’t already:
    • git init
    • Add the Azure DevOps repository as a remote:
    • git remote add origin https://dev.azure.com/your-organization/your-project/_git/your-repo
    • Stage and commit your code:
    • git add .
      git commit -m “Initial commit”
    • Push the code to Azure DevOps:
    • git push -u origin master

Set Up CI/CD Pipeline in Azure DevOps

  1. Create a Pipeline:

    • Go to your Azure DevOps project and navigate to Pipelines.
    • Click Create Pipeline and choose the repository you just pushed your code to.
      Create Pipeline
      Create Pipeline 2
      Create Pipeline 3
      Create Pipeline 4
    • Select YAML for pipeline configuration file created in previous step (azure-pipelines.yml).
      Create Pipeline 5
  2. Save Pipeline

Store Variables in Pipeline

To make your CI/CD pipeline flexible and adaptable, store configuration variables in Azure DevOps pipeline variables. This allows you to update values without modifying the pipeline configuration file directly.

  1. Access Pipeline Variables:

    • In your Azure DevOps project, navigate to Pipelines and select your pipeline.
    • Click on Edit to access the pipeline configuration.
    • Go to the Variables tab to define and manage variables for your pipeline.
      Create Pipeline 6
      Create Pipeline 7
  2. Add Pipeline Variables:
    • Click New Variable and add your variables. Create Pipeline 8 Create Pipeline 9
    • You can define variables for environment-specific configurations, API keys, connection strings, and other sensitive information.
    • Example Variables:
    • Name: DEV_HUB_ORG_ALIAS, Value: DevHub
    • Name: SCRATCH_ORG_ALIAS, Value: ScratchOrg
    • Name: PACKAGE_DESCRIPTION, Value: Package Description
    • Name: PACKAGE_NAME, Value: Package Name
    • Name: PROMOTE_PACKAGE, Value: Promote Package
    • Name: TAG, Value: Tag
    • Name: VERSION_DESCRIPTION, Value: Version Description
    • Name: VERSION_NAME, Value: Version Name
    • Name: VERSION_NUMBER, Value: Version Number
    • Create Pipeline 10

Ensure these variables are used in your pipeline YAML file for greater flexibility.

Run and Monitor Pipeline

  • Run the pipeline. Create Pipeline 11
  • Monitor the pipeline execution in the Azure DevOps portal to ensure that the build and deployment processes are executed successfully.

Conclusion

In conclusion, setting up an Azure CI/CD pipeline for publishing Salesforce second-generation managed packages can significantly enhance your development workflow and deployment processes. By automating the build, test, and deployment stages of your application lifecycle, you can achieve greater efficiency, reduce manual errors, and ensure a smoother deployment experience. Azure DevOps provides a powerful platform for managing your CI/CD pipelines, offering a comprehensive set of tools and features to streamline your development processes. By following the steps outlined in this guide, you can harness the full potential of Azure DevOps and second-generation managed packages to transform your Salesforce development experience.

Start your journey today and experience the transformative impact of CI/CD pipelines in your development projects.


By

We’re a team of Salesforce enthusiasts, here to unlock the full potential of your CRM and Martech investments to support your sales and marketing strategies. With deep expertise on the Salesforce platform and a keen eye for adjacent technologies and process advancements, we provide thought leadership that keeps you informed.

We share strategic guidance on maximizing your Salesforce investments, in-depth insights on the latest Salesforce features and updates, cutting-edge solutions to integrate Salesforce with your broader tech stack, and expert analysis on trends shaping the future of CRM.

Ready to unleash the power of your Salesforce platform? Connect with us.



Topics: DevOps, Platform, Salesforce, Salesforce Analytics Cloud, Salesforce Einstein, Salesforce Experience Cloud, Salesforce Manufacturing Cloud, Salesforce Marketing Cloud, Salesforce Sales Cloud, Salesforce Service Cloud

Start your success story today.