Public/Get-WingetChangelog.ps1

function Get-WingetChangelog {
    <#
    .SYNOPSIS
        Show what changed between package versions (release notes diff).
 
    .DESCRIPTION
        Fetches release notes and changelog information between two versions
        of a package. Aggregates data from GitHub Releases, winget-pkgs commit
        history, and package manifest changes to answer: "What actually changed
        between v1.2 and v1.3?"
 
    .PARAMETER PackageId
        The package ID to check changelog for.
 
    .PARAMETER FromVersion
        Starting version (older). If omitted, uses installed version.
 
    .PARAMETER ToVersion
        Ending version (newer). If omitted, uses latest available.
 
    .PARAMETER ShowManifestDiff
        Also show changes to the winget manifest (installer URLs, switches, etc).
 
    .PARAMETER Limit
        Maximum number of versions to show in the changelog. Default: 10.
 
    .PARAMETER OpenInBrowser
        Open the GitHub releases page in the default browser.
 
    .EXAMPLE
        Get-WingetChangelog -PackageId "Microsoft.VisualStudioCode"
        Shows recent version changes for VS Code.
 
    .EXAMPLE
        Get-WingetChangelog -PackageId "Git.Git" -FromVersion "2.43.0" -ToVersion "2.45.0"
        Shows what changed between Git 2.43 and 2.45.
 
    .EXAMPLE
        Get-WingetChangelog -PackageId "Python.Python.3.12" -ShowManifestDiff
        Shows version history plus manifest changes.
 
    .NOTES
        Author: Matthew Bubb
        Data sourced from winget-pkgs GitHub repository commit history.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [Alias('Id')]
        [string]$PackageId,

        [string]$FromVersion,

        [string]$ToVersion,

        [switch]$ShowManifestDiff,

        [ValidateRange(1, 50)]
        [int]$Limit = 10,

        [switch]$OpenInBrowser
    )

    process {

        # --- Resolve versions ---
        $installedVersion = $null
        $latestVersion = $null

        try {
            $pkg = Microsoft.WinGet.Client\Get-WinGetPackage -Id $PackageId -MatchOption EqualsCaseInsensitive -ErrorAction SilentlyContinue | Select-Object -First 1
            if ($pkg) {
                $installedVersion = $pkg.InstalledVersion
                if ($pkg.AvailableVersions -and $pkg.AvailableVersions.Count -gt 0) {
                    $latestVersion = $pkg.AvailableVersions[0]
                }
            }
        } catch { }

        # Default to "what changed since my version" only when there is something newer;
        # otherwise show the most recent $Limit versions
        if (-not $FromVersion -and $installedVersion -and $pkg.IsUpdateAvailable) { $FromVersion = $installedVersion }

        Write-Host ""
        Write-Host " Package Changelog" -ForegroundColor Cyan
        Write-Host ""
        Write-Host " Package: " -NoNewline -ForegroundColor DarkGray; Write-Host $PackageId -ForegroundColor White
        if ($installedVersion) {
            Write-Host " Installed:" -NoNewline -ForegroundColor DarkGray; Write-Host " $installedVersion" -ForegroundColor Yellow
        }
        if ($latestVersion) {
            Write-Host " Latest: " -NoNewline -ForegroundColor DarkGray; Write-Host " $latestVersion" -ForegroundColor Green
        }
        Write-Host ""

        $packagePath = Get-WingetPkgsPackagePath -PackageId $PackageId
        $repoUrl = "https://github.com/microsoft/winget-pkgs/tree/master/$packagePath"

        try {
            $sortedVersions = @(Get-WingetPkgsVersions -PackageId $PackageId)
            if ($sortedVersions.Count -eq 0) { throw "No versions found in winget-pkgs." }
            if (-not $latestVersion) { $latestVersion = $sortedVersions[0].Name }

            # Keep versions inside the requested range (either bound may be omitted)
            $inRange = @($sortedVersions | Where-Object {
                (-not $FromVersion -or (Compare-WingetVersion -ReferenceVersion $_.Name -DifferenceVersion $FromVersion) -ge 0) -and
                (-not $ToVersion -or (Compare-WingetVersion -ReferenceVersion $_.Name -DifferenceVersion $ToVersion) -le 0)
            })
            if ($inRange.Count -eq 0) { $inRange = $sortedVersions }

            $displayVersions = @($inRange | Select-Object -First $Limit)

            if (-not (Get-WingetBatchGitHubToken) -and $displayVersions.Count -gt 10) {
                Write-Warning "Showing $($displayVersions.Count) versions uses about $($displayVersions.Count + 1) GitHub API requests (60/hour without a token)."
            }

            Write-Host " Version History ($($displayVersions.Count) of $($sortedVersions.Count) total):" -ForegroundColor White
            Write-Host " $('─' * 55)" -ForegroundColor DarkGray

            $prevManifest = $null
            # Oldest first when diffing so each entry shows what it changed
            foreach ($ver in $displayVersions) {
                $versionName = $ver.Name
                $isInstalled = $installedVersion -and (Compare-WingetVersion -ReferenceVersion $versionName -DifferenceVersion $installedVersion) -eq 0
                $isLatest = $latestVersion -and (Compare-WingetVersion -ReferenceVersion $versionName -DifferenceVersion $latestVersion) -eq 0

                $marker = if ($isInstalled -and $isLatest) { "◆ " }
                          elseif ($isInstalled) { "● " }
                          elseif ($isLatest) { "★ " }
                          else { " " }

                $color = if ($isInstalled) { 'Yellow' } elseif ($isLatest) { 'Green' } else { 'White' }
                Write-Host " $marker" -NoNewline -ForegroundColor $color
                Write-Host "v$versionName" -NoNewline -ForegroundColor $color
                if ($isInstalled) { Write-Host " (installed)" -NoNewline -ForegroundColor DarkYellow }
                if ($isLatest) { Write-Host " (latest)" -NoNewline -ForegroundColor DarkGreen }
                Write-Host ""

                $manifest = $null
                try {
                    $manifest = Get-WingetPkgsManifest -PackageId $PackageId -Version $versionName
                } catch {
                    $status = [int]$_.Exception.Response.StatusCode
                    if ($status -in 403, 429) {
                        Write-Warning "GitHub rate limit reached. Run New-WingetBatchGitHubToken for 5,000 requests/hour."
                        break
                    }
                    Write-Verbose "Could not fetch details for ${versionName}: $_"
                }

                if ($manifest) {
                    # Release notes live in the locale manifest
                    $notes = Get-WingetYamlValue -Yaml $manifest.Locale -Key 'ReleaseNotes'
                    $notesUrl = Get-WingetYamlValue -Yaml $manifest.Locale -Key 'ReleaseNotesUrl'
                    if ($notes) {
                        $notes = ($notes -replace '\s+', ' ').Trim()
                        if ($notes.Length -gt 160) { $notes = $notes.Substring(0, 157) + "..." }
                        Write-Host " $notes" -ForegroundColor DarkGray
                    }
                    if ($notesUrl) {
                        Write-Host " Notes: $notesUrl" -ForegroundColor DarkGray
                    }

                    # Manifest diff against the next-older displayed version
                    if ($ShowManifestDiff -and $prevManifest -and $manifest.Installer -and $prevManifest.Installer) {
                        $diffChanges = @()
                        $urlsNew = @([regex]::Matches($manifest.Installer, 'InstallerUrl:\s*(\S+)') | ForEach-Object { $_.Groups[1].Value })
                        $urlsOld = @([regex]::Matches($prevManifest.Installer, 'InstallerUrl:\s*(\S+)') | ForEach-Object { $_.Groups[1].Value })
                        $hostsNew = @($urlsNew | ForEach-Object { ([uri]$_).Host } | Select-Object -Unique)
                        $hostsOld = @($urlsOld | ForEach-Object { ([uri]$_).Host } | Select-Object -Unique)
                        if (Compare-Object $hostsNew $hostsOld) { $diffChanges += "Download host changed: $($hostsOld -join ', ') -> $($hostsNew -join ', ')" }

                        $typesNew = @([regex]::Matches($manifest.Installer, 'InstallerType:\s*(\w+)') | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique)
                        $typesOld = @([regex]::Matches($prevManifest.Installer, 'InstallerType:\s*(\w+)') | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique)
                        if (Compare-Object $typesNew $typesOld) { $diffChanges += "Installer type: $($typesOld -join ', ') -> $($typesNew -join ', ')" }

                        $archNew = @([regex]::Matches($manifest.Installer, 'Architecture:\s*(\w+)') | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique)
                        $archOld = @([regex]::Matches($prevManifest.Installer, 'Architecture:\s*(\w+)') | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique)
                        $added = @($archNew | Where-Object { $_ -notin $archOld })
                        $dropped = @($archOld | Where-Object { $_ -notin $archNew })
                        if ($added) { $diffChanges += "Architectures added: $($added -join ', ')" }
                        if ($dropped) { $diffChanges += "Architectures dropped: $($dropped -join ', ')" }

                        $depsNew = @([regex]::Matches($manifest.Installer, 'PackageIdentifier:\s*(\S+)') | ForEach-Object { $_.Groups[1].Value } | Where-Object { $_ -ne $PackageId })
                        $depsOld = @([regex]::Matches($prevManifest.Installer, 'PackageIdentifier:\s*(\S+)') | ForEach-Object { $_.Groups[1].Value } | Where-Object { $_ -ne $PackageId })
                        if (Compare-Object $depsNew $depsOld) { $diffChanges += "Dependencies: $(@($depsOld) -join ', ') -> $(@($depsNew) -join ', ')" }

                        foreach ($change in $diffChanges) {
                            # Changes are relative to the next-newer version shown above
                            Write-Host " Δ (vs newer) $change" -ForegroundColor DarkCyan
                        }
                    }
                    $prevManifest = $manifest
                }
            }

            Write-Host ""
            Write-Host " Legend: ◆ installed+latest | ● installed | ★ latest" -ForegroundColor DarkGray
            Write-Host " Source: $repoUrl" -ForegroundColor DarkGray
            Write-Host ""

            if ($OpenInBrowser) {
                Start-Process $repoUrl
            }
        }
        catch {
            $status = [int]$_.Exception.Response.StatusCode
            if ($status -eq 404) {
                Write-Error "$PackageId was not found in winget-pkgs (it may come from another source)."
            }
            elseif ($status -in 403, 429) {
                Write-Error "GitHub rate limit reached. Run New-WingetBatchGitHubToken for 5,000 requests/hour."
            }
            else {
                Write-Error "Could not fetch version history for ${PackageId}: $($_.Exception.Message)"
            }
        }
    }
}