functions/invoke-d365runbookanalyzer.ps1


<#
    .SYNOPSIS
        Analyze the runbook
         
    .DESCRIPTION
        Get all the important details from a failed runbook
         
    .PARAMETER Path
        Path to the runbook file that you work against
         
    .EXAMPLE
        PS C:\> Invoke-D365RunbookAnalyzer -Path "C:\DynamicsAX\InstallationRecords\Runbooks\Runbook.xml"
         
        This will analyze the Runbook.xml and output all the details about failed steps, the connected error logs and all the unprocessed steps.
         
    .EXAMPLE
        PS C:\> Get-D365Runbook -Latest | Invoke-D365RunbookAnalyzer
         
        This will find the latest runbook file and have it analyzed by the Invoke-D365RunbookAnalyzer cmdlet to output any error details.
         
    .EXAMPLE
        PS C:\> Get-D365Runbook -Latest | Invoke-D365RunbookAnalyzer | Out-File "C:\Temp\d365fo.tools\runbook-analyze-results.xml"
         
        This will find the latest runbook file and have it analyzed by the Invoke-D365RunbookAnalyzer cmdlet to output any error details.
        The output will be saved into the "C:\Temp\d365fo.tools\runbook-analyze-results.xml" file.
         
    .EXAMPLE
        PS C:\> Get-D365Runbook -Latest | Backup-D365Runbook -Force | Invoke-D365RunbookAnalyzer
         
        This will get the latest runbook from the default location.
        This will backup the file onto the default "c:\temp\d365fo.tools\runbookbackups\".
        This will start the Runbook Analyzer on the backup file.
         
    .NOTES
        Tags: Runbook, Servicing, Hotfix, DeployablePackage, Deployable Package, InstallationRecordsDirectory, Installation Records Directory
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Invoke-D365RunbookAnalyzer {
    [CmdletBinding()]
    [OutputType('System.String')]
    param (
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)]
        [Alias('File')]
        [string] $Path
    )
    
    process {
        if (-not (Test-PathExists -Path $Path -Type Leaf)) { return }

        $null = $sb = New-Object System.Text.StringBuilder
        $null = $sb.AppendLine("<D365FO.Tools.Runbook.Analyzer.Output>")

        [xml]$xmlRunbook = Get-Content $Path

        $failedSteps = $xmlRunbook.SelectNodes("//RunbookStepList/Step/StepState[text()='Failed']")

        $failedSteps | ForEach-Object {
            $null = $sb.AppendLine("<FailedStepInfo>")

            $stepId = $_.ParentNode | Select-Object -ExpandProperty childnodes | Where-Object {$_.name -like 'ID'} | Select-Object -ExpandProperty InnerText
            $failedLogs = $xmlRunbook.SelectNodes("//RunbookLogs/Log/StepID[text()='$stepId']")

            $null = $sb.AppendLine($_.ParentNode.OuterXml)

            $failedLogs | ForEach-Object { $null = $sb.AppendLine( $_.ParentNode.OuterXml)}

            $null = $sb.AppendLine("</FailedStepInfo>")
        }
        
        $inProgressSteps = $xmlRunbook.SelectNodes("//RunbookStepList/Step/StepState[text()='InProgress']")

        $null = $sb.AppendLine("<InProgressStepInfo>")

        $inProgressSteps | ForEach-Object { $null = $sb.AppendLine( $_.ParentNode.OuterXml)}

        $null = $sb.AppendLine("</InProgressStepInfo>")

        $unprocessedSteps = $xmlRunbook.SelectNodes("//RunbookStepList/Step/StepState[text()='NotStarted']")

        $null = $sb.AppendLine("<UnprocessedStepInfo>")

        $unprocessedSteps | ForEach-Object { $null = $sb.AppendLine( $_.ParentNode.OuterXml)}

        $null = $sb.AppendLine("</UnprocessedStepInfo>")

        

        $null = $sb.AppendLine("</D365FO.Tools.Runbook.Analyzer.Output>")

        [xml]$xmlRaw = $sb.ToString()
        
        $stringWriter = New-Object System.IO.StringWriter;
        $xmlWriter = New-Object System.Xml.XmlTextWriter $stringWriter;
        $xmlWriter.Formatting = "indented";
        $xmlRaw.WriteTo($xmlWriter);
        $xmlWriter.Flush();
        $stringWriter.Flush();
        $stringWriter.ToString();
    }
}