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
    )

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

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

    if (-not $FromVersion) { $FromVersion = $installedVersion }
    if (-not $ToVersion) { $ToVersion = $latestVersion }

    # --- Fetch version history from winget-pkgs ---
    $headers = @{ 'User-Agent' = 'WingetBatch-Changelog' }
    $token = Get-WingetBatchGitHubToken -ErrorAction SilentlyContinue
    if ($token) { $headers['Authorization'] = "Bearer $token" }

    $idParts = $PackageId.Split('.')
    $publisher = $idParts[0]
    $firstLetter = $publisher[0].ToString().ToLower()
    $packagePath = $idParts -join '/'
    $apiBase = "https://api.github.com/repos/microsoft/winget-pkgs/contents"
    $manifestDir = "$apiBase/manifests/$firstLetter/$packagePath"

    Write-Host ""
    Write-Host " ╔══════════════════════════════════════════════════════╗" -ForegroundColor Cyan
    Write-Host " ║ Package Changelog ║" -ForegroundColor Cyan
    Write-Host " ╚══════════════════════════════════════════════════════╝" -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 ""

    try {
        $versions = Invoke-RestMethod -Uri $manifestDir -Headers $headers -ErrorAction Stop

        # Sort versions
        $sortedVersions = $versions | Sort-Object { 
            try { [version]($_.name -replace '[^0-9.]', '') } catch { [version]'0.0.0' }
        } -Descending

        # Filter to range if specified
        $inRange = $sortedVersions
        if ($FromVersion -and $ToVersion) {
            $inRange = $sortedVersions | Where-Object {
                try {
                    $v = [version]($_.name -replace '[^0-9.]', '')
                    $from = [version]($FromVersion -replace '[^0-9.]', '')
                    $to = [version]($ToVersion -replace '[^0-9.]', '')
                    $v -ge $from -and $v -le $to
                } catch { $true }
            }
        }

        $displayVersions = $inRange | Select-Object -First $Limit

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

        $prevVersion = $null
        foreach ($ver in $displayVersions) {
            $versionName = $ver.name
            $isInstalled = ($versionName -eq $installedVersion)
            $isLatest = ($versionName -eq $latestVersion)

            # Version line
            $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 ""

            # Fetch commit info for this version (date + author)
            try {
                $versionUrl = "$manifestDir/$versionName"
                $versionFiles = Invoke-RestMethod -Uri $versionUrl -Headers $headers -ErrorAction SilentlyContinue
                if ($versionFiles -and $versionFiles.Count -gt 0) {
                    # Get the default manifest for release notes
                    $defaultManifest = $versionFiles | Where-Object { $_.name -match '\.yaml$' -and $_.name -notmatch 'installer|locale' } | Select-Object -First 1
                    if (-not $defaultManifest) { $defaultManifest = $versionFiles | Where-Object { $_.name -match 'locale.*en-US' } | Select-Object -First 1 }
                    if (-not $defaultManifest) { $defaultManifest = $versionFiles | Select-Object -First 1 }

                    if ($defaultManifest) {
                        $content = Invoke-RestMethod -Uri $defaultManifest.url -Headers $headers -ErrorAction SilentlyContinue
                        $rawContent = $content.content
                        if ($content.encoding -eq 'base64') {
                            $rawContent = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($rawContent))
                        }

                        # Extract release notes
                        if ($rawContent -match 'ReleaseNotes:\s*(.+?)(?:\n\S|\n\s*\w+:|\z)') {
                            $notes = $Matches[1].Trim() -replace '\s+', ' '
                            if ($notes.Length -gt 120) { $notes = $notes.Substring(0, 117) + "..." }
                            Write-Host " $notes" -ForegroundColor DarkGray
                        }
                        elseif ($rawContent -match 'ReleaseNotesUrl:\s*(.+)') {
                            Write-Host " Notes: $($Matches[1].Trim())" -ForegroundColor DarkGray
                        }
                    }
                }
            } catch {
                Write-Verbose "Could not fetch details for $versionName"
            }

            # Manifest diff
            if ($ShowManifestDiff -and $prevVersion) {
                try {
                    $diffChanges = @()
                    # Compare installer manifests between versions
                    $prevFiles = Invoke-RestMethod -Uri "$manifestDir/$($prevVersion.name)" -Headers $headers -ErrorAction SilentlyContinue
                    $currFiles = $versionFiles

                    $prevInstaller = $prevFiles | Where-Object { $_.name -match 'installer' } | Select-Object -First 1
                    $currInstaller = $currFiles | Where-Object { $_.name -match 'installer' } | Select-Object -First 1

                    if ($prevInstaller -and $currInstaller) {
                        $prevContent = Invoke-RestMethod -Uri $prevInstaller.url -Headers $headers -ErrorAction SilentlyContinue
                        $currContent = Invoke-RestMethod -Uri $currInstaller.url -Headers $headers -ErrorAction SilentlyContinue

                        $prevRaw = if ($prevContent.encoding -eq 'base64') { [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($prevContent.content)) } else { $prevContent.content }
                        $currRaw = if ($currContent.encoding -eq 'base64') { [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($currContent.content)) } else { $currContent.content }

                        # Check for installer URL changes
                        $prevUrl = if ($prevRaw -match 'InstallerUrl:\s*(.+)') { $Matches[1].Trim() } else { '' }
                        $currUrl = if ($currRaw -match 'InstallerUrl:\s*(.+)') { $Matches[1].Trim() } else { '' }
                        if ($prevUrl -ne $currUrl -and $prevUrl -and $currUrl) {
                            $diffChanges += "Installer URL changed"
                        }

                        # Check for new installer types
                        $prevTypes = [regex]::Matches($prevRaw, 'InstallerType:\s*(\w+)') | ForEach-Object { $_.Groups[1].Value }
                        $currTypes = [regex]::Matches($currRaw, 'InstallerType:\s*(\w+)') | ForEach-Object { $_.Groups[1].Value }
                        $newTypes = $currTypes | Where-Object { $_ -notin $prevTypes }
                        if ($newTypes) { $diffChanges += "New installer type: $($newTypes -join ', ')" }
                    }

                    if ($diffChanges.Count -gt 0) {
                        foreach ($change in $diffChanges) {
                            Write-Host " Δ $change" -ForegroundColor DarkCyan
                        }
                    }
                } catch { }
            }

            $prevVersion = $ver
        }

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

        # GitHub releases link
        $repoUrl = "https://github.com/microsoft/winget-pkgs/tree/master/manifests/$firstLetter/$packagePath"
        Write-Host " Source: $repoUrl" -ForegroundColor DarkGray
        Write-Host ""

        if ($OpenInBrowser) {
            Start-Process $repoUrl
        }
    }
    catch {
        Write-Error "Could not fetch version history for ${PackageId}: $($_.Exception.Message)"
    }
}