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 ($null -eq $Environment) { $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 Invoke-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 } } |