ALM for Power Platform: Pipelines, Branches, and Avoiding the Export Trap
Application Lifecycle Management on Power Platform has come a long way. Here's how to set up a real CI/CD pipeline using Azure DevOps and the Power Platform Build Tools, and where the sharp edges still are.
For years, “ALM on Power Platform” meant one thing: export the solution, copy the zip file somewhere, import it in the next environment, pray. It worked, technically. It also caused an enormous amount of pain.
Things are genuinely better now. The tooling has matured. Pipelines are viable. But there are still sharp edges, and the documentation doesn’t always tell you where they are.
This article is a practical walkthrough of how I set up ALM for mid-to-large Power Platform projects, using Azure DevOps and the Power Platform Build Tools.
The Shape of a Good Pipeline
A mature Power Platform ALM setup looks like this:
Every developer has their own development environment. Changes flow through source control. Managed solutions land in shared environments. Nothing gets manually imported into Test or Production.
The Source Control Structure
Your repository should store solution source files, not zip files. The Power Platform CLI (pac solution unpack) extracts a solution zip into individual XML and JSON files that can be version-controlled meaningfully.
/solutions
/XYZ_Foundation
/src
/Tables
/xyz_customer
xyz_customer.xml
/Columns
xyz_name.xml
/Workflows
/Order_Processing.json
/XYZ_CoreLogic
/src
...
The unpacked format lets you:
- See actual diffs in PRs (“added column xyz_priority to xyz_task table”)
- Track who changed what
- Resolve merge conflicts on specific components instead of binary zip files
Setting Up the Build Tools
The Power Platform Build Tools extension for Azure DevOps gives you tasks for the most common operations. Install it from the Azure DevOps marketplace.
Key tasks you’ll use:
| Task | What it does |
|---|---|
PowerPlatformToolInstaller | Installs the CLI tools in the agent |
PowerPlatformExportSolution | Exports from a dev environment |
PowerPlatformUnpackSolution | Unpacks to source files |
PowerPlatformPackSolution | Packs source files to zip |
PowerPlatformImportSolution | Imports into a target environment |
PowerPlatformSetSolutionVersion | Bumps the version number |
PowerPlatformRunSolutionChecker | Runs static analysis |
The CI Pipeline
The CI pipeline runs on every PR. Its job is to validate — not deploy.
trigger: none
pr:
branches:
include:
- main
steps:
- task: PowerPlatformToolInstaller@2
inputs:
DefaultVersion: true
- task: PowerPlatformPackSolution@2
inputs:
SolutionSourceFolder: 'solutions/XYZ_Foundation/src'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)/XYZ_Foundation.zip'
SolutionType: 'Managed'
- task: PowerPlatformRunSolutionChecker@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: 'PowerPlatformServiceConnection'
Environment: '$(ValidationEnvironmentUrl)'
FileLocation: 'localFiles'
FilesToAnalyze: '$(Build.ArtifactStagingDirectory)/XYZ_Foundation.zip'
RuleSet: '0ad12346-e108-40b8-a956-9a373e9abea5' # AppSource certification
ErrorLevel: 'CriticalIssueCount'
ErrorThreshold: '0'
If the solution checker fails, the PR fails. This keeps problems out of main.
The CD Pipeline
The CD pipeline runs on merge to main. It packs, versions, publishes as an artifact, and deploys.
trigger:
branches:
include:
- main
variables:
BuildVersion: '1.0.$(Build.BuildId)'
stages:
- stage: Build
jobs:
- job: PackAndPublish
steps:
- task: PowerPlatformToolInstaller@2
inputs:
DefaultVersion: true
- task: PowerPlatformSetSolutionVersion@2
inputs:
SolutionSourceFolder: 'solutions/XYZ_Foundation/src'
SolutionVersion: '$(BuildVersion)'
- task: PowerPlatformPackSolution@2
inputs:
SolutionSourceFolder: 'solutions/XYZ_Foundation/src'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)/XYZ_Foundation_managed.zip'
SolutionType: 'Managed'
- publish: $(Build.ArtifactStagingDirectory)
artifact: solutions
- stage: DeployTest
dependsOn: Build
jobs:
- deployment: DeployToTest
environment: 'Test'
strategy:
runOnce:
deploy:
steps:
- task: PowerPlatformImportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: 'TestServiceConnection'
Environment: '$(TestEnvironmentUrl)'
SolutionInputFile: '$(Pipeline.Workspace)/solutions/XYZ_Foundation_managed.zip'
UseDeploymentSettingsFile: true
DeploymentSettingsFile: 'config/test/deployment-settings.json'
- stage: DeployProd
dependsOn: DeployTest
condition: succeeded()
jobs:
- deployment: DeployToProd
environment: 'Production' # This triggers an approval gate
strategy:
runOnce:
deploy:
steps:
- task: PowerPlatformImportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: 'ProdServiceConnection'
Environment: '$(ProdEnvironmentUrl)'
SolutionInputFile: '$(Pipeline.Workspace)/solutions/XYZ_Foundation_managed.zip'
UseDeploymentSettingsFile: true
DeploymentSettingsFile: 'config/prod/deployment-settings.json'
The environment: 'Production' in Azure DevOps triggers a manual approval gate if you’ve configured one on that environment. This is your checkpoint before production.
Deployment Settings Files
Environment variables and connection references have different values in different environments. You handle this with a deployment settings file:
{
"EnvironmentVariables": [
{
"SchemaName": "xyz_APIBaseUrl",
"Value": "https://api.production.example.com"
}
],
"ConnectionReferences": [
{
"LogicalName": "xyz_SharedDataverse",
"ConnectionId": "abc123def456",
"ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps"
}
]
}
Keep one of these per environment in your repo (config/test/, config/prod/). The connection IDs need to be valid in the target environment — get them by connecting in that environment once and copying the ID.
Where the Sharp Edges Are
Canvas apps are painful. The unpacked format for canvas apps is verbose and prone to merge conflicts. Power Apps Studio generates IDs and layout properties that differ between saves. Consider keeping canvas apps in a separate repository or managing them separately until the tooling improves.
Connection references require manual setup once. Even with deployment settings files, the first time you deploy to an environment, someone needs to create the connections manually. After that, the IDs stay stable.
Solution checker is slow. Running it on every PR adds 5-10 minutes to your pipeline. You might choose to run it nightly instead of on every PR, depending on your team’s tolerance.
Managed solution upgrades can fail. If a previous deployment left the solution in a partially upgraded state, subsequent deployments can fail in confusing ways. Keep your environment’s solution history clean and always run upgrades (not updates) when deleting components.
The Maker Bypass Problem
ALM only works if makers don’t bypass it. Nothing stops someone from logging into the UAT environment and making a direct change. Managed solutions prevent changes to managed components, but there’s nothing stopping someone from creating a new unmanaged customization on top.
The enforcement mechanism is culture and environment permissions. Lock down who has maker access in Test and Production. If someone needs to test something, they do it in their dev environment first.
If you’re using GitHub instead of Azure DevOps, the same concepts apply — the Power Platform GitHub Actions give you equivalent tasks. The pipeline YAML looks different but the flow is the same: export → unpack → commit → build → deploy.
Related articles
Connecting D365 to Everything Else
A practitioner's guide to every integration option Dataverse offers — Webhooks, Service Bus, Virtual Tables, Dual-Write, Power Automate, Custom APIs, Web API, and the .NET SDK. When to use each, when to avoid them, and how to pick the right one.
Dataverse Web API and OData: The Queries I Actually Use
Move past basic list queries. This guide covers filter gotchas, nested expand, FetchXML via HTTP, pagination, and calling the Web API from Power Automate — with real examples throughout.
Power Platform Licensing: What I Wish Someone Told Me on Day One
Licensing isn't a procurement problem — it's an architecture decision. Here's the mental model every solution architect needs before starting a Power Platform project, with real scenarios and a pre-project checklist.