Back to Blog
4 min read

Power Platform ALM: Application Lifecycle Management Best Practices

Application Lifecycle Management (ALM) for Power Platform ensures reliable, governed deployment of solutions. Let’s explore best practices for enterprise ALM.

Environment Strategy

A typical environment structure:

Development -> Build/Test -> UAT -> Production
     |             |          |          |
  Sandbox      Sandbox    Sandbox    Production
  (per dev)   (automated)  (QA)      (locked)

Solution Structure

Organize solutions for maintainability:

├── Core Solution (Foundation)
│   ├── Common Tables
│   ├── Security Roles
│   └── Environment Variables

├── Feature Solutions
│   ├── Sales Module
│   │   ├── Tables
│   │   ├── Model-Driven App
│   │   └── Flows
│   │
│   └── Service Module
│       ├── Tables
│       ├── Canvas Apps
│       └── Flows

└── Customizations Solution (Org-specific)
    └── Configuration Data

Solution Export with PowerShell

# Connect to Power Platform
Install-Module -Name Microsoft.PowerApps.Administration.PowerShell
Install-Module -Name Microsoft.Xrm.Data.PowerShell

$conn = Connect-CrmOnline -ServerUrl "https://yourorg.crm.dynamics.com"

# Export solution
$solutionName = "SalesModule"
$version = "1.0.0.1"

# Update solution version
Set-CrmSolutionVersion -conn $conn -SolutionName $solutionName -Version $version

# Export as managed
Export-CrmSolution -conn $conn `
    -SolutionName $solutionName `
    -Managed $true `
    -SolutionFilePath ".\exports\${solutionName}_${version}_managed.zip"

# Export as unmanaged for source control
Export-CrmSolution -conn $conn `
    -SolutionName $solutionName `
    -Managed $false `
    -SolutionFilePath ".\exports\${solutionName}_${version}_unmanaged.zip"

Solution Unpacking for Source Control

# Unpack solution for version control
pac solution unpack `
    --zipfile ".\exports\SalesModule_1.0.0.1_unmanaged.zip" `
    --folder ".\src\SalesModule" `
    --packagetype Both `
    --allowDelete true `
    --allowWrite true

# Structure after unpacking:
# src/SalesModule/
# ├── Entities/
# │   └── account/
# │       ├── Entity.xml
# │       └── FormXml/
# ├── Workflows/
# ├── CanvasApps/
# └── solution.xml

Azure DevOps Pipeline

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main
  paths:
    include:
      - src/SalesModule/**

pool:
  vmImage: 'windows-latest'

variables:
  - group: PowerPlatform-Credentials
  - name: solutionName
    value: 'SalesModule'

stages:
  - stage: Build
    jobs:
      - job: PackSolution
        steps:
          - task: PowerPlatformToolInstaller@2
            inputs:
              DefaultVersion: true

          - task: PowerPlatformPackSolution@2
            inputs:
              SolutionSourceFolder: '$(Build.SourcesDirectory)/src/$(solutionName)'
              SolutionOutputFile: '$(Build.ArtifactStagingDirectory)/$(solutionName).zip'
              SolutionType: 'Managed'

          - task: PublishBuildArtifacts@1
            inputs:
              PathtoPublish: '$(Build.ArtifactStagingDirectory)'
              ArtifactName: 'solution'

  - stage: DeployTest
    dependsOn: Build
    jobs:
      - deployment: DeployToTest
        environment: 'PowerPlatform-Test'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: PowerPlatformToolInstaller@2

                - task: PowerPlatformImportSolution@2
                  inputs:
                    authenticationType: 'PowerPlatformSPN'
                    PowerPlatformSPN: 'PowerPlatform-Test-Connection'
                    SolutionInputFile: '$(Pipeline.Workspace)/solution/$(solutionName).zip'
                    AsyncOperation: true
                    MaxAsyncWaitTime: '60'

  - stage: DeployProduction
    dependsOn: DeployTest
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployToProd
        environment: 'PowerPlatform-Production'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: PowerPlatformToolInstaller@2

                - task: PowerPlatformImportSolution@2
                  inputs:
                    authenticationType: 'PowerPlatformSPN'
                    PowerPlatformSPN: 'PowerPlatform-Prod-Connection'
                    SolutionInputFile: '$(Pipeline.Workspace)/solution/$(solutionName).zip'
                    AsyncOperation: true

GitHub Actions Alternative

# .github/workflows/power-platform-deploy.yml
name: Power Platform Deployment

on:
  push:
    branches: [main]
    paths:
      - 'src/**'

env:
  SOLUTION_NAME: SalesModule

jobs:
  build:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install Power Platform CLI
        run: |
          dotnet tool install --global Microsoft.PowerApps.CLI.Tool

      - name: Pack Solution
        run: |
          pac solution pack `
            --zipfile "${{ github.workspace }}/solution/${{ env.SOLUTION_NAME }}.zip" `
            --folder "${{ github.workspace }}/src/${{ env.SOLUTION_NAME }}" `
            --packagetype Managed

      - name: Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: solution
          path: solution/

  deploy-test:
    needs: build
    runs-on: windows-latest
    environment: test
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v3
        with:
          name: solution

      - name: Import Solution
        run: |
          pac auth create `
            --url ${{ secrets.TEST_ENVIRONMENT_URL }} `
            --applicationId ${{ secrets.CLIENT_ID }} `
            --clientSecret ${{ secrets.CLIENT_SECRET }} `
            --tenant ${{ secrets.TENANT_ID }}

          pac solution import `
            --path "${{ env.SOLUTION_NAME }}.zip" `
            --async true `
            --max-async-wait-time 60

Configuration Data Migration

# Export configuration data
$schemaPath = ".\config\data-schema.xml"

# Create schema file
@"
<entities>
  <entity name="systemuser" displayname="User" etc="8">
    <fields>
      <field name="systemuserid" displayname="User" type="guid" primaryKey="true"/>
      <field name="fullname" displayname="Full Name"/>
    </fields>
  </entity>
  <entity name="team" displayname="Team" etc="9">
    <!-- Define fields -->
  </entity>
</entities>
"@ | Out-File $schemaPath

# Export data
Export-CrmDataFile -conn $conn `
    -SchemaFile $schemaPath `
    -DataFile ".\config\configuration-data.zip"

# Import data to target
Import-CrmDataFile -conn $targetConn `
    -DataFile ".\config\configuration-data.zip"

Proper ALM ensures Power Platform solutions are reliable, maintainable, and can be safely deployed across environments.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.