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"

    Install-PAC

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

    function Import-Package {
        #[string]$PackageDirectory = "$PipelinePath/drop/PackageDeployer/bin/Release"
        #[string]$LogsDirectory = "$PackageDirectory"
        [string]$LogsDirectory = $PipelinePath
        if ($UseClientSecret) {
            #(New-Object PSCredential "user", $clientKeySS).GetNetworkCredential().Password
            [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"
            #Need to replace AppId and RedirectUri from Library values
        }

        #Write-Host $PackageDirectory
        Write-Host $LogsDirectory        

        $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) {

            $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"

                If ($Deploy.DeploymentType -eq "Unmanaged") {
                    Write-Host "##[section] Switching $PSolution Solution to Unmanaged"
                    # $folder = ($_.solutionpackagefilename).Replace("_managed.zip", "").Replace(".zip", "")
                    # $newFileName = ($_.solutionpackagefilename).Replace("_managed", "")
                    $fileToPack = "$($envName)_$($PSolution).zip"
                    # $_.solutionpackagefilename = $fileToPack
                    $packageFolder = "dataverse_$($PSolution)"
                    Write-Host "##[command] Packing Unmanaged 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 --msapp $_.DirectoryName.msapp --sources $_.FullName

                    }         
                    & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe solution pack -f $PipelinePath\$PSolution\$packageFolder -z $PipelinePath\$PSolution\$fileToPack -p Unmanaged
                }
                else {
                    Write-Host "##[section] Switching $PSolution Solution to Managed"
                    # $folder = ($_.solutionpackagefilename).Replace("_managed.zip", "").Replace(".zip", "")
                    $fileToPack = "$($envName)_$($PSolution)_managed.zip"
                    # $_.solutionpackagefilename = $fileToPack
                    $packageFolder = "dataverse_$($PSolution)"
                    Write-Host "##[command] Packing Managed 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 --msapp "$($_.FullName).msapp" --sources $_.FullName
                        Remove-Item $_.FullName -Recurse -ErrorAction SilentlyContinue
                    }
                    #>

                    
                    & $env:APPDATA\Microsoft.PowerPlatform.DevOps\PACTools\tools\pac.exe solution pack -f $PipelinePath\$PSolution\$packageFolder -z $PipelinePath\$PSolution\$fileToPack -p Managed -same
                }
              
   
                if ($Deploy.PreAction -eq $true) {
                    Write-Host "##[section] Execute Pre Action"
                    Write-Host "$PipelinePath\drop\$PFolder\bin\release\Scripts"
                    . $PipelinePath\drop\$PFolder\bin\release\Scripts\PreAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\drop\$PFolder\bin\release\"
                }

                Write-Host "##[section] Importing package"
                $skipDeploy = $false
                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) {
                            $deployAsHolding = $Deploy.DeployAsHolding
                            [System.Convert]::ToBoolean($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" }
                        
                    #if ($RunLocally) {
                    # Write-Host "Skipping version checking for Local Deployment run"
                    # $skipDeploy = $false; #run locally regardless of version number
                    #}

                    if (!$skipDeploy) {

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

                        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 
                           
                        do {
                            $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;
                                Start-Sleep -Seconds 5
                            }
                            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;
                            }
                        } until ($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32)

                        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 {

                                    $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) {

                                        #$Jobs= (Get-CrmRecordsByFetch -conn $CRMConn -Fetch '
                                        #<fetch count="1"><entity name="importjob"><attribute name="importjobid" /><attribute name="progress" /><attribute name="solutionname" />
                                        # <filter type="and"><condition attribute="solutionname" operator="eq" value="CICDDemoSolution" /></filter></entity></fetch>').CrmRecords
                                        # $job = $Jobs[0];

                                        Write-Host "Polling Promotion status for Solution: $($PSolution) : $($operation.statuscode)"
                                        $anyFailuresInImport = $false;
                                        Start-Sleep -Seconds 5
                                    }
                                    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;
                                    }
                                }until($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32)
                            }
                        }
                    }

                    [int]$elapsedTime = $stopwatch.Elapsed.TotalMinutes      
                    $stopwatch.Stop()
                    Write-Host "##[section] Import Complete in $($elapsedTime) minutes"
                }
                catch {
                    Write-Error "##[error]:$($_)"
                    Write-Host "##[section] Skipping $PSolution due to Solution import error"
                }

                If ($Deploy.DeployData -eq $true -and $anyFailuresInImport -eq $true) {
                    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-Host $_                      
                    }
                }
                else {
                    Write-Host "##[section] No Data to Import for $PSolution"
                }

                if ($Deploy.PostAction -eq $true -and $anyFailuresInImport -eq $true) {
                    Write-Host "##[section] Execute Post Action"
                    Write-Host "$PipelinePath\drop\$PFolder\bin\release\Scripts"
                    . $PipelinePath\drop\$PFolder\bin\release\Scripts\PostAction.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\drop\$PFolder\bin\release\"
                }
            }
            else {
                Write-Host "##[warning] $($package.SolutionName) is not configured for deployment to $env:ENVIRONMENT_NAME in deployPackages.json" 
            }
            Write-Host "##[endgroup]"
        
            Write-Host "##[endgroup]"
        }
    }
    Write-Host Environment $EnvironmentName
    Import-Package
}