Private/PowerAutomateFlows.ps1

function Invoke-FlowAction {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [bool] [Parameter(Mandatory = $true)] $AnyImportSuccessful,
        [bool] [Parameter(Mandatory = $true)] $AlwaysTryActivate,
        [bool] [Parameter(Mandatory = $true)] $FailonError,
        [string] [Parameter(Mandatory = $true)] $SolutionName,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false,
        [System.Collections.Hashtable] [Parameter(Mandatory = $true)] $Deploy
    )

    if ($AnyImportSuccessful -or $AlwaysTryActivate -eq $true) {
        $ProgressPreference = "SilentlyContinue"
        Write-Host "Fetching details for Power Automate flows" -ForegroundColor Green

        # set up connection for manipulating flows
        Write-Host "Getting Environment Id"
        $orgs = Get-CrmRecords -conn $CRMConn -EntityLogicalName organization -TopCount 1 -Fields "organizationid"
        if ($orgs.Count -gt 0) {
            $orgId = $orgs.CrmRecords[0].organizationid

            $Environment = Get-AdminPowerAppEnvironment | Where-Object OrganizationId -eq $orgId.Guid
            if ($null -eq $Environment) {
                # ppdo cannot access Power Apps Admin to get the environment details, notify the user
                $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 ($FailonError -eq $true) {
                    Write-PPDOMessage -Message $PermissionsErrorMessage -Type error -RunLocally $RunLocally -LogError $true
                } 
                else {
                    Write-PPDOMessage -Message $PermissionsErrorMessage -Type warning -RunLocally $RunLocally -LogWarning $true
                }
            }
            else {
                $EnvId = $Environment.EnvironmentName
                Write-Host "Environment Id - $EnvId"

                #check whether setting connection references is enabled
                if ($Deploy.ConnectionReferences.SetConnections -eq $true) {
                    Write-Host "Configuring Connections for Power Automate flows" -ForegroundColor Green
                    Invoke-SetConnectionReferences -CRMConn $CRMConn -SolutionName $SolutionName -EnvId $EnvId -FailOnError $Deploy.ConnectionReferences.FailOnError -RunLocally $RunLocally
                }
                else {
                    Write-PPDOMessage -Message "Skipping setting Connection References per configuration in deployPackages.json" -type "warning" -LogWarning $true -RunLocally $RunLocally
                }

                # check whether activate flows is enabled
                if ($Deploy.Flows.ActivateFlows -eq $true) {
                    Write-Host "Activating Power Automate flows" -ForegroundColor Green
                    Invoke-FlowActivation -CRMConn $CRMConn -PipelinePath $PipelinePath -RunLocally $RunLocally -Deploy $Deploy -SolutionFolder $SolutionFolder -EnvId $EnvId -SolutionName $SolutionName
                }
                else {
                    Write-PPDOMessage -Message "Skipping activating Flows per configuration in deployPackages.json" -type "warning" -LogWarning $true -RunLocally $RunLocally
                }
            }
        }
        else {
            $NoOrganizationsError = "There are no organization records in CRM, unable to set Connection References or activate flows."
            if ($FailonError -eq $true) {
                Write-PPDOMessage -Message $NoOrganizationsError -Type error -RunLocally $RunLocally -LogError $true
            } 
            else {
                Write-PPDOMessage -Message $NoOrganizationsError -Type warning -RunLocally $RunLocally -LogWarning $true
            }
        }
    }
}

function Invoke-FlowActivation {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] [Parameter(Mandatory = $true)] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false,
        [System.Collections.Hashtable] [Parameter(Mandatory = $true)] $Deploy,
        [string] [Parameter(Mandatory = $true)] $EnvId,
        [string] [Parameter(Mandatory = $true)] $SolutionName
    )

    function Invoke-Activate {
        Param(
            [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] [Parameter(Mandatory = $true)] $CRMConn,
            [System.Object] [Parameter(Mandatory = $true)] $FlowsToActivate,
            [ref] [Parameter(Mandatory = $true)] $ErrorCount,
            [switch] $CatchChildFlowActivationErrors
        )

        $FlowsToRetry = @()

        $FlowsToActivate | ForEach-Object {
            $FlowStore = $_
            try {
                $workflow = Get-CrmRecord -conn $CRMConn -EntityLogicalName "workflow" -Id $_.FlowId -Fields "statecode", "name"

                if ($workflow.statecode -ne "Activated") {
                    Write-Host "Flow status is $($workflow.statecode), attempting to activate flow."
                    $activationConnection = $CRMConn

                    if ($_.ActivateAsUser) {
                        Write-Host "ActivateAsUser defined and set to: $($_.ActivateAsUser), attempting to activate flow as this user"
                        $systemuserResult = Get-CrmRecords -conn $CRMConn -EntityLogicalName "systemuser" -FilterAttribute "domainname" -FilterOperator "eq" -FilterValue $_.ActivateAsUser -TopCount 1 -Fields "systemuserid"
                        if ($systemuserResult.Count -gt 0) {
                            $activationConnection.OrganizationWebProxyClient.CallerId = $systemuserResult.CrmRecords[0].systemuserid
                        }
                        else {
                            Write-PPDOMessage -Message "$($_.Exception.Message)" -Type "error" -RunLocally $RunLocally
                            Throw "User $($_.ActivateAsUser) was not found in $($Deploy.EnvironmentName), unable to impersonate them to activate flow $($workflow.name)"
                        }
                    }
                    Write-PPDOMessage "Enabling Flow '$($workflow.name)'" -Type command -RunLocally $RunLocally
                    try {
                        Set-CrmRecordState -conn $activationConnection -EntityLogicalName "workflow" -Id $_.FlowId -StateCode "Activated" -StatusCode "Activated"
                        Write-Host "...Activated" -ForegroundColor Green
                    }
                    catch {
                        if ($_.ToString().Contains("ChildFlowNeverPublished") -and $CatchChildFlowActivationErrors) {
                            $FlowsToRetry += $FlowStore
                        }
                        else {
                            Throw $_
                        }
                    }
                }
            }
            catch {
                Write-PPDOMessage "$($_.Exception.Message)" -Type "error" -RunLocally $RunLocally
                $ErrorCount.Value++
            }
        }
        return $FlowsToRetry
    }

    # Use Override File if it is set
    if ($Deploy.Flows.OverrideFile) {
        $FlowsJSONFilePath = "$PipelinePath\$SolutionFolder\$($Deploy.Flows.OverrideFile)"
    }
    else {
        $FlowsJSONFilePath = "$PipelinePath\$SolutionFolder\Flows_Default.json"
    }
    
    Write-Host "Checking if there are Flows that need to be activated"
    if ($Deploy.Flows.ActivateFlows -ne $true) {
        Write-PPDOMessage -Message "Skipping flow activation, per 'ActivateFlows' flag being false in deployPackages.json." -RunLocally $RunLocally
        return
    }
    if (!(Test-Path -Path $FlowsJSONFilePath)) {
        Write-PPDOMessage -Message "Flow activation is enabled in deployPackages.json, but unable to find the Flows to activate at $FlowsJSONFilePath. Please add the file, or change deployPackages.json to skip activating flows." -RunLocally $RunLocally
        return
    }

    Write-PPDOMessage -Message "Using list of flows to activate at location '$FlowsJSONFilePath'." -RunLocally $RunLocally

    # Get list of flows to activate from JSON
    try {
        $FlowsToActivate = Get-Content -Path $FlowsJSONFilePath | ConvertFrom-Json    
    }
    catch {
        Write-PPDOMessage -Message "$($_.Exception.Message)" -Type "error" -LogError $true -RunLocally $RunLocally
        Throw "An error occurred when trying to get list of flows to activate at location '$FlowsJSONFilePath'."
    }

    Write-PPDOMessage -Message "There are $($FlowsToActivate.Count) Flows to activate." -RunLocally $RunLocally
    if ($FlowsToActivate.Count -le 0) {
        return
    }

    $ErrorCount = 0
    $FlowsToRetry = Invoke-Activate -CRMConn $CRMConn -ErrorCount ([ref]$ErrorCount) -FlowsToActivate $FlowsToActivate -CatchChildFlowActivationErrors
    
    if ($FlowsToRetry.Count -gt 0) {
        Invoke-Activate -CRMConn $CRMConn -ErrorCount ([ref]$ErrorCount) -FlowsToActivate $FlowsToRetry
    }

    if ($Deploy.Flows.FailOnError -eq $true -and $ErrorCount -gt 0) {
        Write-PPDOMessage "There were $ErrorCount Flow activation errors and FailOnError is set to True... exiting." -Type error -RunLocally $RunLocally -LogError $true
        exit 1
    }
}

function Invoke-ProcessAction {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] $CRMConn,
        [bool] [Parameter(Mandatory = $true)] $AnyImportSuccessful,
        [bool] [Parameter(Mandatory = $true)] $AlwaysTryActivate,
        [bool] [Parameter(Mandatory = $true)] $FailonError,
        [string] [Parameter(Mandatory = $true)] $SolutionName,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false,
        [System.Collections.Hashtable] [Parameter(Mandatory = $true)] $Deploy
    )

    if ($AnyImportSuccessful -or $AlwaysTryActivate -eq $true) {
        $ProgressPreference = "SilentlyContinue"
        # check whether activate processes is enabled
        if ($Deploy.Processes.Activate -eq $true) {
            Write-Host "Activating Processes" -ForegroundColor Green
            Invoke-ProcessActivation -CRMConn $CRMConn -PipelinePath $PipelinePath -RunLocally $RunLocally -Deploy $Deploy -SolutionFolder $SolutionFolder -SolutionName $SolutionName
        }
        else {
            Write-PPDOMessage -Message "Skipping activating Processes per configuration in deployPackages.json" -type "warning" -LogWarning $true -RunLocally $RunLocally
        }
    }
}

function Invoke-ProcessActivation {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] [Parameter(Mandatory = $true)] $CRMConn,
        [string] [Parameter(Mandatory = $true)] $PipelinePath,
        [string] [Parameter(Mandatory = $true)] $SolutionFolder,
        [bool] [Parameter(Mandatory = $false)] $RunLocally = $false,
        [System.Collections.Hashtable] [Parameter(Mandatory = $true)] $Deploy,
        [string] [Parameter(Mandatory = $true)] $SolutionName
    )
    
    # Use Override File if it is set
    if ($Deploy.Processes.OverrideFile) {
        $ProcessesJSONFilePath = "$PipelinePath\$SolutionFolder\$($Deploy.Processes.OverrideFile)"
    }
    else {
        $ProcessesJSONFilePath = "$PipelinePath\$SolutionFolder\Processes_Default.json"
    }
    
    Write-Host "Checking if there are Processes that need to be activated"
    if ($Deploy.Processes.Activate -ne $true) {
        Write-PPDOMessage -Message "Skipping workflow activation, per 'Activate' flag being false in deployPackages.json." -RunLocally $RunLocally
        return
    }
    if (!(Test-Path -Path $ProcessesJSONFilePath)) {
        Write-PPDOMessage -Message "Process activation is enabled in deployPackages.json, but unable to find the Processes to activate at $ProcessesJSONFilePath. Please add the file, or change deployPackages.json to skip activating workflows." -RunLocally $RunLocally
        return
    }

    Write-PPDOMessage -Message "Using list of Processes to activate at location '$ProcessesJSONFilePath'." -RunLocally $RunLocally

    # Get list of flows to activate from JSON
    try {
        $ProcessesToActivate = Get-Content -Path $ProcessesJSONFilePath | ConvertFrom-Json    
    }
    catch {
        Write-PPDOMessage -Message "$($_.Exception.Message)" -Type "error" -LogError $true -RunLocally $RunLocally
        Throw "An error occurred when trying to get list of Processes to activate at location '$ProcessesJSONFilePath'."
    }

    Write-PPDOMessage -Message "There are $($ProcessesToActivate.Count) Processes to activate." -RunLocally $RunLocally
    if ($ProcessesToActivate.Count -le 0) {
        return
    }

    $ErrorCount = 0
    $ProcessesToRetry = Invoke-ProcessActivate -CRMConn $CRMConn -ErrorCount ([ref]$ErrorCount) -ProcessesToActivate $ProcessesToActivate -CatchChildProcessActivationErrors
    
    if ($ProcessesToRetry.Count -gt 0) {
        Invoke-ProcessActivate -CRMConn $CRMConn -ErrorCount ([ref]$ErrorCount) -ProcessesToActivate $ProcessesToRetry
    }

    if ($Deploy.Processes.FailOnError -eq $true -and $ErrorCount -gt 0) {
        Write-PPDOMessage "There were $ErrorCount Process activation errors and FailOnError is set to True... exiting." -Type error -RunLocally $RunLocally -LogError $true
        exit 1
    }
}

function Invoke-ProcessActivate {
    Param(
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient] [Parameter(Mandatory = $true)] $CRMConn,
        [System.Object] [Parameter(Mandatory = $true)] $ProcessesToActivate,
        [ref] [Parameter(Mandatory = $true)] $ErrorCount,
        [switch] $CatchChildProcessActivationErrors
    )

    $ProcessesToRetry = @()
    $who = Invoke-CrmWhoAmI $CRMConn 
    [Guid]$assignToId = $who.UserId

    $ProcessesToActivate | ForEach-Object {
        $ProcessStore = $_
        try {
            $workflow = Get-CrmRecord -conn $CRMConn -EntityLogicalName "workflow" -Id $_.ProcessId -Fields "statecode", "name", "owninguser"
            Write-PPDOMessage "Checking Process '$($workflow.name)' - $($_.ProcessId) - Category $($_.Category)" -Type command -RunLocally $RunLocally
            $activateProcess = $false
            $assignProcess = $false
            if ($workflow.statecode -ne "Activated") {
                $activateProcess = $true
            }
            if (($_.Category -eq 0) -or ($_.Category -eq 3)) {
                if ($workflow.owninguser -ne $who.UserId) {
                    $activateProcess = $true
                    $assignProcess = $true
                }
            }
            
            if ($assignProcess -eq $true) {
                if ($workflow.statecode -eq "Activated") {
                    Write-Host "Process status is $($workflow.statecode), attempting to deactivate process."
                    try {
                        Set-CrmRecordState -conn $CRMConn -EntityLogicalName "workflow" -Id $_.ProcessId -StateCode "Draft" -StatusCode "Draft"
                        Write-Host "...Deactivated" -ForegroundColor Green
                    }
                    catch {
                        Write-Host "Error Deactivating process: " $_ -ForegroundColor Red
                    }
                }
                try {
                    Write-Host "Process '$($workflow.name)' assigned to $($workflow.owninguser). Assigning to $assignToId" -ForegroundColor Green
                    Set-CrmRecordOwner -conn $CRMConn -EntityLogicalName "workflow" -Id $_.ProcessId -PrincipalId $assignToId
                }
                catch {
                    Write-Host "Error Assigning process: " $_ -ForegroundColor Red
                }
            }

            if ($activateProcess) {
                Write-Host "Process status is $($workflow.statecode), attempting to activate process."
                $activationConnection = $CRMConn

                if ($_.ActivateAsUser) {
                    Write-Host "ActivateAsUser defined and set to: $($_.ActivateAsUser), attempting to activate process as this user"
                    $systemuserResult = Get-CrmRecords -conn $CRMConn -EntityLogicalName "systemuser" -FilterAttribute "domainname" -FilterOperator "eq" -FilterValue $_.ActivateAsUser -TopCount 1 -Fields "systemuserid"
                    if ($systemuserResult.Count -gt 0) {
                        $activationConnection.OrganizationWebProxyClient.CallerId = $systemuserResult.CrmRecords[0].systemuserid
                    }
                    else {
                        Write-PPDOMessage -Message "$($_.Exception.Message)" -Type "error" -RunLocally $RunLocally
                        Throw "User $($_.ActivateAsUser) was not found in $($Deploy.EnvironmentName), unable to impersonate them to activate process $($workflow.name)"
                    }
                }
                Write-PPDOMessage "Enabling Process '$($workflow.name)'" -Type command -RunLocally $RunLocally
                try {
                    Set-CrmRecordState -conn $activationConnection -EntityLogicalName "workflow" -Id $_.ProcessId -StateCode "Activated" -StatusCode "Activated"
                    Write-Host "...Activated" -ForegroundColor Green
                }
                catch {
                    if ($_.ToString().Contains("ChildProcessNeverPublished") -and $CatchChildProcessActivationErrors) {
                        $ProcessesToRetry += $ProcessStore
                    }
                    else {
                        Throw $_
                    }
                }
            }
        }
        catch {
            Write-PPDOMessage "$($_.Exception.Message)" -Type "error" -RunLocally $RunLocally
            $ErrorCount.Value++
        }
    }
    return $ProcessesToRetry
}


function Get-FlowsToBeDeployed {
    Param(
        [string] [Parameter(Mandatory = $true)] $StartPath,
        [array] [Parameter(Mandatory = $false)] $patchSolutionNames
    )
    try {
        Write-Host "Generating Flows_Default.json for activating post-deploy"
        $FlowLocations = @(Join-Path $StartPath "src\Workflows")
        $FlowJSON = @()
        $AddedFlows = @()

        # If there are patches
        if ($patchSolutionNames.Count -gt 0) {
            foreach ($PatchName in $patchSolutionNames) {
                $FlowLocations += (Join-Path $StartPath "Patches\$PatchName\Workflows")
            }
        }

        foreach ($FlowLocation in $FlowLocations) {
            $Flows = Get-ChildItem -Path $FlowLocation -Filter *.json -ErrorAction SilentlyContinue
            if ($Flows) {
                $Flows | ForEach-Object {
                    $FlowName = $_.BaseName.SubString(0, $_.BaseName.Length - 36)
                    $FlowID = $_.BaseName.Replace($FlowName, '')
                    if ($AddedFlows -notcontains $FlowID) {
                        $FlowJSON += @([ordered]@{FlowId = $FlowID; FlowName = $FlowName; ActivateAsUser = ""; })
                        $AddedFlows += $FlowID
                    }
                }
            }
        }

        if ($FlowJSON.Count -gt 0) {
            ConvertTo-Json -Depth 3 $FlowJSON | Format-Json | Out-FileUtf8NoBom $StartPath\Flows_Default.json
        }
    }
    catch {
        Write-Host $_
        pause
    }
}

function Get-ProcessesToBeDeployed {
    Param(
        [string] [Parameter(Mandatory = $true)] $StartPath,
        [array] [Parameter(Mandatory = $false)] $patchSolutionNames
    )
    try {
        Write-Host "Generating Processes_Default.json for activating post-deploy"
        $ProcessesLocations = @(Join-Path $StartPath "src\Workflows")
        $ProcessesJSON = @()
        $AddedProcessess = @()

        # If there are patches
        if ($patchSolutionNames.Count -gt 0) {
            foreach ($PatchName in $patchSolutionNames) {
                $ProcessesLocations += (Join-Path $StartPath "Patches\$PatchName\Workflows")
            }
        }
        
        foreach ($ProcessesLocation in $ProcessesLocations) {
            $Processes = Get-ChildItem -Path $ProcessesLocation -Filter "*xaml.data.xml" -ErrorAction SilentlyContinue
            if ($Processes) {
                $Processes | ForEach-Object {
                    $ProcessName = $_.BaseName.SubString(0, $_.BaseName.Length - 47)
                    $ProcessId = $_.BaseName.SubString($ProcessName.Length + 1, 36)
                    [xml]$ProcessXml = Get-Content $_.FullName
                    $category = [int]$ProcessXml.Workflow.Category
                    if ($AddedProcessess -notcontains $ProcessId) {
                        $ProcessesJSON += @([ordered]@{ProcessId = $ProcessId; ProcessName = $ProcessName; Category = $category; ActivateAsUser = ""; })
                        $AddedProcessess += $ProcessId
                    }
                }
            }
        }

        if ($ProcessesJSON.Count -gt 0) {
            ConvertTo-Json -Depth 3 $ProcessesJSON | Format-Json | Out-FileUtf8NoBom $StartPath\Processes_Default.json
        }
    }
    catch {
        Write-Host $_
        pause
    }
}