Public/Plugins/Deploy-DataversePlugins.ps1
|
function Deploy-DataversePlugins { <# .SYNOPSIS Deploys plugin assemblies and registers steps to Dataverse. .DESCRIPTION Deploys plugin assemblies using Web API and registers/updates SDK message processing steps and images using the Dataverse Web API. Supports both classic plugin assemblies and plugin packages (NuGet). .PARAMETER RegistrationFile Path to the registrations.json file. .PARAMETER Connection CrmServiceClient connection object from Connect-DataverseEnvironment. .PARAMETER Force Remove orphaned steps that exist in Dataverse but not in configuration. .PARAMETER SkipAssembly Skip deploying the assembly, only register/update steps. .PARAMETER DetectDrift Run drift detection after deployment. .PARAMETER WhatIf Show what would be deployed without making changes. .EXAMPLE $conn = Connect-DataverseEnvironment -Interactive Deploy-DataversePlugins -RegistrationFile "./registrations.json" -Connection $conn .EXAMPLE Deploy-DataversePlugins -RegistrationFile "./registrations.json" -Connection $conn -Force Deploys and removes orphaned steps. .OUTPUTS Deployment summary object. #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string]$RegistrationFile, [Parameter(Mandatory = $true)] [Microsoft.Xrm.Tooling.Connector.CrmServiceClient]$Connection, [Parameter()] [switch]$Force, [Parameter()] [switch]$SkipAssembly, [Parameter()] [switch]$DetectDrift ) $isWhatIf = $WhatIfPreference -or $PSCmdlet.MyInvocation.BoundParameters["WhatIf"] if (-not (Test-Path $RegistrationFile)) { throw "Registration file not found: $RegistrationFile" } $registrations = Read-RegistrationJson -Path $RegistrationFile if (-not $registrations -or -not $registrations.assemblies) { throw "Invalid or empty registrations.json" } Write-Log "Plugin Deployment Tool" if ($isWhatIf) { Write-LogWarning "Running in WhatIf mode - no changes will be made" } $apiUrl = Get-WebApiBaseUrl -Connection $Connection $authHeaders = Get-AuthHeaders -Connection $Connection Write-LogSuccess "Connected to: $($Connection.ConnectedOrgFriendlyName)" $totalStepsCreated = 0 $totalStepsUpdated = 0 $totalImagesCreated = 0 $totalImagesUpdated = 0 $totalOrphansWarned = 0 $totalOrphansDeleted = 0 foreach ($asmReg in $registrations.assemblies) { Write-Log "" Write-Log ("=" * 60) Write-Log "Deploying: $($asmReg.name) ($($asmReg.type))" Write-Log ("=" * 60) $solutionUniqueName = $asmReg.solution $solution = $null $publisherPrefix = $null if ($asmReg.type -eq "Nuget" -and -not $solutionUniqueName) { Write-LogError "Solution is required for plugin packages (Nuget type)" continue } if ($solutionUniqueName -and -not $isWhatIf) { Write-Log "Looking up solution: $solutionUniqueName" $solution = Get-Solution -ApiUrl $apiUrl -AuthHeaders $authHeaders -UniqueName $solutionUniqueName if (-not $solution) { Write-LogError "Solution not found: $solutionUniqueName" continue } $publisherPrefix = $solution.publisherprefix Write-LogSuccess "Solution found: $($solution.friendlyname) (prefix: $publisherPrefix)" } # Deploy assembly if (-not $SkipAssembly) { $repoRoot = Split-Path $RegistrationFile -Parent $deployPath = if ($asmReg.type -eq "Nuget" -and $asmReg.packagePath) { Join-Path $repoRoot $asmReg.packagePath } else { Join-Path $repoRoot $asmReg.path } if (-not (Test-Path $deployPath)) { Write-LogWarning "Assembly/package not found: $deployPath" continue } $deploySuccess = Deploy-PluginAssembly -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -Path $deployPath -AssemblyName $asmReg.name -Type $asmReg.type ` -SolutionUniqueName $solutionUniqueName -PublisherPrefix $publisherPrefix ` -WhatIf:$isWhatIf if (-not $deploySuccess -and -not $isWhatIf) { Write-LogError "Failed to deploy assembly, skipping step registration" continue } } # Get assembly record $assembly = $null if (-not $isWhatIf) { $assembly = Get-PluginAssembly -ApiUrl $apiUrl -AuthHeaders $authHeaders -Name $asmReg.name if (-not $assembly) { Write-LogError "Assembly not found in Dataverse: $($asmReg.name)" continue } Write-Log "Assembly ID: $($assembly.pluginassemblyid)" if ($solutionUniqueName) { Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -SolutionUniqueName $solutionUniqueName ` -ComponentId $assembly.pluginassemblyid ` -ComponentType $script:ComponentType.PluginAssembly | Out-Null } } $configuredStepNames = @() # Process each plugin foreach ($plugin in $asmReg.plugins) { Write-Log "" Write-Log "Plugin: $($plugin.typeName)" $pluginType = $null if (-not $isWhatIf -and $assembly) { $pluginType = Get-PluginType -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -AssemblyId $assembly.pluginassemblyid -TypeName $plugin.typeName if (-not $pluginType -and $plugin.steps.Count -gt 0 -and $asmReg.type -eq "Assembly") { Write-Log " Registering new plugin type: $($plugin.typeName)" $pluginType = New-PluginType -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -AssemblyId $assembly.pluginassemblyid -TypeName $plugin.typeName if ($pluginType) { Write-LogSuccess " Plugin type created" if ($solutionUniqueName) { Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -SolutionUniqueName $solutionUniqueName ` -ComponentId $pluginType.plugintypeid ` -ComponentType $script:ComponentType.PluginType | Out-Null } } } elseif (-not $pluginType -and $plugin.steps.Count -gt 0) { Write-LogWarning " Plugin type not found: $($plugin.typeName)" continue } } # Process steps foreach ($step in $plugin.steps) { Write-Log " Step: $($step.name)" $configuredStepNames += $step.name $message = $null $filter = $null if (-not $isWhatIf) { $message = Get-SdkMessage -ApiUrl $apiUrl -AuthHeaders $authHeaders -MessageName $step.message if (-not $message) { Write-LogError " SDK Message not found: $($step.message)" continue } $filterParams = @{ ApiUrl = $apiUrl AuthHeaders = $authHeaders MessageId = $message.sdkmessageid EntityLogicalName = $step.entity } if ($step.secondaryEntity) { $filterParams["SecondaryEntityLogicalName"] = $step.secondaryEntity } $filter = Get-SdkMessageFilter @filterParams if (-not $filter) { Write-LogError " SDK Message Filter not found" continue } } $stageValue = $script:DataverseStageValues[$step.stage] $modeValue = $script:DataverseModeValues[$step.mode] $existingStep = $null if (-not $isWhatIf) { try { $existingStep = Get-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders -StepName $step.name } catch { } } $stepData = @{ Name = $step.name Stage = $stageValue Mode = $modeValue ExecutionOrder = $step.executionOrder FilteringAttributes = $step.filteringAttributes Configuration = $step.configuration PluginTypeId = $pluginType.plugintypeid MessageId = $message.sdkmessageid FilterId = $filter.sdkmessagefilterid } $stepId = $null if ($existingStep) { Write-Log " Updating existing step..." if (-not $isWhatIf) { Update-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -StepId $existingStep.sdkmessageprocessingstepid -StepData $stepData $stepId = $existingStep.sdkmessageprocessingstepid $totalStepsUpdated++ Write-LogSuccess " Step updated" if ($solutionUniqueName) { Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -SolutionUniqueName $solutionUniqueName ` -ComponentId $stepId ` -ComponentType $script:ComponentType.SdkMessageProcessingStep | Out-Null } } else { Write-Log " [WhatIf] Would update step" $totalStepsUpdated++ } } else { Write-Log " Creating new step..." if (-not $isWhatIf) { $newStep = New-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders -StepData $stepData $stepId = $newStep.sdkmessageprocessingstepid $totalStepsCreated++ Write-LogSuccess " Step created" if ($solutionUniqueName) { Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -SolutionUniqueName $solutionUniqueName ` -ComponentId $stepId ` -ComponentType $script:ComponentType.SdkMessageProcessingStep | Out-Null } } else { Write-Log " [WhatIf] Would create step" $totalStepsCreated++ } } # Process images foreach ($image in $step.images) { Write-Log " Image: $($image.name) ($($image.imageType))" $imageTypeValue = $script:DataverseImageTypeValues[$image.imageType] $existingImages = @() if (-not $isWhatIf -and $stepId) { try { $existingImages = Get-StepImages -ApiUrl $apiUrl -AuthHeaders $authHeaders -StepId $stepId } catch { } } $existingImage = $existingImages | Where-Object { $_.name -eq $image.name } | Select-Object -First 1 $imageData = @{ Name = $image.name EntityAlias = if ($image.entityAlias) { $image.entityAlias } else { $image.name } ImageType = $imageTypeValue Attributes = $image.attributes StepId = $stepId } if ($existingImage) { if (-not $isWhatIf) { Write-Log " Updating existing image..." Update-StepImage -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -ImageId $existingImage.sdkmessageprocessingstepimageid -ImageData $imageData $totalImagesUpdated++ Write-LogSuccess " Image updated" } else { Write-Log " [WhatIf] Would update image" $totalImagesUpdated++ } } else { if (-not $isWhatIf) { Write-Log " Creating new image..." $newImage = New-StepImage -ApiUrl $apiUrl -AuthHeaders $authHeaders -ImageData $imageData $totalImagesCreated++ Write-LogSuccess " Image created" if ($solutionUniqueName -and $newImage.sdkmessageprocessingstepimageid) { Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -SolutionUniqueName $solutionUniqueName ` -ComponentId $newImage.sdkmessageprocessingstepimageid ` -ComponentType $script:ComponentType.SdkMessageProcessingStepImage | Out-Null } } else { Write-Log " [WhatIf] Would create image" $totalImagesCreated++ } } } } } # Check for orphaned steps if (-not $isWhatIf -and $assembly) { Write-Log "" Write-Log "Checking for orphaned steps..." $existingSteps = Get-ProcessingStepsForAssembly -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -AssemblyId $assembly.pluginassemblyid foreach ($existingStep in $existingSteps) { if ($configuredStepNames -notcontains $existingStep.name) { if ($Force) { Write-LogWarning "Deleting orphaned step: $($existingStep.name)" try { Remove-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders ` -StepId $existingStep.sdkmessageprocessingstepid $totalOrphansDeleted++ Write-LogSuccess " Deleted" } catch { Write-LogError " Failed to delete: $($_.Exception.Message)" } } else { Write-LogWarning "Orphaned step found: $($existingStep.name)" Write-Log " Use -Force to delete orphaned steps" $totalOrphansWarned++ } } } } } # Summary Write-Log "" Write-Log ("=" * 60) Write-Log "Deployment Summary" Write-Log ("=" * 60) Write-LogSuccess "Steps created: $totalStepsCreated" Write-LogSuccess "Steps updated: $totalStepsUpdated" Write-LogSuccess "Images created: $totalImagesCreated" Write-LogSuccess "Images updated: $totalImagesUpdated" if ($totalOrphansWarned -gt 0) { Write-LogWarning "Orphaned steps (not deleted): $totalOrphansWarned" } if ($totalOrphansDeleted -gt 0) { Write-LogWarning "Orphaned steps deleted: $totalOrphansDeleted" } if ($isWhatIf) { Write-Log "" Write-LogWarning "WhatIf mode: No actual changes were made" } # Run drift detection if requested if ($DetectDrift -and -not $isWhatIf) { Write-Log "" Write-Log "Running post-deployment drift detection..." Get-DataversePluginDrift -RegistrationFile $RegistrationFile -Connection $Connection } return [PSCustomObject]@{ StepsCreated = $totalStepsCreated StepsUpdated = $totalStepsUpdated ImagesCreated = $totalImagesCreated ImagesUpdated = $totalImagesUpdated OrphansWarned = $totalOrphansWarned OrphansDeleted = $totalOrphansDeleted } } |