Private/Get-AvmChangelog.ps1

function Get-AvmChangelog {
    <#
    .SYNOPSIS
        Fetches changelog/release-note lines mentioning breaking changes between two versions.
    #>

    [CmdletBinding()]
    param(
        [PSCustomObject]$Reference,
        [string]$TargetVersion,
        [int]$MaxLines = 10
    )

    $result = [PSCustomObject]@{
        available = $false
        lines     = @()
        source    = $null
    }

    try {
        if ($Reference.ecosystem -eq 'bicep') {
            $changelogUrl = "https://raw.githubusercontent.com/Azure/bicep-registry-modules/main/avm/$($Reference.category)/$($Reference.group)/$($Reference.module)/CHANGELOG.md"
            $content = Invoke-AvmGitHubRequest -Uri $changelogUrl
            if ($content) {
                $result.source    = $changelogUrl
                $result.available = $true
                $result.lines     = ExtractBreakingLines -Content $content -MaxLines $MaxLines
            }
        } elseif ($Reference.ecosystem -eq 'terraform') {
            $repoName = $Reference.module -replace '^avm-', 'terraform-azurerm-avm-'
            $releasesUrl = "https://api.github.com/repos/Azure/$repoName/releases"
            $releases = Invoke-AvmGitHubRequest -Uri $releasesUrl

            if ($releases) {
                $curVer = [System.Version]($Reference.currentVersion -replace '^v|~>\s*', '')
                $tgtVer = [System.Version]($TargetVersion -replace '^v', '')

                $relevantBodies = $releases |
                    Where-Object {
                        $tag = $_.tag_name -replace '^v', ''
                        try {
                            $v = [System.Version]$tag
                            $v -gt $curVer -and $v -le $tgtVer
                        } catch { $false }
                    } |
                    ForEach-Object { $_.body }

                $combined         = $relevantBodies -join "`n"
                $result.source    = $releasesUrl
                $result.available = $true
                $result.lines     = ExtractBreakingLines -Content $combined -MaxLines $MaxLines
            }
        }
    } catch {
        Write-Verbose "Changelog unavailable for $($Reference.registryPath): $_"
    }

    return $result
}

function ExtractBreakingLines {
    param([string]$Content, [int]$MaxLines = 10)

    $lines   = $Content -split "`n"
    $results = [System.Collections.Generic.List[string]]::new()
    $inBreakingSection = $false

    foreach ($line in $lines) {
        # Detect a Breaking Changes section heading
        if ($line -match '^#{1,4}\s*(BREAKING CHANGE|Breaking Changes)') {
            $inBreakingSection = $true
            continue
        }
        # Stop at the next same-or-higher-level heading
        if ($inBreakingSection -and $line -match '^#{1,4}\s+' -and $line -notmatch '(?i)breaking') {
            $inBreakingSection = $false
        }
        # Collect non-empty content lines within the section
        if ($inBreakingSection -and $line.Trim() -ne '') {
            $results.Add($line.Trim())
            if ($results.Count -ge $MaxLines) { break }
        }
        # Also capture inline BREAKING CHANGE markers outside sections
        if (-not $inBreakingSection -and $line -match '(?i)BREAKING CHANGE') {
            $results.Add($line.Trim())
            if ($results.Count -ge $MaxLines) { break }
        }
    }

    # If nothing found in sections, fall back to lines mentioning removal/rename
    if ($results.Count -eq 0) {
        $results.AddRange(
            @($lines | Where-Object { $_ -match '(?i)(remov|renam|deprecat|incompatible)' } |
              Select-Object -First $MaxLines)
        )
    }

    return $results.ToArray()
}