Bulk unlist old NuGet package versions

Posted on by Bertrand Thomas
Nuget Dotnet Powershell

Keeping only the latest versions of your NuGet packages listed on the official NuGet gallery is easy to automate with a PowerShell function built on dotnet nuget delete.

$packages = @(
    "Withywoods.Configuration",
    "Withywoods.Net.Http",
    "Withywoods.System"
)

Unlist-OldNuGetVersions `
    -PackageIds $packages `
    -ApiKey "oy2xxxxx" `
    -KeepLatest 3

function Unlist-OldNuGetVersions {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $true)]
        [string[]] $PackageIds,

        [Parameter(Mandatory = $true)]
        [string] $ApiKey,

        [Parameter(Mandatory = $false)]
        [int] $KeepLatest = 3
    )

    begin {
        $source = "https://api.nuget.org/v3/index.json"
        Write-Host "Starting bulk unlist (hide) on nuget.org" -ForegroundColor Cyan
        Write-Host "Keeping newest $KeepLatest version(s) per package"
        Write-Host "Packages: $($PackageIds -join ', ')" -ForegroundColor Cyan
        Write-Host ""
    }

    process {
        foreach ($packageId in $PackageIds) {
            Write-Host "──────────────────────────────────────────────" -ForegroundColor DarkGray
            Write-Host "Processing: $packageId" -ForegroundColor Green

            $regUrl = "https://api.nuget.org/v3/registration5-gz-semver2/$($packageId.ToLower())/index.json"

            try {
                $index = Invoke-RestMethod -Uri $regUrl -UseBasicParsing -ErrorAction Stop

                # Collect versions
                $allVersions = @()
                foreach ($page in $index.items) {
                    if ($page.items) {
                        foreach ($pkg in $page.items) {
                            if ($pkg.catalogEntry -and $pkg.catalogEntry.version) {
                                $allVersions += $pkg.catalogEntry.version
                            }
                        }
                    }
                }

                if ($allVersions.Count -eq 0) {
                    Write-Warning "No versions found for $packageId → skipping"
                    continue
                }

                Write-Host "  Found $($allVersions.Count) versions" -ForegroundColor Cyan

                # Sort descending (strip pre-release for basic ordering)
                $sorted = $allVersions | Sort-Object {
                    [Version]($_ -replace '[-+].*$', '')
                } -Descending -Unique

                $toUnlist = $sorted | Select-Object -Skip $KeepLatest

                if ($toUnlist.Count -eq 0) {
                    Write-Host "  Nothing to unlist (≤ $KeepLatest versions)" -ForegroundColor Cyan
                    continue
                }

                Write-Host "  To unlist $($toUnlist.Count) older version(s):" -ForegroundColor Magenta
                $toUnlist | ForEach-Object { Write-Host "    - $_" }

                # Optional: uncomment if you want manual confirmation per package
                # $confirm = Read-Host "  Proceed for $packageId? (YES/no)"
                # if ($confirm -notin 'YES','yes','Y','y') { Write-Host "  Skipped."; continue }

                foreach ($version in $toUnlist) {
                    $target = "$packageId $version"
                    $action = "Unlist (hide) version on nuget.org"

                    if ($PSCmdlet.ShouldProcess($target, $action)) {
                        Write-Host "  Unlisting $target ..." -ForegroundColor Yellow
                        dotnet nuget delete $packageId $version `
                            --api-key $ApiKey `
                            --source $source `
                            --non-interactive

                        Start-Sleep -Seconds 1.8  # Rate limit buffer
                    }
                    else {
                        Write-Host "  Skipped $target (WhatIf / Confirm)" -ForegroundColor DarkYellow
                    }
                }

                Write-Host "  Done: $packageId" -ForegroundColor Green
            }
            catch {
                Write-Error "Failed for $packageId`: $($_.Exception.Message)"
                Write-Host "  URL: $regUrl" -ForegroundColor Red
            }
        }
    }

    end {
        Write-Host ""
        Write-Host "Bulk operation finished." -ForegroundColor Cyan
    }
}