UDE, Pipelines, and Modern ALM: Moving Development to Test and Production

Dynamics 365 F&O UDE Series | Part 10

UDE, Pipelines, and Modern ALM: Moving Development to Test and Production

The Dev-Test-UAT-Prod promotion model, release branches, immutable artifacts, approvals/checks, and a controlled deployment strategy
August 2026 | Fatih Demirci | fatihdemirci.net

Introduction

In the previous part, we built a pipeline that compiles the X++ metadata in our Git repository on a Microsoft-hosted agent and produces a Power Platform unified package. We now have a UnifiedPackage built from a specific Git commit with specific NuGet references and stored as an Azure DevOps artifact.

However, producing the package does not mean that we have moved the development to UAT or Production. If we do not draw a clear boundary between build and deployment, we end up rebuilding for every environment, losing visibility of who approved what, and being unable to trace the code in Production back to a specific commit.

In this part, we will examine the release/deployment pipeline that promotes the build output through Test, UAT, and Production in a controlled manner. We will clarify the branch strategy, approvals and checks in Azure DevOps Environments, the service connection structure, and deployment rules. At the end, we will monitor the package sent from DevOps in a real Test/UAT environment and verify that our development has reached the F&O user interface.

Where this part fits in the series: We are completing the development, Git, branch, build, and package structure established in the earlier parts with a real Test/UAT deployment. The same artifact, approval, and traceability principles will also apply when moving to Production.

What We Carry Forward from the Previous Part

At the end of Part 9, the build pipeline produced the following information together:

  • Git commit SHA and branch information

  • Platform and application NuGet versions

  • Build.BuildNumber and the pipeline run link

  • Power Platform UnifiedPackage.zip artifact

  • The classic AXDeployableRuntime.zip output, when required

The release pipeline is not responsible for compiling the source code again. Its job is to move this controlled output to the correct environment under the correct identity.

Core principle: Build once and promote the same artifact. The package validated in Test and the package deployed to Production must be byte-for-byte identical.

Dev, Test, UAT, and Production Do Not Mean the Same Thing

On the UDE side, we need to distinguish the Development environment from controlled deployment environments. Development is part of the daily development cycle; Test, UAT, and Production are part of release management.

Environment Primary purpose Deployment approach Approval
Developer UDE Coding, debugging, and quick validation Direct/incremental deployment from Visual Studio Developer control
Test Technical validation and smoke testing Automatic or controlled through the release pipeline Usually automatic
UAT / Pre-Prod Business acceptance and go/no-go The same UnifiedPackage artifact Functional/business approval
Production Live operation Controlled deployment during a maintenance window Technical + business approval

The critical point is this: a change deployed directly from Visual Studio to a Developer UDE is not the package that will later be promoted to UAT. Controlled environments must receive an artifact produced by the pipeline from an approved commit in Git.

Branch Strategy: Where Does the Release Branch Fit?

The branch structure may vary with team size. In Dynamics 365 projects, I find it useful to keep the boundaries between feature, dev, release, and main clearly visible.

Branch Purpose Build / deployment behavior
feature/* A single development or work item Fast validation before the PR; it is not deployed to a shared environment without control.
dev Integration branch for completed developments A candidate for daily builds and the technical Test environment.
release/* Freeze the version that will move to UAT and Production UnifiedPackage is produced from the controlled commit on this branch.
main Reference for the approved version deployed to Production After the Production deployment, the release branch is merged and tagged.
hotfix/* A limited fix for an urgent Production issue Created from the Production version/tag; it produces a new artifact and passes through the approval chain.

A sample flow could look like this:

  1. Merge the feature/1450-sales-order-control branch into dev through a PR.

  2. Create release/2026.08 from dev when preparing the UAT candidate.

  3. The build pipeline compiles the commit on the release branch and produces the UnifiedPackage artifact.

  4. Validate the same artifact in the Test and UAT environments.

  5. After Production approval, deploy the same artifact to Production.

  6. After a successful deployment, merge the release branch into main and create a tag such as v2026.08.1.

My approach: Once UAT has started, accept only fixes that belong to the current release into the release branch. Keeping new features in the dev branch prevents the UAT scope from changing continuously.

Separating the Build Pipeline from the Release Pipeline

Topic Build pipeline Release / deployment pipeline
Input Git commit + NuGet references Approved UnifiedPackage artifact
Primary action Compile X++ and produce the package Deploy the package to the target environment
When repeated When a new commit arrives In each environment for the same artifact
Permissions Repository and feed access Target-environment service connection
Control PR/build validation Approval, Business Hours, and Exclusive Lock

It is not mandatory to separate the two pipelines; build and deployment steps can also be defined in a single multi-stage YAML file. In larger projects, however, keeping the build artifact in a separate run makes it easier to restart a release and trace which package was sent to which environment.

Approvals and Checks with Azure DevOps Environments

An Azure DevOps Environment is more than a label that stores an environment name. When a deployment job targets it, Azure DevOps records deployment history and applies the approvals and checks defined by the resource owner before the stage begins.

Azure DevOps environment Recommended check Recommended owner
Test Automatic deployment without approval Technical team
UAT At least one approval; Business Hours if needed Project manager / functional lead
Production Approval + Business Hours + Exclusive Lock Change board / business owner / technical owner

Defining the approval mechanism under Approvals and checks on the Production environment creates a stronger boundary than relying only on a ManualValidation step in YAML. A person who changes the pipeline YAML cannot then remove the Production approval rule alone; control remains with the environment owner.

Important: Approval is not simply a mechanism for someone to press a button. The approver should not approve a deployment without seeing the artifact number, UAT result, maintenance window, and recovery plan.

Azure DevOps Environments showing D365-UAT and D365-Prod
Figure 1 – D365-UAT and D365-Prod under Azure DevOps Environments
Approvals and checks configuration for the D365-UAT environment
Figure 2 – Approvals and checks configuration for the D365-UAT environment

Service Connections and Workload Identity Federation

The release pipeline should not connect to the target environment with a user account. Authentication should use a Power Platform service connection backed by a Microsoft Entra ID app registration. Current Microsoft examples recommend workload identity federation instead of storing a client secret.

Connection Target Example name
Test service connection Test environment URL PowerPlatform-Test
UAT service connection UAT environment URL PowerPlatform-UAT
Production service connection Production environment URL PowerPlatform-Prod

A separate federated credential can be defined for each service connection under a single app registration. Projects that require stricter isolation can use a separate app registration and service connection for each environment.

The app registration must be created as an application user in every target environment and assigned a security role that is sufficient for package deployment. Microsoft’s tutorial uses the System Administrator role; in an enterprise project, it is better to validate the required permissions and use the narrowest practical set of roles whenever possible.

Security rule: Client secrets, user passwords, and environment URLs must not be written directly into YAML. Service connections, variable groups, and environment-based permission boundaries should be used together.

Power Platform service connections in Azure DevOps project settings
Figure 3 – Power Platform service connections in Azure DevOps project settings

Sample Release Pipeline YAML

The following example promotes the UnifiedPackage artifact produced by the ude-build pipeline from Part 9 to Test, UAT, and Production. Test runs automatically; approvals for UAT and Production are configured on Azure DevOps Environments.

Agent note: The Power Platform Deploy Package task requires a Windows agent. Because Finance and Operations package deployments can take longer than one hour, a self-hosted Windows agent may be preferable. If a Microsoft-hosted agent is used, set the deployment job timeout to at least 180 minutes; larger packages may require more time.

trigger: none

resources:
  pipelines:
  - pipeline: UDEBuild
    source: 'ude-build'
    trigger:
      branches:
        include:
        - release/*

pool:
  name: 'Self-Hosted-Windows'

variables:
  TestServiceConnection: 'PowerPlatform-Test'
  UATServiceConnection: 'PowerPlatform-UAT'
  ProdServiceConnection: 'PowerPlatform-Prod'
  TestEnvironmentUrl: 'https://yourtest.crm.dynamics.com'
  UATEnvironmentUrl: 'https://youruat.crm.dynamics.com'
  ProdEnvironmentUrl: 'https://yourprod.crm.dynamics.com'
  UnifiedPackagePath: '$(Pipeline.Workspace)/UDEBuild/UnifiedPackage/UnifiedPackage.zip'

stages:
- stage: DeployToTest
  displayName: 'Deploy to Test'
  jobs:
  - deployment: DeployTest
    displayName: 'Deploy unified package to Test'
    environment: 'Test'
    timeoutInMinutes: 180
    strategy:
      runOnce:
        deploy:
          steps:
          - download: UDEBuild
            artifact: 'UnifiedPackage'
          - task: PowerPlatformToolInstaller@2
            displayName: 'Install Power Platform Build Tools'
          - task: PowerPlatformWhoAmI@2
            displayName: 'Verify Test connection'
            inputs:
              authenticationType: 'PowerPlatformSPN'
              PowerPlatformSPN: '$(TestServiceConnection)'
          - task: PowerPlatformDeployPackage@2
            displayName: 'Deploy package to Test'
            inputs:
              authenticationType: 'PowerPlatformSPN'
              PowerPlatformSPN: '$(TestServiceConnection)'
              PackageFile: '$(UnifiedPackagePath)'
              Environment: '$(TestEnvironmentUrl)'

- stage: DeployToUAT
  displayName: 'Deploy to UAT'
  dependsOn: DeployToTest
  condition: succeeded()
  jobs:
  - deployment: DeployUAT
    displayName: 'Deploy unified package to UAT'
    environment: 'UAT'  # Approval/check is configured on this environment
    timeoutInMinutes: 180
    strategy:
      runOnce:
        deploy:
          steps:
          - download: UDEBuild
            artifact: 'UnifiedPackage'
          - task: PowerPlatformToolInstaller@2
            displayName: 'Install Power Platform Build Tools'
          - task: PowerPlatformWhoAmI@2
            displayName: 'Verify UAT connection'
            inputs:
              authenticationType: 'PowerPlatformSPN'
              PowerPlatformSPN: '$(UATServiceConnection)'
          - task: PowerPlatformDeployPackage@2
            displayName: 'Deploy package to UAT'
            inputs:
              authenticationType: 'PowerPlatformSPN'
              PowerPlatformSPN: '$(UATServiceConnection)'
              PackageFile: '$(UnifiedPackagePath)'
              Environment: '$(UATEnvironmentUrl)'

- stage: DeployToProduction
  displayName: 'Deploy to Production'
  dependsOn: DeployToUAT
  condition: succeeded()
  lockBehavior: sequential  # Use together with an Exclusive Lock check
  jobs:
  - deployment: DeployProd
    displayName: 'Deploy unified package to Production'
    environment: 'Production'  # Approval/check is configured here
    timeoutInMinutes: 240
    strategy:
      runOnce:
        deploy:
          steps:
          - download: UDEBuild
            artifact: 'UnifiedPackage'
          - task: PowerPlatformToolInstaller@2
            displayName: 'Install Power Platform Build Tools'
          - task: PowerPlatformWhoAmI@2
            displayName: 'Verify Production connection'
            inputs:
              authenticationType: 'PowerPlatformSPN'
              PowerPlatformSPN: '$(ProdServiceConnection)'
          - task: PowerPlatformDeployPackage@2
            displayName: 'Deploy package to Production'
            inputs:
              authenticationType: 'PowerPlatformSPN'
              PowerPlatformSPN: '$(ProdServiceConnection)'
              PackageFile: '$(UnifiedPackagePath)'
              Environment: '$(ProdEnvironmentUrl)'

Reading the Pipeline Steps

Step Task Purpose
Artifact download download: UDEBuild Downloads the same UnifiedPackage.zip produced by the build pipeline.
Tool installer PowerPlatformToolInstaller@2 Prepares the current Power Platform CLI-based Build Tools tasks.
Connection check PowerPlatformWhoAmI@2 Validates the service connection identity before deployment begins.
Package deployment PowerPlatformDeployPackage@2 Deploys the unified package to the target unified environment.
Environment deployment job Defines the boundary for approvals/checks and deployment history.

Every stage uses the same UnifiedPackagePath variable. Not rebuilding the package after it has been validated in Test is one of the most important principles in this modern ALM flow.

Azure DevOps pipeline run with the Build and Deploy to UAT stages completed
Figure 4 – Azure DevOps pipeline run with the Build and Deploy to UAT stages completed

How Should We Design the Deployment Strategy?

Test: Fast, Automated Feedback

After the release branch build completes, the package can be deployed automatically to the Test environment. Basic sign-in, batch, integration, and critical business-process smoke tests are run there. The goal is not flawless user acceptance testing, but a quick confirmation that the package installs and operates correctly at a technical level.

UAT: Business Acceptance and Release Freeze

The UAT stage should begin only after Test succeeds and the designated approver gives approval. While UAT is in progress, only fixes within the scope of the same release should be accepted into the release branch. Every fix produces a new commit and a new artifact; the previous artifact is never replaced silently.

Production: Maintenance Window and Two-Layer Control

In addition to an environment approval, Production can use Business Hours or ManualValidation. Because deployment may take a long time, the maintenance window should account not only for the start time but also for the expected total duration and the post-deployment smoke-test period.

Using an Exclusive Lock check on the Production environment prevents two releases from moving to Production at the same time. `lockBehavior: sequential` makes the handling of queued releases explicit.

Go/No-Go Check Before Production

  • Have the artifact name, build number, and Git commit SHA been verified?

  • Has UAT been accepted, and have all open critical issues been closed?

  • Are the package platform/application versions compatible with the environment version?

  • Did the service connection and WhoAmI check succeed in the latest run?

  • Have the maintenance window and estimated deployment duration been communicated?

  • Have the owners of batch jobs, integrations, and external systems been notified?

  • Are the last known good artifact and the rollback/forward-fix plan ready?

  • Are the owners of the post-deployment smoke tests identified?

Practical recommendation: Add the build number, commit SHA, UAT result, maintenance window, and change-record link to the approval description. The approver should not have to gather this information from other places.

Post-Deployment Validation

A successful pipeline does not, by itself, prove that the business application works as expected. A short but standardized smoke-test checklist should be run after deployment.

  • Sign-in and access to the main workspaces/forms

  • Critical business processes included in the release scope

  • Status of batch jobs and scheduled tasks

  • OData, custom services, dual-write, and other integrations

  • Opening critical reports and outputs

  • Azure DevOps deployment history and the package deployment record

Let’s Discuss Rollback Up Front

Package deployment should not be treated as if it were as simple as reverting a Git commit. The impact on code, metadata, schema, and business data may differ. A rollback plan therefore cannot consist only of the sentence, ‘We will deploy the old zip file again.’

Keeping the last known good artifact is important; however, for some issues it may be safer to produce a new forward fix than to return to the previous package. Environment backup/restore and business-recovery steps should also be considered for changes that affect schema or data.

Warning: Before deployment begins, it should be documented who can make the recovery decision, how quickly the decision must be made, and how any data impact will be handled. A Production incident is not the right time to discuss this plan for the first time.

What Should the Hotfix Flow Look Like?

For an urgent Production issue, use a shorter but still traceable hotfix flow instead of disabling the approval chain completely:

  1. Create a hotfix/* branch from the main branch or release tag that represents the Production version.

  2. Keep the fix limited in scope and review it through a PR.

  3. Produce a new UnifiedPackage artifact from the new commit.

  4. Run the shortest practical Test/UAT validation.

  5. Promote to Production with an emergency change record and authorized approval.

  6. Merge the hotfix back into main, release, and, when necessary, dev.

Permission to bypass Azure DevOps checks should belong only to the resource administrator. If a bypass is used, the deployment record must continue to show who performed it.

Deployment History and Traceability

The value of modern ALM is not only automation, but also the ability to answer questions after the fact. Azure DevOps Environments deployment history shows which pipeline run targeted which environment, the result, and the approval details. On the unified-environment side, Finance and Operation Package Manager provides a second control point where we can monitor the status and processing history of submitted packages.

A release record should contain at least the build number, commit SHA, source branch, package version, target environment, approver, deployment start and end times, and smoke-test result. Checking both the Azure DevOps record and the package status in Package Manager validates both sides of the chain.

Monitoring the Package in Power Platform

After deployment completes, we should not be satisfied simply because the pipeline is green. When we open the relevant Test/UAT environment in Power Platform admin center and navigate to the Dynamics 365 apps list, we can see the Finance and Operation Package Manager app. This app lets us monitor the environment-side processing of the unified package deployment.

Accessing the Finance and Operation Package Manager app in the Test/UAT environment
Figure 5 – Accessing Finance and Operation Package Manager in the Test/UAT environment

When we open the app, the Active Packages list displays the packages submitted by the DevOps pipeline. Package Type, Build Type, Status Reason, and Modified On help us understand the current stage of the deployment. In the following example, a Status Reason of Completed indicates that processing has finished on the environment side. This record still does not replace functional validation inside the application.

Packages sent from DevOps shown as Completed in the Active Packages list
Figure 6 – Packages sent from DevOps shown as Completed in the Active Packages list

Final Validation of the Development in UAT

A Completed package status means that the technical deployment is finished. As the final step, we should open the F&O application and confirm that the feature we developed is actually visible in the target environment. The Customers menu item that we added to Accounts payable at the beginning of the series appears in the Test/UAT environment, providing a simple but important smoke test of the full chain from source code to the user interface.

Customers menu item under Accounts payable in the UAT environment
Figure 7 – Customers menu item under Accounts payable in the UAT environment

With this screen, we have verified end to end that the metadata committed to Git was built, converted into a UnifiedPackage artifact, deployed through DevOps to a separate Test/UAT environment, and delivered to the end-user interface.

Common Issues

Error / symptom Likely cause and check
WhoAmI authentication failed The federated credential subject does not match the service connection name, or the application user is missing.
PackageFile not found The pipeline resource alias, artifact name, or path under Pipeline.Workspace is incorrect.
Deploy task unsupported OS Power Platform Deploy Package must run on a Windows agent.
Deployment timeout The hosted-agent limit is insufficient; increase the timeout or use a self-hosted Windows agent.
Approval is not triggered The stage may be using a normal job; the deployment job must specify the target Azure DevOps environment.
Deployment to the wrong environment The service connection and Environment URL variable do not point to the same target.
Two deployments overlap The Production environment has no Exclusive Lock check, or lock behavior is not defined.
Version mismatch The artifact’s platform/application versions are not compatible with the target environment.

Practical Summary of This Part

In this part, we connected all the links in the development infrastructure that we built from scratch at the beginning of the series. We created the Git repository and branch structure, developed and validated the X++ metadata in the UDE Development environment, and produced a UnifiedPackage artifact from a specific commit through an Azure DevOps pipeline.

We then deployed the same package through DevOps to a separate Test/UAT unified environment. We monitored the process until it showed Completed in Finance and Operation Package Manager, and then verified that our development reached the target environment by locating the Customers menu item under Accounts payable in the F&O interface.

This completes the technical backbone of the UDE series, from preparing the Development environment to performing a controlled Test/UAT deployment. The same principles—promoting the same package, authorized approval, a maintenance window, post-deployment validation, and a recovery plan—will apply to Production. In my view, the greatest benefit of modern ALM is trust and traceability before speed.

Next in the Series: Coding with AI

In this part, we completed the flow of building a Development infrastructure from scratch, testing the development in our own UDE environment, and deploying it through DevOps to another Test/UAT environment. In the next article, we will focus more on the coding experience than on infrastructure: how to use AI while developing in X++, how to provide the right context, how to review generated code, and where developer responsibility begins. The current technical series can therefore be considered complete with this part, while AI-assisted development will continue as a new topic.

References

Regards.
Fatih Demirci
www.fatihdemirci.net

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