All articles

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.

· 12 min read

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:

graph TD A["Developer Environment\n(Unmanaged)"] -->|"PR to main branch"| B["CI Pipeline\n(validate + solution checker)"] B -->|"Merge to main"| C["CD Pipeline → Test/UAT\n(Managed)"] C -->|"Manual approval gate"| D["CD Pipeline → Production\n(Managed)"]

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:

TaskWhat it does
PowerPlatformToolInstallerInstalls the CLI tools in the agent
PowerPlatformExportSolutionExports from a dev environment
PowerPlatformUnpackSolutionUnpacks to source files
PowerPlatformPackSolutionPacks source files to zip
PowerPlatformImportSolutionImports into a target environment
PowerPlatformSetSolutionVersionBumps the version number
PowerPlatformRunSolutionCheckerRuns 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.

Share this article LinkedIn X / Twitter

Related articles