Private/GitHub/Restore-CgrGitHubPagesConfiguration.ps1
|
function Restore-CgrGitHubPagesConfiguration { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Called only from the approved post-verification orchestration after the public command mutation gate.')] [CmdletBinding()] param( [Parameter(Mandatory)] [psobject] $Plan, [Parameter(Mandatory)] [psobject] $SourceRepository, [Parameter(Mandatory)] [psobject] $DestinationRepository, [System.Collections.IList] $CompletedSteps, [ref] $FailureStage, [ValidateNotNullOrEmpty()] [string] $HostName = 'github.com' ) $pages = Assert-CgrGitHubPagesPlanEvidence -Plan $Plan -SourceRepository $SourceRepository -HostName $HostName $reviewedConfiguration = [pscustomobject] @{ Configured = [bool] (Get-CgrObjectProperty -InputObject $pages -Name 'Configured') BuildType = Get-CgrObjectProperty -InputObject $pages -Name 'BuildType' Source = Get-CgrObjectProperty -InputObject $pages -Name 'Source' CustomDomain = Get-CgrObjectProperty -InputObject $pages -Name 'CustomDomain' HttpsEnforced = Get-CgrObjectProperty -InputObject $pages -Name 'HttpsEnforced' Representability = Get-CgrObjectProperty -InputObject $pages -Name 'Representability' } $externalState = [pscustomobject] @{ Ownership = 'ExternalNotMigrated' Readiness = Get-CgrObjectProperty -InputObject $pages -Name 'ExternalReadiness' Migrated = $false } $configured = [bool] $reviewedConfiguration.Configured $existing = Get-CgrGitHubApiOptional -Path "repos/$($DestinationRepository.FullName)/pages" -HostName $HostName if (-not $configured) { if ($null -ne $existing) { $exception = [System.InvalidOperationException]::new("Destination '$($DestinationRepository.FullName)' unexpectedly has GitHub Pages configured even though the reviewed source did not. The destination was left unchanged.") throw [System.Management.Automation.ErrorRecord]::new($exception, 'DestinationPagesUnexpectedlyConfigured', [System.Management.Automation.ErrorCategory]::InvalidResult, $DestinationRepository.FullName) } if ($null -ne $CompletedSteps) { $CompletedSteps.Add([pscustomobject] @{ Order = $CompletedSteps.Count + 1 Name = 'VerifyReviewedNoPagesState' MutatedGitHub = $false Verified = $true Succeeded = $true ReviewedConfiguration = $reviewedConfiguration }) } if ($FailureStage) { $FailureStage.Value = 'ReleasePagesActivationGuard' } Enable-CgrPagesWorkflowActivationAfterRestore -DestinationRepository $DestinationRepository -HostName $HostName return [pscustomobject] @{ PSTypeName = 'CopyGitHubRepo.PagesRestoreResult'; Repository = $DestinationRepository.FullName Status = 'ReviewedNotConfigured'; Configured = $false; Restored = $false; Verified = $true GuardReleased = $true; CustomDomainStatus = 'NotApplicable'; IsSuccessful = $true; IsComplete = $true ReviewedConfiguration = $reviewedConfiguration DestinationMutationAttempted = $false DestinationCreationAttempted = $false DestinationCreationSucceeded = $false AppliedConfiguration = $null LastSuccessfulPagesStage = 'ReleasePagesActivationGuard' ExternalState = $externalState DnsMutationAttempted = $false AutomaticRollbackAttempted = $false } } if ($null -ne $existing) { $exception = [System.InvalidOperationException]::new("Destination '$($DestinationRepository.FullName)' already has GitHub Pages configured before the reviewed restoration stage. Refusing to overwrite unreviewed Pages state.") throw [System.Management.Automation.ErrorRecord]::new($exception, 'DestinationPagesAlreadyConfigured', [System.Management.Automation.ErrorCategory]::ResourceExists, $DestinationRepository.FullName) } Assert-CgrDestinationPagesSource -DestinationRepository $DestinationRepository -Pages $pages -HostName $HostName $customDomain = $reviewedConfiguration.CustomDomain $buildType = $reviewedConfiguration.BuildType $createBody = @{ build_type = $buildType } if ($buildType -eq 'legacy') { $source = $reviewedConfiguration.Source $createBody.source = @{ branch = Get-CgrObjectProperty -InputObject $source -Name 'Branch'; path = Get-CgrObjectProperty -InputObject $source -Name 'Path' } } elseif ($buildType -ne 'workflow') { $exception = [System.InvalidOperationException]::new("Reviewed Pages build type '$buildType' is unsupported for restoration.") throw [System.Management.Automation.ErrorRecord]::new($exception, 'ApprovedPagesBuildTypeUnsupported', [System.Management.Automation.ErrorCategory]::NotImplemented, $buildType) } $mode = [string] (Get-CgrObjectProperty -InputObject $Plan -Name 'Mode') $replacementMode = $mode -in @('SameNameReplacement', 'ExistingDestinationReplacement') $handoffRequired = $replacementMode -and -not [string]::IsNullOrWhiteSpace([string] $customDomain) $handoff = $null $createStep = $null $configurationStep = $null $claimStep = $null try { if ($handoffRequired) { if ($FailureStage) { $FailureStage.Value = 'ValidatePagesCustomDomainHandoff' } $archiveName = [string] (Get-CgrObjectProperty -InputObject $Plan -Name 'ArchiveRepository') if ([string]::IsNullOrWhiteSpace($archiveName)) { $exception = [System.InvalidOperationException]::new('Replacement custom-domain handoff requires the reviewed archive repository identity.') throw [System.Management.Automation.ErrorRecord]::new($exception, 'PagesCustomDomainArchiveIdentityMissing', [System.Management.Automation.ErrorCategory]::InvalidData, $customDomain) } $archive = Get-CgrRepository -Repository $archiveName -HostName $HostName $replacement = Get-CgrRepository -Repository $DestinationRepository.FullName -HostName $HostName $archiveId = Get-CgrObjectProperty -InputObject $archive -Name 'Id' $replacementId = Get-CgrObjectProperty -InputObject $replacement -Name 'Id' if ($null -eq $archiveId -or $null -eq $replacementId -or [long] $archiveId -eq [long] $replacementId) { $exception = [System.InvalidOperationException]::new('Archive/replacement immutable repository identity could not be proven before custom-domain handoff.') throw [System.Management.Automation.ErrorRecord]::new($exception, 'PagesCustomDomainRepositoryIdentityInvalid', [System.Management.Automation.ErrorCategory]::InvalidResult, $archiveName) } if ($mode -eq 'SameNameReplacement') { $sourceState = Get-CgrObjectProperty -InputObject $Plan -Name 'SourceState' $reviewedSourceId = Get-CgrObjectProperty -InputObject $sourceState -Name 'RepositoryId' if ($null -ne $reviewedSourceId -and [long] $archiveId -ne [long] $reviewedSourceId) { $exception = [System.InvalidOperationException]::new("Archive repository '$archiveName' no longer has the reviewed source repository identity. The custom domain was not released.") throw [System.Management.Automation.ErrorRecord]::new($exception, 'PagesCustomDomainArchiveIdentityMismatch', [System.Management.Automation.ErrorCategory]::InvalidResult, $archiveName) } } $archivePages = Get-CgrGitHubApiOptional -Path "repos/$($archive.FullName)/pages" -HostName $HostName $archiveDomain = if ($archivePages) { Get-CgrObjectProperty -InputObject $archivePages -Name 'cname' } else { $null } $archiveReleaseRequired = [string] $archiveDomain -ceq [string] $customDomain if ($mode -eq 'SameNameReplacement' -and -not $archiveReleaseRequired) { $exception = [System.InvalidOperationException]::new("The preserved archive does not currently own the exact reviewed custom domain '$customDomain'. Ownership state is ambiguous, so no domain was released or claimed.") throw [System.Management.Automation.ErrorRecord]::new($exception, 'PagesCustomDomainArchiveBindingMismatch', [System.Management.Automation.ErrorCategory]::InvalidResult, $customDomain) } $handoff = [pscustomobject] @{ ReviewedCustomDomain = [string] $customDomain ArchiveRepository = $archive.FullName ArchiveRepositoryId = [long] $archiveId ReplacementRepository = $replacement.FullName ReplacementRepositoryId = [long] $replacementId ArchiveObservedCustomDomain = $archiveDomain ArchiveReleaseRequired = $archiveReleaseRequired ArchiveReleaseAttempted = $false ArchiveReleaseSucceeded = -not $archiveReleaseRequired ReplacementClaimAttempted = $false ReplacementClaimSucceeded = $false ReplacementReadBackSucceeded = $false HttpsIntent = $reviewedConfiguration.HttpsEnforced ExternalReadiness = Get-CgrObjectProperty -InputObject $pages -Name 'ExternalReadiness' DnsMutationAttempted = $false AutomaticRollbackAttempted = $false } if ($null -ne $CompletedSteps) { $CompletedSteps.Add([pscustomobject] @{ Order = $CompletedSteps.Count + 1 Name = 'ValidatePagesCustomDomainHandoff' MutatedGitHub = $false Verified = $true Succeeded = $true CustomDomainHandoff = $handoff ReviewedConfiguration = $reviewedConfiguration }) } if ($archiveReleaseRequired) { if ($FailureStage) { $FailureStage.Value = 'ReleaseArchivedPagesCustomDomain' } $releaseStep = [pscustomobject] @{ Order = if ($null -ne $CompletedSteps) { $CompletedSteps.Count + 1 } else { 0 } Name = 'ReleaseArchivedPagesCustomDomain' MutatedGitHub = $true Verified = $false ReviewedCustomDomain = [string] $customDomain ArchiveRepository = $archive.FullName ArchiveRepositoryId = [long] $archiveId Attempted = $true Succeeded = $false CustomDomainHandoff = $handoff } if ($null -ne $CompletedSteps) { $CompletedSteps.Add($releaseStep) } $handoff.ArchiveReleaseAttempted = $true Invoke-CgrGitHubApiMutation -Method PUT -Path "/repos/$($archive.FullName)/pages" -Body @{ cname = $null } -HostName $HostName | Out-Null $archiveReadBack = Get-CgrGitHubApiOptional -Path "repos/$($archive.FullName)/pages" -HostName $HostName $archiveReadBackDomain = if ($archiveReadBack) { Get-CgrObjectProperty -InputObject $archiveReadBack -Name 'cname' } else { $null } if (-not [string]::IsNullOrWhiteSpace([string] $archiveReadBackDomain)) { $exception = [System.InvalidOperationException]::new("Archive '$($archive.FullName)' still reports custom domain '$archiveReadBackDomain' after the reviewed release operation. Replacement claim was not attempted.") throw [System.Management.Automation.ErrorRecord]::new($exception, 'PagesCustomDomainArchiveReleaseVerificationFailed', [System.Management.Automation.ErrorCategory]::InvalidResult, $handoff) } $handoff.ArchiveReleaseSucceeded = $true $releaseStep.Succeeded = $true $releaseStep.Verified = $true } } if ($FailureStage) { $FailureStage.Value = 'CreateReplacementGitHubPages' } $createStep = [pscustomobject] @{ Order = if ($null -ne $CompletedSteps) { $CompletedSteps.Count + 1 } else { 0 } Name = 'CreateReplacementGitHubPages' MutatedGitHub = $true Verified = $false Attempted = $true Succeeded = $false ReviewedConfiguration = $reviewedConfiguration AppliedConfiguration = $null } if ($null -ne $CompletedSteps) { $CompletedSteps.Add($createStep) } Invoke-CgrGitHubApiMutation -Method POST -Path "/repos/$($DestinationRepository.FullName)/pages" -Body $createBody -HostName $HostName | Out-Null $createStep.Succeeded = $true $createStep.AppliedConfiguration = [pscustomobject] @{ BuildType = $buildType Source = if ($buildType -eq 'legacy') { $reviewedConfiguration.Source } else { $null } } $updateBody = @{} if (-not [string]::IsNullOrWhiteSpace([string] $customDomain)) { $updateBody.cname = [string] $customDomain } $httpsEnforced = $reviewedConfiguration.HttpsEnforced $httpsEnforcementStatus = if ($null -eq $httpsEnforced) { 'NotReviewed' } else { 'PendingVerification' } if ($null -ne $httpsEnforced -and (-not [bool] $httpsEnforced -or [string]::IsNullOrWhiteSpace([string] $customDomain))) { $updateBody.https_enforced = [bool] $httpsEnforced } if ($updateBody.Count -gt 0) { if ($FailureStage) { $FailureStage.Value = if ($handoffRequired) { 'ClaimReplacementPagesCustomDomain' } else { 'ConfigureReplacementGitHubPages' } } $configurationStep = [pscustomobject] @{ Order = if ($null -ne $CompletedSteps) { $CompletedSteps.Count + 1 } else { 0 } Name = 'ConfigureReplacementGitHubPages' MutatedGitHub = $true Verified = $false Attempted = $true Succeeded = $false ReviewedConfiguration = $reviewedConfiguration AppliedConfiguration = $null } if ($null -ne $CompletedSteps) { $CompletedSteps.Add($configurationStep) } if ($handoffRequired) { $handoff.ReplacementClaimAttempted = $true $claimStep = [pscustomobject] @{ Order = if ($null -ne $CompletedSteps) { $CompletedSteps.Count + 1 } else { 0 } Name = 'ClaimReplacementPagesCustomDomain' MutatedGitHub = $true Verified = $false ReviewedCustomDomain = [string] $customDomain ReplacementRepository = $DestinationRepository.FullName Attempted = $true Succeeded = $false AutomaticRollbackAttempted = $false CustomDomainHandoff = $handoff } if ($null -ne $CompletedSteps) { $CompletedSteps.Add($claimStep) } } try { Invoke-CgrGitHubApiMutation -Method PUT -Path "/repos/$($DestinationRepository.FullName)/pages" -Body $updateBody -HostName $HostName | Out-Null $configurationStep.Succeeded = $true } catch { $httpsUpdateRequested = $updateBody.ContainsKey('https_enforced') -and [bool] $updateBody.https_enforced $customDomainUpdateRequested = $updateBody.ContainsKey('cname') $certificateNotReady = $_.Exception.Message -match '(?i)certificate does not exist yet' $certificatePending = $httpsUpdateRequested -and -not $customDomainUpdateRequested -and $certificateNotReady if (-not $certificatePending) { throw } $httpsEnforcementStatus = 'PendingCertificate' $configurationStep.Succeeded = $true } $configurationStep.AppliedConfiguration = [pscustomobject] @{ CustomDomain = if (-not [string]::IsNullOrWhiteSpace([string] $customDomain)) { [string] $customDomain } else { $null } HttpsEnforced = if ($updateBody.ContainsKey('https_enforced') -and $httpsEnforcementStatus -ne 'PendingCertificate') { [bool] $httpsEnforced } else { $null } } if ($handoffRequired) { $handoff.ReplacementClaimSucceeded = $true $claimStep.Succeeded = $true } } if ($FailureStage) { $FailureStage.Value = if ($handoffRequired) { 'VerifyReplacementPagesCustomDomain' } else { 'VerifyReplacementGitHubPages' } } $verifyDomain = -not [string]::IsNullOrWhiteSpace([string] $customDomain) $readBack = Assert-CgrDestinationPagesReadBack -DestinationRepository $DestinationRepository -Pages $pages -VerifyCustomDomain:$verifyDomain -HostName $HostName $actualHttps = Get-CgrObjectProperty -InputObject $readBack -Name 'https_enforced' $actualCertificate = Get-CgrObjectProperty -InputObject $readBack -Name 'https_certificate' if ($null -ne $httpsEnforced) { if ($null -ne $actualHttps -and [bool] $actualHttps -eq [bool] $httpsEnforced) { $httpsEnforcementStatus = 'Restored' } elseif ([bool] $httpsEnforced -and $null -eq $actualCertificate) { $httpsEnforcementStatus = 'PendingCertificate' } } $createStep.Verified = $true if ($configurationStep) { $configurationStep.Verified = $true } if ($handoffRequired) { $handoff.ReplacementReadBackSucceeded = $true if ($claimStep) { $claimStep.Verified = $true } $archiveFinal = Get-CgrGitHubApiOptional -Path "repos/$archiveName/pages" -HostName $HostName $archiveFinalDomain = if ($archiveFinal) { Get-CgrObjectProperty -InputObject $archiveFinal -Name 'cname' } else { $null } if ([string] $archiveFinalDomain -ceq [string] $customDomain) { $exception = [System.InvalidOperationException]::new("Archive '$archiveName' still owns the reviewed production custom domain after replacement verification.") throw [System.Management.Automation.ErrorRecord]::new($exception, 'PagesCustomDomainHandoffVerificationFailed', [System.Management.Automation.ErrorCategory]::InvalidResult, $handoff) } } if ($FailureStage) { $FailureStage.Value = 'ReleasePagesActivationGuard' } Enable-CgrPagesWorkflowActivationAfterRestore -DestinationRepository $DestinationRepository -HostName $HostName return [pscustomobject] @{ PSTypeName = 'CopyGitHubRepo.PagesRestoreResult'; Repository = $DestinationRepository.FullName Status = 'Restored'; Configured = $true; BuildType = $buildType; Source = $reviewedConfiguration.Source CustomDomain = $customDomain; HttpsEnforced = $httpsEnforced; HttpsEnforcementStatus = $httpsEnforcementStatus Restored = $true; Verified = $true; GuardReleased = $true CustomDomainStatus = if ($verifyDomain) { if ($handoffRequired) { 'HandedOff' } else { 'Restored' } } else { 'NotConfigured' } CustomDomainHandoff = $handoff ExternalReadiness = Get-CgrObjectProperty -InputObject $pages -Name 'ExternalReadiness' DnsMigrated = $false ReadBack = $readBack; IsSuccessful = $true; IsComplete = $true ReviewedConfiguration = $reviewedConfiguration DestinationMutationAttempted = $true DestinationCreationAttempted = $true DestinationCreationSucceeded = $true AppliedConfiguration = [pscustomobject] @{ BuildType = $buildType Source = if ($buildType -eq 'legacy') { $reviewedConfiguration.Source } else { $null } CustomDomain = if (-not [string]::IsNullOrWhiteSpace([string] $customDomain)) { [string] $customDomain } else { $null } HttpsEnforced = if ($httpsEnforcementStatus -eq 'Restored') { [bool] $httpsEnforced } else { $null } } LastSuccessfulPagesStage = 'ReleasePagesActivationGuard' ExternalState = $externalState DnsMutationAttempted = $false AutomaticRollbackAttempted = $false } } catch { if ($null -ne $handoff) { $errorId = if ([string]::IsNullOrWhiteSpace([string] $_.FullyQualifiedErrorId)) { 'PagesCustomDomainHandoffFailed' } else { ([string] $_.FullyQualifiedErrorId -split ',')[0] } $category = if ($null -ne $_.CategoryInfo) { $_.CategoryInfo.Category } else { [System.Management.Automation.ErrorCategory]::OperationStopped } $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new($_.Exception, $errorId, $category, $handoff)) } throw } } |