src/Solutions/Customization/Ribbon/Get-XrmRibbon.ps1

<#
    .SYNOPSIS
    Retrieve the ribbon customizations (RibbonDiffXml) of several tables with one solution export.

    .DESCRIPTION
    Export an unmanaged solution holding the tables and read the RibbonDiffXml of each one in customizations.xml.
    Without SolutionUniqueName, a temporary solution holding the tables is created with the publisher given by PublisherUniqueName (or the organization default publisher), exported, then removed, even on failure.
    Returns one object per table: EntityLogicalName, RibbonDiffXml (XmlElement, $null when the table has none).

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (Dataverse ServiceClient)

    .PARAMETER EntityLogicalName
    Logical names of the tables.

    .PARAMETER SolutionUniqueName
    Existing unmanaged solution holding the tables, exported instead of a temporary one.

    .PARAMETER PublisherUniqueName
    Publisher of the temporary solution. Ignored when SolutionUniqueName is given. (Default: organization default publisher)

    .PARAMETER OutputPath
    Work folder for the export. (Default: TEMP folder)

    .OUTPUTS
    PSCustomObject[]. EntityLogicalName, RibbonDiffXml.

    .EXAMPLE
    $ribbons = Get-XrmRibbon -XrmClient $xrmClient -EntityLogicalName "account", "contact", "lead";
    $ribbons | ForEach-Object { "$($_.EntityLogicalName): $($_.RibbonDiffXml.CustomActions.ChildNodes.Count) custom actions" };

    .LINK
    https://github.com/AymericM78/PowerDataOps/blob/main/documentation/commands/Get-XrmRibbon.md
#>

function Get-XrmRibbon {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $EntityLogicalName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SolutionUniqueName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $PublisherUniqueName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutputPath = [System.IO.Path]::GetTempPath()
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        $useTempSolution = (-not $PSBoundParameters.ContainsKey('SolutionUniqueName'));
        $tempSolutionName = "RibbonExport_$([Guid]::NewGuid().ToString('N').Substring(0, 8))";
        $tempSolutionCreated = $false;
        $solutionFilePath = $null;
        $extractPath = $null;
        $exportedSolution = $SolutionUniqueName;
        try {
            if ($useTempSolution) {
                if ($PSBoundParameters.ContainsKey('PublisherUniqueName')) {
                    $publisher = $XrmClient | Get-XrmPublisher -PublisherUniqueName $PublisherUniqueName;
                    if (-not $publisher) {
                        throw "Publisher '$PublisherUniqueName' not found.";
                    }
                    $publisherRef = $publisher.Reference;
                }
                else {
                    $defaultSolution = $XrmClient | Get-XrmSolution -SolutionUniqueName "Default" -Columns "publisherid";
                    $publisherRef = $defaultSolution.publisherid_Value;
                }
                $exportedSolution = $tempSolutionName;
                $XrmClient | Add-XrmSolution -DisplayName $tempSolutionName -UniqueName $tempSolutionName -PublisherReference $publisherRef | Out-Null;
                $tempSolutionCreated = $true;
                foreach ($tableName in $EntityLogicalName) {
                    $entityMetadata = $XrmClient | Get-XrmEntityMetadata -LogicalName $tableName -Filter Entity;
                    $XrmClient | Add-XrmSolutionComponent -SolutionUniqueName $tempSolutionName -ComponentId $entityMetadata.MetadataId -ComponentType 1 -DoNotIncludeSubcomponents $true | Out-Null;
                }
            }

            $solutionFilePath = $XrmClient | Export-XrmSolution -SolutionUniqueName $exportedSolution -Managed $false -ExportPath $OutputPath;
            $extractPath = Join-Path $OutputPath "RibbonExtract_$([Guid]::NewGuid().ToString('N').Substring(0, 8))";
            Expand-Archive -Path $solutionFilePath -DestinationPath $extractPath -Force;
            $customizationsPath = Join-Path $extractPath "customizations.xml";
            if (-not (Test-Path $customizationsPath)) {
                throw "customizations.xml not found in exported solution.";
            }
            [xml]$customizationsXml = Get-Content -Path $customizationsPath -Raw;

            foreach ($tableName in $EntityLogicalName) {
                $entityNode = $customizationsXml.ImportExportXml.Entities.Entity | Where-Object { $_.EntityInfo.entity.Name -eq $tableName };
                if (-not $entityNode) {
                    throw "Entity '$tableName' not found in the exported solution '$exportedSolution'.";
                }
                [PSCustomObject]@{
                    EntityLogicalName = $tableName;
                    RibbonDiffXml     = $entityNode.SelectSingleNode("RibbonDiffXml");
                };
            }
        }
        finally {
            # Cleanup temp solution: a cleanup failure is reported but does not hide the original error
            if ($tempSolutionCreated) {
                try {
                    $XrmClient | Uninstall-XrmSolution -SolutionUniqueName $tempSolutionName;
                }
                catch {
                    Write-Warning "Temporary solution '$tempSolutionName' was not removed: $($_.Exception.Message)";
                }
            }
            if ($extractPath -and (Test-Path $extractPath)) {
                Remove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue;
            }
            if ($useTempSolution -and $solutionFilePath -and (Test-Path $solutionFilePath)) {
                Remove-Item -Path $solutionFilePath -Force -ErrorAction SilentlyContinue;
            }
        }
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Get-XrmRibbon -Alias *;

Register-ArgumentCompleter -CommandName Get-XrmRibbon -ParameterName "EntityLogicalName" -ScriptBlock {
    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)
    $validLogicalNames = Get-XrmEntitiesLogicalName;
    return $validLogicalNames | Where-Object { $_ -like "$wordToComplete*" };
}