Power Platform: export Solutions with an Azure DevOps Pipeline Template

Why?

In one of my previous posts Power Platform: export a Solution with an Azure DevOps Pipeline, I shared a straight forward Pipeline to export a Power Platform solution. For one solution this is fine. However if you want to do this multiple times, duplicating the whole Pipeline becomes inefficient. Let’s see if we can export solutions from a Pipeline using some kind of Template construction 💪👍.

What?

In this post, I will show that we can use a single Template. From that moment on, we can use the Template can in multiple DevOps Pipelines. My first example is to export multiple Solutions in one Pipeline (re)using a single Template.

How?

In my example below there are some improvements that I added simultaneously:

  • I added another variable to the Library that represents reusable data within the Power Platform Environment.
    We will use the variable PowerPlatformSolution02 in addition to PowerPlatformSolution01 to do the same set of Tasks on different solutions:
Variable Group of Library per Environment
  • I added a variable to the master Pipeline called gitUserEmailPipeline.
    This will clearly separate it visually from the gitUserEmail parameter being used in the Template and it will give the running user the option to override the value of this variable without having to update the Pipeline or the Library. There are situations that a running user is able to run the Pipeline but has no permissions to edit the Pipeline or Library Variables. This is where Pipeline Variables configured to “Let users override this value when running this pipeline” will be very useful.
Variables determined in the Pipeline (could also be moved to the Library Group if permitted)
  • I added a PowerPlatformSetSolutionVersion Task. This Task is to make sure that the Version of the Solution is incremented automatically in DEV.
    SolutionVersionNumber: ‘${{ parameters.SolutionVersionPrefix }}.$(Build.BuildNumber)’
    I am combining the SolutionVersionPrefix Parameter with one of the Predefined variables – Azure Pipelines | Microsoft Docs: Build.BuildNumber. This makes sure every Pipeline run will create a unique version of the solution to export.
  • I discovered a new variant of exporting a Power Platform Solution: the Managed Solution unpacked as individual files. This means I want that variant saved as source code as well! 🤓
  • Instead of exporting all 4 variants individually from the Power Platform DEV Environment, I am using the two main exports of the .zip files to unpack. This means less requests towards the Power Platform Environment and for future use cases, (un)packing from the source code .zip files is a better practice.

First it is important to mention that we can use Templates in a lot of ways, so this is just an example. You can always find more details at the documentation pages: Templates – Azure Pipelines | Microsoft Docs.
Second it is important to explain up front that we will use Parameters to pass information from the running Pipeline to a Template file. Keep this in mind when looking at the examples.

1) Because every Pipeline definition is also saved in our Git Repo, I want to create a more manageable folder structure. I created a folder to save the Pipelines and within that folder a sub folder to save Templates. We can do this in the Repos section of the Azure DevOps Project as explained previously in Power Platform: setting up an Azure DevOps Pipeline:

Example Folder Structure in Git Repo to save Pipelines as well as Templates.
In this example the path of two Pipelines = /Pipelines/multiapp10buildonegitmultibranch.yml and /Pipelines/teamkickstart10buildselfbranch.yml that can both reference the same Template.

2) The Pipeline can now reference the .yml Template file to execute reusable Tasks.
This makes the Pipeline very simple:

trigger: none

pool:
  vmImage: windows-latest

variables:
  - group: 'insbadev01'

resources:
  repositories:
  - repository: GitRepoBuild
    type: git
    name: 'Power Platform Applications' #Cannot be variable $(GitBuildRepo)
    ref: '$(Build.SourceBranchName)'

stages:
- stage: 'buildinsbadev01'
  displayName: 'Build from insbadev01'
  jobs:
    - job: 'Build01'
      displayName: '$(PowerPlatformSolution02) build'
      steps:
      - template: 'Templates/BuildSolutionOneGitOwnBranche.yml'
        parameters:
          connectionString: '$(EnvironmentServiceConnection)'         
          SolutionInternalName: $(PowerPlatformSolution02)        
          SolutionVersionPrefix: '$(SolutionVersionPrefix)'
          gitUserEmail: '$(gitUserEmailPipeline)'

A second Pipeline can then use the same Template even multiple times in the same run:

trigger: none

pool:
  vmImage: windows-latest

variables:
  - group: 'insbadev01'

stages:
- stage: 'buildinsbadev01'
  displayName: 'Build from insbadev01'
  jobs:
    - job: 'Build01'
      displayName: '$(PowerPlatformSolution01) build'
      steps:
      - template: 'Templates/BuildSolutionOneGitOwnBranche.yml'
        parameters:
          connectionString: 'insbadev01connection'
          environment: 'insbadev01'
          SolutionInternalName: $(PowerPlatformSolution01)        
          SolutionVersionPrefix: '0.95'
          gitUserEmail: '$(gitUserEmailPipeline)'
    - job: 'Build02'
      displayName: '$(PowerPlatformSolution02) build'
      dependsOn: 'Build01'
      condition: succeeded()
      steps:
      - template: 'Templates/BuildSolutionOneGitOwnBranche.yml'
        parameters:
          connectionString: 'insbadev01connection'
          environment: 'insbadev01'
          SolutionInternalName: $(PowerPlatformSolution02)        
          SolutionVersionPrefix: '0.92'
          gitUserEmail: '$(gitUserEmailPipeline)'

Note the dependsOn property of the second job. Because it depends on the first job, the second job will wait until the first job is successfully completed.
In these examples the path of the Template = /Pipelines/Templates/BuildSolutionOneGitOwnBranche.yml

3) In the Template file we have more complex and reusable code. The advantage is that we can manage this centrally:

parameters:
- name: 'asyncTimeout'
  type: number
  default: 3600

- name: 'connectionString'
  type: string

- name: 'environment'
  type: string

- name: 'gitUserEmail'
  type: string

- name: 'SolutionInternalName'
  type: string

- name: 'SolutionVersionPreFix'
  type: string

steps:
      - checkout: self
        persistCredentials: true

      - task: PowerPlatformToolInstaller@0 #Always Install this when using PowerPlatformBuiltTools on machine
        displayName: 'Power Platform Tool Installer'

      # Solutions logic #
      - task: PowerPlatformSetSolutionVersion@0 #Set Version in DEV with unique Build Number included
        displayName: 'Solution Versioning'
        inputs:
          authenticationType: 'PowerPlatformSPN'
          PowerPlatformSPN: ${{ parameters.connectionString }}
          SolutionName: '${{ parameters.SolutionInternalName }}'
          SolutionVersionNumber: '${{ parameters.SolutionVersionPrefix }}.$(Build.BuildNumber)'

      - task: PowerPlatformPublishCustomizations@0 #Publish Environment Customizations
        displayName: 'Solutions - Publish customizations'
        inputs:
          authenticationType: 'PowerPlatformSPN'
          PowerPlatformSPN: ${{ parameters.connectionString }}

      - task: PowerPlatformExportSolution@0 #Export Unmanaged Solution to machine as .zip file locally
        displayName: 'Solutions - UnmSol zip export zip locally'
        inputs:
          authenticationType: 'PowerPlatformSPN'
          PowerPlatformSPN: ${{ parameters.connectionString }}
          SolutionName: '${{ parameters.SolutionInternalName }}'
          SolutionOutputFile: '$(GitDirectoryExportedSolutions)\${{ parameters.SolutionInternalName }}UM.zip'
          AsyncOperation: true
          MaxAsyncWaitTime: '60'
          ExportAutoNumberingSettings: true

      - task: PowerShell@2  #Push Unmanaged Solution .zip file to Git Repo 
        displayName: 'Solutions - UnmSol zip to repo'
        inputs:
          targetType: 'inline'
          script: |      
            # Write your PowerShell commands here.        
            ls ${env:GitDirectoryExportedSolutions}
            git config --global user.email ${{ parameters.gitUserEmail }}
            git config --global user.name ${{ parameters.gitUserEmail }}
            git checkout $(Build.SourceBranchName)
            git add ${env:GitDirectoryExportedSolutions}
            git commit -am "Added Solution Export ${{ parameters.SolutionInternalName }}UM V${{ parameters.SolutionVersionPrefix }}.$(Build.BuildNumber) as zip"
            git push origin $(Build.SourceBranchName)

      - task: PowerPlatformUnpackSolution@0 #Unpack Unmanaged Solution .zip file locally
        displayName: 'Solutions - UnmSol zip unpack locally'
        inputs:
          SolutionInputFile: '$(GitDirectoryExportedSolutions)\${{ parameters.SolutionInternalName }}UM.zip'
          SolutionTargetFolder: '$(GitDirectoryExportedSolutions)\${{ parameters.SolutionInternalName }}Unmanaged'

      - task: PowerShell@2  #Push unpacked Unmanaged Solution files to Git Repo
        displayName: 'Solutions - UnmSol folders to repo'
        inputs:
          targetType: 'inline'
          script: |      
            # Write your PowerShell commands here.        
            ls ${env:GitDirectoryExportedSolutions}
            git config --global user.email ${{ parameters.gitUserEmail }}
            git config --global user.name ${{ parameters.gitUserEmail }}
            git checkout $(Build.SourceBranchName)
            git add ${env:GitDirectoryExportedSolutions}\${{ parameters.SolutionInternalName }}Unmanaged
            git commit -am "Added Solution ${{ parameters.SolutionInternalName }}Unmanaged V${{ parameters.SolutionVersionPrefix }}.$(Build.BuildNumber) unpacked"
            git push origin $(Build.SourceBranchName)

      - task: PowerPlatformExportSolution@0  #Export Managed Solution to machine as .zip file locally
        displayName: 'Solutions - ManSol export zip locally'
        inputs:
          authenticationType: 'PowerPlatformSPN'
          PowerPlatformSPN: ${{ parameters.connectionString }}
          SolutionName: '${{ parameters.SolutionInternalName }}'
          SolutionOutputFile: '$(GitDirectoryExportedSolutions)\${{ parameters.SolutionInternalName }}M.zip'
          Managed: true
          AsyncOperation: true
          MaxAsyncWaitTime: '60'
          ExportAutoNumberingSettings: true

      - task: PowerShell@2  #Push Managed Solution .zip file to Git Repo
        displayName: 'Solutions - ManSol zip to repo'
        inputs:
          targetType: 'inline'
          script: |      
            # Write your PowerShell commands here.        
            ls ${env:GitDirectoryExportedSolutions}
            git config --global user.email ${{ parameters.gitUserEmail }}
            git config --global user.name ${{ parameters.gitUserEmail }}
            git checkout $(Build.SourceBranchName)
            git add ${env:GitDirectoryExportedSolutions}
            git commit -am "Added Solution Export ${{ parameters.SolutionInternalName }}M V${{ parameters.SolutionVersionPrefix }}.$(Build.BuildNumber) as zip"        
            git push origin $(Build.SourceBranchName)

      - task: PowerPlatformUnpackSolution@0 #Unpack Managed Solution .zip file locally
        displayName: 'Solutions - ManSol zip unpack locally'
        inputs:
          SolutionInputFile: '$(GitDirectoryExportedSolutions)\${{ parameters.SolutionInternalName }}M.zip'
          SolutionTargetFolder: '$(GitDirectoryExportedSolutions)\${{ parameters.SolutionInternalName }}Managed' 
          SolutionType: Managed 
        
      - task: PowerShell@2  #Push unpacked Managed Solution files to Git repo
        displayName: 'Solutions - ManSol folders to repo'    
        inputs:
          targetType: 'inline'
          script: |      
            # Write your PowerShell commands here.        
            ls ${env:GitDirectoryExportedSolutions}
            git config --global user.email ${{ parameters.gitUserEmail }}
            git config --global user.name ${{ parameters.gitUserEmail }}
            git checkout $(Build.SourceBranchName)
            git add ${env:GitDirectoryExportedSolutions}\${{ parameters.SolutionInternalName }}Managed
            git commit -am "Added Solution ${{ parameters.SolutionInternalName }}Managed V${{ parameters.SolutionVersionPrefix }}.$(Build.BuildNumber) unpacked"
            git push origin $(Build.SourceBranchName)

The end result of one Pipeline run will be 8 updates to the Git Repo:

Commits to Git Repo of one Pipeline using the same Template twice in one Run

And from this moment on, we are saving all variants of both Power Platform Solutions. This means we have them backed up as source code in our Git Repo 😎.

12 thoughts on “Power Platform: export Solutions with an Azure DevOps Pipeline Template

  1. i am getting below error when i upload my second solution . I just use array of solution and calling template one by one from my master pipeline

    Switched to a new branch ‘main’
    [main ca55b9f] Added Solution Export Test2UM V0.95.20230419.5 as zip
    1 file changed, 0 insertions(+), 0 deletions(-)
    To https://dev.azure.com/Linujohn/SDSTEST/_git/PowerPlatformSolutions
    ! [rejected] main -> main (fetch first)
    error: failed to push some refs to ‘https://dev.azure.com/Linujohn/SDSTEST/_git/PowerPlatformSolutions’
    hint: Updates were rejected because the remote contains work that you do
    hint: not have locally. This is usually caused by another repository pushing
    hint: to the same ref. You may want to first integrate the remote changes
    hint: (e.g., ‘git pull …’) before pushing again.
    hint: See the ‘Note about fast-forwards’ in ‘git push –help’ for details

    1. Hi John,
      Based on the notification it seems like you are committing both runs at the same moment? In my example of the “second pipeline”, I force the second job to wait until the first job completes with the “dependsOn” parameter.
      Unfortunately I am not a GIT expert either, but to troubleshoot you can see what happens if you create two pipelines and test each with separate runs.

      Sorry if I am not able to help much, but I recommend the community forum: https://developercommunity.visualstudio.com/AzureDevOps?sort=newest to see if any expert can help.

    2. Had the same issue and took me a day to resolve you. The issue is you changes on your local which isn’t in the remote repo therefore the main branch is treated as a new branch. You will want to change your git line to somethine like this:

      # Write your PowerShell commands here.
      ls ${env:GetDirectoryUnpackedSolutions}\${env:GitDirectoryExportedSolutions}
      git config –global user.email ${{ parameters.gitUserEmail }}
      git config –global user.name ${{ parameters.gitUserEmail }}
      git pull origin $(Build.SourceBranchName)
      git checkout -b $(Build.SourceBranchName)
      git add ${env:GetDirectoryUnpackedSolutions}\${env:GitDirectoryExportedSolutions}
      git commit -am “Added Solution Export ${{ parameters.SolutionInternalName }}UM V${{ parameters.SolutionVersionPrefix }}.$(Build.BuildNumber) as zip”
      git push -u origin $(Build.SourceBranchName)

      You will want to check as a new branch as per error : Switched to a new branch ‘main’

      then pull any changes and push to the origin branch.

      I hope this helps

  2. Hello there again!!

    I am confused here with this message !“ I renamed the variable gituser in the running Pipeline to gitUserEmailPipeline. This will clearly separate it from the parameter being used in the Template.” as I can’t see any git sure being defined in the variable group except in the template yml file

    1. You are absolutely right Shomari!
      This is not explained very clearly and I apologize for that 😅.
      I updated the blog post to indicate that some variables are defined as a Pipeline Variable. Does it clarify that the Master Pipeline itself can also have Variables and that I used that functionality to make the SolutionVersionPrefix as well as the gitUserEmailPipeline variable?
      This way a user that runs the Pipeline is able to update these variables before starting the run.

      1. Again thanks for the speedy reply!!😌😌. That make sense so I guess it’s the new update yml file was to demonstrate the point or is there now three yml file now: the template file, the multi build and now the self branch one?

        Thanks in advance!

        1. In this example at the end, there should be two .yml files.
          One main file that is directly linked to the Pipeline.
          One template file that is usable within this (and other) Pipeline(s).

          I can understand the confusion because my first Pipeline in the original blog post was: multiapp10buildonegitmultibranch.yml
          The updated Pipeline with more Pipeline Variables is: teamkickstart10buildselfbranch.yml

          However they both use the same template so for this example that demonstrates the power of Pipeline Templates.
          The first Pipeline had a hard coded SolutionVersionPrefix in the code and the updated Pipeline has this configured as an overridable Pipeline Variable. Clearer?

      2. Good evening Django,

        I have been looking at your GitHub and found the release scripts and I just wanted to ask if I need to create a separate pipeline for release and can I manage this in the azure release under pipelines with tiggers to automate my releases to various instances

        And secondly is it possible to have a global template the multiple environments can reference please

        1. Hi Shomari,
          Azure DevOps has a lot of automation options with triggers as well as additional actions within the pipeline. This gives you the flexibility to decide how you want to trigger / setup the pipeline for any scenario.
          When you have setup your template(s) correctly with the right parameters, you should be able to reuse that template with as many solutions, environments and scenario’s as desired. It just may take some time and business alignment to decide upon the best scenario that fits your requirements the best.
          Good luck! 💪👍

  3. Hi There!!

    Is step 2 code for BuildSolutionOneGitbranche.ylm and step code three for multipleapp10buildonegitmultiplebranch.yml please?

    And secondly do I need to create a service connect for each the different environment ?

    1. Hi Shomari,
      Step 2 is the main Pipeline with the Git Repo path =

      Step 3 is the reusable Template with the Git Repo path =
      /Pipelines/Templates/BuildSolutionOneGitOwnBranche.yml

      Step 2 references the Template in Step 3.

Leave a comment