All Blog Posts

Thursday, December 5, 2024

Deploy D365 FO Code to Power Platform Environment with Azure DevOps Pipelines

Deploy D365 FO Code to Power Platform Environment with Azure DevOps Pipelines

As Dynamics 365 for Finance and Operations (D365 FO) evolves, so does its deployment model. Lifecycle Services (LCS) has been the cornerstone of D365 FO deployments for nearly eight years, but Microsoft is steering towards Power Platform environments. While LCS is still operational, the transition to Power Platform environments will eventually be mandatory, even though no official end date has been announced.

For now, projects can start managing certain environments within the Power Platform framework. This brings new challenges and opportunities, particularly in how deployments are handled. If your project already uses Power Platform environments, you’ll need a modern pipeline in Azure DevOps to manage installations.

This guide not only explains the differences between LCS and Power Platform deployments but also provides a detailed, reusable YAML pipeline for deploying to both environments, helping you streamline your transition.


Required Azure DevOps Extensions

To build and deploy your packages, you need the following Azure DevOps extensions:


Understanding the Change: LCS vs. Power Platform Deployments

Deployments for LCS and Power Platform environments differ significantly, especially in the tasks and workflow. Below, we compare the processes and highlight the changes required.

Build Automation Reference

If you're new to D365 FO Azure pipelines, it's highly recommended to familiarize yourself with the official documentation on build automation using Microsoft-hosted agents and Azure Pipelines.

Create Deployable Package

This task generates a deployable package for D365 FO. In LCS deployments, this package is a zipped all-in-one package, while Power Platform deployments require a different format.

Ensure you're using version 2 of the XppCreatePackage@2 task for compatibility. Key parameters include:

  • CreateCloudPackage: Generates a package for Power Platform environments.
  • CreateRegularPackage: Generates a package for LCS environments.
- task: XppCreatePackage@2
  inputs:
    XppToolsPath: >
      $(Pipeline.Workspace)/NuGets/Microsoft.Dynamics.AX.Platform.CompilerPackage
    XppBinariesPath: '$(Build.BinariesDirectory)'
    XppBinariesSearch: '*'
    CreateCloudPackage: true
    CloudPackagePlatVersion: '7.0.0.0'
    CloudPackageAppVersion: '10.0.0.0'
    CloudPackageOutputLocation: >
      $(Build.ArtifactStagingDirectory)/PowerPlatform
    CreateRegularPackage: true
    DeployablePackagePath: >
      $(Build.ArtifactStagingDirectory)/LCS/AllInOne_$(Build.BuildNumber).zip

For a detailed explanation of the XppCreatePackage task parameters and how to configure them, refer to the official Microsoft documentation.

XppCreatePackage Output

Below is an example of how the outputs are structured after running the XppCreatePackage task:

LCS/
  AllInOne_1.1.24340.14.zip
PowerPlatform/
  PackageAssets/
    [Content_Types].xml
    customizations.xml
    d365fodevblogmodule_1_0_0_1_managed.zip
    en-us/
      EndHtml/
      WelcomeHtml/
    ExampleSolution_1_0_0_1_managed.zip
    ImportConfig.xml
    manifest.ppkg.json
    solution.xml
  TemplatePackage.dll

LCS Upload Task

With Power Platform deployments, you no longer need to upload the package to the LCS Asset Library. This eliminates the need for the LCSAssetUpload task, simplifying the process.

- task: LCSAssetUpload@2
  name: 'AssetUpload'
  displayName: 'Upload Package to LCS'
  inputs:
    serviceConnectionName: 'YourLCSServiceConnectorName'
    projectId: 'YourLCSProjectId'
    assetType: '10'
    assetPath: >
      $(Pipeline.Workspace)/drop/LCS/AllInOne_$(Build.BuildNumber).zip
    assetName: 'AllInOne_$(Build.BuildNumber)'

Deployment Tasks: LCS vs. Power Platform

Deployment involves significant changes. With LCS, you use the LCSAssetDeploy task, while Power Platform deployments require a series of Power Platform-specific tasks.

LCS Deployment

- task: LCSAssetDeploy@4
  displayName: 'Deploy to LCS environment'
  inputs:
    serviceConnectionName: 'YourLCSServiceConnectorName'
    projectId: 'YourLCSProjectId'
    environmentId: 'YourLCSEnvironmentId'
    fileAssetId: 'LCSAssetUploadOutput'
    deploymentType: 'hq'
    releaseName: '$(Build.BuildNumber)'

Power Platform Deployment

For Power Platform, you authenticate first and then deploy using task PowerPlatformDeployPackage.

- task: PowerPlatformToolInstaller@2
  inputs:
    DefaultVersion: true

- task: PowerPlatformWhoAmi@2
  inputs:
    authenticationType: 'PowerPlatformSPN'
    PowerPlatformSPN: 'YourPowerPlatformConnector'
    Environment: 'https://[YourDataverseEnvironmentName].crm4.dynamics.com/'

- task: PowerPlatformDeployPackage@2
  inputs:
    authenticationType: 'PowerPlatformSPN'
    PowerPlatformSPN: 'YourPowerPlatformConnector'
    Environment: 'https://[YourDataverseEnvironmentName].crm4.dynamics.com/'
    PackageFile: > 
      $(Pipeline.Workspace)/drop/PowerPlatform/TemplatePackage.dll
    MaxAsyncWaitTime: '180'

Key parameters:

  • PowerPlatformSPN: Service connection for authentication.
  • Environment: URL of the target Power Platform environment.

Complete CI/CD Pipeline Example

Below is a full pipeline example demonstrating multiple stages for building and deploying code. It supports both LCS and Power Platform deployments.

Azure DevOps Import Repository menu

YAML Pipeline for Deploying D365 FO to LCS and Power Platform Environments

trigger:
  branches:
    include:
      - main

name: '1.1.$(Year:yy)$(DayOfYear).$(Rev:r)'

variables:
  EnvironmentType: 'PowerPlatform' # PowerPlatform or LCS
  AppSuitePackage: 'Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp'
  App1Package: 'Microsoft.Dynamics.AX.Application1.DevALM.BuildXpp'
  App2Package: 'Microsoft.Dynamics.AX.Application2.DevALM.BuildXpp'
  PlatPackage: 'Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp'
  ToolsPackage: 'Microsoft.Dynamics.AX.Platform.CompilerPackage'
  D365LCSEnv: 'YourAzureDevOpsEnvForLCS'
  LCSServiceConnector: 'YourLCSServiceConnectorName'
  LCSProjectId: 'YourLCSProjectId'
  D365Env: 'YourAzureDevOpsEnvForD365FO'
  PowerPlatformConnector: 'YourPowerPlatformConnector'
  EnvironmentDataverseUrl: 'https://[YourDataverseEnvironmentName].crm4.dynamics.com/'
  LCSEnvironmentId: 'YourLCSEnvironmentId'

stages:
  - stage: Build
    displayName: 'Build Dynamics 365 solution'
    jobs:
      - job: XppCompileJob
        displayName: 'Compile X++'
        pool:
          name: 'Azure Pipelines'
          vmImage: 'windows-latest'
          demands:
            - msbuild
            - visualstudio
        steps:
          - checkout: self

          - task: NuGetCommand@2
            displayName: 'NuGet install packages'
            inputs:
              command: custom
              arguments: 'install -Noninteractive "$(Build.SourcesDirectory)/Pipeline/Config/packages.config" -ConfigFile "$(Build.SourcesDirectory)/Pipeline/Config/nuget.config" -Verbosity Detailed -ExcludeVersion -OutputDirectory "$(Pipeline.Workspace)/NuGets"'

          - task: XppUpdateModelVersion@0
            inputs:
              XppSourcePath: '$(Build.SourcesDirectory)/App/SourceCode'
              XppDescriptorSearch: '**\Descriptor\*.xml'
              XppLayer: '10'
              VersionNumber: '$(Build.BuildNumber)'

          - task: CopyFiles@2
            displayName: 'Copy binary dependencies to build binaries directory: $(Build.BinariesDirectory)'
            inputs:
              SourceFolder: '$(Build.SourcesDirectory)/App/SourceCode'
              Contents: '**/bin/**'
              TargetFolder: '$(Build.BinariesDirectory)'

          - task: VSBuild@1
            displayName: 'Build solution'
            inputs:
              solution: '$(Build.SourcesDirectory)/Solution/BuildSolution/BuildSolution.sln'
              vsVersion: 'latest'
              msbuildArgs: >
                /p:BuildTasksDirectory="$(Pipeline.Workspace)/NuGets/$(ToolsPackage)/DevAlm"
                /p:MetadataDirectory="$(Build.SourcesDirectory)/App/SourceCode"
                /p:FrameworkDirectory="$(Pipeline.Workspace)/NuGets/$(ToolsPackage)"
                /p:ReferenceFolder="$(Pipeline.Workspace)/NuGets/$(PlatPackage)/ref/net40;$(Pipeline.Workspace)/NuGets/$(App1Package)\ref\net40;$(Pipeline.Workspace)/NuGets/$(App2Package)/ref/net40;$(Pipeline.Workspace)/NuGets/$(AppSuitePackage)/ref/net40;$(Build.SourcesDirectory)/App/SourceCode;$(Build.BinariesDirectory)"
                /p:ReferencePath="$(Pipeline.Workspace)/NuGets/$(ToolsPackage)"
                /p:OutputDirectory="$(Build.BinariesDirectory)"
 
          - task: XppCreatePackage@2
            displayName: 'Create Deployable Package'
            inputs:
              XppToolsPath: '$(Pipeline.Workspace)/NuGets/$(ToolsPackage)'
              XppBinariesPath: '$(Build.BinariesDirectory)'
              XppBinariesSearch: '*'
              CreateCloudPackage: true
              CloudPackagePlatVersion: '7.0.0.0'
              CloudPackageAppVersion: '10.0.0.0'
              CloudPackageOutputLocation: '$(Build.ArtifactStagingDirectory)/PowerPlatform'
              CreateRegularPackage: true
              DeployablePackagePath: '$(Build.ArtifactStagingDirectory)/LCS/AllInOne_$(Build.BuildNumber).zip'

          - task: PublishBuildArtifacts@1
            displayName: 'Publish artifact: drop'
            inputs:
              PathtoPublish: '$(Build.ArtifactStagingDirectory)'
              ArtifactName: 'drop'
            condition: succeeded()

  - stage: LCSUpload
    displayName: 'Upload to Lifecycle Services (LCS)'
    dependsOn:
      - Build
    condition: eq(variables['EnvironmentType'], 'LCS')
    jobs:
      - deployment: Upload
        displayName: 'Asset upload'
        pool:
          name: 'Azure Pipelines'
          vmImage: 'windows-latest'
        environment: $(D365LCSEnv)
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: 'drop'
                  patterns: 'drop/LCS/AllInOne_$(Build.BuildNumber).zip'

                - task: LCSAssetUpload@2
                  name: 'AssetUpload'
                  displayName: 'Upload Package to LCS'
                  inputs:
                    serviceConnectionName: $(LCSServiceConnector)
                    projectId: $(LCSProjectId)
                    assetType: '10'
                    assetPath: >
                      $(Pipeline.Workspace)/drop/LCS/AllInOne_$(Build.BuildNumber).zip
                    assetName: 'AllInOne_$(Build.BuildNumber)'
 
  - stage: Deploy
    displayName: 'Deploy to environment'
    dependsOn:
      - Build
      - LCSUpload
    condition: |
      and(
        succeeded('Build'),
        or(
          eq(variables['EnvironmentType'], 'PowerPlatform'),
          succeeded('LCSUpload')
        )
      )    
    jobs:
    - deployment: Deploy
      displayName: 'Asset deployment'
      timeoutInMinutes: 360
      pool:
        name: 'Azure Pipelines'
        vmImage: 'windows-latest'
      environment: '$(D365Env)'
      strategy:
        runOnce:
          deploy:
            steps:
            - ${{ if eq(variables['EnvironmentType'], 'PowerPlatform') }}:
              - download: current
                artifact: 'drop'
                patterns: 'drop/PowerPlatform'

              - task: PowerPlatformToolInstaller@2
                inputs:
                  DefaultVersion: true

              - task: PowerPlatformWhoAmi@2
                inputs:
                  authenticationType: 'PowerPlatformSPN'
                  PowerPlatformSPN: $(PowerPlatformConnector)
                  Environment: $(EnvironmentDataverseUrl)

              - task: PowerPlatformDeployPackage@2
                inputs:
                  authenticationType: 'PowerPlatformSPN'
                  PowerPlatformSPN: $(PowerPlatformConnector)
                  Environment: $(EnvironmentDataverseUrl)
                  PackageFile: > 
                    $(Pipeline.Workspace)/drop/PowerPlatform/TemplatePackage.dll
                  MaxAsyncWaitTime: '180'
      
            - ${{ else }}:
              - task: LCSAssetDeploy@4
                displayName: 'Deploy to LCS environment'
                inputs:
                  serviceConnectionName: $(LCSServiceConnector)
                  projectId: $(LCSProjectId)
                  environmentId: $(LCSEnvironmentId)
                  fileAssetId: $[ stageDependencies.LCSUpload.Upload.outputs['Upload.AssetUpload.FileAssetId'] ]
                  deploymentType: 'hq'
                  releaseName: '$(Build.BuildNumber)'

Conclusion

The shift from LCS to Power Platform is more than just a technical change—it’s an opportunity to modernize your deployment processes. By adapting your Azure DevOps pipelines with the Power Platform Build Tools and updated tasks, you’ll be ready for the future of D365 FO deployments.

If you’d like to share your experience with Power Platform deployments or have any questions, feel free to connect with me on LinkedIn. I’d be happy to discuss and exchange ideas!