Public/Test-GitHubRepositoryMigration.ps1

function Test-GitHubRepositoryMigration {
    <#
    .SYNOPSIS
    Verifies a GitHub repository migration.

    .DESCRIPTION
    Performs read-only verification of a completed repository migration. Plain Snapshot
    verification compares the source and destination default-branch Git trees and confirms
    the destination has the expected single-root-commit history shape. Snapshot migrations
    that preserved GitHub Releases are verified from the immutable migration plan reviewed
    before execution: generated checkpoint order and parentage, checkpoint tree equivalence,
    selected release-tag targets, reviewed source HEAD state, recreated GitHub Release
    metadata/assets, and Latest designation are read independently from the destination.

    FullHistory verification compares ordinary branch and tag targets, reachable commit
    counts, branch-tip trees, the default branch, and reachable Git LFS object availability.
    FullHistory release verification retains its existing live source-selection behavior.

    -VerifyPages extends the independent verification boundary using immutable Pages evidence
    from -ApprovedPlan. It reads GitHub-side Pages configuration from the destination and
    compares configured state, build type, exact legacy branch/path, custom domain, and
    deterministic HTTPS-enforcement intent. Replacement verification also proves the reviewed
    production domain is absent from the archive and present on the replacement. External DNS,
    domain-verification, and certificate readiness are reported separately and never treated
    as migrated configuration.

    Snapshot -IncludeReleases requires -ApprovedPlan so verification cannot rerun live release
    selection, filtering, or topology discovery and silently redefine the expected state.
    -VerifyPages likewise requires -ApprovedPlan so mutable live source Pages discovery never
    becomes verification authority. The approved plan can be taken from the Plan property
    returned by Copy-GitHubRepository. Verification never repairs or mutates either repository.

    .PARAMETER SourceRepository
    Specifies the source repository as owner/name. For approved-plan verification this name is
    checked against the reviewed plan while immutable evidence defines the expected state.

    .PARAMETER DestinationRepository
    Specifies the destination repository as owner/name. This is the repository whose migrated
    state is independently read and verified.

    .PARAMETER ContentMode
    Selects the verification contract. Snapshot is the default. FullHistory preserves the
    existing branch/tag/history/LFS verification contract.

    .PARAMETER IncludeReleases
    Adds GitHub Release verification. FullHistory uses the existing live source-selection
    contract. Snapshot requires -ApprovedPlan and verifies exactly its reviewed selection.

    .PARAMETER VerifyPages
    Adds read-only GitHub Pages verification from immutable Pages evidence in -ApprovedPlan.
    This does not query, copy, or mutate DNS, domain-verification, or certificate state.

    .PARAMETER ReleaseTag
    Includes only source GitHub Releases whose tag names match one or more PowerShell wildcard
    patterns for FullHistory verification. Snapshot approved-plan verification does not accept
    live release filters because the reviewed plan is authoritative.

    .PARAMETER ReleaseExcludeTag
    Excludes source GitHub Releases whose tag names match one or more PowerShell wildcard
    patterns for FullHistory release verification.

    .PARAMETER IncludePrerelease
    Includes source prereleases in FullHistory release verification.

    .PARAMETER IncludeDraftReleases
    Includes source draft releases in FullHistory release verification.

    .PARAMETER ReleaseCount
    Limits FullHistory release verification to the newest N selected source releases.

    .PARAMETER ApprovedPlan
    Supplies the immutable reviewed CopyGitHubRepo migration plan for Snapshot release and/or
    GitHub Pages verification. Pages verification consumes Plan.Pages as expected evidence and
    never reruns live source Pages discovery as authority.

    .PARAMETER HostName
    Specifies the GitHub host used for discovery and authentication. The default is github.com.

    .EXAMPLE
    Test-GitHubRepositoryMigration `
        -SourceRepository infoconex/source `
        -DestinationRepository infoconex/destination

    Verifies a plain Snapshot migration.

    .EXAMPLE
    Test-GitHubRepositoryMigration `
        -SourceRepository infoconex/source `
        -DestinationRepository infoconex/destination `
        -ContentMode FullHistory `
        -IncludeReleases `
        -ReleaseTag 'v2.*'

    Verifies FullHistory plus the selected live source releases using the existing contract.

    .EXAMPLE
    Test-GitHubRepositoryMigration `
        -SourceRepository infoconex/source `
        -DestinationRepository infoconex/destination `
        -ContentMode Snapshot `
        -IncludeReleases `
        -ApprovedPlan $migration.Plan

    Independently verifies Snapshot release checkpoints and recreated releases from the exact
    immutable evidence that was reviewed before migration execution.

    .EXAMPLE
    Test-GitHubRepositoryMigration `
        -SourceRepository infoconex/source `
        -DestinationRepository infoconex/destination `
        -ContentMode Snapshot `
        -VerifyPages `
        -ApprovedPlan $migration.Plan

    Independently verifies supported destination GitHub Pages state from reviewed plan evidence.

    .INPUTS
    None. This command does not accept pipeline input.

    .OUTPUTS
    CopyGitHubRepo.MigrationVerificationResult. When Pages verification is requested, the
    result includes PagesVerification and PagesVerified and IsSuccessful reflects both Git
    content and Pages verification. External readiness is evidence only and is not migration
    success criteria.

    .LINK
    https://github.com/infoconex/copy-github-repo

    .LINK
    https://github.com/infoconex/copy-github-repo/blob/main/docs/product/product-contract.md
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$')]
        [string] $SourceRepository,

        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$')]
        [string] $DestinationRepository,

        [ValidateSet('Snapshot', 'FullHistory')]
        [string] $ContentMode = 'Snapshot',

        [switch] $IncludeReleases,

        [switch] $VerifyPages,

        [string[]] $ReleaseTag,

        [string[]] $ReleaseExcludeTag,

        [switch] $IncludePrerelease,

        [switch] $IncludeDraftReleases,

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

        [psobject] $ApprovedPlan,

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

    Assert-CgrSupportedHostName -HostName $HostName

    $releaseFilterWasSpecified = $PSBoundParameters.ContainsKey('ReleaseTag') -or
    $PSBoundParameters.ContainsKey('ReleaseExcludeTag') -or
    $IncludePrerelease -or
    $IncludeDraftReleases -or
    $PSBoundParameters.ContainsKey('ReleaseCount')

    if ($releaseFilterWasSpecified -and -not $IncludeReleases) {
        $message = 'Release verification filter parameters require -IncludeReleases.'
        $exception = [System.InvalidOperationException]::new($message)
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $exception,
            'ReleaseFilterRequiresIncludeReleases',
            [System.Management.Automation.ErrorCategory]::InvalidArgument,
            'IncludeReleases'
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    $snapshotReleaseVerification = $ContentMode -eq 'Snapshot' -and $IncludeReleases
    if ($ApprovedPlan -and -not ($snapshotReleaseVerification -or $VerifyPages)) {
        $message = '-ApprovedPlan is supported only with -ContentMode Snapshot -IncludeReleases unless -VerifyPages is requested.'
        $exception = [System.InvalidOperationException]::new($message)
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $exception,
            'ApprovedPlanRequiresSnapshotReleaseVerification',
            [System.Management.Automation.ErrorCategory]::InvalidArgument,
            'ApprovedPlan'
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    if ($VerifyPages) {
        if (-not $ApprovedPlan) {
            $message = 'GitHub Pages verification requires -ApprovedPlan so expected Pages state comes from immutable reviewed evidence.'
            $exception = [System.InvalidOperationException]::new($message)
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'PagesVerificationPlanRequired',
                [System.Management.Automation.ErrorCategory]::InvalidArgument,
                'ApprovedPlan'
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        $approvedPages = Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'Pages'
        $approvedRestorePages = [bool] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'RestorePages')
        $approvedContentMode = [string] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'ContentMode')
        $approvedSource = [string] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'SourceRepository')
        $approvedDestination = [string] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'DestinationRepository')
        $approvedHost = [string] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'HostName')
        if (-not $approvedRestorePages -or $null -eq $approvedPages -or
            $approvedContentMode -ne $ContentMode -or
            -not [string]::Equals($approvedSource, $SourceRepository, [System.StringComparison]::OrdinalIgnoreCase) -or
            -not [string]::Equals($approvedDestination, $DestinationRepository, [System.StringComparison]::OrdinalIgnoreCase) -or
            (-not [string]::IsNullOrWhiteSpace($approvedHost) -and -not [string]::Equals($approvedHost, $HostName, [System.StringComparison]::OrdinalIgnoreCase))) {
            $message = 'The supplied approved plan does not contain Pages restoration evidence bound to the requested source, destination, content mode, and GitHub host.'
            $exception = [System.InvalidOperationException]::new($message)
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'PagesVerificationPlanInvalid',
                [System.Management.Automation.ErrorCategory]::InvalidData,
                'ApprovedPlan'
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    if ($snapshotReleaseVerification) {
        if (-not $ApprovedPlan) {
            $message = 'Snapshot release verification requires -ApprovedPlan so expected checkpoints and releases come from immutable reviewed evidence.'
            $exception = [System.InvalidOperationException]::new($message)
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'SnapshotReleaseVerificationPlanRequired',
                [System.Management.Automation.ErrorCategory]::InvalidArgument,
                'ApprovedPlan'
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
        if ($releaseFilterWasSpecified) {
            $message = 'Snapshot release verification cannot use live release filters with -ApprovedPlan. The reviewed release selection is authoritative.'
            $exception = [System.InvalidOperationException]::new($message)
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'SnapshotReleaseVerificationFiltersNotAllowed',
                [System.Management.Automation.ErrorCategory]::InvalidArgument,
                'ApprovedPlan'
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        $approvedContentMode = [string] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'ContentMode')
        $approvedIncludeReleases = [bool] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'IncludeReleases')
        $checkpointPlan = Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'ReleaseCheckpointPlan'
        $approvedSelection = Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'ReleaseSelection'
        if ($approvedContentMode -ne 'Snapshot' -or -not $approvedIncludeReleases -or $null -eq $checkpointPlan -or $null -eq $approvedSelection) {
            $message = 'The supplied approved plan does not contain complete Snapshot -IncludeReleases verification evidence.'
            $exception = [System.InvalidOperationException]::new($message)
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $exception,
                'SnapshotReleaseVerificationPlanInvalid',
                [System.Management.Automation.ErrorCategory]::InvalidData,
                'ApprovedPlan'
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    $prerequisites = Get-CgrPrerequisiteStatus -HostName $HostName
    if (-not $prerequisites.Git.Found) {
        $exception = [System.InvalidOperationException]::new(
            "Git is required for repository migration verification. Install 'git' from https://git-scm.com/."
        )
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $exception,
            'GitNotFound',
            [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
            'git'
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    if (-not $prerequisites.GitHubCli.Found) {
        $exception = [System.InvalidOperationException]::new(
            "GitHub CLI is required for repository migration verification. Install 'gh' from https://cli.github.com/."
        )
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $exception,
            'GitHubCliNotFound',
            [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
            'gh'
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    if (-not $prerequisites.Authentication.Authenticated) {
        $exception = [System.InvalidOperationException]::new($prerequisites.Authentication.Message)
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $exception,
            'GitHubCliNotAuthenticated',
            [System.Management.Automation.ErrorCategory]::SecurityError,
            $HostName
        )
        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    if ($snapshotReleaseVerification) {
        $destination = Get-CgrRepository -Repository $DestinationRepository -HostName $HostName
        $approvedSourceName = [string] (Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'SourceRepository')
        $source = [pscustomobject] @{
            FullName = if ([string]::IsNullOrWhiteSpace($approvedSourceName)) { $SourceRepository } else { $approvedSourceName }
            HostName = $HostName
        }

        $result = Invoke-CgrApprovedSnapshotReleaseVerification `
            -SourceRepository $source `
            -DestinationRepository $destination `
            -ReleaseCheckpointPlan $checkpointPlan

        $gitContentSuccessful = [bool] $result.IsSuccessful
        $targetEvidenceComplete = @($result.ReleaseTags).Count -eq @($approvedSelection.Releases).Count -and
        -not @($result.ReleaseTags | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.DestinationCommitSha) })
        $releaseVerification = $null
        if ($targetEvidenceComplete) {
            $releaseVerification = Test-CgrGitHubReleaseMigration `
                -SourceRepository $source `
                -DestinationRepository $destination `
                -ApprovedSelection $approvedSelection `
                -DestinationTagTargets @($result.ReleaseTags) `
                -RequireExactDestinationReleaseSet `
                -HostName $HostName
        }

        $releasesVerified = [bool] ($releaseVerification -and $releaseVerification.IsSuccessful)
        $result | Add-Member -NotePropertyName GitContentSuccessful -NotePropertyValue $gitContentSuccessful -Force
        $result | Add-Member -NotePropertyName ReleaseVerification -NotePropertyValue $releaseVerification -Force
        $result | Add-Member -NotePropertyName ReleasesVerified -NotePropertyValue $releasesVerified -Force
        $result | Add-Member -NotePropertyName IsSuccessful -NotePropertyValue ([bool] ($gitContentSuccessful -and $releasesVerified)) -Force

        if ($VerifyPages) {
            $beforePagesSuccessful = [bool] $result.IsSuccessful
            $pagesVerification = Test-CgrGitHubPagesMigration -Plan $ApprovedPlan -DestinationRepository $destination -HostName $HostName
            $result | Add-Member -NotePropertyName PagesVerification -NotePropertyValue $pagesVerification -Force
            $result | Add-Member -NotePropertyName PagesVerified -NotePropertyValue ([bool] $pagesVerification.IsSuccessful) -Force
            $result | Add-Member -NotePropertyName IsSuccessful -NotePropertyValue ([bool] ($beforePagesSuccessful -and $pagesVerification.IsSuccessful)) -Force
        }
        return $result
    }

    $source = Get-CgrRepository -Repository $SourceRepository -HostName $HostName
    $destination = Get-CgrRepository -Repository $DestinationRepository -HostName $HostName

    if ($ContentMode -eq 'FullHistory') {
        $result = Invoke-CgrRepositoryFullHistoryVerification `
            -SourceRepository $source `
            -DestinationRepository $destination

        if ($IncludeReleases) {
            $releaseVerificationParameters = @{
                SourceRepository = $source
                DestinationRepository = $destination
                ReleaseTag = $ReleaseTag
                ReleaseExcludeTag = $ReleaseExcludeTag
                IncludePrerelease = $IncludePrerelease
                IncludeDraftReleases = $IncludeDraftReleases
                HostName = $HostName
            }
            if ($PSBoundParameters.ContainsKey('ReleaseCount')) {
                $releaseVerificationParameters.ReleaseCount = $ReleaseCount
            }

            $gitContentSuccessful = [bool] $result.IsSuccessful
            $releaseVerification = Test-CgrGitHubReleaseMigration @releaseVerificationParameters
            $result | Add-Member -NotePropertyName GitContentSuccessful -NotePropertyValue $gitContentSuccessful -Force
            $result | Add-Member -NotePropertyName ReleaseVerification -NotePropertyValue $releaseVerification -Force
            $result | Add-Member -NotePropertyName ReleasesVerified -NotePropertyValue ([bool] $releaseVerification.IsSuccessful) -Force
            $result | Add-Member -NotePropertyName IsSuccessful -NotePropertyValue ([bool] ($gitContentSuccessful -and $releaseVerification.IsSuccessful)) -Force
        }

        if ($VerifyPages) {
            $beforePagesSuccessful = [bool] $result.IsSuccessful
            $pagesVerification = Test-CgrGitHubPagesMigration -Plan $ApprovedPlan -DestinationRepository $destination -HostName $HostName
            $result | Add-Member -NotePropertyName PagesVerification -NotePropertyValue $pagesVerification -Force
            $result | Add-Member -NotePropertyName PagesVerified -NotePropertyValue ([bool] $pagesVerification.IsSuccessful) -Force
            $result | Add-Member -NotePropertyName IsSuccessful -NotePropertyValue ([bool] ($beforePagesSuccessful -and $pagesVerification.IsSuccessful)) -Force
        }

        return $result
    }

    $approvedSourceState = if ($VerifyPages) { Get-CgrObjectProperty -InputObject $ApprovedPlan -Name 'SourceState' } else { $null }
    $result = Invoke-CgrRepositorySnapshotVerification `
        -SourceRepository $source `
        -DestinationRepository $destination `
        -ApprovedSourceState $approvedSourceState

    if ($VerifyPages) {
        $gitContentSuccessful = [bool] $result.IsSuccessful
        $pagesVerification = Test-CgrGitHubPagesMigration -Plan $ApprovedPlan -DestinationRepository $destination -HostName $HostName
        $result | Add-Member -NotePropertyName GitContentSuccessful -NotePropertyValue $gitContentSuccessful -Force
        $result | Add-Member -NotePropertyName PagesVerification -NotePropertyValue $pagesVerification -Force
        $result | Add-Member -NotePropertyName PagesVerified -NotePropertyValue ([bool] $pagesVerification.IsSuccessful) -Force
        $result | Add-Member -NotePropertyName IsSuccessful -NotePropertyValue ([bool] ($gitContentSuccessful -and $pagesVerification.IsSuccessful)) -Force
    }

    return $result
}