Private/Replacement/Write-CgrExistingDestinationRecoveryReport.ps1

function Write-CgrExistingDestinationRecoveryReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [psobject] $Plan,

        [Nullable[long]] $OriginalDestinationRepositoryId,

        [string] $OriginalDestinationRepositoryNodeId,

        [AllowNull()]
        [psobject] $ArchiveRepository,

        [AllowNull()]
        [psobject] $DestinationRepository,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $FailureStage,

        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord] $ErrorRecord,

        [object[]] $CompletedSteps = @(),

        [psobject] $ReleasePreservation,

        [string] $PreferredReportPath
    )

    $reportPath = if (-not [string]::IsNullOrWhiteSpace($PreferredReportPath)) {
        "$PreferredReportPath.recovery.json"
    }
    else {
        $safeDestinationName = $Plan.DestinationRepository -replace '[^A-Za-z0-9_.-]', '-'
        $timestamp = [DateTimeOffset]::UtcNow.ToString('yyyyMMddTHHmmssfffZ')
        Join-Path (Join-Path (Get-Location) '.copy-github-repo/recovery') "$safeDestinationName-$timestamp.json"
    }

    $resolvedParent = Split-Path -Parent $reportPath
    if (-not [string]::IsNullOrWhiteSpace($resolvedParent)) {
        New-Item -Path $resolvedParent -ItemType Directory -Force | Out-Null
    }

    $archiveRepositoryId = if ($ArchiveRepository) { Get-CgrObjectProperty -InputObject $ArchiveRepository -Name 'Id' } else { $null }
    $archiveRepositoryNodeId = if ($ArchiveRepository) { Get-CgrObjectProperty -InputObject $ArchiveRepository -Name 'NodeId' } else { $null }
    $replacementRepositoryId = if ($DestinationRepository) { Get-CgrObjectProperty -InputObject $DestinationRepository -Name 'Id' } else { $null }
    $replacementRepositoryNodeId = if ($DestinationRepository) { Get-CgrObjectProperty -InputObject $DestinationRepository -Name 'NodeId' } else { $null }
    $archiveIdentityPreserved = $null -ne $archiveRepositoryId -and $null -ne $OriginalDestinationRepositoryId -and $archiveRepositoryId -eq $OriginalDestinationRepositoryId
    $replacementDistinct = $null -ne $replacementRepositoryId -and $null -ne $OriginalDestinationRepositoryId -and $replacementRepositoryId -ne $OriginalDestinationRepositoryId
    $lastCompletedStep = @($CompletedSteps | Sort-Object Order | Select-Object -Last 1)
    if ($lastCompletedStep.Count -eq 0) { $lastCompletedStep = $null } else { $lastCompletedStep = $lastCompletedStep[0] }
    $customDomainHandoff = if ($ErrorRecord.TargetObject -and -not [string]::IsNullOrWhiteSpace([string] (Get-CgrObjectProperty -InputObject $ErrorRecord.TargetObject -Name 'ReviewedCustomDomain'))) {
        $ErrorRecord.TargetObject
    }
    else {
        $handoffStep = @($CompletedSteps | Where-Object { $_.PSObject.Properties['CustomDomainHandoff'] } | Select-Object -Last 1)
        if ($handoffStep.Count -gt 0) { $handoffStep[0].CustomDomainHandoff } else { $null }
    }

    $reviewedPages = Get-CgrObjectProperty -InputObject $Plan -Name 'Pages'
    $createPagesStep = @($CompletedSteps | Where-Object { $_.Name -eq 'CreateReplacementGitHubPages' } | Select-Object -Last 1)
    $configurePagesStep = @($CompletedSteps | Where-Object { $_.Name -eq 'ConfigureReplacementGitHubPages' } | Select-Object -Last 1)
    $successfulPagesSteps = @($CompletedSteps | Where-Object {
            $_.Name -in @('VerifyReviewedNoPagesState', 'ValidatePagesCustomDomainHandoff', 'ReleaseArchivedPagesCustomDomain', 'CreateReplacementGitHubPages', 'ConfigureReplacementGitHubPages', 'ClaimReplacementPagesCustomDomain', 'RestoreGitHubPages') -and
            ((Get-CgrObjectProperty -InputObject $_ -Name 'Verified') -eq $true -or (Get-CgrObjectProperty -InputObject $_ -Name 'Succeeded') -eq $true)
        } | Sort-Object Order)
    $pagesApplied = [ordered] @{}
    if ($createPagesStep.Count -gt 0 -and [bool] (Get-CgrObjectProperty -InputObject $createPagesStep[0] -Name 'Succeeded')) {
        $applied = Get-CgrObjectProperty -InputObject $createPagesStep[0] -Name 'AppliedConfiguration'
        if ($applied) {
            $pagesApplied.BuildType = Get-CgrObjectProperty -InputObject $applied -Name 'BuildType'
            $source = Get-CgrObjectProperty -InputObject $applied -Name 'Source'
            if ($null -ne $source) { $pagesApplied.Source = $source }
        }
    }
    if ($configurePagesStep.Count -gt 0 -and [bool] (Get-CgrObjectProperty -InputObject $configurePagesStep[0] -Name 'Succeeded')) {
        $applied = Get-CgrObjectProperty -InputObject $configurePagesStep[0] -Name 'AppliedConfiguration'
        if ($applied) {
            $domain = Get-CgrObjectProperty -InputObject $applied -Name 'CustomDomain'
            $https = Get-CgrObjectProperty -InputObject $applied -Name 'HttpsEnforced'
            if (-not [string]::IsNullOrWhiteSpace([string] $domain)) { $pagesApplied.CustomDomain = $domain }
            if ($null -ne $https) { $pagesApplied.HttpsEnforced = [bool] $https }
        }
    }
    $archiveReleaseEvidence = if ($customDomainHandoff) {
        [pscustomobject] @{
            Repository = Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ArchiveRepository'
            RepositoryId = Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ArchiveRepositoryId'
            ReviewedCustomDomain = Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReviewedCustomDomain'
            Attempted = [bool] (Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ArchiveReleaseAttempted')
            Succeeded = [bool] (Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ArchiveReleaseSucceeded')
            KnownOwnsReviewedDomain = if ([bool] (Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ArchiveReleaseSucceeded')) { $false } else { $null }
        }
    }
    else { $null }
    $replacementClaimEvidence = if ($customDomainHandoff) {
        [pscustomobject] @{
            Repository = Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReplacementRepository'
            RepositoryId = Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReplacementRepositoryId'
            ReviewedCustomDomain = Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReviewedCustomDomain'
            Attempted = [bool] (Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReplacementClaimAttempted')
            Succeeded = [bool] (Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReplacementClaimSucceeded')
            ReadBackSucceeded = [bool] (Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReplacementReadBackSucceeded')
            KnownOwnsReviewedDomain = if ([bool] (Get-CgrObjectProperty -InputObject $customDomainHandoff -Name 'ReplacementReadBackSucceeded')) { $true } else { $null }
        }
    }
    else { $null }
    $pagesRecovery = if ([bool] (Get-CgrObjectProperty -InputObject $Plan -Name 'RestorePages') -and $reviewedPages) {
        [pscustomobject] @{
            RestoreRequested = $true
            ReviewedConfiguration = [pscustomobject] @{
                Configured = [bool] (Get-CgrObjectProperty -InputObject $reviewedPages -Name 'Configured')
                BuildType = Get-CgrObjectProperty -InputObject $reviewedPages -Name 'BuildType'
                Source = Get-CgrObjectProperty -InputObject $reviewedPages -Name 'Source'
                CustomDomain = Get-CgrObjectProperty -InputObject $reviewedPages -Name 'CustomDomain'
                HttpsEnforced = Get-CgrObjectProperty -InputObject $reviewedPages -Name 'HttpsEnforced'
                Representability = Get-CgrObjectProperty -InputObject $reviewedPages -Name 'Representability'
            }
            DestinationMutationAttempted = [bool] ($createPagesStep.Count -gt 0 -or $configurePagesStep.Count -gt 0)
            DestinationCreationAttempted = [bool] ($createPagesStep.Count -gt 0)
            DestinationCreationSucceeded = [bool] ($createPagesStep.Count -gt 0 -and (Get-CgrObjectProperty -InputObject $createPagesStep[0] -Name 'Succeeded'))
            AppliedConfiguration = if ($pagesApplied.Count -gt 0) { [pscustomobject] $pagesApplied } else { $null }
            ArchiveCustomDomainRelease = $archiveReleaseEvidence
            ReplacementCustomDomainClaim = $replacementClaimEvidence
            LastSuccessfulStage = if ($successfulPagesSteps.Count -gt 0) { $successfulPagesSteps[-1].Name } else { $null }
            ExternalState = [pscustomobject] @{
                Ownership = 'ExternalNotMigrated'
                Readiness = Get-CgrObjectProperty -InputObject $reviewedPages -Name 'ExternalReadiness'
                Migrated = $false
            }
            DnsMutationAttempted = $false
            AutomaticRollbackAttempted = $false
        }
    }
    else { $null }

    $recoveryResult = [pscustomobject] @{
        PSTypeName = 'CopyGitHubRepo.ExistingDestinationRecoveryResult'
        SchemaVersion = 1
        Status = 'ExistingDestinationReplacementFailed'
        RecordedAtUtc = [DateTimeOffset]::UtcNow.ToString('o')
        SourceRepository = $Plan.SourceRepository
        OriginalDestinationRepository = $Plan.DestinationRepository
        OriginalDestinationRepositoryId = $OriginalDestinationRepositoryId
        OriginalDestinationRepositoryNodeId = $OriginalDestinationRepositoryNodeId
        DestinationRepository = $Plan.DestinationRepository
        ArchiveRepository = if ($ArchiveRepository) { $ArchiveRepository.FullName } else { $Plan.ArchiveRepository }
        ArchiveRepositoryId = $archiveRepositoryId
        ArchiveRepositoryNodeId = $archiveRepositoryNodeId
        ArchivedOriginalIdentityPreserved = $archiveIdentityPreserved
        ReplacementRepository = if ($DestinationRepository) { $DestinationRepository.FullName } else { $null }
        ReplacementRepositoryId = $replacementRepositoryId
        ReplacementRepositoryNodeId = $replacementRepositoryNodeId
        ReplacementHasDistinctIdentity = $replacementDistinct
        ContentMode = $Plan.ContentMode
        FailureStage = $FailureStage
        LastCompletedStep = $lastCompletedStep
        ErrorId = $ErrorRecord.FullyQualifiedErrorId
        ErrorMessage = $ErrorRecord.Exception.Message
        CompletedSteps = @($CompletedSteps)
        Pages = $pagesRecovery
        CustomDomainHandoff = $customDomainHandoff
        ReleasePreservation = $ReleasePreservation
        Recovery = [pscustomobject] @{
            ExistingDestinationWasDeleted = $false
            ExistingDestinationPreservedAsArchive = [bool] $ArchiveRepository
            ArchivedOriginalIdentityPreserved = $archiveIdentityPreserved
            ArchiveRepository = if ($ArchiveRepository) { $ArchiveRepository.FullName } else { $Plan.ArchiveRepository }
            ReplacementWasCreated = [bool] $DestinationRepository
            ReplacementHasDistinctIdentity = $replacementDistinct
            AutomaticDeletionAttempted = $false
            AutomaticRollbackAttempted = $false
            RecommendedActions = @(
                'Do not delete the archived repository until the replacement migration is verified.'
                'Inspect the failure stage, immutable repository identities, and completed steps recorded in this report.'
                'Inspect Pages recovery evidence before attempting any manual Pages change.'
                'If custom-domain handoff began, inspect CustomDomainHandoff before attempting any manual ownership change.'
                'If a replacement repository was created, inspect and verify it before deciding whether to retry.'
                'The previous destination remains preserved under the archive repository name after a successful verified rename.'
            )
        }
    }

    $recoveryResult |
        ConvertTo-Json -Depth 30 |
        Set-Content -Path $reportPath -Encoding utf8NoBOM

    return [System.IO.Path]::GetFullPath($reportPath)
}