Power Platform EXPORT to DevOps - Complex Pipeline
This document provides a proven, reusable pattern for implementing advanced Power Platform DevOps pipelines. Use this template to standardize and automate complex solution export, documentation, and version control processes.
Pattern Summary
Pattern Name: Power Platform Complex Solution Export Pipeline
Category: DevOps & CI/CD
Platform: Azure DevOps + Power Platform
Difficulty: Intermediate to Advanced
Time to Implement: 2-4 hours
What This Pattern Solves
- Managing multiple interdependent solutions in a single pipeline.
- Automating the export of Power Platform Portals and reference data.
- Generating solution documentation automatically.
- Cleaning up the repository before processing new solution versions.
- Committing all unpacked solution components, portal data, and documentation to a new branch.
Pattern Outcomes
After implementing this pattern, you will have:
✅ Fully automated export for multiple solutions, portals, and data.
✅ Version-controlled source code for all solution components, including portals.
✅ Automated documentation generation for tables, relationships, and security roles.
✅ Consistent build artifacts published for deployment pipelines.
✅ A clean, auditable Git history with a dedicated branch for each build.
Pattern Overview
This pattern details a complex Azure DevOps pipeline designed for sophisticated Power Platform ALM scenarios. It handles the export of multiple solutions, downloads portal data, exports reference data, generates technical documentation, and commits everything to a new Git branch. This is ideal for large projects with multiple solution layers and a need for automated documentation.
What This Pipeline Does
Prerequisites
Before implementing this pipeline, ensure you have:
- Azure DevOps Organization with appropriate permissions.
- Power Platform Environment (Development/Source).
- Service Principal with required permissions for all operations.
- Git Repository for solution source control.
- Power Platform Build Tools extension installed in Azure DevOps.
- Third-party documentation generation tools (e.g.,
mightora-powerplatform-documentationgenerator-erdiagram,documentSolutionTables) installed or available. - A Personal Access Token (PAT) with Code (Read & write) permissions for committing to the wiki.
Pipeline Configuration
Variables
The pipeline relies on several variables to manage solution names, environment details, and other configurations. These should be configured in the pipeline’s variable settings in Azure DevOps.
This pipeline is designed to be run against a development environment to export multiple solutions, check them, unpack them, and commit them to source control.
name: $(TeamProject)_$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
variables:
- name: varPowerPlatformSPN
value: <your-service-principal-name>
- name: varSolutionName
value: <your-solution-name-1>
- name: varSolutionName2
value: <your-solution-name-2>
- name: varSolutionName3
value: <your-solution-name-3>
- name: varSolutionName4
value: <your-solution-name-4>
- name: varWebsiteId
value: <your-portal-website-id>
- name: varEnvironmentURL
value: <your-environment-url>
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
persistCredentials: true
clean: true
- task: PowerPlatformToolInstaller@2
inputs:
DefaultVersion: true
AddToolsToPath: true
## Clean out wiki
- task: PowerShell@2
displayName: 'Clean wiki-output directory'
inputs:
targetType: 'inline'
script: |
$outputDir = "$(Build.SourcesDirectory)\wiki-output\"
if (Test-Path $outputDir) {
Remove-Item -Path $outputDir -Recurse -Force
Write-Output "Cleaned directory: $outputDir"
} else {
Write-Output "Directory does not exist: $outputDir"
}
- task: PowerShell@2
displayName: 'Clean wiki-docx-output directory except template.docx'
inputs:
targetType: 'inline'
script: |
$outputDir = "$(Build.SourcesDirectory)\wiki-docx-output\"
$templateFile = "$(Build.SourcesDirectory)\wiki-docx-output\template.docx"
if (Test-Path $outputDir) {
Get-ChildItem -Path $outputDir -Recurse | Where-Object { $_.FullName -ne $templateFile } | Remove-Item -Recurse -Force
Write-Output "Cleaned directory: $outputDir except for $templateFile"
} else {
Write-Output "Directory does not exist: $outputDir"
}
## SOLUTION 1
- task: PowerPlatformSetSolutionVersion@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName)'
SolutionVersionNumber: '1.3.0.$(Build.BuildID)'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName)_managed.zip'
Managed: true
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName).zip'
Managed: false
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformChecker@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
FilesToAnalyze: '$(Build.ArtifactStagingDirectory)\$(varSolutionName).zip'
RuleSet: '083a2ef5-7e0e-4754-9d88-9455142dc08b'
ErrorLevel: 'InformationalIssueCount'
FailOnPowerAppsCheckerAnalysisError: false
- task: PowerPlatformUnpackSolution@2
inputs:
SolutionInputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName).zip'
SolutionTargetFolder: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName)'
SolutionType: 'Both'
ProcessCanvasApps: true
OverwriteFiles: true
## SOLUTION 2
- task: PowerPlatformSetSolutionVersion@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName2)'
SolutionVersionNumber: '1.3.0.$(Build.BuildID)'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName2)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName2)_managed.zip'
Managed: true
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName2)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName2).zip'
Managed: false
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformChecker@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
FilesToAnalyze: '$(Build.ArtifactStagingDirectory)\$(varSolutionName2).zip'
RuleSet: '083a2ef5-7e0e-4754-9d88-9455142dc08b'
ErrorLevel: 'InformationalIssueCount'
FailOnPowerAppsCheckerAnalysisError: false
- task: PowerPlatformUnpackSolution@2
inputs:
SolutionInputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName2).zip'
SolutionTargetFolder: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName2)'
SolutionType: 'Both'
ProcessCanvasApps: true
OverwriteFiles: true
## SOLUTION 3
- task: PowerPlatformSetSolutionVersion@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName3)'
SolutionVersionNumber: '1.3.0.$(Build.BuildID)'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName3)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName3)_managed.zip'
Managed: true
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName3)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName3).zip'
Managed: false
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformChecker@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
FilesToAnalyze: '$(Build.ArtifactStagingDirectory)\$(varSolutionName3).zip'
RuleSet: '083a2ef5-7e0e-4754-9d88-9455142dc08b'
ErrorLevel: 'InformationalIssueCount'
FailOnPowerAppsCheckerAnalysisError: false
- task: PowerPlatformUnpackSolution@2
inputs:
SolutionInputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName3).zip'
SolutionTargetFolder: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName3)'
SolutionType: 'Both'
ProcessCanvasApps: true
OverwriteFiles: true
## SOLUTION 4
- task: PowerPlatformSetSolutionVersion@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName4)'
SolutionVersionNumber: '1.3.0.$(Build.BuildID)'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName4)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName4)_managed.zip'
Managed: true
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformExportSolution@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
SolutionName: '$(varSolutionName4)'
SolutionOutputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName4).zip'
Managed: false
AsyncOperation: true
MaxAsyncWaitTime: '60'
- task: PowerPlatformChecker@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
FilesToAnalyze: '$(Build.ArtifactStagingDirectory)\$(varSolutionName4).zip'
RuleSet: '083a2ef5-7e0e-4754-9d88-9455142dc08b'
ErrorLevel: 'InformationalIssueCount'
FailOnPowerAppsCheckerAnalysisError: false
- task: PowerPlatformUnpackSolution@2
inputs:
SolutionInputFile: '$(Build.ArtifactStagingDirectory)\$(varSolutionName4).zip'
SolutionTargetFolder: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName4)'
SolutionType: 'Both'
ProcessCanvasApps: true
OverwriteFiles: true
## PORTAL
- task: PowerPlatformDownloadPaportal@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
DownloadPath: '$(Build.SourcesDirectory)\src\ppages'
WebsiteId: '$(varWebsiteId)'
Overwrite: true
ModelVersion: '2'
- task: PowerPlatformDownloadPaportal@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
DownloadPath: '$(Build.ArtifactStagingDirectory)\ppages'
WebsiteId: '$(varWebsiteId)'
Overwrite: true
ModelVersion: '2'
- task: PowerPlatformExportData@2
inputs:
authenticationType: 'PowerPlatformSPN'
PowerPlatformSPN: '$(varPowerPlatformSPN)'
Environment: '$(BuildTools.EnvironmentUrl)'
SchemaFile: '$(Build.SourcesDirectory)\referenceData\data_schema.xml'
DataFile: '$(Build.ArtifactStagingDirectory)\rferenceData.zip'
Overwrite: true
Verbose: true
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)\rferenceData.zip'
destinationFolder: '$(Build.SourcesDirectory)\referenceData'
cleanDestinationFolder: true
overwriteExistingFiles: true
- task: PowerShell@2
inputs:
targetType: 'inline'
script: 'pac solution create-settings --solution-zip $(Build.ArtifactStagingDirectory)/$(varSolutionName3).zip --settings-file $(Build.SourcesDirectory)/src/solutions/$(varSolutionName3)-settings.json'
## Generate Documentation
- task: PowerShell@2
displayName: 'Clean wiki directory'
inputs:
targetType: 'inline'
script: |
$outputDir = "$(Build.SourcesDirectory)\wiki\"
if (Test-Path $outputDir) {
Remove-Item -Path $outputDir -Recurse -Force
Write-Output "Cleaned directory: $outputDir"
} else {
Write-Output "Directory does not exist: $outputDir"
}
- task: PowerShell@2
displayName: 'Clean wiki-output directory'
inputs:
targetType: 'inline'
script: |
$outputDir = "$(Build.SourcesDirectory)\wiki-output\"
if (Test-Path $outputDir) {
Remove-Item -Path $outputDir -Recurse -Force
Write-Output "Cleaned directory: $outputDir"
} else {
Write-Output "Directory does not exist: $outputDir"
}
- task: PowerShell@2
displayName: 'Clean wiki-docx-output directory except template.docx'
inputs:
targetType: 'inline'
script: |
$outputDir = "$(Build.SourcesDirectory)\wiki-docx-output\"
$templateFile = "$(Build.SourcesDirectory)\wiki-docx-output\template.docx"
if (Test-Path $outputDir) {
Get-ChildItem -Path $outputDir -Recurse | Where-Object { $_.FullName -ne $templateFile } | Remove-Item -Recurse -Force
Write-Output "Cleaned directory: $outputDir except for $templateFile"
} else {
Write-Output "Directory does not exist: $outputDir"
}
## Check if any steps need repeating for split solutions
- task: mightora-powerplatform-documentationgenerator-erdiagram@1
inputs:
locationOfUnpackedSolution: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName2)'
wikiLocation: '$(Build.SourcesDirectory)\wiki\data-dictionary\entity-relationship-diagram'
authors: 'cg'
- task: documentSolutionTables@2
continueOnError: true
inputs:
locationOfUnpackedSolution: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName2)'
wikiLocation: '$(Build.SourcesDirectory)\wiki\data-dictionary\tables'
useSingleFile: true
- task: documentTableRelationships@2
continueOnError: true
inputs:
locationOfUnpackedSolution: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName2)'
wikiLocation: '$(Build.SourcesDirectory)\wiki\data-dictionary\entity-relationship-diagram'
useSingleFile: true
createFullERD: true
authors: 'cg'
- task: documentRoles@2
continueOnError: true
inputs:
locationOfUnpackedSolution: '$(Build.SourcesDirectory)\src\solutions\$(varSolutionName3)'
wikiLocation: '$(Build.SourcesDirectory)\wiki\security-roles'
useSingleFile: true
- task: convertConvertInlineDiagrams@1
continueOnError: true
inputs:
locationOfSourceMDFiles: '$(Build.SourcesDirectory)/wiki'
outputLocation: '$(Build.SourcesDirectory)/wiki-output'
- task: mightora-UploadMDToWiki@0
continueOnError: true
inputs:
ADOBaseUrl: '$(System.CollectionUri)'
wikiSource: '$(Build.SourcesDirectory)/wiki-output'
MDRepositoryName: 'Power-Platform'
MDVersion: '$(Build.BuildNumber)'
MDTitle: 'Title'
WikiDestination: 'Architecture/Delivered'
HeaderMessage: '<mark>DO NOT EDIT DIRECTLY - GENERATED FROM Dataverse REPO</mark>'
env:
SYSTEM_ACCESSTOKEN: $(PAT)
- task: convertMarkdownToDocx@1
continueOnError: true
inputs:
locationOfMDFiles: '$(Build.SourcesDirectory)/wiki-output/'
outputLocation: '$(Build.SourcesDirectory)/wiki-docx-output'
templateFile: '$(Build.SourcesDirectory)/wiki-docx-output/template.docx'
- task: CmdLine@2
inputs:
script: |
echo commit all changes
git config user.email "$(Build.RequestedForEmail)"
git config user.name "$(Build.RequestedFor)"
git checkout -b <your-branch-name>
git add --all
git commit -m "Latest solution changes."
echo push code to new repo
git -c http.extraheader="AUTHORIZATION: bearer $(System.AccessToken)" push origin <your-branch-name>
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
