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 Write-Host "##[group] Installing Dependencies" Install-PAC if (!$RunLocally) { Install-ConfigMigrationModule Install-XrmModule Install-PowerAppsCheckerModule Install-PowerAppsAdmin } else { Write-Host "Preparing local run" } Write-Host "##[endgroup]" 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-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-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" } #ENVIRONMENT PRE-ACTION if ($null -ne $EnvConfig -and $EnvConfig.PreAction -eq $true) { Write-Host "##[section] Execute Environment Pre Action" . "$PipelinePath\Common\Environments\Scripts\PreAction.ps1" -Conn $CRMConn -PipelinePath $PipelinePath -EnvironmentName $EnvConfig.EnvironmentName -EnvironmentUrl $DeployServerUrl $EnvConfig.PreFunctions | ForEach-Object { & $_ -Conn $CRMConn } Write-Host "##[command] Environment Pre Action Complete" } else { Write-Host "##[warning] Environment Pre Action not registered to execute" } foreach ($package in $Packages) { $skipDeploy = $false $anyFailuresInImport = $false; $Deploy = $package.DeployTo | Where-Object { $_.EnvironmentName -eq $EnvironmentName } if ($null -ne $Deploy) { $PSolution = $package.SolutionName Write-Host "##[group] Preparing Deployment for $PSolution" Write-Host "Deployment step manifest - $Deploy" 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 "##[command] Version to be deployed : $deployingVersion" if ($deployingVersion -le $SolutionVersion) { $skipDeploy = $true; Write-Host "##[warning] Skipping Deployment as Target has same or newer" } ########################## IMPORT if (!$skipDeploy) { # Powerapps Solution Checker if ($Deploy.PowerAppsChecker -eq $true -and $UseClientSecret -eq $true) { Write-Host "##[command] Running PowerApps Solution Checker for $PSolution" Start-SolutionChecker -PipelinePath $PipelinePath -SolutionPath $PipelinePath\$PSolution\$fileToPack -SolutionName $PSolution -ClientId $UserName -ClientSecret $Password -TenantId "$($CRMConn.TenantId)" } else { Write-Host "##[warning] 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\" } else { Write-Host "Deployment PreAction step not registered to excecute" } } $activatePlugIns = $true; $overwriteUnManagedCustomizations = $true; $skipDependancyOnProductUpdateCheckOnInstall = $true; $isInternalUpgrade = $false; Write-Host "##[section] Initiating Import and deployment to $($DeployServerUrl)" Write-Host "##[command] 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-Host "##[error]: Unable to import solution - please check Solution import history in https://make.powerapps.com/environments" Write-Host "##[warning] Import Failed: $($operation.statuscode)" Write-Host "##[warning] Error Code: $($operation.errorcode)" Write-Host "##[warning] $($operation.friendlymessage)" $anyFailuresInImport = $true; } } catch { Write-Host "Retrying Polling import status" $Retrycount = $Retrycount + 1 if ($Retrycount -gt 3) { $statuscode = 32; $anyFailuresInImport = $true; Write-Host "##[error]: Unable to poll status - please check Solution import history in https://make.powerapps.com/environments" Write-Host "##[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) { # PRE UPGRADE if ($Deploy.PreUpgrade -eq $true) { if (Test-Path -Path $PipelinePath\$PSolution\Scripts\PreUpgrade.ps1) { Write-Host "##[section] Execute Pre Upgrade from $PipelinePath\$PSolution\Scripts" . $PipelinePath\$PSolution\Scripts\PreUpgrade.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$PSolution\" } 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-Host "##[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-Host "##[error]: Unable to promote or delete solution - please check Solution import history in https://make.powerapps.com/environments" Write-Host "##[warning] Delete and Promote Failed: #($operation.statuscode)" Write-Host "##[warning] Error Code: $($operation.errorcode)" Write-Host "##[warning] $($operation.friendlymessage)" $anyFailuresInImport = $true; } } catch { Write-Host "Retrying Polling Upgrade status" $Retrycount = $Retrycount + 1 if ($Retrycount -gt 3) { $statuscode = 32; $anyFailuresInImport = $true; Write-Host "##[error]: Unable to poll status - please check Solution import history in https://make.powerapps.com/environments" Write-Host "##[error]:$($_.Exception.Message)" break; } } }until($statuscode -eq 30 -or $statuscode -eq 31 -or $statuscode -eq 32) } # Post UPGRADE if ($Deploy.PostUpgrade -eq $true) { if (Test-Path -Path $PipelinePath\$PSolution\Scripts\PostUpgrade.ps1) { Write-Host "##[section] Execute Post Upgrade from $PipelinePath\$PSolution\Scripts" . $PipelinePath\$PSolution\Scripts\PostUpgrade.ps1 -Conn $CRMConn -EnvironmentName $Deploy.EnvironmentName -Path "$PipelinePath\$PSolution\" } else { Write-Host "Deployment PostUpgrade step not registered to excecute" } } } # 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-Host "##[error]: Unable to import configuration data - please review Pipeline error logs" Write-Host "##[error]:$($_.Exception.Message)" } Write-Host "##[endgroup]" } 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\" } else { Write-Host "Deployment PostAction step not registered to excecute" } } # 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 if ($UseClientSecret) { Add-PowerAppsAccount -ApplicationId $UserName -ClientSecret $Password -TenantID $CRMConn.TenantId } else { Add-PowerAppsAccount -Username $UserName -Password $Password } $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 "$PSolution" $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 | Where-Object { $null -eq $_.connectionid } | ForEach-Object { $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 | 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 $conn -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-Host "##[command] Setting Connection Reference to use $($connection[0].DisplayName)" Set-CrmRecord -conn $impersonationConn -EntityLogicalName $_.logicalname -Id $_.connectionreferenceid -Fields @{"connectionid" = $connection[0].ConnectionName } } } else { Write-Host "##[warning] No Connection has been set up of type $connectionType, some of your Flows may not Activate succesfully" } } 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" $FlowsToActivate = Get-Content -Path $PipelinePath\$PSolution\$($Deploy.Flows.OverrideFile) | ConvertFrom-Json } else { Write-Host "Using Flows_Default.json for Flow Activation" $FlowsToActivate = Get-Content -Path $PipelinePath\$PSolution\Flows_Default.json | ConvertFrom-Json } Write-Host "There are $($FlowsToActivate.Count) Flows that need activating" $FlowsToActivate | ForEach-Object { $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-Host "##[command] Enabling Flow $($workflow.name)" try { Set-CrmRecordState -conn $impersonationConn -EntityLogicalName workflow -Id $_.FlowId -StateCode Activated -StatusCode Activated } catch { Write-Host "##[warning] 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" Write-Host $_ } } } Write-Host "##[warning] User $($_.ActivateAsUser) was not found in $($Deploy.EnvironmentName)" } else { Write-Host "Attempting to Activate Flow as Owner of Connection Reference" $solutions = Get-CrmRecords -conn $CRMConn -EntityLogicalName solution -FilterAttribute "uniquename" -FilterOperator "eq" -FilterValue "$PSolution" $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 $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 $conn -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-Host "##[command] Enabling Flow $($workflow.name)" $impersonationConn = $CRMConn $impersonationConn.OrganizationWebProxyClient.CallerId = $impersonationCallerId Set-CrmRecordState -conn $impersonationConn -EntityLogicalName workflow -Id $_.FlowId -StateCode Activated -StatusCode Activated } } } } } 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" : "" } "@ } } [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-Host "##[error]:$($_.Exception.Message)" } } else { Write-Host "##[warning] $($package.SolutionName) is not configured for deployment to $env:ENVIRONMENT_NAME in deployPackages.json" } } Write-Host "##[endgroup]" #EXECUTE ENVIRONMENT POST ACTION if ($null -ne $EnvConfig -and $EnvConfig.PostAction -eq $true) { Write-Host "##[section] Execute Environment Post Action" . "$PipelinePath\Common\Environments\Scripts\PostAction.ps1" -Conn $CRMConn -PipelinePath $PipelinePath -EnvironmentName $EnvConfig.EnvironmentName -EnvironmentUrl $DeployServerUrl $EnvConfig.PostFunctions | ForEach-Object { & $_ -Conn $CRMConn } Write-Host "##[command] Environment Post Action Complete" } else { Write-Host "Environment PostAction step not registered to excecute" } } Write-Host Environment $EnvironmentName Import-Package } |