Scripts/SolutionDeploy.ps1

#
# SolutionDeploy.ps1
#

function Write-PPDOMessage {
    [CmdletBinding()]
    param (
        [Parameter()] 
        [string] $Message,
        [Parameter()]
        [ValidateSet('group', 'warning', 'error', 'section', 'debug', 'command', 'endgroup')]
        [string] $Type,
        [Parameter()] 
        [bool] $LogError = $false,
        [Parameter()] 
        [bool] $LogWarning = $false,
        [Parameter()] 
        [bool] $RunLocally = $false
    )
    switch ($Type) {
        'group' { 
            if ($RunLocally) {
                Write-Host $Message -BackgroundColor Magenta
            }
            else {
                Write-Host "##[group]$Message"
            }
        } 'warning' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor DarkYellow
            }
            else {
                if ($LogWarning) {
                    Write-Host "##vso[task.logissue type=warning]$Message"
                }
                else {
                    Write-Host "##[warning]$Message"
                }                
            }
        } 'error' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Red
            }
            else {
                if ($LogError) {
                    Write-Host "##vso[task.logissue type=error]$Message"
                }
                else {
                    Write-Host "##[error]$Message"
                }                
            }
        } 'section' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Green
            }
            else {
                Write-Host "##[section]$Message"
            }
        } 'debug' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Magenta
            }
            else {
                Write-Host "##[debug]$Message"
            }
        } 'command' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Blue
            }
            else {
                Write-Host "##[command]$Message"
            }
        } 'endgroup' {
            if ($RunLocally) {
                Write-Host ":END:" -BackgroundColor Magenta
            }
            else {
                Write-Host "##[endgroup]"
            }
        }
        Default {
            Write-Host $Message
        }
    }
}



function Connect-Cli {
    [CmdletBinding()]
    Param(
        [string] [Parameter(Mandatory = $true)] $DeployServerUrl,
        [string] [Parameter(Mandatory = $true)] $UserName,
        [string] [Parameter(Mandatory = $false)] $Password = "",
        [string] [Parameter(Mandatory = $true)] $TenantId,
        [bool] [Parameter(Mandatory = $false)] $UseClientSecret = $false,
        [string] [Parameter(Mandatory = $false)] $EnvironmentName 
    )

    if ($UseClientSecret) {
        Write-Host "Using Service Principal"
        Write-Host "Connecting to PAC CLI"
        & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe auth create --name $EnvironmentName --url $DeployServerUrl --tenant $CRMConn.TenantId --applicationId $UserName --clientSecret $Password

        Write-Host "Setting Add-PowerAppsAccount"
        Add-PowerAppsAccount -ApplicationId $UserName -ClientSecret $Password -TenantID $CRMConn.TenantId

        Write-Host "Authenticate Azure CLI"
        az login --service-principal -u $UserName -p $Password --tenant $CRMConn.TenantId
    }
    else {
        Write-Host "Using named account"Write-PPDOMessage
        Write-Host "Connecting to PAC CLI"

        if ([string]::IsNullOrEmpty($Password)) {
            & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe auth create --name $EnvironmentName --url $DeployServerUrl --tenant $CRMConn.TenantId --username $UserName --password $Password

            Write-Host "Setting Add-PowerAppsAccount"
            Add-PowerAppsAccount -Username $UserName
        }else {
            $ssPassword = ConvertTo-SecureString $Password -AsPlainText -Force
            & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe auth create --name $EnvironmentName --url $DeployServerUrl --tenant $CRMConn.TenantId --username $UserName --password $ssPassword

            Write-Host "Setting Add-PowerAppsAccount"
            Add-PowerAppsAccount -Username $UserName -Password $ssPassword
        }
    }        

    & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe org who

}


function Start-DeploySolution {
    Param(
        [string] [Parameter(Mandatory = $true)] $DeployServerUrl,
        [string] [Parameter(Mandatory = $true)] $UserName,
        [string] [Parameter(Mandatory = $false)] $Password = "",
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [bool] [Parameter(Mandatory = $false)] $UseClientSecret = $false,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false,
        [string] [Parameter(Mandatory = $false)] $EnvironmentName = $env:ENVIRONMENT_NAME
    )

    ######################## SETUP
    . "$PSScriptRoot\..\Private\_SetupTools.ps1"

    Write-Host "Using Microsoft.PowerPlatform.DevOps version :" (Get-Module -Name Microsoft.PowerPlatform.DevOps -ListAvailable).Version
    #region "Dependencies"
    Write-PPDOMessage -Message "Installing Dependencies" -Type group -RunLocally $RunLocally
    Install-PAC

    if (!$RunLocally) {
        Install-ConfigMigrationModule
        Install-XrmModule
        Install-PowerAppsCheckerModule
        Install-PowerAppsAdmin
    }
    else {
        Write-Host "Preparing local run"
    }

    Write-PPDOMessage -Type endgroup -RunLocally $RunLocally
    #endregion
    function Import-Package {
        if ($UseClientSecret) {
            [string]$CrmConnectionString = "AuthType=ClientSecret;Url=$DeployServerUrl;ClientId=$UserName;ClientSecret=$Password"
        }
        else {
            [string]$CrmConnectionString = "AuthType=OAuth;Username=$UserName;Password=$Password;Url=$DeployServerUrl;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97;LoginPrompt=never"
        }

        $Packages = Get-Content "$PipelinePath\deployPackages.json" -Encoding UTF8 | ConvertFrom-Json

        #handle Environments file should it be missing. Same for DeployPackages and SolutionChecker files!

        $Environments = Get-Content "$PipelinePath\Environments.json" | ConvertFrom-Json
        $EnvConfig = $Environments | Where-Object { $_.EnvironmentName -eq $EnvironmentName }

        Write-PPDOMessage "Creating CRM connection" -Type section -RunLocally $RunLocally
        $CRMConn = Get-CrmConnection -ConnectionString $CrmConnectionString -Verbose #-MaxCrmConnectionTimeOutMinutes 10
        #Set-CrmConnectionTimeout -conn $CRMConn -TimeoutInSeconds 600

        if ($false -eq $CRMConn.IsReady) {
            Write-Host "An error occurred: " $CRMConn.LastCrmError
            Write-Host $CRMConn.LastCrmException.Message
            Write-Host $CRMConn.LastCrmException.Source
            Write-Host $CRMConn.LastCrmException.StackTrace
            throw "Could not establish connection with server"
        }

        #connecting PAC cli - need this git config --global core.longpaths true
        Connect-Cli -DeployServerUrl $DeployServerUrl -UserName $UserName -Password $Password -TenantId $CRMConn.TenantId -UseClientSecret $UseClientSecret -EnvironmentName $EnvironmentName
     


        #ENVIRONMENT PRE-ACTION
        if ($null -ne $EnvConfig -and $EnvConfig.PreAction -eq $true) {
    
            Write-PPDOMessage "Execute Environment Pre Action" -Type section -RunLocally $RunLocally
            . "$PipelinePath\Common\Environments\Scripts\PreAction.ps1" -Conn $CRMConn -PipelinePath $PipelinePath -EnvironmentName $EnvConfig.EnvironmentName -EnvironmentUrl $DeployServerUrl
            $EnvConfig.PreFunctions | ForEach-Object {
                & $_ -Conn $CRMConn
            }
            Write-PPDOMessage "Environment Pre Action Complete" -Type command -RunLocally $RunLocally
        }
        else {
            Write-PPDOMessage "Environment Pre Action not registered to execute" -Type warning -RunLocally $RunLocally
        }   

        foreach ($package in $Packages) {
            $Deploy = $package.DeployTo | Where-Object { $_.EnvironmentName -eq $EnvironmentName }


            $deployStepCheck = $false;
            if ($null -ne $Deploy -and $null -ne $Deploy.Deploy) {  
                    $deployStepCheck = $Deploy.Deploy
                    Write-PPDOMessage "Using Deploy Flag value - $($Deploy.Deploy)"
            }
            elseif ($null -ne $Deploy -and $null -eq $Deploy.Deploy) {
                $deployStepCheck = $true
                Write-PPDOMessage "Using Deploy Block - True"
            }else{
                $deployStepCheck = $false
                Write-PPDOMessage "Deploy Block - False"
            }
          
            if ($deployStepCheck -eq $true) {  
                $PSolution = $package.SolutionName             
                $SolutionFolder = $package.SolutionName

                $versionFile = "$($PSolution).version"
                Write-PPDOMessage "Preparing Deployment for $PSolution" -Type group -RunLocally $RunLocally
                Write-Host "Deployment step manifest - $Deploy"
                Write-Host ""

                Write-Host "Getting Solutions & Versions to be Deployed..."
                try {
                    $solutionsToDeploy = Get-Content -Path $PipelinePath\$SolutionFolder\$versionFile | ConvertFrom-Json
                }
                catch {
                    #Legacy Solution Packaging Support
                    $solutionVersion = Get-Content -Path $PipelinePath\$SolutionFolder\$versionFile
                    $solutionsToDeploy = @([ordered]@{SolutionName = $package.SolutionName; Version = $solutionVersion ; })
                }
                $skipPatch = $false
                
                $solutionsToDeploy | ForEach-Object {
                    $deployAsNew = $false
                    $skipDeploy = $false 
                    $anyFailuresInImport = $false; 
        
                    $patchDeploy = $false  
                      
                    $PSolution = $_.SolutionName
                    $deployingVersion = $_.Version
                    $packageFolder = "dataverse_$($PSolution)"
                    if ($PSolution.contains("_Patch")) {
                        $patchDeploy = $true
                    }
                    #region 'Preparing Deployment'

                    Write-PPDOMessage "Preparing $PSolution Solution as $($Deploy.DeploymentType)" -Type section -RunLocally $RunLocally

                    $fileToPack = "$($Deploy.EnvironmentName)_$($PSolution)_$($Deploy.DeploymentType).zip"               

                    Write-PPDOMessage "Packing Solution $PSolution" -Type command -RunLocally $RunLocally
                    #Checking for Canvas App
                    $canvasApps = Get-ChildItem -Path $PipelinePath\$SolutionFolder\$packageFolder\CanvasApps\ -Directory -ErrorAction SilentlyContinue 
                    # pack canvas apps
                    $canvasApps | ForEach-Object {
                        Write-Host "Packing Canvas App $($_.name)";
                        & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe canvas pack --sources $_.FullName --msapp "$($_.FullName).msapp"
                        Remove-Item $_.FullName -Recurse -ErrorAction SilentlyContinue
                    }
                    if ($Deploy.DeploymentType.ToLower() -eq "unmanaged") {
                        & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe solution pack -f $PipelinePath\$SolutionFolder\$packageFolder -z $PipelinePath\$SolutionFolder\$fileToPack -p Unmanaged
                    }
                    else {
                        & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe solution pack -f $PipelinePath\$SolutionFolder\$packageFolder -z $PipelinePath\$SolutionFolder\$fileToPack -p Managed -same
                    }         
               
                    Write-PPDOMessage "Importing package" -Type section -RunLocally $RunLocally
                
                    try {
                        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                        $error.Clear()
                        Write-PPDOMessage "Deploying $PSolution as $($Deploy.DeploymentType) to - $EnvironmentName" -Type section -RunLocally $RunLocally

                        if (!$patchDeploy) {
                            Write-PPDOMessage "Checking to make sure there is no existing $($package.SolutionName)_Upgrade solution" -Type command -RunLocally $RunLocally
                            $ugSolution = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute uniquename -FilterOperator like -FilterValue "$($package.SolutionName)_Upgrade" -Fields uniquename
                            if ($ugSolution.CrmRecords.Count -gt 0) {
                                Write-PPDOMessage "Found holding solution $($ugSolution.CrmRecords[0].uniquename), removing it" -Type warning -RunLocally $RunLocally

                                Write-Host "Applying Upgrade to Solution"
                                $promoteRequestId = $CRMConn.DeleteAndPromoteSolutionAsync($PSolution);                          

                                if (($null -eq $promoteRequestId) -or ($promoteRequestId -eq [Guid]::Empty)) {
                                    Write-PPDOMessage "Unable to promote or delete solution - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                    $anyFailuresInImport = $true;
                                }
                                else { 
                                    Write-Host Async Operation ID: $promoteRequestId 
                                    do {
                                        try {
                                            Start-Sleep -Seconds 10
                                            $operation = Get-CrmRecord -conn $CRMConn -EntityLogicalName asyncoperation -Id ($promoteRequestId) -Fields name, statuscode, friendlymessage, completedon, errorcode
                                            [int]$statuscode = $operation.statuscode_Property.value.Value;
                                                                    
                                            if ($statuscode -le 30) {
                                                Write-Host "Polling Promotion status for Solution: $($PSolution) : $($operation.statuscode)"
                                                $anyFailuresInImport = $false;
                                            }
                                            elseif ($statuscode -eq 31 -or $statuscode -eq 32) {
                                                if ($operation.friendlymessage -match "have an upgrade that is ready to be applied") {
                                                    Write-PPDOMessage "$($operation.friendlymessage)" -Type warning -RunLocally $RunLocally 
                                                    $anyFailuresInImport = $false;   
                                                }
                                                else {
                                                    Write-PPDOMessage "Unable to promote or delete solution - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                                    Write-PPDOMessage "Delete and Promote Failed: #($operation.statuscode)" -Type warning -RunLocally $RunLocally
                                                    Write-PPDOMessage "Error Code: $($operation.errorcode)" -Type warning -RunLocally $RunLocally
                                                    Write-PPDOMessage "$($operation.friendlymessage)" -Type warning -RunLocally $RunLocally
                                                    exit 1
                                                    $anyFailuresInImport = $true;    
                                                }
                                            }
                                        }
                                        catch {
                                            Write-Host "Retrying Polling Upgrade status"
                                            $Retrycount = $Retrycount + 1
                                            if ($Retrycount -gt 3) {
                                                $statuscode = 32;
                                                $anyFailuresInImport = $true;
                                                Write-PPDOMessage "Unable to poll status - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                                Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                                                exit 1
                                                break;
                                            }
                                        }
                                    }until($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32)
                                }
                            }
                        }                        

                        #Get Currently Deployed Solution Version
                        Write-Host "Getting Current Solution Version from Target"
                        $SolutionQuery = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -Fields 'solutionid', 'friendlyname', 'version', 'uniquename' -FilterAttribute uniquename -FilterOperator eq -FilterValue $PSolution
                        $Solution = $SolutionQuery.CrmRecords[0]
                        Write-Host $Solution.uniquename " - " $Solution.version

                        if (!$Solution) { 
                            $deployAsHolding = $false;
                            $deployAsNew = $true;
                            if (!$skipPatch) {
                                Write-Host "Solution not found in Target, Importing as New"  
                            }                          
                            $SolutionVersion = [version]"0.0.0.0"
                        }
                        else {
                            $SolutionVersion = $Solution.version
                            Write-Host "Found: $SolutionVersion in $EnvironmentName"

                            if ($null -ne $Deploy.DeployAsHolding -and !$patchDeploy) {
                                [bool]$deployAsHolding = [System.Convert]::ToBoolean($Deploy.DeployAsHolding)
                            }
                            else {
                                $deployAsHolding = $false
                            }
                        }
                        
                        Write-PPDOMessage "Version to be deployed : $deployingVersion" -Type command -RunLocally $RunLocally
                        Write-Host "skipPatch : $skipPatch"
                        Write-Host "patchDeploy : $patchDeploy"  
                        [version]$depVersion = $deployingVersion
                        [version]$solVersion = $solutionVersion               
                        if ($depVersion -le $solVersion) { $skipDeploy = $true; Write-PPDOMessage "Skipping Deployment as Target has same or newer" -Type warning -RunLocally $RunLocally }
                        if ($depVersion -ne $solVersion -and !$patchDeploy -and !$deployAsNew) { $skipPatch = $true; Write-PPDOMessage "Setting skipPatch to true as parent solutions don't match" -Type warning -RunLocally $RunLocally }
                        if ($skipPatch -and $patchDeploy) {
                            $skipDeploy = $true
                        }

                        ########################## IMPORT
                        if (!$skipDeploy) {

                            # Powerapps Solution Checker
                            if ($Deploy.PowerAppsChecker -eq $true -and $UseClientSecret -eq $true) {
                                Write-PPDOMessage "Running PowerApps Solution Checker for $PSolution" -Type command -RunLocally $RunLocally
                                Start-SolutionChecker -PipelinePath $PipelinePath -SolutionPath $PipelinePath\$SolutionFolder\$fileToPack -SolutionName $PSolution -ClientId $UserName -ClientSecret $Password -TenantId "$($CRMConn.TenantId)"
                            }
                            else {
                                Write-PPDOMessage "Powerapps Checker not configured. Add PowerAppsChecker: True as a property in the DeployTo section for your Solution" -Type warning -RunLocally $RunLocally -LogWarning $true                           
                            }
            
                            # PRE ACTION
                            if ($Deploy.PreAction -eq $true -and !$patchDeploy) {
                                if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\PreAction.ps1) {
                                    Write-PPDOMessage "Execute Pre Action from $PipelinePath\$SolutionFolder\Scripts" -Type section -RunLocally $RunLocally
                                    . $PipelinePath\$SolutionFolder\Scripts\PreAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
                                }
                                else {
                                    Write-PPDOMessage "Deployment PreAction step not registered to excecute" -Type warning -RunLocally $RunLocally
                                }
                            }                        

                            $activatePlugIns = $true;
                            $overwriteUnManagedCustomizations = $false;
                            $skipDependancyOnProductUpdateCheckOnInstall = $true;
                            $isInternalUpgrade = $false;   
                            
                            if ($Deploy.OverwriteUnmanagedCustomisations -eq $true) {
                                $overwriteUnManagedCustomizations = $true
                            }

                            Write-PPDOMessage "Initiating Import and deployment to $($DeployServerUrl)" -Type section -RunLocally $RunLocally
                            Write-PPDOMessage "Import as Holding solution : $($deployAsHolding)" -Type command -RunLocally $RunLocally
                        
                            $importId = [guid]::Empty 
                            $result = $CRMConn.ImportSolutionToCrmAsync("$PipelinePath\$SolutionFolder\$fileToPack", [ref]$importId,
                                $activatePlugIns,
                                $overwriteUnManagedCustomizations, 
                                $skipDependancyOnProductUpdateCheckOnInstall,
                                $deployAsHolding,
                                $isInternalUpgrade)

                            Write-Host Async Operation ID: $result 
                            Write-Host Import Job ID: $importId 
                        
                            $Retrycount = 0;

                            # IMPORT
                            do {

                                try {

                                    Start-Sleep -Seconds 10 

                                    $operation = Get-CrmRecord -conn $CRMConn -EntityLogicalName asyncoperation -Id ($result) -Fields name, statuscode, friendlymessage, completedon, errorcode
                                    [int]$statuscode = $operation.statuscode_Property.value.Value;

                                    if ($statuscode -le 30) {
                                        $job = Get-CrmRecord -conn $CRMConn -EntityLogicalName importjob -Id ($importId) -Fields progress 
                                        Write-Host "Polling Import for Solution: $($PSolution) : $($operation.statuscode) - $($job.progress)%"
                                        $anyFailuresInImport = $false;
                                    }
                                    elseif ($statuscode -eq 31 -or $statuscode -eq 32) {
                                        Write-PPDOMessage "Unable to import solution - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                        Write-PPDOMessage "Import Failed: $($operation.statuscode)" -Type warning -RunLocally $RunLocally
                                        Write-PPDOMessage "Error Code: $($operation.errorcode)" -Type warning -RunLocally $RunLocally
                                        Write-PPDOMessage "$($operation.friendlymessage)" -Type warning -RunLocally $RunLocally
                                        if ($operation.friendlymessage -match "forcibly closed") {
                                            $anyFailuresInImport = $false;  
                                        }
                                        else {
                                            exit 1
                                            $anyFailuresInImport = $true;    
                                        }
                                    }
                                }
                                catch {
                                    Write-Host "Retrying Polling import status"
                                    $Retrycount = $Retrycount + 1
                                    if ($Retrycount -gt 3) {
                                        $statuscode = 32;
                                        $anyFailuresInImport = $true;
                                        Write-PPDOMessage "Unable to poll status - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                        Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                                        exit 1
                                        break;
                                    }
                                }
                            } until ($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32)

                            $Retrycount = 0;

                                # UPGRADE
                                if ($deployAsHolding -eq $true -and $anyFailuresInImport -eq $false) {
                            
                                    # PRE UPGRADE
                                    if ($Deploy.PreUpgrade -eq $true -and !$patchDeploy) {
                                        if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\PreUpgrade.ps1) {
                                            Write-PPDOMessage "Execute Pre Upgrade from $PipelinePath\$SolutionFolder\Scripts" -Type section -RunLocally $RunLocally
                                            . $PipelinePath\$SolutionFolder\Scripts\PreUpgrade.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
                                        }
                                        else {
                                            Write-Host "Deployment PreUpgrade step not registered to excecute"
                                        }
                                    }


                                    Write-Host "Applying Upgrade to Solution"
                                    $promoteRequestId = $CRMConn.DeleteAndPromoteSolutionAsync($PSolution);                          

                                    if (($null -eq $promoteRequestId) -or ($promoteRequestId -eq [Guid]::Empty)) {
                                        Write-PPDOMessage "Unable to promote or delete solution - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                        $anyFailuresInImport = $true;
                                    }
                                    else { 
                                        Write-Host Async Operation ID: $promoteRequestId 
                                        do {
                                            try {
                                                Start-Sleep -Seconds 10
                                                $operation = Get-CrmRecord -conn $CRMConn -EntityLogicalName asyncoperation -Id ($promoteRequestId) -Fields name, statuscode, friendlymessage, completedon, errorcode
                                                [int]$statuscode = $operation.statuscode_Property.value.Value;
                                                                        
                                                if ($statuscode -le 30) {
                                                    Write-Host "Polling Promotion status for Solution: $($PSolution) : $($operation.statuscode)"
                                                    $anyFailuresInImport = $false;
                                                }
                                                elseif ($statuscode -eq 31 -or $statuscode -eq 32) {
                                                    if ($operation.friendlymessage -match "have an upgrade that is ready to be applied") {
                                                        Write-PPDOMessage "$($operation.friendlymessage)" -Type warning -RunLocally $RunLocally
                                                        $anyFailuresInImport = $false;
                                                    }
                                                    else {
                                                        Write-PPDOMessage "Unable to promote or delete solution - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                                        Write-PPDOMessage "Delete and Promote Failed: #($operation.statuscode)" -Type warning -RunLocally $RunLocally
                                                        Write-PPDOMessage "Error Code: $($operation.errorcode)" -Type warning -RunLocally $RunLocally
                                                        Write-PPDOMessage "$($operation.friendlymessage)" -Type warning -RunLocally $RunLocally
                                                        if ($operation.friendlymessage -match "forcibly closed") {
                                                            $anyFailuresInImport = $false;  
                                                        }
                                                        else {
                                                            exit 1
                                                            $anyFailuresInImport = $true;    
                                                        }
                                                    }
                                                }
                                            }
                                            catch {
                                                Write-Host "Retrying Polling Upgrade status"
                                                $Retrycount = $Retrycount + 1
                                                if ($Retrycount -gt 3) {
                                                    $statuscode = 32;
                                                    $anyFailuresInImport = $true;
                                                    Write-PPDOMessage "Unable to poll status - please check Solution import history in https://make.powerapps.com/environments" -Type error -RunLocally $RunLocally -LogError $true
                                                    Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                                                    exit 1
                                                    break;
                                                }
                                            }
                                        }until($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32)
                                    }

                                    # Post UPGRADE
                                    if ($Deploy.PostUpgrade -eq $true -and !$patchDeploy) {
                                        if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\PostUpgrade.ps1) {
                                            Write-PPDOMessage "Execute Post Upgrade from $PipelinePath\$SolutionFolder\Scripts" -Type section -RunLocally $RunLocally
                                            . $PipelinePath\$SolutionFolder\Scripts\PostUpgrade.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
                                        }
                                        else {
                                            Write-Host "Deployment PostUpgrade step not registered to excecute"
                                        }
                                    }
                                }






                                

                                if (!$patchDeploy) {

                                    # POST ACTION
                                    if ($Deploy.PostAction -eq $true -and $anyFailuresInImport -eq $false -and !$patchDeploy) {
                                        if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\PostAction.ps1) {
                                            Write-PPDOMessage "Execute Post Action from $PipelinePath\$SolutionFolder\Scripts" -Type section -RunLocally $RunLocally
                                            . $PipelinePath\$SolutionFolder\Scripts\PostAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
                                        }
                                        else {
                                            Write-PPDOMessage "Deployment PostAction step not registered to excecute" -Type warning -RunLocally $RunLocally
                                        }
                                    }
                                }




                                #region 'Portal CLI'
                                if ($null -ne $package.Portal -and $anyFailuresInImport -eq $false) { 

                                    try{
                                        Connect-Cli -DeployServerUrl $DeployServerUrl -UserName $UserName -Password $Password -TenantId $CRMConn.TenantId -UseClientSecret $UseClientSecret -EnvironmentName $EnvironmentName
                                        
                                        Write-PPDOMessage "Deploying Portal configuration via PAPORTAL CLI"
                                        Write-PPDOMessage "Deployment Profile - $($Deploy.EnvironmentName)"
                                        Write-PPDOMessage "Locating Portal Source $($package.Portal) from $PipelinePath\$SolutionFolder\Deployment\"

                                        & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe paportal upload --path "$PipelinePath\$SolutionFolder\Deployment\$($package.Portal)" --deploymentProfile "$($Deploy.EnvironmentName)"
                                    }catch {
                                        Write-PPDOMessage "Powerapps PAC PAPORTAL CLI" -Type error -RunLocally $RunLocally -LogError $true
                                        Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                                    }
                            
                                }
                            #endregion



                                #region 'Reference Data'
                                # DATA CONFIGURATION
                                if ($Deploy.DeployData -eq $true -and $anyFailuresInImport -eq $false) {


                                    Connect-Cli -DeployServerUrl $DeployServerUrl -UserName $UserName -Password $Password -TenantId $CRMConn.TenantId -UseClientSecret $UseClientSecret -EnvironmentName $EnvironmentName

                                    Write-PPDOMessage "Importing reference Data ..." -Type group -RunLocally $RunLocally
                                    try {
                                        if (Test-Path -Path $PipelinePath\$SolutionFolder\ReferenceData\data.zip) {
                                            Write-PPDOMessage "Config data.zip found, importing now."
                                            Import-CrmDataFile -CrmConnection $CRMConn -DataFile $PipelinePath\$SolutionFolder\ReferenceData\data.zip -ConcurrentThreads 5 -Verbose                    

                                            #CLI
                                            #& $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe data import -d "$PipelinePath\$SolutionFolder\ReferenceData\data.zip" -env "$($Deploy.EnvironmentName)"

                                        }
                                        else {
                                            Write-Host "Config Data file does not Exist"
                                        }
                                    }
                                    catch {
                                        Write-PPDOMessage "Unable to import configuration data - please review Pipeline error logs" -Type error -RunLocally $RunLocally -LogError $true
                                        Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                                        exit 1           
                                    }
                                }
                                else {
                                    Write-PPDOMessage "No Data to Import for $PSolution" -Type section -RunLocally $RunLocally
                                }
                                Write-PPDOMessage -Type endgroup -RunLocally $RunLocally
                                #endregion




                                

                                        
                            $ProgressPreference = "Continue"
                            [int]$elapsedTime = $stopwatch.Elapsed.TotalMinutes      
                            $stopwatch.Stop()
                            Write-PPDOMessage "Import Complete in $($elapsedTime) minutes" -Type section -RunLocally $RunLocally
                        }
                    }
                    catch {
                        Write-PPDOMessage "Skipping $PSolution due to Solution import error" -Type section -RunLocally $RunLocally
                        Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                    }
                }
                if (!$skipDeploy -or $Deploy.Flows.AlwaysTryActivate -eq $true) {
                    $FlowsToRetry = @()
                    Write-Host "Establishing Connection References and Activating Flows" -ForegroundColor Green
                    $ProgressPreference = "SilentlyContinue"
                    # Activate Flows and Establish Connection References
                    Write-Host "Getting Environment Id"
                    $orgs = Get-CrmRecords -conn $CRMConn -EntityLogicalName organization
                    if ($orgs.Count -gt 0) {
                        $orgId = $orgs.CrmRecords[0].organizationid

                        $Environment = Get-AdminPowerAppEnvironment | Where-Object OrganizationId -eq $orgId.Guid
                        $EnvId = $Environment.EnvironmentName
                        Write-Host "Environment Id - $EnvId"
                        Write-Host "Checking if there are Connections References in the Solution that Need to be Wired Up"
                        $solutions = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute "uniquename" -FilterOperator "eq" -FilterValue "$($package.SolutionName)"
                        $solutionId = $solutions.CrmRecords[0].solutionid
                        $connRefs = (Get-CrmRecords -conn $CRMConn -EntityLogicalName connectionreference -FilterAttribute "solutionid" -FilterOperator eq -FilterValue $solutionid -Fields connectionreferencelogicalname, connectionid, connectorid, connectionreferenceid).CrmRecords
                        $connRefs |  ForEach-Object {
                            #Where-Object { $null -eq $_.connectionid } |
                            $connectionType = $_.connectorid.Replace("/providers/Microsoft.PowerApps/apis/", "")
                            Write-Host "Found Connection Reference $($_.connectionreferencelogicalname) "
                            # without a Connection to $connectionType"
                        
                            Write-Host "Getting Connections in Environment"
                            $connection = Get-AdminPowerAppConnection -EnvironmentName $EnvId | Select-Object -ExpandProperty Statuses -Property ConnectionName, DisplayName, ConnectorName, CreatedBy, CreatedTime | Where-Object {($_.status -eq "Connected") -and ($_.ConnectorName -eq $connectionType)} | Sort-Object -Property CreatedTime
                            #| Where-Object ConnectorName -eq $connectionType
                            if ($connection) {
                                # Get Dataverse systemuserid for the system user that maps to the aad user guid that created the connection
                                $systemusers = Get-CrmRecords -conn $CRMConn -EntityLogicalName systemuser -FilterAttribute "azureactivedirectoryobjectid" -FilterOperator "eq" -FilterValue $connection[0].CreatedBy.id -Fields domainname
                                if ($systemusers.Count -gt 0) {
                                    Write-Host "Impersonating the Owner of the Connection - $($systemusers.CrmRecords[0].domainname)"
                                    # Impersonate the Dataverse systemuser that created the connection when updating the connection reference
                                    $impersonationCallerId = $systemusers.CrmRecords[0].systemuserid
                                    $impersonationConn = $CRMConn
                                    $impersonationConn.OrganizationWebProxyClient.CallerId = $impersonationCallerId 
                                    Write-PPDOMessage "Setting Connection Reference to use $($connection[0].DisplayName)" -Type command -RunLocally $RunLocally
                                    Set-CrmRecord -conn $impersonationConn -EntityLogicalName $_.logicalname -Id $_.connectionreferenceid -Fields @{"connectionid" = $connection[0].ConnectionName }                                    
                                }
                            }
                            else {
                                Write-PPDOMessage "No Connection has been set up of type $connectionType, some of your Flows may not Activate succesfully" -Type warning -RunLocally $RunLocally -LogWarning $true
                            }
                        
                        }

                        Write-Host "Checking if there are Flows that need to be Activated"
                        if ($Deploy.Flows.ActivateFlows -eq $true) {
                            if ($Deploy.Flows.OverrideFile) {
                                Write-Host "Using $($Deploy.Flows.OverrideFile) for Flow Activation"
                                try {
                                    $FlowsToActivate = Get-Content -Path $PipelinePath\$SolutionFolder\$($Deploy.Flows.OverrideFile) -ErrorAction SilentlyContinue | ConvertFrom-Json    
                                }
                                catch {
                                    
                                }
                                
                            }
                            else {
                                Write-Host "Using Flows_Default.json for Flow Activation"
                                try {
                                    $FlowsToActivate = Get-Content -Path $PipelinePath\$SolutionFolder\Flows_Default.json -ErrorAction SilentlyContinue | ConvertFrom-Json 
                                }
                                catch {
                                    $FlowsToActivate = $null
                                }                                
                            }
                            Write-Host "There are $($FlowsToActivate.Count) Flows that need activating"
                            $ErrorCount = 0
                            if ($FlowsToActivate.Count -gt 0) {
                                $FlowsToActivate | ForEach-Object {
                                    $FlowStore = $_
                                    $workflow = Get-CrmRecord -conn $CRMConn -EntityLogicalName workflow -Id $_.FlowId -Fields clientdata, category, statecode, name
                                    if ($_.ActivateAsUser) {
                                        Write-Host "ActivateAsUser defined and set to : $($_.ActivateAsUser), attempting to Active Flow as this user"
                                        $systemuserResult = Get-CrmRecords -conn $CRMConn -EntityLogicalName systemuser -FilterAttribute "domainname" -FilterOperator "eq" -FilterValue $_.ActivateAsUser
                                        if ($systemuserResult.Count -gt 0) {
                                            $systemUserId = $systemuserResult.CrmRecords[0].systemuserid
                                            #Activate the workflow using the owner.
                                            if ($workflow.statecode -ne "Activated") {
                                                $impersonationConn = $CRMConn
                                                $impersonationCallerId = $systemUserId
                                                $impersonationConn.OrganizationWebProxyClient.CallerId = $impersonationCallerId 
                                                Write-PPDOMessage "Enabling Flow '$($workflow.name)'" -Type command -RunLocally $RunLocally
                                                try {
                                                    Set-CrmRecordState -conn $impersonationConn -EntityLogicalName workflow -Id $_.FlowId -StateCode Activated -StatusCode Activated    
                                                }
                                                catch {
                                                    Write-PPDOMessage "There was an error activating the Flow, please confirm that the user exists and that the appropriate connections have been created as this user in the Environment" -Type warning -RunLocally $RunLocally -LogWarning $true
                                                    Write-Host $_
                                                    if ($_.ToString().Contains("ChildFlowNeverPublished")) {
                                                        $FlowsToRetry += $FlowStore
                                                    }
                                                    else {
                                                        $ErrorCount++    
                                                    }
                                                    
                                                }
                                            
                                            }    
                                        }
                                        Write-PPDOMessage "User $($_.ActivateAsUser) was not found in $($Deploy.EnvironmentName)" -Type warning -RunLocally $RunLocally -LogWarning $true
                                    }
                                    else {
                                        Write-Host "Checking if '$($workflow.name)' needs Activating..."
                                        $solutions = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute "uniquename" -FilterOperator "eq" -FilterValue "$($package.SolutionName)"
                                        $solutionId = $solutions.CrmRecords[0].solutionid
                                        $connRefs = (Get-CrmRecords -conn $CRMConn -EntityLogicalName connectionreference -FilterAttribute "solutionid" -FilterOperator eq -FilterValue $solutionid -Fields connectionreferencelogicalname, connectionid, connectorid, connectionreferenceid).CrmRecords
                                        $connRefToUse = $connRefs | Where-Object { $null -ne $_.connectionid } | Select-Object -First 1 -ErrorAction SilentlyContinue
                                        if ($null -ne $connRefToUse) {
                                            Write-Host "---- Connection Ref Details ----"
                                            Write-Host "Connectiod ID : " $connRefToUse.ConnectionId 
                                            Write-Host $connRefToUse
                                            Write-Host "--------------------------------"
                                            $connection = Get-AdminPowerAppConnection -EnvironmentName $EnvId -Filter $connRefToUse.ConnectionId    
                                        } 
                                        else {
                                            Write-PPDOMessage "There was an error getting a Connection to use, please confirm that the user exists and that the appropriate connections have been created as this user in the Environment" -Type warning -RunLocally $RunLocally -LogWarning $true
                                            $ErrorCount++
                                        }                                           
                                        # Get Dataverse systemuserid for the system user that maps to the aad user guid that created the connection
                                        if ($null -ne $connection) {
                                            Write-Host "---- Connection Details -----"
                                            Write-Host "Connection UserID : " $connection[0].CreatedBy.id
                                            Write-Host $connection[0]
                                            Write-Host "-----------------------------"
                                            $systemusers = Get-CrmRecords -conn $CRMConn -EntityLogicalName systemuser -FilterAttribute "azureactivedirectoryobjectid" -FilterOperator "eq" -FilterValue $connection[0].CreatedBy.id
                                        }                
                                        else {
                                            Write-PPDOMessage "There was an error getting the owner of the Connection to use, please confirm that the user exists and that the appropriate connections have been created as this user in the Environment" -Type warning -RunLocally $RunLocally -LogWarning $true
                                            $ErrorCount++
                                        }                            
                                        if ($systemusers.Count -gt 0) {
                                            # Impersonate the Dataverse systemuser that created the connection when updating the connection reference
                                            $impersonationCallerId = $systemusers.CrmRecords[0].systemuserid
                                            if ($workflow.statecode -ne "Activated") {
                                                Write-PPDOMessage "Enabling Flow '$($workflow.name)' as Owner of Connection Reference" -Type command -RunLocally $RunLocally
                                                $impersonationConn = $CRMConn
                                                $impersonationConn.OrganizationWebProxyClient.CallerId = $impersonationCallerId 
                                                try {
                                                    Set-CrmRecordState -conn $impersonationConn -EntityLogicalName workflow -Id $_.FlowId -StateCode Activated -StatusCode Activated    
                                                    Write-Host "...Activated" -ForegroundColor Green
                                                }
                                                catch {
                                                    Write-PPDOMessage "There was an error activating the Flow, please confirm that the user exists and that the appropriate connections have been created as this user in the Environment" -Type warning -RunLocally $RunLocally -LogWarning $true
                                                    Write-Host $_
                                                    if ($_.ToString().Contains("ChildFlowNeverPublished")) {
                                                        $FlowsToRetry += $FlowStore
                                                    }
                                                    else {
                                                        $ErrorCount++    
                                                    }
        
        
                                                }
                                                    
                                            }
        
                                        }                                          
                                    }
                                }  
                            }
                            
                            $FlowsToRetry | ForEach-Object {
                                Write-Host
                                Write-Host "Retrying Flows that failed due to Child Flows" -ForegroundColor Green
                                $workflow = Get-CrmRecord -conn $CRMConn -EntityLogicalName workflow -Id $_.FlowId -Fields clientdata, category, statecode, name
                                if ($_.ActivateAsUser) {
                                    Write-Host "ActivateAsUser defined and set to : $($_.ActivateAsUser), attempting to Active Flow as this user"
                                    $systemuserResult = Get-CrmRecords -conn $CRMConn -EntityLogicalName systemuser -FilterAttribute "domainname" -FilterOperator "eq" -FilterValue $_.ActivateAsUser
                                    if ($systemuserResult.Count -gt 0) {
                                        $systemUserId = $systemuserResult.CrmRecords[0].systemuserid
                                        #Activate the workflow using the owner.
                                        if ($workflow.statecode -ne "Activated") {
                                            $impersonationConn = $CRMConn
                                            $impersonationCallerId = $systemUserId
                                            $impersonationConn.OrganizationWebProxyClient.CallerId = $impersonationCallerId 
                                            Write-PPDOMessage "Enabling Flow '$($workflow.name)'" -Type command -RunLocally $RunLocally
                                            try {
                                                Set-CrmRecordState -conn $impersonationConn -EntityLogicalName workflow -Id $_.FlowId -StateCode Activated -StatusCode Activated    
                                            }
                                            catch {
                                                Write-PPDOMessage "There was an error activating the Flow, please confirm that the user exists and that the appropriate connections have been created as this user in the Environment" -Type warning -RunLocally $RunLocally -LogWarning $true
                                                Write-Host $_
                                                $ErrorCount++    
                                            
                                                
                                            }
                                        
                                        }    
                                    }
                                    Write-PPDOMessage "User $($_.ActivateAsUser) was not found in $($Deploy.EnvironmentName)" -Type warning -RunLocally $RunLocally -LogWarning $true
                                }
                                else {
                                    Write-Host "Checking if '$($workflow.name)' needs Activating..."
                                    
                                    $solutions = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute "uniquename" -FilterOperator "eq" -FilterValue "$($package.SolutionName)"
                                    $solutionId = $solutions.CrmRecords[0].solutionid
                                    $connRefs = (Get-CrmRecords -conn $CRMConn -EntityLogicalName connectionreference -FilterAttribute "solutionid" -FilterOperator eq -FilterValue $solutionid -Fields connectionreferencelogicalname, connectionid, connectorid, connectionreferenceid).CrmRecords
                                    $connRefToUse = $connRefs | Where-Object { $null -ne $_.connectionid } | Select-Object -First 1 -ErrorAction SilentlyContinue
                                    $connection = Get-AdminPowerAppConnection -EnvironmentName $EnvId -Filter $connRefToUse.ConnectionId
                                    # Get Dataverse systemuserid for the system user that maps to the aad user guid that created the connection
                                    $systemusers = Get-CrmRecords -conn $CRMConn -EntityLogicalName systemuser -FilterAttribute "azureactivedirectoryobjectid" -FilterOperator "eq" -FilterValue $connection[0].CreatedBy.id
                                    if ($systemusers.Count -gt 0) {
                                        # Impersonate the Dataverse systemuser that created the connection when updating the connection reference
                                        $impersonationCallerId = $systemusers.CrmRecords[0].systemuserid
                                        if ($workflow.statecode -ne "Activated") {
                                            Write-PPDOMessage "Enabling Flow '$($workflow.name)' as Owner of Connection Reference" -Type command -RunLocally $RunLocally
                                            $impersonationConn = $CRMConn
                                            $impersonationConn.OrganizationWebProxyClient.CallerId = $impersonationCallerId 
                                            try {
                                                Set-CrmRecordState -conn $impersonationConn -EntityLogicalName workflow -Id $_.FlowId -StateCode Activated -StatusCode Activated    
                                            }
                                            catch {
                                                Write-PPDOMessage "There was an error activating the Flow, please confirm that the user exists and that the appropriate connections have been created as this user in the Environment" -Type warning -RunLocally $RunLocally -LogWarning $true
                                                Write-Host $_
                                                $ErrorCount++    
                                            }


                                    
                                            
                                        }

                                    }
                                    
                                }
                            }
                            if ($Deploy.Flows.FailonError -eq $true -and $ErrorCount -gt 0) {
                                Write-PPDOMessage "There were $ErrorCount Flow activation errors and FailonError is set to True... exiting." -Type error -RunLocally $RunLocally -LogError $true
                                exit 1                     
                            }

                        }
                                
                        else {
                            Write-Host @"
No Flows were specified for activation. If you wish to include flows for activation, please add the following in deployPackages.json
 
"Flows":
{
    "ActivateFlows": "true",
    "OverrideFile" : "",
    "FailonError" : "false"
}
                         
"@

                        }
                    }
                }
                Write-PPDOMessage -Type endgroup -RunLocally $RunLocally
                #endregion
            }
            else {
                Write-PPDOMessage "$($package.SolutionName) is not configured for deployment to $env:ENVIRONMENT_NAME in deployPackages.json" -Type warning -RunLocally $RunLocally -LogWarning $true
            }
        
        }


        #EXECUTE ENVIRONMENT POST ACTION
        if ($null -ne $EnvConfig -and $EnvConfig.PostAction -eq $true) {
            Write-PPDOMessage "Execute Environment Post Action" -Type section -RunLocally $RunLocally
            . "$PipelinePath\Common\Environments\Scripts\PostAction.ps1" -Conn $CRMConn -PipelinePath $PipelinePath -EnvironmentName $EnvConfig.EnvironmentName -EnvironmentUrl $DeployServerUrl
            $EnvConfig.PostFunctions | ForEach-Object {
                & $_ -Conn $CRMConn
            }
            Write-PPDOMessage "Environment Post Action Complete" -Type command -RunLocally $RunLocally
        }
        else {
            Write-PPDOMessage "Environment PostAction step not registered to excecute" -Type warning -RunLocally $RunLocally
        }
    }
    Write-Host Environment $EnvironmentName
    Import-Package
}