Public/Copy-GitHubRepository.ps1
|
function Copy-GitHubRepository { <# .SYNOPSIS Publishes or copies a GitHub repository from an approved repository copy plan. .DESCRIPTION Plans or executes a GitHub repository copy. Snapshot is the default and publishes the approved source default-branch content as one unrelated root commit without prior Git history, other branches, or tags. FullHistory is explicit and preserves the approved branches, tags, commits, and reachable Git LFS objects. Planning captures immutable source-state evidence. Execution uses that same plan and fails closed with SourceStateChangedSincePlanning if the source no longer matches the approved state before mutation. Verification compares the destination with the approved/copied source evidence rather than rereading a moving source branch or ref set. An existing destination is never silently overwritten. Replacement requires an explicit archive plan and exact confirmation, and the prior repository is preserved before a fresh replacement is created. -Force does not bypass exact replacement confirmation. After content verification, supported repository settings are restored and verified, followed by transferable repository protection. -SkipSettings skips both configuration stages. Protection that cannot be transferred safely is reported rather than weakened. Mutating execution supports -WhatIf and -Confirm. Non-interactive mutation requires -Force. Changing destination visibility requires -Force. GitHub Pages restoration and Actions activation are reserved switches and are not yet implemented for mutating execution. Version 1 supports github.com only. .PARAMETER SourceRepository Specifies the source repository as owner/name. The source must exist and have a default branch with content. Planning captures immutable source Git state. .PARAMETER DestinationRepository Specifies the destination as owner/name. A different existing destination is never overwritten; use -ExistingDestinationArchiveName to archive and replace it explicitly. The same owner/name selects same-name replacement. .PARAMETER ContentMode Selects Snapshot or FullHistory. Snapshot is the default clean-publication mode. FullHistory preserves approved Git history, branches, tags, and reachable Git LFS objects. .PARAMETER DestinationVisibility Specifies public, private, or internal destination visibility. If omitted, source visibility is preserved. A deliberate visibility change requires -Force. .PARAMETER ArchiveRepositoryName Specifies the archive repository name for same-name replacement. The archive must not already exist. .PARAMETER SameNameConfirmation Supplies the exact case-sensitive confirmation for same-name replacement in SOURCE=owner/source;ARCHIVE=owner/archive;REPLACEMENT=owner/source form. -Force and -Confirm:$false do not bypass this requirement. .PARAMETER ExistingDestinationArchiveName Specifies the archive repository name used to preserve an already-existing different destination before creating its replacement. .PARAMETER ExistingDestinationConfirmation Supplies the exact case-sensitive confirmation for an existing-destination replacement in DESTINATION=owner/destination;ARCHIVE=owner/archive;REPLACEMENT=owner/destination form. -Force and -Confirm:$false do not bypass this requirement. .PARAMETER CommitMessage Specifies the Snapshot root commit message. The default is 'Initial repository commit'. FullHistory preserves existing commits and does not rewrite them. .PARAMETER RestorePages Requests GitHub Pages restoration. Planning records the request, but mutating execution rejects it because Pages restoration is not yet implemented. .PARAMETER EnableActionsAfterMigration Requests Actions activation after the copy. Planning records the request, but mutating execution rejects it because this behavior is not yet implemented. .PARAMETER SkipSettings Skips restoration of supported repository settings and repository protection. Content verification still runs. .PARAMETER PlanOnly Returns a read-only repository copy plan without mutation. The plan contains immutable source-state evidence used to bind later review and execution. .PARAMETER NonInteractive Prevents interactive confirmation prompts. Non-interactive mutation requires -Force, and replacement modes still require exact confirmation values. .PARAMETER OutputMode Selects PlanOnly output: Interactive returns the structured plan, Plain returns Markdown, and Json returns JSON. Execution always returns structured results. .PARAMETER ReportPath Writes the plan or execution report to the selected path. Recovery reporting uses this as the preferred path when mutation has begun. .PARAMETER HostName Specifies the GitHub host. The default is github.com. Version 1 supports github.com only. .PARAMETER Force Acknowledges non-interactive mutation and deliberate visibility changes and suppresses routine confirmation prompts. Force does not bypass exact archive and replacement confirmation. .EXAMPLE Copy-GitHubRepository -SourceRepository infoconex/source -DestinationRepository infoconex/destination -PlanOnly Creates a read-only Snapshot repository copy plan with immutable source commit and tree evidence. .EXAMPLE Copy-GitHubRepository -SourceRepository infoconex/source -DestinationRepository infoconex/destination Publishes the approved Snapshot source state as one new unrelated root commit, verifies it, and restores supported configuration. .EXAMPLE Copy-GitHubRepository -SourceRepository infoconex/source -DestinationRepository infoconex/destination -ContentMode FullHistory Copies and verifies the approved history-preserving branch/tag/ref state. .INPUTS None. This command does not accept pipeline input. .OUTPUTS CopyGitHubRepo.MigrationPlan for structured planning, System.String for Plain or Json plan output, and CopyGitHubRepo.MigrationExecutionResult for execution. .LINK https://github.com/infoconex/copy-github-repo .LINK https://github.com/infoconex/copy-github-repo/blob/main/docs/product/product-contract.md #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] 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', [ValidateSet('public', 'private', 'internal')] [string] $DestinationVisibility, [ValidatePattern('^[A-Za-z0-9_.-]+$')] [string] $ArchiveRepositoryName, [string] $SameNameConfirmation, [ValidatePattern('^[A-Za-z0-9_.-]+$')] [string] $ExistingDestinationArchiveName, [string] $ExistingDestinationConfirmation, [ValidateNotNullOrEmpty()] [string] $CommitMessage = 'Initial repository commit', [switch] $RestorePages, [switch] $EnableActionsAfterMigration, [switch] $SkipSettings, [switch] $PlanOnly, [switch] $NonInteractive, [ValidateSet('Interactive', 'Plain', 'Json')] [string] $OutputMode = 'Interactive', [string] $ReportPath, [ValidateNotNullOrEmpty()] [string] $HostName = 'github.com', [switch] $Force ) Assert-CgrSupportedHostName -HostName $HostName $prerequisites = Get-CgrPrerequisiteStatus -HostName $HostName if (-not $prerequisites.Git.Found) { $exception = [System.InvalidOperationException]::new("Git is required for repository copy planning. 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 copy planning. 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) } $source = Get-CgrRepository -Repository $SourceRepository -HostName $HostName $destinationVisibilityWasProvided = $PSBoundParameters.ContainsKey('DestinationVisibility') $resolvedDestinationVisibility = if ($destinationVisibilityWasProvided) { $DestinationVisibility } else { $source.Visibility } $plan = New-CgrMigrationPlan ` -SourceRepository $source ` -DestinationRepository $DestinationRepository ` -ContentMode $ContentMode ` -DestinationVisibility $resolvedDestinationVisibility ` -ArchiveRepositoryName $ArchiveRepositoryName ` -ExistingDestinationArchiveName $ExistingDestinationArchiveName ` -CommitMessage $CommitMessage ` -RestorePages:$RestorePages ` -EnableActionsAfterMigration:$EnableActionsAfterMigration ` -SkipSettings:$SkipSettings ` -HostName $HostName ` -PlanOnly:$PlanOnly if ($PlanOnly) { if ($ReportPath) { Write-CgrMigrationPlanReport -Plan $plan -Path $ReportPath } if ($OutputMode -eq 'Json') { return Format-CgrMigrationPlan -Plan $plan -Format Json } if ($OutputMode -eq 'Plain') { return Format-CgrMigrationPlan -Plan $plan -Format Markdown } return $plan } if ($RestorePages) { $message = 'Pages restoration is not implemented yet. Remove -RestorePages and review the plan output for unsupported settings.' $exception = [System.NotSupportedException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'RestorePagesExecutionNotImplemented', [System.Management.Automation.ErrorCategory]::NotImplemented, 'RestorePages') $PSCmdlet.ThrowTerminatingError($errorRecord) } if ($EnableActionsAfterMigration) { $message = 'Actions activation after repository copy is not implemented yet. Remove -EnableActionsAfterMigration and review the plan output.' $exception = [System.NotSupportedException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'EnableActionsAfterMigrationExecutionNotImplemented', [System.Management.Automation.ErrorCategory]::NotImplemented, 'EnableActionsAfterMigration') $PSCmdlet.ThrowTerminatingError($errorRecord) } if ($NonInteractive -and -not $Force) { $message = 'Non-interactive mutation requires -Force for repository copies.' $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'NonInteractiveExecutionRequiresForce', [System.Management.Automation.ErrorCategory]::InvalidOperation, $DestinationRepository) $PSCmdlet.ThrowTerminatingError($errorRecord) } $isVisibilityChange = $plan.DestinationVisibility -ne $source.Visibility if ($isVisibilityChange -and -not $Force) { $message = "Changing '$($source.FullName)' from '$($source.Visibility)' to '$($plan.DestinationVisibility)' visibility requires explicit acknowledgement. Rerun with -Force only when this visibility change is intentional." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'VisibilityChangeRequiresForce', [System.Management.Automation.ErrorCategory]::PermissionDenied, $DestinationRepository) $PSCmdlet.ThrowTerminatingError($errorRecord) } if ($Force) { $ConfirmPreference = 'None' } $target = $plan.DestinationRepository $action = switch ($plan.Mode) { 'SameNameReplacement' { if ($plan.ContentMode -eq 'FullHistory') { "Preserve '$($plan.SourceRepository)' as '$($plan.ArchiveRepository)', create '$target', and copy the approved FullHistory state" } else { "Preserve '$($plan.SourceRepository)' as '$($plan.ArchiveRepository)', create '$target', and publish the approved Snapshot state" } } 'ExistingDestinationReplacement' { "Preserve existing destination '$target' as '$($plan.ArchiveRepository)', create a fresh replacement, and copy the approved $($plan.ContentMode) state" } default { if ($plan.ContentMode -eq 'FullHistory') { "Create '$target' and copy the approved FullHistory state" } else { "Create '$target' and publish the approved Snapshot state" } } } if (-not $PSCmdlet.ShouldProcess($target, $action)) { return $plan } $resolvedSameNameConfirmation = $SameNameConfirmation $resolvedExistingDestinationConfirmation = $ExistingDestinationConfirmation $confirmWasExplicitlyDisabled = $PSBoundParameters.ContainsKey('Confirm') -and -not [bool] $PSBoundParameters['Confirm'] if ($plan.Mode -eq 'SameNameReplacement' -and [string]::IsNullOrWhiteSpace($resolvedSameNameConfirmation) -and -not $NonInteractive -and -not $confirmWasExplicitlyDisabled) { $expectedConfirmation = "SOURCE=$($plan.SourceRepository);ARCHIVE=$($plan.ArchiveRepository);REPLACEMENT=$($plan.DestinationRepository)" $resolvedSameNameConfirmation = Read-Host "Type exactly: $expectedConfirmation" } if ($plan.Mode -eq 'ExistingDestinationReplacement' -and [string]::IsNullOrWhiteSpace($resolvedExistingDestinationConfirmation) -and -not $NonInteractive -and -not $confirmWasExplicitlyDisabled) { $expectedConfirmation = "DESTINATION=$($plan.DestinationRepository);ARCHIVE=$($plan.ArchiveRepository);REPLACEMENT=$($plan.DestinationRepository)" $resolvedExistingDestinationConfirmation = Read-Host "Type exactly: $expectedConfirmation" } return Invoke-CgrApprovedMigrationPlan ` -Plan $plan ` -SourceRepository $source ` -SameNameConfirmation $resolvedSameNameConfirmation ` -ExistingDestinationConfirmation $resolvedExistingDestinationConfirmation ` -HostName $HostName ` -ReportPath $ReportPath } |