Scripts/SolutionDeploy.ps1

# SolutionDeploy.ps1

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 
    )


    $Env:PAC_CLI_SPN_SECRET = $Password

    if ($UseClientSecret) {
        Write-Host "Using Service Principal"
        Write-Host "Connecting to PAC CLI"

        $pdel = & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth delete --name ppdo
        # Escaped quotes required to get secrets working that started with -
        & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth create --applicationId $UserName --clientSecret `"$Password`" --environment $DeployServerUrl --tenant $($CRMConn.TenantId) --name ppdo
        # Select ppdo connection to use (user may have more than one pac auth saved)
        & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth select --name ppdo

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

        Write-Host "Authenticate Azure CLI"
        $checkBroker = az config get | ConvertFrom-Json
        if ($global:devops_FullTool) {
            if (($checkBroker.core.name.IndexOf("allow_broker") -ge 0) -and ($checkBroker.core[$checkBroker.core.name.IndexOf("allow_broker")].value)) {
                az config set core.allow_broker=false
                az login --service-principal -u $UserName -p="$Password" --tenant $CRMConn.TenantId --allow-no-subscriptions
                az config set core.allow_broker=false
            }
        }
        else {
            az login --service-principal -u $UserName -p="$Password" --tenant $CRMConn.TenantId --allow-no-subscriptions
        }
    }
    else {
        Write-Host "Using named account"
        Write-Host "Connecting to PAC CLI"

        if ([string]::IsNullOrEmpty($Password)) {
            New-PACAuth
            Write-Host "Setting Add-PowerAppsAccount"
            Add-PowerAppsAccount -Username $UserName
        }
        else {
            $ssPassword = ConvertTo-SecureString $Password -AsPlainText -Force
            New-PACAuth
            Write-Host "Setting Add-PowerAppsAccount"
            Add-PowerAppsAccount -Username $UserName -Password $ssPassword
        }
    }        

    & $env:APPDATA\Capgemini.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,
        [bool] [Parameter(Mandatory = $false)] $VerboseLogging = $false
    )

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

    Write-Host "Using Capgemini.PowerPlatform.DevOps version:" (Get-Module -Name Capgemini.PowerPlatform.DevOps -ListAvailable)[0].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" | 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
        if ($VerboseLogging) {
            $CRMConn = Get-CrmConnection -ConnectionString $CrmConnectionString -Verbose
        }
        else {
            $CRMConn = Get-CrmConnection -ConnectionString $CrmConnectionString
        }

        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 {
                Write-PPDOMessage "Deploy Block - False"
            }
          
            if ($deployStepCheck -ne $true) {
                Write-PPDOMessage "$($package.SolutionName) is not configured for deployment to $EnvironmentName in deployPackages.json" -Type warning -RunLocally $RunLocally -LogWarning $true
                continue
            }
            
            $PSolution = $package.SolutionName             
            $SolutionFolder = $package.SolutionName
                
            $deployFromZip = $false;
            if ($null -ne $Deploy -and $null -ne $Deploy.DeployFromZip) {
                [bool]$deployFromZip = [System.Convert]::ToBoolean($Deploy.DeployFromZip)
                Write-PPDOMessage "Using DeployFromZip Flag value - $($Deploy.DeployFromZip)"
            }
                
            $deployPatchFromZip = $false;
            if ($null -ne $Deploy -and $null -ne $Deploy.DeployPatchFromZip) {
                [bool]$deployPatchFromZip = [System.Convert]::ToBoolean($Deploy.DeployPatchFromZip)
                Write-PPDOMessage "Using DeployPatchFromZip Flag value - $($Deploy.DeployPatchFromZip)"
            }

            $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 {
                $solutionsToDeployJson = Get-Content -Path $PipelinePath\$SolutionFolder\$versionFile | ConvertFrom-Json
                # Sort by version
                $solutionsToDeploy = $solutionsToDeployJson | Sort-Object { [version]$_.Version }
            }
            catch {
                # Legacy Solution Packaging Support
                $solutionVersion = Get-Content -Path $PipelinePath\$SolutionFolder\$versionFile
                $solutionsToDeploy = @([ordered]@{SolutionName = $package.SolutionName; Version = $solutionVersion ; })
            }
            $allSolutionsSuccessfullyImported = $true
            $anyPatchOrSolutionSuccessfullyImported = $false
            $patchDeploy = $false
                
            $solutionsToDeploy | ForEach-Object {
                $stageForUpgrade = $false
                $anyFailuresInImport = $false
                $patchDeploy = $false  
                      
                $PSolution = $_.SolutionName
                $deployingVersion = $_.Version
                Write-Host "Starting deployment for solution $PSolution, version $deployingVersion"
                    
                if ($PSolution.contains("_Patch")) {
                    $patchDeploy = $true
                    $packageFolder = "Patches\$PSolution"
                }
                else {
                    $packageFolder = "src"
                }

                #region Preparing Deployment
                try {
                    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    $error.Clear()

                    # Get Currently Deployed Solution Version
                    Write-Host "Getting current version of $($Solution.uniquename) in $EnvironmentName"
                    $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
                        $stageForUpgrade = $false
                        Write-Host "Solution not found in $EnvironmentName, 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
                        }
                        if ($null -ne $Deploy.StageForUpgrade -and !$patchDeploy) {
                            [bool]$StageForUpgrade = [System.Convert]::ToBoolean($Deploy.StageForUpgrade)
                        }
                        else {
                            $StageForUpgrade = $false
                        }
                    }
                         
                    Write-PPDOMessage "Version to be deployed: $deployingVersion" -Type command -RunLocally $RunLocally
                    Write-Host "Deploying as Patch: $patchDeploy"  
                    [version]$depVersion = $deployingVersion
                    [version]$solVersion = $solutionVersion               
                    if ($depVersion -le $solVersion) {
                        Write-PPDOMessage "Skipping Deployment as $EnvironmentName has same or newer" -Type warning -RunLocally $RunLocally
                        continue
                    }

                    Write-PPDOMessage "Deploying $PSolution as $($Deploy.DeploymentType) to - $EnvironmentName" -Type section -RunLocally $RunLocally
                            
                    ########################### PACK PRE ACTION
                    if ($Deploy.PackPreAction -eq $true -and !$patchDeploy) {
                        if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\PackPreAction.ps1) {
                            Write-PPDOMessage "Execute Pack Pre Action from $PipelinePath\$SolutionFolder\Scripts" -Type section -RunLocally $RunLocally
                            . $PipelinePath\$SolutionFolder\Scripts\PackPreAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
                        }
                        else {
                            Write-PPDOMessage "Deployment PackPreAction step not registered to execute" -Type warning -RunLocally $RunLocally
                        }
                    }

                    Write-PPDOMessage "Preparing $PSolution Solution as $($Deploy.DeploymentType)" -Type section -RunLocally $RunLocally
                        
                    if (($patchDeploy -and ($deployPatchFromZip -eq $true)) -or (!$patchDeploy -and ($deployFromZip -eq $true))) {
                        if ($Deploy.DeploymentType.ToLower() -eq "unmanaged") {
                            $solutionZipFile = "Deploy\$PSolution.zip"
                        }
                        else {
                            $solutionZipFile = "Deploy\$($PSolution)_managed.zip"
                        }
                    }
                    else {
                        $solutionZipFile = "$($Deploy.EnvironmentName)_$($PSolution)_$($Deploy.DeploymentType).zip"
                        Invoke-PackSolution -anyFailuresInImport ([ref]$anyFailuresInImport) -CRMConn $CRMConn -fileToPack $solutionZipFile `
                            -SolutionFolder $SolutionFolder -SolutionName $PSolution -packageFolder $packageFolder `
                            -DeploymentType $Deploy.DeploymentType -RunLocally $RunLocally
                        if (!$patchDeploy) {
                            $upgradeSolutionName = "$($package.SolutionName)_Upgrade"
                            Invoke-PreUpgrade -anyFailuresInImport ([ref]$anyFailuresInImport) -CRMConn $CRMConn -upgradeSolutionName $upgradeSolutionName `
                                -solutionName $PSolution -RunLocally $RunLocally
                        }   
                    } 
                        
                    $solutionZipFilePath = "$PipelinePath\$SolutionFolder\$solutionZipFile"
                    Write-Host "Deploying zip file from: $solutionZipFilePath"

                    if (!$deployAsHolding -and !$patchDeploy) {
                        Write-PPDOMessage "Checking to make sure there is no existing $($package.SolutionName)_Patch solution" -Type command -RunLocally $RunLocally
                        $patchSolution = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute uniquename -FilterOperator like -FilterValue "$($package.SolutionName)_Patch%" -Fields uniquename
                        if ($patchSolution.CrmRecords.Count -gt 0) {
                            Write-PPDOMessage "Setting DeployAsHolding to True as there is a patch in $EnvironmentName" -Type command -RunLocally $RunLocally
                            $deployAsHolding = $true
                        }
                    }

                    # Maybe replace this with Microsoft's Invoke-PPDOPowerAppsChecker?
                    Invoke-PPDOPowerAppsChecker $solutionZipFilePath $RunLocally
                        
                    #region Solution Pre Action
                    Write-Host "Check if should do preaction"
                    Write-Host "Flags: Deploy.PreAction $($Deploy.PreAction), Deploy.PatchPreAction $($Deploy.PatchPreAction), patchDeploy $patchDeploy"
                    
                    if (($Deploy.PreAction -eq $true -and !$patchDeploy) -or ($Deploy.PatchPreAction -eq $true -and $patchDeploy)) {

                        if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\PreAction.ps1) {
                            
                            if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\Environments.json) {

                                $SolutionEnvironments = Get-Content "$PipelinePath\$SolutionFolder\Scripts\Environments.json" | ConvertFrom-Json
                                $SolnEnvConfig = $SolutionEnvironments | Where-Object { $_.EnvironmentName -eq $EnvironmentName }
                                        
                                if ($null -ne $SolnEnvConfig) {
    
                                    Write-Host "Execute Specified Pre Action functions from $PipelinePath\$SolutionFolder\Scripts"
                                    . "$PipelinePath\$SolutionFolder\Scripts\PreAction.ps1" -Conn $CRMConn -Path "$PipelinePath\$SolutionFolder\" -EnvironmentName $Deploy.EnvironmentName
                                    $SolnEnvConfig.PreFunctions | ForEach-Object {
                                        & $_ -Conn $CRMConn
                                    }
                                    Write-PPDOMessage "Solution Environment Pre Action Complete" -Type command -RunLocally $RunLocally
                                }
                                else {
                                    Write-PPDOMessage "Solution Environment Pre Action not registered to execute" -Type warning -RunLocally $RunLocally
                                } 
                            }
                            else {
                                Write-Host "Execute Pre Action from $PipelinePath\$SolutionFolder\Scripts"
                                . $PipelinePath\$SolutionFolder\Scripts\PreAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
                            }
                        }
                        else {
                            Write-PPDOMessage "Deployment PreAction step not registered to execute" -Type warning -RunLocally $RunLocally
                        }
                    }                        
                    #endregion

                    #region Import
                    $overwriteUnManagedCustomizations = $false;                            
                    if ($Deploy.OverwriteUnmanagedCustomisations -eq $true) {
                        $overwriteUnManagedCustomizations = $true
                    }

                    if ($Deploy.PreUpgrade -eq $true) {
                        $PreUpgrade = $true
                    }
                    else {
                        $PreUpgrade = $false
                    }

                    if ($Deploy.PostUpgrade -eq $true) {
                        $PostUpgrade = $true
                    }
                    else {
                        $PostUpgrade = $false
                    }

                    Invoke-SolutionImport -anyFailuresInImport ([ref]$anyFailuresInImport) -CRMConn $CRMConn -DeployServerUrl $DeployServerUrl `
                        -solutionZipFilePath $solutionZipFilePath -SolutionFolder $SolutionFolder -EnvironmentName $Deploy.EnvironmentName `
                        -deployAsHolding $deployAsHolding -stageForUpgrade $stageForUpgrade -patchDeploy $patchDeploy -PreUpgrade $PreUpgrade `
                        -overwriteUnManagedCustomizations $overwriteUnManagedCustomizations -PostUpgrade $PostUpgrade -RunLocally $RunLocally
                        
                    if ($anyFailuresInImport -eq $true) {
                        $allSolutionsSuccessfullyImported = $false
                    }
                    else {
                        $anyPatchOrSolutionSuccessfullyImported = $true 
                    }
                                       
                    $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
                }
            }
            #endregion
            
            if ($allSolutionsSuccessfullyImported) {
                
                if ($anyPatchOrSolutionSuccessfullyImported -or ($Deploy.AlwaysDeployData -eq $true -and $Deploy.DeployData -eq $true)) {
                    Invoke-ImportReferenceData $CRMConn $PipelinePath $SolutionFolder $RunLocally
                }

                Invoke-SolutionPostAction $CRMConn $PipelinePath $SolutionFolder $patchDeploy $RunLocally
            }

            #region Connection References & Flows
            if ($anyPatchOrSolutionSuccessfullyImported -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
                        if ($Environment -eq $null) {
                            $PermissionsErrorMessage = "To use this function, the service principal needs registered as a Management Application in Azure. You can do so when running PPDO locally."
                            if ($Deploy.Flows.FailonError -eq $true) {
                                Write-PPDOMessage -Message $PermissionsErrorMessage -Type error -RunLocally $RunLocally -LogError $true
                            } 
                            else {
                                Write-PPDOMessage -Message $PermissionsErrorMessage -Type warning -RunLocally $RunLocally -LogWarning $true
                            }
                        }
                        $EnvId = $Environment.EnvironmentName
                        Write-Host "Environment Id - $EnvId"

                    Invoke-FixConnectionReferences $CRMConn "$($package.SolutionName)" $EnvId $RunLocally

                        Execute-FlowActivation $CRMConn $PipelinePath $RunLocally
                    }
                    else {
                        $NoOrganizationsError = "There are no organization records in CRM, unable to activate flows."
                        if ($Deploy.Flows.FailonError -eq $true) {
                            Write-PPDOMessage -Message $NoOrganizationsError -Type error -RunLocally $RunLocally -LogError $true
                        } 
                        else {
                            Write-PPDOMessage -Message $NoOrganizationsError -Type warning -RunLocally $RunLocally -LogWarning $true
                        }
                    }
                }
                #endregion
                
            #region Cleanup
            if ($Deploy.CleanupAction -eq $true -and $Deploy.Deploy -eq $true) {
                if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\CleanupAction.ps1) {
                    Write-PPDOMessage "**************************************************** CLEANUP START"
                    Write-PPDOMessage "Execute Solution CleanupAction from $PipelinePath\$SolutionFolder\Scripts" -Type section -RunLocally $RunLocally
                    . $PipelinePath\$SolutionFolder\Scripts\CleanupAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
                    Write-PPDOMessage "**************************************************** CLEANUP END"
                }
            }
            else {
                Write-PPDOMessage "Deployment CleanupAction for $($Deploy.EnvironmentName) step not registered to execute" -Type warning -RunLocally $RunLocally
            }
            #endregion

            Write-PPDOMessage -Type endgroup -RunLocally $RunLocally
        }

        #region Env Post Action
        Invoke-EnvironmentPostAction $CRMConn $DeployServerUrl $PipelinePath $RunLocally
        #endregion
    }
    Write-Host Environment $EnvironmentName
    try {
        Import-Package
    }
    catch {
        Write-Host "An error occurred in Import-Package:"
        Throw $_
    }
    finally {
        if ($RunLocally) {
            az account clear
            $pdel = & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth delete --name ppdo
            pause
        }
    }    
}

function Invoke-PackSolution {
    Param(
        [ref]$anyFailuresInImport,
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $fileToPack,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [string] [Parameter(Mandatory = $true)] $SolutionName,
        [string] [Parameter(Mandatory = $true)] $packageFolder,
        [string] [Parameter(Mandatory = $true)] $DeploymentType,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )
    
    # Checking for Canvas App
    $canvasApps = Get-ChildItem -Path $PipelinePath\$SolutionName\$packageFolder\CanvasApps\src -Directory -ErrorAction SilentlyContinue 
    # Pack canvas apps
    $canvasApps | ForEach-Object {
        Write-Host "Packing Canvas App $($_.name)";
        & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe canvas pack --sources $_.FullName --msapp "$($_.Parent.FullName)\..\$($_.Name)_DocumentUri.msapp"
        Remove-Item $_.FullName -Recurse -ErrorAction SilentlyContinue
    }
    Write-PPDOMessage "Packing Solution $SolutionName" -Type command -RunLocally $RunLocally

    if ($Deploy.DeploymentType.ToLower() -eq "unmanaged") {
        try {
            Write-Host "Unpacking solution $PSolution";
            & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe solution pack -f $PipelinePath\$SolutionFolder\$packageFolder -z $PipelinePath\$SolutionFolder\$fileToPack -p Unmanaged 3>&1 | Tee-Object -Variable pacOutput
            if ($pacOutput -match "Error:") { Throw $pacOutput -match "Error:" }
        }
        catch {
            $anyFailuresInImport.Value = $true;
            Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
            exit 1
            break;
        }
    }
    else {
        try {
            Write-Host "Unpacking solution $($PSolution)_managed";
            & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe solution pack -f $PipelinePath\$SolutionFolder\$packageFolder -z $PipelinePath\$SolutionFolder\$fileToPack -p Managed --useUnmanagedFileForMissingManaged | Tee-Object -Variable pacOutput
            if ($pacOutput -match "Error:") { Throw $pacOutput -match "Error:" }
        }
        catch {
            $anyFailuresInImport.Value = $true;
            Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
            exit 1
            break;
        }
    }
}

# Invoke-PowerAppsChecker is taken by Microsoft
function Invoke-PPDOPowerAppsChecker {
    Param(
        [string] [Parameter(Mandatory = $true)] $solutionZipFilePath,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )
    
    Write-PPDOMessage "Running PowerApps Solution Checker for $PSolution" -Type command -RunLocally $RunLocally
    try {
        #if no SolutionChecker file then ignore running?
        if (Test-Path -Path $PipelinePath\SolutionChecker.json) {
            $checkSettings = Get-Content $PipelinePath\SolutionChecker.json | ConvertFrom-Json
            $checkSettings.Geo
                                                           
            if ($checkSettings.ExcludedFileNamePattern.Length -gt 0) {
                $filePattern = $checkSettings.ExcludedFileNamePattern -Join ", " 
            }
            else {
                $filePattern = "*json*" #need to find a default file if none exist
            }

            # Need to make all these nested blocks cleaner
            # Check rules collection exist
            if (Test-Path -Path $PipelinePath\SolutionCheckRules.json) {   
                & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe solution check --path $solutionZipFilePath --geo $checkSettings.Geo --excludedFiles $"$filePattern" --ruleLevelOverride $PipelinePath\SolutionCheckRules.json  3>&1 | Tee-Object -Variable pacOutput
            }
            else {
                & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe solution check --path $solutionZipFilePath --geo $checkSettings.Geo --excludedFiles $"$filePattern" 3>&1 | Tee-Object -Variable pacOutput
            }
            if ($pacOutput -match "Error:") { Throw $pacOutput -match "Error:" }
        }
    }
    catch {
        Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
    }
    #need to add support for Rule overide
}

function Invoke-SolutionImport {
    Param(
        [ref]$anyFailuresInImport,
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $DeployServerUrl,
        [string] [Parameter(Mandatory = $true)] $solutionZipFilePath,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [string] [Parameter(Mandatory = $true)] $EnvironmentName,
        [bool] [Parameter(Mandatory = $true)] $deployAsHolding,
        [bool] [Parameter(Mandatory = $true)] $stageForUpgrade,
        [bool] [Parameter(Mandatory = $true)] $overwriteUnManagedCustomizations,
        [bool] [Parameter(Mandatory = $true)] $PreUpgrade,
        [bool] [Parameter(Mandatory = $true)] $PostUpgrade,
        [bool] [Parameter(Mandatory = $true)] $patchDeploy,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )
    
    $activatePlugIns = $true;
    $skipDependencyOnProductUpdateCheckOnInstall = $true;

    Write-PPDOMessage "Initiating Import and deployment to $($DeployServerUrl)" -Type section -RunLocally $RunLocally
    Write-PPDOMessage "Import as Holding solution: $($deployAsHolding)" -Type command -RunLocally $RunLocally
    Write-PPDOMessage "Stage for Upgrade: $($stageForUpgrade)" -Type command -RunLocally $RunLocally
    Write-PPDOMessage "Activate Plugins: $($activatePlugIns)" -Type command -RunLocally $RunLocally
    Write-PPDOMessage "Overwrite Unmanaged Customisations: $($overwriteUnManagedCustomizations)" -Type command -RunLocally $RunLocally
    Write-PPDOMessage "Skip Dependency Checks: $($skipDependencyOnProductUpdateCheckOnInstall)" -Type command -RunLocally $RunLocally
                        
    $retryCount = 0;
    $importSuccess = $false;
    do {
        try {
            $pacCLI = "$env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe"
            [Array]$PACarguments = "solution", "import", "--path", $solutionZipFilePath, "--async", "--max-async-wait-time", "240"
            if ($activatePlugIns) { $PACarguments += "--activate-plugins" }
            if ($overwriteUnManagedCustomizations) { $PACarguments += "--force-overwrite" }
            if ($skipDependencyOnProductUpdateCheckOnInstall) { $PACarguments += "--skip-dependency-check" }
            if ($deployAsHolding -and !$stageForUpgrade) { $PACarguments += "--stage-and-upgrade" }
            if ($stageForUpgrade) { $PACarguments += "--import-as-holding" }
            & $pacCLI $PACarguments 3>&1 | Tee-Object -Variable pacOutput

                                    
            if ($pacOutput -match "Error:") { Throw $pacOutput -match "Error:" }
            if ($pacOutput -match "FAILURE:") { Throw $pacOutput -match "FAILURE:" }
            $importSuccess = $true;
        }
        catch {
            if (($pacOutput -match "Uninstall") -or ($pacOutput -match "PublishAll")) {
                Write-PPDOMessage "Waiting for another Solution Operation to complete" -Type group -RunLocally $RunLocally
                Start-Sleep -Seconds 30   
            }    
            else {
                $retryCount = 50
            }                                                          
            $retryCount = $retryCount + 1
            if ($retryCount -gt 50) {
                $anyFailuresInImport.Value = $true;
                Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                exit 1
                break;
            }
        }
    } until ($importSuccess -eq $true)

    ########################### UPGRADE
    if ($stageForUpgrade -eq $true -and $anyFailuresInImport -eq $false) {
        Invoke-SolutionUpgrade -anyFailuresInImport ([ref]$anyFailuresInImport) -CRMConn $CRMConn -SolutionFolder $SolutionFolder -EnvironmentName $EnvironmentName `
            -PreUpgrade $PreUpgrade -PostUpgrade $PostUpgrade -patchDeploy $patchDeploy -RunLocally $RunLocally
    }
}

function Invoke-PreUpgrade {
    Param(
        [ref]$anyFailuresInImport,
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $upgradeSolutionName,
        [string] [Parameter(Mandatory = $true)] $solutionName,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )
    
    Write-PPDOMessage "Checking to make sure there is no existing $upgradeSolutionName solution" -Type command -RunLocally $RunLocally
    $ugSolution = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute uniquename -FilterOperator like -FilterValue "$upgradeSolutionName" -Fields uniquename
    if ($ugSolution.CrmRecords.Count -gt 0) {
        Write-PPDOMessage "Found holding solution $($ugSolution.CrmRecords[0].uniquename), applying upgrade" -Type warning -RunLocally $RunLocally

        Write-Host "Applying Upgrade to Solution $solutionName"
        $retryCount = 0;
        $upgradeSuccess = $false;
        do {
            try {
                & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe solution upgrade --solution-name $solutionName --async 3>&1 | Tee-Object -Variable pacOutput
                if ($pacOutput -match "Error:") { Throw $pacOutput -match "Error:" }
                if ($pacOutput -match "FAILURE:") { Throw $pacOutput -match "FAILURE:" }
                $upgradeSuccess = $true;
            }
            catch {
                if (($pacOutput -match "Uninstall") -or ($pacOutput -match "PublishAll")) {
                    Write-PPDOMessage "Waiting for another Solution Operation to complete" -Type group -RunLocally $RunLocally
                    Start-Sleep -Seconds 30   
                }    
                else {
                    $retryCount = 50
                }                                                          
                $retryCount = $retryCount + 1
                if ($retryCount -gt 50) {
                    $anyFailuresInImport.Value = $true;
                    Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                    exit 1
                    break;
                }                                 
            }
        } until ($upgradeSuccess -eq $true)
    }
}

function Invoke-SolutionUpgrade {
    Param(
        [ref]$anyFailuresInImport,
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [string] [Parameter(Mandatory = $true)] $EnvironmentName,
        [bool] [Parameter(Mandatory = $true)] $PreUpgrade,
        [bool] [Parameter(Mandatory = $true)] $PostUpgrade,
        [bool] [Parameter(Mandatory = $true)] $patchDeploy,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )
    
    # PRE UPGRADE
    if ($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 $EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
        }
        else {
            Write-Host "Deployment PreUpgrade step not registered to execute"
        }
    }

    Write-Host "Applying Upgrade to Solution $PSolution"
    $retryCount = 0;
    $upgradeSuccess = $false;
    do {
        try {
            & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe solution upgrade --solution-name $PSolution --async --max-async-wait-time 120 3>&1 | Tee-Object -Variable pacOutput
            if ($pacOutput -match "Error:") { Throw $pacOutput -match "Error:" }
            if ($pacOutput -match "FAILURE:") { Throw $pacOutput -match "FAILURE:" }
            $upgradeSuccess = $true;
        }
        catch {
            if (($pacOutput -match "_Upgrade' is not found in the target Dataverse organization")) {
                Write-PPDOMessage "Upgrade completed outside of this operation" -Type group -RunLocally $RunLocally
                $upgradeSuccess = $true;
            } 
            if (($pacOutput -match "Uninstall") -or ($pacOutput -match "PublishAll")) {
                Write-PPDOMessage "Waiting for another Solution Operation to complete" -Type group -RunLocally $RunLocally
                Start-Sleep -Seconds 30   
            }     
            else {
                $retryCount = 50
            }                                                          
            $retryCount = $retryCount + 1
            if ($retryCount -gt 50) {
                $anyFailuresInImport.Value = $true;
                Write-PPDOMessage "$($_.Exception.Message)" -Type error -RunLocally $RunLocally
                exit 1
                break;
            }                                 
        }
    } until ($upgradeSuccess -eq $true)

    # Post UPGRADE
    if ($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 $EnvironmentName -Path "$PipelinePath\$SolutionFolder\"
        }
        else {
            Write-Host "Deployment PostUpgrade step not registered to execute"
        }
    }
}

function Invoke-FixConnectionReferences {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $SolutionName,
        [string] [Parameter(Mandatory = $true)] $EnvId,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )
    
    Write-Host "Checking for Connections References in $SolutionName"
    $solutions = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute "uniquename" -FilterOperator "eq" -FilterValue $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 {
        $connectionType = $_.connectorid.Replace("/providers/Microsoft.PowerApps/apis/", "")
        Write-Host "Found Connection Reference $($_.connectionreferencelogicalname), searching for related Connection"

        $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 setup of type $connectionType, some of your Flows may not Activate succesfully" -Type warning -RunLocally $RunLocally -LogWarning $true
        }
    }
}

function Invoke-ImportReferenceData {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )

    if ($Deploy.DeployData -eq $true) {                            

        Write-PPDOMessage "Importing reference Data for $PSolution..." -Type group -RunLocally $RunLocally
        try {
            if (Test-Path -Path $PipelinePath\$SolutionFolder\ReferenceData\Extracted) {
                Write-PPDOMessage "Extracted Data Found... packaging for Import"
                Add-7zip $PipelinePath\$SolutionFolder\ReferenceData\Extracted\*.* $PipelinePath\$SolutionFolder\ReferenceData\data.zip
            }
            if (Test-Path -Path $PipelinePath\$SolutionFolder\ReferenceData\data.zip) {
                Write-PPDOMessage "Config data.zip found, importing now."
                if ($Deploy.LegacyDataTool) {
                    Write-Host "Importing Data using Legacy Data Tool"
                    If ($VerboseLogging) {
                        Import-CrmDataFile -CrmConnection $CRMConn -DataFile $PipelinePath\$SolutionFolder\ReferenceData\data.zip -ConcurrentThreads 5 -Verbose    
                    }
                    else {
                        Import-CrmDataFile -CrmConnection $CRMConn -DataFile $PipelinePath\$SolutionFolder\ReferenceData\data.zip -ConcurrentThreads 5 
                    }
                }
                else {
                    Write-Host "Importing Data using PAC Data"
                    & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe data import --data $PipelinePath\$SolutionFolder\ReferenceData\data.zip 3>&1 | Tee-Object -Variable pacOutput
                }
            }
            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
}

function Invoke-SolutionPostAction {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [bool] [Parameter(Mandatory = $false)] $patchDeploy = $false,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )
    
    ########################### POST ACTION
    Write-Host "Params: patchDeploy $patchDeploy, Deploy.PostAction $($Deploy.PostAction), Deploy.PatchPostAction $($Deploy.PatchPostAction)"
    if (($Deploy.PostAction -eq $true -and !$patchDeploy) -or ($Deploy.PatchPostAction -eq $true -and $patchDeploy)) {
        if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\PostAction.ps1) {
            if (Test-Path -Path $PipelinePath\$SolutionFolder\Scripts\Environments.json) {
                $SolutionEnvironments = Get-Content "$PipelinePath\$SolutionFolder\Scripts\Environments.json" | ConvertFrom-Json
                $SolnEnvConfig = $SolutionEnvironments | Where-Object { $_.EnvironmentName -eq $EnvironmentName }
                                        
                if ($null -ne $SolnEnvConfig) {
                    Write-PPDOMessage "Execute Specified Post Action Functions from $PipelinePath\$SolutionFolder\Scripts" -Type section -RunLocally $RunLocally
                    . "$PipelinePath\$SolutionFolder\Scripts\PostAction.ps1" -Conn $CRMConn -Path "$PipelinePath\$SolutionFolder\" -EnvironmentName $Deploy.EnvironmentName
                    $SolnEnvConfig.PostFunctions | ForEach-Object {
                        & $_ -Conn $CRMConn
                    }
                    Write-PPDOMessage "Solution Environment Post Action Complete" -Type command -RunLocally $RunLocally
                }
                else {
                    Write-PPDOMessage "Solution Environment Post Action not registered to execute" -Type warning -RunLocally $RunLocally
                } 
            }
            else {
                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 execute" -Type warning -RunLocally $RunLocally
        }
    }
}

function Invoke-EnvironmentPostAction {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $DeployServerUrl,
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false
    )

    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 execute" -Type warning -RunLocally $RunLocally
    }
}