Public/Remove-EvergreenLibraryAppVersion.ps1

function Remove-EvergreenLibraryAppVersion {
    <#
        .EXTERNALHELP Evergreen-help.xml
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName,
            HelpMessage = "Specify the path to the library.",
            ParameterSetName = "Path")]
        [ValidateNotNull()]
        [System.IO.FileInfo] $Path,

        [Parameter(Mandatory = $false, Position = 1)]
        [ValidateRange(1, [System.Int32]::MaxValue)]
        [System.Int32] $Keep = 3,

        [Parameter(Mandatory = $false, Position = 2)]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $Name
    )

    begin {
    }

    process {
        Write-Verbose -Message "$($MyInvocation.MyCommand): Validating library path '$Path'. Keep: $Keep."
        if (-not (Test-Path -Path $Path -PathType "Container")) {
            $Msg = "Cannot use path $Path because it does not exist or is not a directory."
            throw [System.IO.DirectoryNotFoundException]::New($Msg)
        }

        $LibraryFile = Join-Path -Path $Path -ChildPath "EvergreenLibrary.json"
        if (-not (Test-Path -Path $LibraryFile -PathType "Leaf")) {
            $Msg = "$Path is not an Evergreen Library. Cannot find EvergreenLibrary.json. Create a library with New-EvergreenLibrary."
            throw [System.IO.FileNotFoundException]::New($Msg)
        }

        Write-Verbose -Message "$($MyInvocation.MyCommand): Reading library manifest '$LibraryFile'."
        $Library = Get-Content -Path $LibraryFile -ErrorAction "Stop" | ConvertFrom-Json -ErrorAction "Stop"

        $TargetApplications = $Library.Applications
        if ($PSBoundParameters.ContainsKey("Name")) {
            Write-Verbose -Message "$($MyInvocation.MyCommand): Filtering applications by name: $($Name -join ', ')."
            $TargetApplications = $Library.Applications | Where-Object { $_.Name -in $Name }
        }
        Write-Verbose -Message "$($MyInvocation.MyCommand): Selected $(@($TargetApplications).Count) of $(@($Library.Applications).Count) applications."

        foreach ($Application in $TargetApplications) {
            $AppPath = Join-Path -Path $Path -ChildPath $Application.Name
            $AppManifest = Join-Path -Path $AppPath -ChildPath "$($Application.Name).json"
            Write-Verbose -Message "$($MyInvocation.MyCommand): Processing '$($Application.Name)'. App manifest: '$AppManifest'."

            if (-not (Test-Path -Path $AppManifest -PathType "Leaf")) {
                Write-Verbose -Message "$($MyInvocation.MyCommand): Skipping '$($Application.Name)' because its app manifest is missing."
                Write-Warning -Message "$($MyInvocation.MyCommand): App manifest missing: $AppManifest"
                continue
            }

            try {
                Write-Verbose -Message "$($MyInvocation.MyCommand): Reading app manifest '$AppManifest'."
                [System.Array] $AppVersions = @(Get-Content -Path $AppManifest -ErrorAction "Stop" | ConvertFrom-Json -ErrorAction "Stop")
            }
            catch {
                Write-Verbose -Message "$($MyInvocation.MyCommand): Skipping '$($Application.Name)' because its app manifest could not be read."
                Write-Warning -Message "$($MyInvocation.MyCommand): Failed reading $AppManifest with: $($_.Exception.Message)"
                continue
            }
            Write-Verbose -Message "$($MyInvocation.MyCommand): Found $($AppVersions.Count) version entries for '$($Application.Name)'."

            if ($AppVersions.Count -le $Keep) {
                Write-Verbose -Message "$($MyInvocation.MyCommand): No pruning required for '$($Application.Name)': $($AppVersions.Count) entries is within the keep limit of $Keep. Installers and manifest unchanged."
                Write-Output -InputObject ([PSCustomObject] @{
                        ApplicationName = $Application.Name
                        Keep            = $Keep
                        KeptCount       = $AppVersions.Count
                        RemovedCount    = 0
                        RemovedFiles    = @()
                        ManifestPath    = $AppManifest
                    })
                continue
            }

            Write-Verbose -Message "$($MyInvocation.MyCommand): Sorting versions by parsed version, version text, then manifest index (descending). Entries with parsed versions take priority."
            $ItemsWithSortData = for ($i = 0; $i -lt $AppVersions.Count; $i++) {
                Get-VersionItemSortData -Item $AppVersions[$i] -Index $i
            }

            $SortedItems = $ItemsWithSortData | Sort-Object -Property `
            @{ Expression = { $_.HasParsedVersion }; Descending = $true }, `
            @{ Expression = { $_.ParsedVersion }; Descending = $true }, `
            @{ Expression = { $_.VersionText }; Descending = $true }, `
            @{ Expression = { $_.Index }; Descending = $true }

            [System.Array] $RetainedItems = @($SortedItems | Select-Object -First $Keep)
            [System.Array] $PrunedItems = @($SortedItems | Select-Object -Skip $Keep)
            Write-Verbose -Message "$($MyInvocation.MyCommand): Retaining $($RetainedItems.Count) entries and pruning $($PrunedItems.Count) entries for '$($Application.Name)'."
            foreach ($Retained in $RetainedItems) {
                Write-Verbose -Message "$($MyInvocation.MyCommand): Retaining version '$($Retained.VersionText)' (manifest index $($Retained.Index), parsed version: $($Retained.HasParsedVersion)). Installer: '$($Retained.Item.Path)'."
            }

            $RemovedFiles = New-Object -TypeName "System.Collections.ArrayList"
            foreach ($Pruned in $PrunedItems) {
                Write-Verbose -Message "$($MyInvocation.MyCommand): Pruning version '$($Pruned.VersionText)' (manifest index $($Pruned.Index), parsed version: $($Pruned.HasParsedVersion)). Installer: '$($Pruned.Item.Path)'."
                if ([System.String]::IsNullOrEmpty($Pruned.Item.Path)) {
                    Write-Verbose -Message "$($MyInvocation.MyCommand): Skipping installer removal for version '$($Pruned.VersionText)': no installer path is recorded."
                    continue
                }

                if (-not (Test-Path -Path $Pruned.Item.Path -PathType "Leaf")) {
                    Write-Verbose -Message "$($MyInvocation.MyCommand): Skipping installer removal: '$($Pruned.Item.Path)' does not exist or is not a file."
                    continue
                }

                if ($PSCmdlet.ShouldProcess($Pruned.Item.Path, "Remove old installer")) {
                    Write-Verbose -Message "$($MyInvocation.MyCommand): Removing installer '$($Pruned.Item.Path)'."
                    Remove-Item -Path $Pruned.Item.Path -Force -ErrorAction "Stop"
                    $RemovedFiles.Add($Pruned.Item.Path) | Out-Null
                    Write-Verbose -Message "$($MyInvocation.MyCommand): Removed installer '$($Pruned.Item.Path)'."
                }
                else {
                    Write-Verbose -Message "$($MyInvocation.MyCommand): Installer removal not approved by ShouldProcess (WhatIf or confirmation declined): '$($Pruned.Item.Path)'."
                }
            }

            [System.Array] $RetainedObjects = @($RetainedItems | Select-Object -ExpandProperty "Item")
            if ($PSCmdlet.ShouldProcess($AppManifest, "Update retained versions")) {
                Write-Verbose -Message "$($MyInvocation.MyCommand): Writing $($RetainedObjects.Count) retained entries to '$AppManifest'."
                $RetainedObjects | ConvertTo-Json -ErrorAction "Stop" | Out-File -FilePath $AppManifest -Encoding "Utf8" -NoNewline -ErrorAction "Stop"
                Write-Verbose -Message "$($MyInvocation.MyCommand): Updated app manifest '$AppManifest'."
            }
            else {
                Write-Verbose -Message "$($MyInvocation.MyCommand): Manifest update not approved by ShouldProcess (WhatIf or confirmation declined): '$AppManifest'."
            }

            Write-Verbose -Message "$($MyInvocation.MyCommand): Completed '$($Application.Name)': $($RetainedObjects.Count) entries selected for retention, $($PrunedItems.Count) entries selected for pruning, $($RemovedFiles.Count) installer files removed."
            Write-Output -InputObject ([PSCustomObject] @{
                    ApplicationName = $Application.Name
                    Keep            = $Keep
                    KeptCount       = $RetainedObjects.Count
                    RemovedCount    = $PrunedItems.Count
                    RemovedFiles    = $RemovedFiles
                    ManifestPath    = $AppManifest
                })
        }
    }
}