UDE Build and Package Creation with an Azure DevOps Pipeline

Dynamics 365 F&O UDE Series | Part 9

UDE Build and Package Creation with an Azure DevOps Pipeline

NuGet packages, Azure Artifacts, YAML pipelines, X++ builds, and Power Platform unified package creation
August 2026 | fatihdemirci.net

Introduction

In the previous part of the series, we discussed how a local build and deployment to an online UDE environment are separate steps. During day-to-day development, we can run a quick project build in Visual Studio and deploy our changes to the connected environment.

When we move to controlled environments such as UAT or production, however, a build created on a developer’s computer is not enough. The same Git commit must be compiled against the same references, and the resulting package must be stored as a pipeline artifact.

In this part, we will create an Azure DevOps YAML pipeline that runs on a Microsoft-hosted Windows agent. The pipeline will compile the X++ metadata in the repository, create a unified package and, when required, a traditional deployable package, and then publish the outputs as artifacts.

Scope of this article: This part focuses on build and package creation. Deploying the resulting unified package to Test, UAT, and Production environments with approval mechanisms will be the subject of the next part.

Overall Flow

At its simplest, we can think of the build pipeline as the following flow:

Input Process Output
Git repository NuGet restore Compiler and reference packages
Metadata + .sln/.rnrproj Compile X++ with VSBuild / MSBuild X++ binary outputs
X++ binary outputs XppCreatePackage@3 Unified package and/or deployable package
Package outputs PublishBuildArtifacts Artifact consumed by the release pipeline

Prerequisites

Before creating the pipeline, several components must be ready in the repository and the Azure DevOps organization.

  1. An Azure DevOps organization, project, and Git repository
  2. The Dynamics 365 Finance and Operations Tools and Power Platform Build Tools extensions
  3. An .rnrproj file representing the package to build and, preferably, an .sln file
  4. Custom metadata, the Descriptor folder, and model descriptor files
  5. X++ Compiler and Build Reference NuGet packages compatible with the target environment version
  6. Reader permission for the pipeline build service account on the Azure Artifacts feed

Important: The pipeline cannot determine what to build merely by finding AxClass, AxTable, or AxForm XML files. The repository must contain the .rnrproj file representing the package to build, along with the model descriptor information.

Aligning the Repository Structure with Parts 7 and 8

We can make the repository structure used in the previous parts a little more concrete for the build pipeline:

D365FO-UDE
│
├── Metadata
│   └── DMRCustomizations
│       ├── Descriptor
│       └── DMRCustomizations
│
├── Projects
│   └── DMR_FD_AITest1
│       ├── DMR_FD_AITest1.sln
│       └── DMR_FD_AITest1.rnrproj
│
├── Build
│   ├── azure-pipelines.yml
│   ├── packages.config
│   └── nuget.config
│
└── README.md

Here, MetadataPath, SolutionPath, and the location of the Build folder will be defined as variables in YAML. This means we will not need to repeat paths in every step when adapting the pipeline file to different projects.

Required Azure DevOps Extensions

We need two extensions in the Azure DevOps organization. Dynamics 365 Finance and Operations Tools provides the XppCreatePackage@3 task, while Power Platform Build Tools supports the release and deployment steps that follow.

Figure 1 - Required extensions in the Azure DevOps organization
Figure 1 – Required extensions in the Azure DevOps organization

NuGet Packages Required for X++ Builds

A Microsoft-hosted agent does not include the PackagesLocalDirectory available on a traditional development VM. We therefore need to provide the X++ compiler and Microsoft reference binaries to the pipeline through NuGet packages.

Microsoft’s current example uses the following five packages for a complete X++ build:

NuGet package Purpose
Microsoft.Dynamics.AX.Platform.CompilerPackage xppc.exe, MSBuild tasks, and X++ build tools
Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp Build reference binaries for the Platform module
Microsoft.Dynamics.AX.Application1.DevALM.BuildXpp First part of the Application reference package
Microsoft.Dynamics.AX.Application2.DevALM.BuildXpp Second part of the Application reference package
Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp Application Suite build reference binaries

The Application package is split into two parts because of Azure DevOps package size limits. Two packages may be sufficient for limited projects that develop only at the platform level; for projects extending Finance or Supply Chain functionality, using all five packages is safer.

Version rule: The versions of the NuGet references used during the build must be compatible with the target environment version. When a new quality update is applied to the environment, the pipeline package versions must also be reviewed.

Where Can We Download the NuGet Packages?

In the current Unified Experience flow, the packages can be downloaded using the Download Dynamics 365 FnO NuGets for CI/CD option in the Power Platform Visual Studio extension. Depending on the access or tenant scenario, the LCS Shared Asset Library can also be used.

Creating the FinOpsNuGet Feed in Azure Artifacts

Instead of adding the downloaded NuGet packages to the repository, we will store them in an Azure Artifacts feed. This prevents source control from growing with unnecessary binary files and lets us manage package versions through packages.config.

  • In the Azure DevOps project, open Artifacts → Create Feed.
  • Set the feed name to FinOpsNuGet.
  • Push the five .nupkg files compatible with the target environment version to the feed.
  • Under Feed Settings → Permissions, grant Reader permission to the project build service account.
  • Copy the v3 endpoint URL from the Connect to feed page.
nuget.exe push -Source "https://pkgs.dev.azure.com/<org>/<project>/_packaging/FinOpsNuGet/nuget/v3/index.json" -ApiKey AZ <package-name>.nupkg
Figure 2 - Compiler and build reference packages uploaded to the FinOpsNuGet feed
Figure 2 – Compiler and build reference packages uploaded to the FinOpsNuGet feed

The nuget.config File

nuget.config determines the feed from which the pipeline downloads packages. The safest approach is to obtain the feed URL from the Azure DevOps Connect to feed page.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="FinOpsNuGet"
         value="https://pkgs.dev.azure.com/<org>/<project>/_packaging/FinOpsNuGet/nuget/v3/index.json" />
  </packageSources>
</configuration>

Security note: Do not store a username or personal access token for the feed in nuget.config. Use the Azure DevOps pipeline’s own build identity and the feed permission model.

The packages.config File

packages.config determines which NuGet packages are restored and their exact versions. The values below belong to the 10.0.47 / PU71 example shown in the screenshots; verify them against the .nupkg versions in your own feed.

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Microsoft.Dynamics.AX.Platform.CompilerPackage"
           version="7.0.7858.115" targetFramework="net40" />
  <package id="Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp"
           version="7.0.7858.115" targetFramework="net40" />
  <package id="Microsoft.Dynamics.AX.Application1.DevALM.BuildXpp"
           version="10.0.2527.135" targetFramework="net40" />
  <package id="Microsoft.Dynamics.AX.Application2.DevALM.BuildXpp"
           version="10.0.2527.135" targetFramework="net40" />
  <package id="Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp"
           version="10.0.2527.135" targetFramework="net40" />
</packages>

Note: Platform and application packages use different version formats. It is normal for Platform packages to use 7.0.x and Application packages to use 10.0.x.

Creating the YAML Pipeline

We can prepare the Build/azure-pipelines.yml file in the repository using the basic flow below. For the first attempt, it is better to run the pipeline manually. Once the build is stable, a nightly schedule or dev/main branch trigger can be added.

The following example uses the DMR_FD_AITest1 solution and the Metadata folder:

trigger: none

pool:
  vmImage: 'windows-latest'

variables:
  PlatformVersion: '7.0.7858.115'
  ApplicationVersion: '10.0.2527.135'

  BuildConfigPath: '$(Build.SourcesDirectory)/Build'
  NuGetInstallDir: '$(Build.SourcesDirectory)/NuGets'
  MetadataPath: '$(Build.SourcesDirectory)/Metadata'
  SolutionPath: '$(Build.SourcesDirectory)/Projects/DMR_FD_AITest1/DMR_FD_AITest1.sln'

  CompilerPackage: '$(NuGetInstallDir)/Microsoft.Dynamics.AX.Platform.CompilerPackage'
  PlatformBuildRef: '$(NuGetInstallDir)/Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp'
  App1BuildRef: '$(NuGetInstallDir)/Microsoft.Dynamics.AX.Application1.DevALM.BuildXpp'
  App2BuildRef: '$(NuGetInstallDir)/Microsoft.Dynamics.AX.Application2.DevALM.BuildXpp'
  AppSuiteBuildRef: '$(NuGetInstallDir)/Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp'

  UnifiedPackageOutput: '$(Build.ArtifactStagingDirectory)/UnifiedPackage'

stages:
- stage: Build
  displayName: 'Compile X++ and Create Package'
  jobs:
  - job: BuildXpp
    displayName: 'Build X++ and package'
    timeoutInMinutes: 120
    steps:
    - checkout: self
      clean: true

    - task: NuGetCommand@2
      displayName: 'Restore X++ build packages'
      inputs:
        command: 'custom'
        arguments: >
          install "$(BuildConfigPath)/packages.config"
          -ConfigFile "$(BuildConfigPath)/nuget.config"
          -OutputDirectory "$(NuGetInstallDir)"
          -ExcludeVersion
          -Verbosity Detailed
          -Noninteractive

    - task: VSBuild@1
      displayName: 'Build X++ solution'
      inputs:
        solution: '$(SolutionPath)'
        vsVersion: '17.0'
        msbuildArgs: >
          /p:BuildTasksDirectory="$(CompilerPackage)/DevAlm"
          /p:MetadataDirectory="$(MetadataPath)"
          /p:FrameworkDirectory="$(CompilerPackage)"
          /p:ReferenceFolder="$(PlatformBuildRef)/ref/net40;$(App1BuildRef)/ref/net40;$(App2BuildRef)/ref/net40;$(AppSuiteBuildRef)/ref/net40;$(MetadataPath);$(Build.BinariesDirectory)"
          /p:ReferencePath="$(CompilerPackage)"
          /p:OutputDirectory="$(Build.BinariesDirectory)"
          /p:CompilerMetadata="$(Build.BinariesDirectory)"

    - task: NuGetToolInstaller@1
      displayName: 'Install NuGet 3.3.0 for packaging'
      inputs:
        versionSpec: '3.3.0'

    - task: XppCreatePackage@3
      displayName: 'Create Power Platform unified package'
      inputs:
        XppToolsPath: '$(CompilerPackage)'
        CreateCloudPackage: true
        CloudPackagePlatVersion: '$(PlatformVersion)'
        CloudPackageAppVersion: '$(ApplicationVersion)'
        CloudPackageOutputLocation: '$(UnifiedPackageOutput)'
        DeployablePackagePath: '$(Build.ArtifactStagingDirectory)/AXDeployableRuntime.zip'

    - task: ArchiveFiles@2
      displayName: 'Zip unified package'
      inputs:
        rootFolderOrFile: '$(UnifiedPackageOutput)'
        includeRootFolder: false
        archiveType: 'zip'
        archiveFile: '$(Build.ArtifactStagingDirectory)/UnifiedPackage.zip'

    - task: PublishBuildArtifacts@1
      displayName: 'Publish unified package artifact'
      inputs:
        PathtoPublish: '$(Build.ArtifactStagingDirectory)/UnifiedPackage.zip'
        ArtifactName: 'UnifiedPackage'

    - task: PublishBuildArtifacts@1
      displayName: 'Publish traditional deployable package'
      inputs:
        PathtoPublish: '$(Build.ArtifactStagingDirectory)/AXDeployableRuntime.zip'
        ArtifactName: 'LCSPackage'

Understanding Each Pipeline Step

Step Task What does it do?
1. Checkout checkout: self Checks out the relevant Git commit into a clean working directory.
2. NuGet restore NuGetCommand@2 Downloads the compiler and reference packages. -ExcludeVersion keeps folder paths stable across version changes.
3. X++ compile VSBuild@1 Compiles the solution and metadata with MSBuild. If it fails, first check the restore logs and the ReferenceFolder paths.
4. NuGet 3.3.0 NuGetToolInstaller@1 Installs NuGet 3.3.0 for compatibility with the deployable package format.
5. Package XppCreatePackage@3 Creates the Power Platform unified package and, when required, AXDeployableRuntime.zip.
6. Zip ArchiveFiles@2 Converts the unified package folder into the zip file expected by the deployment task.
7. Artifact PublishBuildArtifacts@1 Stores the outputs under a specific build run and makes them available to the release pipeline.
Figure 3 - Our sample pipeline: build/package and UAT deployment run as separate stages
Figure 3 – Our sample pipeline: build/package and UAT deployment run as separate stages

Unified Package vs. Traditional Deployable Package

Topic Traditional deployable package Power Platform unified package
Primary use Traditional LCS-based deployment flows Power Platform unified environment CI/CD flow
X++ output Contains AOT package binaries Contains the X++ runtime package
Dataverse solution Requires a separate deployment flow Can be included in the same package when required
Pipeline task Can be created with XppCreatePackage XppCreatePackage@3 + CreateCloudPackage
Deployment approach LCS / traditional environment update PowerPlatformPackageDeploy or pac package deploy

For new Unified Experience projects, my approach is to treat the unified package as the primary output. If the customer’s landscape still includes traditional LCS-based environments, creating a traditional deployable package from the same build can be useful during the transition. Dataverse managed solutions can also be added to the unified package when required.

Running the Pipeline for the First Time

  1. Commit the Build/azure-pipelines.yml file to the repository.
  2. On the Pipelines → New Pipeline page, select Azure Repos Git and the relevant repository.
  3. Select Existing Azure Pipelines YAML file and point to Build/azure-pipelines.yml.
  4. Check the MetadataPath, SolutionPath, PlatformVersion, and ApplicationVersion values, and then run the pipeline manually.
  5. Verify the NuGet restore and VSBuild logs first, followed by the XppCreatePackage@3 logs.
  6. Under Run Summary → Artifacts, verify the UnifiedPackage output and, if required, LCSPackage.
Figure 4 - Creating a pipeline from an existing YAML file in Azure Repos Git
Figure 4 – Creating a pipeline from an existing YAML file in Azure Repos Git
Figure 5 - Deployable and unified package outputs created by the pipeline
Figure 5 – Deployable and unified package outputs created by the pipeline

Branch Trigger or Nightly Build?

When the pipeline is first created, running it manually with trigger: none makes troubleshooting easier. Once the structure is stable, different strategies can be used depending on the project’s needs.

Approach When is it appropriate? Notes
Pull Request validation When a compile check is required for every PR Package creation can remain optional
Dev branch trigger When a shared build is required after every merge into the dev branch Provides fast feedback
Nightly build When building every commit is expensive for large solutions Suitable for a daily full build and package creation
Main / release trigger When a package is created for a release candidate The artifact is connected to the release pipeline

In large Dynamics 365 projects, creating a full X++ package for every feature commit can be unnecessarily expensive. A more balanced approach is limited validation during the PR stage, controlled builds on the dev branch, and full package creation at night.

Common Errors

Error / symptom Probable cause and check
NuGet restore 401 The build service account does not have Reader permission on the FinOpsNuGet feed.
References cannot be found One of the five NuGet packages is missing, the packages.config version is incorrect, or the ReferenceFolder path is invalid.
The XppCreatePackage@3 task cannot be found The Dynamics 365 Finance and Operations Tools extension is not installed or is outdated.
No X++ binary package(s) found The VSBuild output was not generated in the expected Build.BinariesDirectory location, or the .rnrproj file is not building the correct package.
fnomoduledefinition.json file not found CloudPackageOutputLocation or XppToolsPath is incorrect, so the package folder structure was not created.
Version mismatch CloudPackagePlatVersion / CloudPackageAppVersion is not compatible with the restored NuGet packages.
Package server error The NuGet 3.3.0 installation shown in Microsoft’s example is missing from the packaging step.
Descriptor / model cannot be found The Descriptor folder under Metadata was not added to the repository.

Versioning and Traceability

The pipeline should not only create a package; it should also make the package’s origin visible. Build.BuildNumber, Git commit SHA, branch, platform/application versions, package name, and the pipeline run link should be included in the artifact or release notes.

This lets us trace which commit produced the package running in an environment. Instead of saying, “This code built successfully on Fatih’s computer,” we have a verifiable release record tied to a specific commit and pipeline run.

After a Quality Update

Upload the new Compiler and Build Reference packages to the feed, and align packages.config with the PlatformVersion and ApplicationVersion values in YAML using the same version set. Then verify the full X++ build and package creation with a manual run.

My recommendation: Before scheduling a quality update and a production release for the same day, verify the build pipeline against the new reference set in a separate run. Managing NuGet packages and the environment version together is a fundamental part of ALM discipline in the UDE era.

Practical Summary

With an Azure DevOps pipeline, the controlled commit in Git becomes the source of truth. The compiler and reference set are versioned through NuGet, VSBuild compiles on a clean agent, and XppCreatePackage@3 publishes the output as an artifact.

In my view, the most important benefit of this structure is not merely automation, but reproducibility. We can look back and see which references were used to build a commit and which package was produced.

Next Part

In the next part, I will cover the release/deployment pipeline that moves the UnifiedPackage artifact to Test, UAT, and Production environments. Service connections, workload identity federation, approval mechanisms, and deployment history will be the main topics.

Best regards.
Fatih Demirci
www.fatihdemirci.net

 
  • Trackback are closed
  • Comments (0)
  1. No comments yet.