Private/GitHub/Test-CgrGitHubReleaseMigration.ps1

function Test-CgrGitHubReleaseMigration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [psobject] $SourceRepository,

        [Parameter(Mandatory)]
        [psobject] $DestinationRepository,

        [string[]] $ReleaseTag,

        [string[]] $ReleaseExcludeTag,

        [switch] $IncludePrerelease,

        [switch] $IncludeDraftReleases,

        [ValidateRange(1, [int]::MaxValue)]
        [int] $ReleaseCount,

        [ValidateNotNullOrEmpty()]
        [string] $HostName = 'github.com'
    )

    $selectionParameters = @{
        Repository = $SourceRepository
        ReleaseTag = $ReleaseTag
        ReleaseExcludeTag = $ReleaseExcludeTag
        IncludePrerelease = $IncludePrerelease
        IncludeDraftReleases = $IncludeDraftReleases
        HostName = $HostName
    }
    if ($PSBoundParameters.ContainsKey('ReleaseCount')) {
        $selectionParameters.ReleaseCount = $ReleaseCount
    }

    $sourceSelection = Get-CgrGitHubReleaseSelection @selectionParameters
    $releaseResults = [System.Collections.Generic.List[object]]::new()
    $checks = [System.Collections.Generic.List[object]]::new()

    foreach ($sourceRelease in @($sourceSelection.Releases)) {
        $tagName = [string] $sourceRelease.TagName
        $escapedTag = [uri]::EscapeDataString($tagName)
        $mismatches = [System.Collections.Generic.List[string]]::new()

        $destinationCommitResult = Invoke-CgrGitHubApiReadRequest -ArgumentList @(
            'api', '--hostname', $HostName,
            "repos/$($DestinationRepository.FullName)/commits/$escapedTag", '--jq', '.sha'
        )
        $destinationCommitSha = if ($destinationCommitResult.ExitCode -eq 0) {
            [string] @($destinationCommitResult.Output)[0]
        }
        elseif ([string] $destinationCommitResult.ErrorText -match '(?i)404|not found') {
            $null
        }
        else {
            $diagnostic = Protect-CgrDiagnosticText -Text ([string] $destinationCommitResult.ErrorText)
            $message = "Unable to read destination tag commit '$tagName' while verifying GitHub Releases. $diagnostic"
            $exception = [System.InvalidOperationException]::new($message.Trim())
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'DestinationGitHubReleaseTagReadFailed',
                [System.Management.Automation.ErrorCategory]::ReadError,
                $tagName
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        if ([string]::IsNullOrWhiteSpace($destinationCommitSha)) {
            $mismatches.Add('DestinationTagMissing')
        }
        elseif ($destinationCommitSha -ne [string] $sourceRelease.TargetCommitSha) {
            $mismatches.Add('TagTarget')
        }

        $destinationReleaseResult = Invoke-CgrGitHubApiReadRequest -ArgumentList @(
            'api', '--hostname', $HostName,
            "repos/$($DestinationRepository.FullName)/releases/tags/$escapedTag"
        )

        $destinationRelease = $null
        if ($destinationReleaseResult.ExitCode -ne 0) {
            if ([string] $destinationReleaseResult.ErrorText -match '(?i)404|not found') {
                $mismatches.Add('DestinationReleaseMissing')
            }
            else {
                $diagnostic = Protect-CgrDiagnosticText -Text ([string] $destinationReleaseResult.ErrorText)
                $message = "Unable to read destination GitHub Release '$tagName' during verification. $diagnostic"
                $exception = [System.InvalidOperationException]::new($message.Trim())
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'DestinationGitHubReleaseVerificationReadFailed',
                    [System.Management.Automation.ErrorCategory]::ReadError,
                    $tagName
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }
        }
        else {
            $destinationReleaseJson = ($destinationReleaseResult.Output | ForEach-Object { [string] $_ }) -join "`n"
            try {
                $destinationRelease = $destinationReleaseJson | ConvertFrom-Json -Depth 100
            }
            catch {
                $diagnostic = Protect-CgrDiagnosticText -Text $_.Exception.Message
                $message = "Destination GitHub Release '$tagName' returned invalid JSON during verification. $diagnostic"
                $exception = [System.InvalidOperationException]::new($message)
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'DestinationGitHubReleaseVerificationResponseInvalid',
                    [System.Management.Automation.ErrorCategory]::InvalidData,
                    $tagName
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }
        }

        if ($destinationRelease) {
            if ([string] $destinationRelease.tag_name -ne $tagName) { $mismatches.Add('TagName') }
            if ([string] $destinationRelease.name -ne [string] $sourceRelease.Name) { $mismatches.Add('Name') }
            if ([string] $destinationRelease.body -ne [string] $sourceRelease.Body) { $mismatches.Add('Body') }
            if ([bool] $destinationRelease.draft -ne [bool] $sourceRelease.Draft) { $mismatches.Add('Draft') }
            if ([bool] $destinationRelease.prerelease -ne [bool] $sourceRelease.Prerelease) { $mismatches.Add('Prerelease') }

            $destinationAssets = @($destinationRelease.assets)
            if ($destinationAssets.Count -ne @($sourceRelease.Assets).Count) {
                $mismatches.Add('AssetCount')
            }

            foreach ($sourceAsset in @($sourceRelease.Assets)) {
                $destinationAsset = @($destinationAssets | Where-Object { [string] $_.name -eq [string] $sourceAsset.Name }) | Select-Object -First 1
                if ($null -eq $destinationAsset) {
                    $mismatches.Add("AssetMissing:$($sourceAsset.Name)")
                    continue
                }
                if ([string] $destinationAsset.label -ne [string] $sourceAsset.Label) { $mismatches.Add("AssetLabel:$($sourceAsset.Name)") }
                if ([long] $destinationAsset.size -ne [long] $sourceAsset.Size) { $mismatches.Add("AssetSize:$($sourceAsset.Name)") }
                if ([string] $destinationAsset.content_type -ne [string] $sourceAsset.ContentType) { $mismatches.Add("AssetContentType:$($sourceAsset.Name)") }
                if (-not [string]::IsNullOrWhiteSpace([string] $sourceAsset.Digest) -and
                    [string] $destinationAsset.digest -ne [string] $sourceAsset.Digest) {
                    $mismatches.Add("AssetDigest:$($sourceAsset.Name)")
                }
            }
        }

        $releasePassed = $mismatches.Count -eq 0
        $releaseResults.Add([pscustomobject] @{
                TagName = $tagName
                SourceReleaseId = $sourceRelease.ReleaseId
                DestinationReleaseId = if ($destinationRelease) { $destinationRelease.id } else { $null }
                SourceCommitSha = $sourceRelease.TargetCommitSha
                DestinationCommitSha = $destinationCommitSha
                IsLatest = [bool] $sourceRelease.IsLatest
                SourceAssetCount = @($sourceRelease.Assets).Count
                DestinationAssetCount = if ($destinationRelease) { @($destinationRelease.assets).Count } else { 0 }
                Mismatches = $mismatches.ToArray()
                IsSuccessful = $releasePassed
            })
        $checks.Add([pscustomobject] @{
                Name = "GitHubRelease:$tagName"
                Passed = $releasePassed
                Expected = 'Selected source release metadata, tag target, and assets'
                Actual = if ($releasePassed) { 'Matched' } else { $mismatches.ToArray() }
            })
    }

    $destinationLatestTag = $null
    $latestPassed = $true
    if ($sourceSelection.SourceLatestSelected) {
        $destinationLatestResult = Invoke-CgrGitHubApiReadRequest -ArgumentList @(
            'api', '--hostname', $HostName,
            "repos/$($DestinationRepository.FullName)/releases/latest"
        )
        if ($destinationLatestResult.ExitCode -eq 0) {
            $destinationLatestJson = ($destinationLatestResult.Output | ForEach-Object { [string] $_ }) -join "`n"
            try {
                $destinationLatest = $destinationLatestJson | ConvertFrom-Json -Depth 100
                $destinationLatestTag = [string] $destinationLatest.tag_name
            }
            catch {
                $diagnostic = Protect-CgrDiagnosticText -Text $_.Exception.Message
                $message = "Destination latest GitHub Release returned invalid JSON during verification. $diagnostic"
                $exception = [System.InvalidOperationException]::new($message)
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $exception,
                    'DestinationGitHubLatestReleaseVerificationResponseInvalid',
                    [System.Management.Automation.ErrorCategory]::InvalidData,
                    $DestinationRepository.FullName
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }
        }
        elseif ([string] $destinationLatestResult.ErrorText -notmatch '(?i)404|not found') {
            $diagnostic = Protect-CgrDiagnosticText -Text ([string] $destinationLatestResult.ErrorText)
            $message = "Unable to read the destination latest GitHub Release during verification. $diagnostic"
            $exception = [System.InvalidOperationException]::new($message.Trim())
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'DestinationGitHubLatestReleaseVerificationReadFailed',
                [System.Management.Automation.ErrorCategory]::ReadError,
                $DestinationRepository.FullName
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        $latestPassed = $destinationLatestTag -eq [string] $sourceSelection.SourceLatestTag
        $checks.Add([pscustomobject] @{
                Name = 'GitHubLatestReleaseMatches'
                Passed = $latestPassed
                Expected = $sourceSelection.SourceLatestTag
                Actual = $destinationLatestTag
            })
    }

    $successfulReleaseCount = @($releaseResults | Where-Object { $_.IsSuccessful }).Count
    [pscustomobject] @{
        PSTypeName = 'CopyGitHubRepo.ReleaseVerificationResult'
        SchemaVersion = 1
        SourceRepository = $SourceRepository.FullName
        DestinationRepository = $DestinationRepository.FullName
        AvailableSourceReleaseCount = $sourceSelection.AvailableReleaseCount
        SelectedReleaseCount = $sourceSelection.SelectedReleaseCount
        VerifiedReleaseCount = $successfulReleaseCount
        SelectedAssetCount = $sourceSelection.SelectedAssetCount
        SourceLatestTag = $sourceSelection.SourceLatestTag
        SourceLatestSelected = $sourceSelection.SourceLatestSelected
        DestinationLatestTag = $destinationLatestTag
        LatestReleaseMatches = if ($sourceSelection.SourceLatestSelected) { $latestPassed } else { $null }
        IncludePatterns = @($sourceSelection.IncludePatterns)
        ExcludePatterns = @($sourceSelection.ExcludePatterns)
        IncludePrerelease = $sourceSelection.IncludePrerelease
        IncludeDraftReleases = $sourceSelection.IncludeDraftReleases
        ReleaseCount = $sourceSelection.ReleaseCount
        Releases = $releaseResults.ToArray()
        Checks = $checks.ToArray()
        IsSuccessful = -not ($checks | Where-Object { -not $_.Passed })
    }
}