Scripts/SolutionDeploy.ps1

#
# SolutionDeploy.ps1
#


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
    Install-PAC

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

    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

        Write-Host "##[section] Creating CRM connection"
        $CRMConn = Get-CrmConnection -ConnectionString $CrmConnectionString -Verbose #-MaxCrmConnectionTimeOutMinutes 10
        #Set-CrmConnectionTimeout -conn $CRMConn -TimeoutInSeconds 600

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

        foreach ($package in $Packages) {
            $skipDeploy = $false 
            $anyFailuresInImport = $false; 

            $Deploy = $package.DeployTo | Where-Object { $_.EnvironmentName -eq $EnvironmentName }
            if ($null -ne $Deploy) {
                Write-Host "Deployment step manifest"
                Write-Host $Deploy
                $PSolution = $package.SolutionName
                Write-Host "##[group] Preparing Deployment for $PSolution"
                Write-Host "##[section] Preparing $PSolution Solution as $($Deploy.DeploymentType)"

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

                Write-Host "##[command] Packing Solution $PSolution" 
                #Checking for Canvas App
                $canvasApps = Get-ChildItem -Path $PipelinePath\$PSolution\$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\$PSolution\$packageFolder -z $PipelinePath\$PSolution\$fileToPack -p Unmanaged
                }
                else {
                    & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe solution pack -f $PipelinePath\$PSolution\$packageFolder -z $PipelinePath\$PSolution\$fileToPack -p Managed -same
                }         
               
                Write-Host "##[section] Importing package"
                
                try {
                    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    $error.Clear()
                    Write-Host "##[section] Deploying $($package.SolutionName) as $($Deploy.DeploymentType) to - $EnvironmentName" 
                    #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 $($package.SolutionName)
                    $Solution = $SolutionQuery.CrmRecords[0]

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

                        if ($null -ne $Deploy.DeployAsHolding) {
                            [bool]$deployAsHolding = [System.Convert]::ToBoolean($Deploy.DeployAsHolding)
                        }
                        else {
                            $deployAsHolding = $false
                        }
                    }
                        
                    Write-Host "Getting Version to be Deployed..."
                    $deployingVersion = Get-Content -Path $PipelinePath\$PSolution\$PSolution.version
                    Write-Host "Version to be deployed : $deployingVersion"
                    if ($deployingVersion -le $SolutionVersion) { $skipDeploy = $true; Write-Host "Skipping Deployment as Target has same or newer" }
                        
                    ########################## IMPORT
                    if (!$skipDeploy) {

                        # Powerapps Solution Checker
                        if ($Deploy.PowerAppsChecker -eq $true -and $UseClientSecret -eq $true) {
                            Start-SolutionChecker -PipelinePath $PipelinePath -SolutionPath $PipelinePath\$PSolution\$fileToPack -SolutionName $PSolution -ClientId $UserName -ClientSecret $Password -TenantId "$($CRMConn.TenantId)"
                        }
                        else {
                            Write-Host "Powerapps Checker not configured. Add PowerAppsChecker: True as a property in the DeployTo section for your Solution"
                        }
            
                        # PRE ACTION
                        if ($Deploy.PreAction -eq $true) {
                            if (Test-Path -Path $PipelinePath\$PSolution\Scripts\PreAction.ps1) {
                                Write-Host "##[section] Execute Pre Action from $PipelinePath\$PSolution\Scripts"
                                . $PipelinePath\$PSolution\Scripts\PreAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$PSolution\"
                            }
                        }

                        $activatePlugIns = $true;
                        $overwriteUnManagedCustomizations = $true;
                        $skipDependancyOnProductUpdateCheckOnInstall = $true;
                        $isInternalUpgrade = $false;                   

                        Write-Host "Initiating Import and deployment to $($DeployServerUrl)"
                        Write-Host "Import as Holding solution : $($deployAsHolding)"
                        
                        $importId = [guid]::Empty 
                        $result = $CRMConn.ImportSolutionToCrmAsync("$PipelinePath\$PSolution\$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 5

                                $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-Error "##[error]: Unable to import solution - please check Solution import history in https://make.powerapps.com/environments"
                                    Write-Warning "##[warning] Import Failed: $($operation.statuscode)"
                                    Write-Warning "##[warning] Error Code: $($operation.errorcode)"
                                    Write-Warning "##[warning] $($operation.friendlymessage)"
                                   
                                    $anyFailuresInImport = $true;
                                }
                            }
                            catch {
                                Write-Host "Retrying Polling import status"
                                $Retrycount = $Retrycount + 1
                                if ($Retrycount -gt 3) {
                                    $statuscode = 32;
                                    $anyFailuresInImport = $true;
                                    Write-Error "##[error]: Unable to polling or import solution - please check Solution import history in https://make.powerapps.com/environments"
                                    Write-Error "##[error]:$($_.Exception.Message)"
                                    break;
                                }
                            }
                        } until ($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32)


                        $Retrycount = 0;
                        # UPGRADE
                        if ($deployAsHolding -eq $true -and $anyFailuresInImport -eq $false) {
                         
                            Write-Host "Applying Upgrade to Solution"
                            $promoteRequestId = $CRMConn.DeleteAndPromoteSolutionAsync($PSolution);                          

                            if (($null -eq $promoteRequestId) -or ($promoteRequestId -eq [Guid]::Empty)) {
                                Write-Error "##[error]: Unable to promote or delete solution - please check Solution import history in https://make.powerapps.com/environments"
                                $anyFailuresInImport = $true;
                            }
                            else { 
                                Write-Host Async Operation ID: $promoteRequestId 
                                do {
                                    try {
                                        Start-Sleep -Seconds 5
                                        $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) {
                                            Write-Error "##[error]: Unable to promote or delete solution - please check Solution import history in https://make.powerapps.com/environments"
                                            Write-Warning "##[warning] Delete and Promote Failed: #($operation.statuscode)"
                                            Write-Warning "##[warning] Error Code: $($operation.errorcode)"
                                            Write-Warning "##[warning] $($operation.friendlymessage)"
                                       
                                            $anyFailuresInImport = $true;
                                        }
                                    }
                                    catch {
                                        Write-Host "Retrying Polling Upgrade status"
                                        $Retrycount = $Retrycount + 1
                                        if ($Retrycount -gt 3) {
                                            $statuscode = 32;
                                            $anyFailuresInImport = $true;
                                            Write-Error "##[error]: Unable to polling or Upgrade of solution - please check Solution import history in https://make.powerapps.com/environments"
                                            Write-Error "##[error]:$($_.Exception.Message)"
                                            break;
                                        }
                                    }
                                }until($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32)
                            }
                        }

                        # DATA CONFIGURATION
                        if ($Deploy.DeployData -eq $true -and $anyFailuresInImport -eq $false) {
                            Write-Host "##[group] Importing reference Data ..."
                            try {
                                if (Test-Path -Path $PipelinePath\$PSolution\ReferenceData\data.zip) {
                                    Import-CrmDataFile -CrmConnection $CRMConn -DataFile $PipelinePath\$PSolution\ReferenceData\data.zip -EnabledBatchMode -Verbose                    
                                }
                                else {
                                    Write-Host "Config Data file does not Exist"
                                }
                            }
                            catch {
                                Write-Error "##[error]: Unable to import configuration data - please review Pipeline error logs"
                                Write-Error "##[error]:$($_.Exception.Message)"              
                            }
                        }
                        else {
                            Write-Host "##[section] No Data to Import for $PSolution"
                        }

                        # POST ACTION
                        if ($Deploy.PostAction -eq $true -and $anyFailuresInImport -eq $false) {
                            if (Test-Path -Path $PipelinePath\$PSolution\Scripts\PostAction.ps1) {
                                Write-Host "##[section] Execute Post Action from $PipelinePath\$PSolution\Scripts"
                                . $PipelinePath\$PSolution\Scripts\PostAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$PSolution\"
                            }
                        }
                    }

                    [int]$elapsedTime = $stopwatch.Elapsed.TotalMinutes      
                    $stopwatch.Stop()
                    Write-Host "##[section] Import Complete in $($elapsedTime) minutes"
                }
                catch {
                    Write-Host "##[section] Skipping $PSolution due to Solution import error"
                    Write-Error "##[error]:$($_.Exception.Message)"
                }
            }
            else {
                Write-Host "##[warning] $($package.SolutionName) is not configured for deployment to $env:ENVIRONMENT_NAME in deployPackages.json" 
            }
            Write-Host "##[endgroup]"
        }
    }
    Write-Host Environment $EnvironmentName
    Import-Package
}