Private/Migration/Copy-CgrRepositorySnapshot.ps1
|
function Copy-CgrRepositorySnapshot { <# .SYNOPSIS Publishes an approved source tree as a clean unrelated Snapshot history. .DESCRIPTION Clones only the approved source branch and verifies the cloned HEAD against reviewed source-state evidence. Plain Snapshot creates one unrelated root commit. When an immutable release-checkpoint plan is supplied, execution fetches only the reviewed source commits by SHA, verifies each reviewed tree, and constructs a new linear destination history in the exact reviewed checkpoint order. It never reruns live release selection or topology ordering. Selected release tags are recreated from the exact reviewed tag evidence and checkpoint mapping. Source lightweight and annotated tags are both normalized to lightweight destination refs that point at the newly generated Snapshot checkpoint commits; source annotated-tag object identity and metadata are intentionally not preserved by Snapshot. Generated checkpoint messages use the deterministic convention "Snapshot release checkpoint <order>: <reviewed tag names>" and an optional final reviewed HEAD commit uses "Snapshot current state". Destination commit identities, authorship, timestamps, and ancestry are intentionally new. .NOTES This is a Git mutation boundary called after the public ShouldProcess decision. The temporary workspace is always removed. Approved-state or reviewed release-tag mismatch stops before the destination branch or release tags are published. When a caller supplies RecoveryGeneratedCommits, constructed commit evidence is retained outside the temporary workspace without changing publication behavior. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public Copy-GitHubRepository command performs the ShouldProcess check before calling this Git boundary.' )] [CmdletBinding()] param( [Parameter(Mandatory)] [psobject] $SourceRepository, [Parameter(Mandatory)] [psobject] $DestinationRepository, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $BranchName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $CommitMessage, [psobject] $ApprovedSourceState, [psobject] $ReleaseCheckpointPlan, [System.Collections.Generic.List[object]] $RecoveryGeneratedCommits ) $workspacePath = Join-Path ([System.IO.Path]::GetTempPath()) "copy-github-repo-$([guid]::NewGuid().ToString('N'))" $sourcePath = Join-Path $workspacePath 'source' try { New-Item -Path $workspacePath -ItemType Directory -Force | Out-Null Send-CgrActivityEvent -Name 'CloneSourceContent' -State Started -Message "Clone source content from '$($SourceRepository.FullName)'" $cloneSourceResult = Invoke-CgrGitCommand ` -HostName $SourceRepository.HostName ` -Environment @{ GIT_LFS_SKIP_SMUDGE = '1' } ` -ArgumentList @('clone', '--depth', '1', '--branch', $BranchName, $SourceRepository.CloneUrl, $sourcePath) if ($cloneSourceResult.ExitCode -ne 0) { Send-CgrActivityEvent -Name 'CloneSourceContent' -State Failed -Message "Clone source content from '$($SourceRepository.FullName)'" $message = "Git failed to clone source repository '$($SourceRepository.FullName)'. $($cloneSourceResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SourceRepositoryCloneFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $SourceRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } Send-CgrActivityEvent -Name 'CloneSourceContent' -State Completed -Message "Clone source content from '$($SourceRepository.FullName)'" $sourceCommitResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $sourcePath, 'rev-parse', 'HEAD') if ($sourceCommitResult.ExitCode -ne 0) { $message = "Git failed to read source commit for '$($SourceRepository.FullName)'. $($sourceCommitResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SourceRepositoryCommitReadFailed', [System.Management.Automation.ErrorCategory]::InvalidData, $SourceRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $sourceCommitSha = [string] @($sourceCommitResult.Output)[0] $treeShaResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $sourcePath, 'rev-parse', 'HEAD^{tree}') if ($treeShaResult.ExitCode -ne 0) { $message = "Git failed to read source tree for '$($SourceRepository.FullName)'. $($treeShaResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SourceRepositoryTreeReadFailed', [System.Management.Automation.ErrorCategory]::InvalidData, $SourceRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $treeSha = [string] @($treeShaResult.Output)[0] if ($ApprovedSourceState) { $expectedCommitSha = [string] (Get-CgrObjectProperty -InputObject $ApprovedSourceState -Name 'CommitSha') $expectedTreeSha = [string] (Get-CgrObjectProperty -InputObject $ApprovedSourceState -Name 'TreeSha') $expectedBranch = [string] (Get-CgrObjectProperty -InputObject $ApprovedSourceState -Name 'DefaultBranch') if ($BranchName -ne $expectedBranch -or $sourceCommitSha -ne $expectedCommitSha -or $treeSha -ne $expectedTreeSha) { $message = "Source repository '$($SourceRepository.FullName)' no longer matches the approved Snapshot source state. Nothing was published. Recreate and review the repository copy plan." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SourceStateChangedSincePlanning', [System.Management.Automation.ErrorCategory]::InvalidResult, $SourceRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } } $checkpoints = @() $releaseEvidence = @() $finalHeadCheckpointRequired = $false if ($ReleaseCheckpointPlan) { $plannedSourceHead = Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'SourceHead' if ($null -eq $plannedSourceHead -or [string] (Get-CgrObjectProperty -InputObject $plannedSourceHead -Name 'CommitSha') -ne $sourceCommitSha -or [string] (Get-CgrObjectProperty -InputObject $plannedSourceHead -Name 'TreeSha') -ne $treeSha) { $message = 'The approved Snapshot release-checkpoint plan does not match the reviewed source HEAD state. Nothing was published. Recreate and review the repository copy plan.' $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseCheckpointPlanSourceMismatch', [System.Management.Automation.ErrorCategory]::InvalidData, $SourceRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $checkpoints = @(Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'Checkpoints') $releaseEvidence = @(Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'ReleaseEvidence') $finalHeadCheckpointRequired = [bool] (Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'FinalHeadCheckpointRequired') } if ($releaseEvidence.Count -gt 0) { foreach ($evidence in $releaseEvidence) { $tagName = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'TagName') $expectedTagObjectType = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'TagObjectType') $expectedTagObjectSha = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'TagObjectSha') $expectedPeeledCommitSha = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'PeeledCommitSha') $mappedCheckpoint = @($checkpoints | Where-Object { [string] (Get-CgrObjectProperty -InputObject $_ -Name 'SourceCommitSha') -eq $expectedPeeledCommitSha }) if ([string]::IsNullOrWhiteSpace($tagName) -or $mappedCheckpoint.Count -ne 1 -or $tagName -notin @(Get-CgrObjectProperty -InputObject $mappedCheckpoint[0] -Name 'TagNames')) { $message = "Approved Snapshot release-tag evidence for '$tagName' does not map exactly once to its reviewed checkpoint boundary. Nothing was published." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseTagPlanMappingInvalid', [System.Management.Automation.ErrorCategory]::InvalidData, $tagName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $tagRefResult = Invoke-CgrGitHubApiReadRequest ` -ArgumentList @('api', '--hostname', $SourceRepository.HostName, "repos/$($SourceRepository.FullName)/git/ref/tags/$([uri]::EscapeDataString($tagName))") if ($tagRefResult.ExitCode -ne 0) { $message = "Selected release tag '$tagName' no longer matches the approved Snapshot release-tag evidence. Nothing was published. Recreate and review the repository copy plan." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseTagStateChangedSincePlanning', [System.Management.Automation.ErrorCategory]::InvalidResult, $tagName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $tagRefJson = ($tagRefResult.Output | ForEach-Object { [string] $_ }) -join "`n" $tagRef = $tagRefJson | ConvertFrom-Json -Depth 20 $actualTagObjectType = [string] $tagRef.object.type $actualTagObjectSha = [string] $tagRef.object.sha if ($actualTagObjectType -ne $expectedTagObjectType -or $actualTagObjectSha -ne $expectedTagObjectSha) { $message = "Selected release tag '$tagName' changed after planning. Expected reviewed $expectedTagObjectType object '$expectedTagObjectSha' but found $actualTagObjectType object '$actualTagObjectSha'. Nothing was published." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseTagStateChangedSincePlanning', [System.Management.Automation.ErrorCategory]::InvalidResult, $tagName) $PSCmdlet.ThrowTerminatingError($errorRecord) } } } $commitIdentity = Get-CgrGitCommitIdentity -HostName $DestinationRepository.HostName $generatedCommits = [System.Collections.Generic.List[object]]::new() $parentCommitSha = $null Send-CgrActivityEvent -Name 'PublishSnapshot' -State Started -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" foreach ($checkpoint in $checkpoints) { $reviewedSourceCommitSha = [string] (Get-CgrObjectProperty -InputObject $checkpoint -Name 'SourceCommitSha') $reviewedSourceTreeSha = [string] (Get-CgrObjectProperty -InputObject $checkpoint -Name 'SourceTreeSha') $checkpointOrder = [int] (Get-CgrObjectProperty -InputObject $checkpoint -Name 'Order') $tagNames = @(Get-CgrObjectProperty -InputObject $checkpoint -Name 'TagNames') $fetchCheckpointResult = Invoke-CgrGitCommand ` -HostName $SourceRepository.HostName ` -Environment @{ GIT_LFS_SKIP_SMUDGE = '1' } ` -ArgumentList @('-C', $sourcePath, 'fetch', '--depth', '1', 'origin', $reviewedSourceCommitSha) if ($fetchCheckpointResult.ExitCode -ne 0) { Send-CgrActivityEvent -Name 'PublishSnapshot' -State Failed -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" $message = "Git failed to fetch reviewed Snapshot checkpoint source commit '$reviewedSourceCommitSha'. $($fetchCheckpointResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseCheckpointFetchFailed', [System.Management.Automation.ErrorCategory]::ReadError, $reviewedSourceCommitSha) $PSCmdlet.ThrowTerminatingError($errorRecord) } $checkpointTreeResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $sourcePath, 'rev-parse', "$reviewedSourceCommitSha^{tree}") if ($checkpointTreeResult.ExitCode -ne 0) { $message = "Git failed to read the fetched tree for reviewed Snapshot checkpoint '$reviewedSourceCommitSha'. $($checkpointTreeResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseCheckpointTreeReadFailed', [System.Management.Automation.ErrorCategory]::InvalidData, $reviewedSourceCommitSha) $PSCmdlet.ThrowTerminatingError($errorRecord) } $actualCheckpointTreeSha = [string] @($checkpointTreeResult.Output)[0] if ($actualCheckpointTreeSha -ne $reviewedSourceTreeSha) { $message = "Reviewed Snapshot checkpoint source commit '$reviewedSourceCommitSha' no longer resolves to planned tree '$reviewedSourceTreeSha'. Nothing was published." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseCheckpointTreeMismatch', [System.Management.Automation.ErrorCategory]::InvalidResult, $reviewedSourceCommitSha) $PSCmdlet.ThrowTerminatingError($errorRecord) } $checkpointMessage = "Snapshot release checkpoint ${checkpointOrder}: $($tagNames -join ', ')" $checkpointCommitArguments = @('-C', $sourcePath, '-c', "user.name=$($commitIdentity.Name)", '-c', "user.email=$($commitIdentity.Email)", 'commit-tree', $reviewedSourceTreeSha) if ($parentCommitSha) { $checkpointCommitArguments += @('-p', $parentCommitSha) } $checkpointCommitArguments += @('-m', $checkpointMessage) $checkpointCommitResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList $checkpointCommitArguments if ($checkpointCommitResult.ExitCode -ne 0) { Send-CgrActivityEvent -Name 'PublishSnapshot' -State Failed -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" $message = "Git failed to create Snapshot release checkpoint $checkpointOrder for '$($DestinationRepository.FullName)'. $($checkpointCommitResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'DestinationRepositorySnapshotCheckpointCommitFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $DestinationRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $parentCommitSha = [string] @($checkpointCommitResult.Output)[0] $generatedCommit = [pscustomobject] @{ Kind = 'ReleaseCheckpoint' Order = $checkpointOrder SourceCommitSha = $reviewedSourceCommitSha SourceTreeSha = $reviewedSourceTreeSha TagNames = $tagNames CommitSha = $parentCommitSha TreeSha = $reviewedSourceTreeSha Message = $checkpointMessage Published = $false } $generatedCommits.Add($generatedCommit) if ($null -ne $RecoveryGeneratedCommits) { $RecoveryGeneratedCommits.Add($generatedCommit) } } if ($checkpoints.Count -eq 0 -or $finalHeadCheckpointRequired) { $currentStateMessage = if ($checkpoints.Count -eq 0) { $CommitMessage } else { 'Snapshot current state' } $currentStateCommitArguments = @('-C', $sourcePath, '-c', "user.name=$($commitIdentity.Name)", '-c', "user.email=$($commitIdentity.Email)", 'commit-tree', $treeSha) if ($parentCommitSha) { $currentStateCommitArguments += @('-p', $parentCommitSha) } $currentStateCommitArguments += @('-m', $currentStateMessage) $currentStateCommitResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList $currentStateCommitArguments if ($currentStateCommitResult.ExitCode -ne 0) { Send-CgrActivityEvent -Name 'PublishSnapshot' -State Failed -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" $message = "Git failed to create snapshot commit for '$($DestinationRepository.FullName)'. $($currentStateCommitResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'DestinationRepositorySnapshotCommitFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $DestinationRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $parentCommitSha = [string] @($currentStateCommitResult.Output)[0] $generatedCommit = [pscustomobject] @{ Kind = if ($checkpoints.Count -eq 0) { 'SnapshotRoot' } else { 'CurrentHead' } Order = $generatedCommits.Count + 1 SourceCommitSha = $sourceCommitSha SourceTreeSha = $treeSha TagNames = @() CommitSha = $parentCommitSha TreeSha = $treeSha Message = $currentStateMessage Published = $false } $generatedCommits.Add($generatedCommit) if ($null -ne $RecoveryGeneratedCommits) { $RecoveryGeneratedCommits.Add($generatedCommit) } } $commitSha = $parentCommitSha $rootCommitSha = [string] (Get-CgrObjectProperty -InputObject $generatedCommits[0] -Name 'CommitSha') $releaseTags = [System.Collections.Generic.List[object]]::new() foreach ($evidence in $releaseEvidence) { $tagName = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'TagName') $peeledCommitSha = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'PeeledCommitSha') $generatedCheckpoint = @($generatedCommits | Where-Object { (Get-CgrObjectProperty -InputObject $_ -Name 'Kind') -eq 'ReleaseCheckpoint' -and [string] (Get-CgrObjectProperty -InputObject $_ -Name 'SourceCommitSha') -eq $peeledCommitSha }) if ($generatedCheckpoint.Count -ne 1) { $message = "Reviewed Snapshot release tag '$tagName' could not be mapped to exactly one generated destination checkpoint commit. Nothing was published." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'SnapshotReleaseTagDestinationMappingInvalid', [System.Management.Automation.ErrorCategory]::InvalidData, $tagName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $releaseTags.Add([pscustomobject] @{ TagName = $tagName SourceTagObjectType = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'TagObjectType') SourceTagObjectSha = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'TagObjectSha') SourcePeeledCommitSha = $peeledCommitSha DestinationCommitSha = [string] (Get-CgrObjectProperty -InputObject $generatedCheckpoint[0] -Name 'CommitSha') DestinationTagType = 'lightweight' }) } $gitLfs = Invoke-CgrActivityStage ` -Name 'TransferGitLfs' ` -Message "Transfer Git LFS objects to '$($DestinationRepository.FullName)'" ` -Action { Copy-CgrGitLfsObject -SourcePath $sourcePath -SourceRepository $SourceRepository -DestinationRepository $DestinationRepository -BranchName $BranchName } $pushArguments = @('-C', $sourcePath, 'push') if ($releaseTags.Count -gt 0) { $pushArguments += '--atomic' } $pushArguments += @($DestinationRepository.CloneUrl, "$($commitSha):refs/heads/$BranchName") foreach ($releaseTag in $releaseTags) { $pushArguments += "$($releaseTag.DestinationCommitSha):refs/tags/$($releaseTag.TagName)" } $pushResult = Invoke-CgrGitCommand -HostName $DestinationRepository.HostName -ArgumentList $pushArguments if ($pushResult.ExitCode -ne 0) { Send-CgrActivityEvent -Name 'PublishSnapshot' -State Failed -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" $message = "Git failed to push snapshot history and reviewed release tags to '$($DestinationRepository.FullName)'. $($pushResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'DestinationRepositorySnapshotPushFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $DestinationRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } foreach ($generatedCommit in $generatedCommits) { $generatedCommit.Published = $true } $remoteRefResult = Invoke-CgrGitCommand -HostName $DestinationRepository.HostName -ArgumentList @('ls-remote', '--heads', $DestinationRepository.CloneUrl, $BranchName) if ($remoteRefResult.ExitCode -ne 0) { Send-CgrActivityEvent -Name 'PublishSnapshot' -State Failed -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" $message = "Git failed to verify destination branch '$BranchName' for '$($DestinationRepository.FullName)'. $($remoteRefResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'DestinationRepositorySnapshotVerifyFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $DestinationRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } $remoteRef = (@($remoteRefResult.Output) | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine if ($remoteRef -notmatch [regex]::Escape($commitSha)) { Send-CgrActivityEvent -Name 'PublishSnapshot' -State Failed -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" $message = "Destination branch '$BranchName' does not reference snapshot commit '$commitSha'." $exception = [System.InvalidOperationException]::new($message) $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'DestinationRepositorySnapshotVerifyMismatch', [System.Management.Automation.ErrorCategory]::InvalidData, $DestinationRepository.FullName) $PSCmdlet.ThrowTerminatingError($errorRecord) } Send-CgrActivityEvent -Name 'PublishSnapshot' -State Completed -Message "Publish clean Snapshot to '$($DestinationRepository.FullName)'" return [pscustomobject] @{ PSTypeName = 'CopyGitHubRepo.SnapshotCopyResult' SourceRepository = $SourceRepository.FullName SourceCommitSha = $sourceCommitSha DestinationRepository = $DestinationRepository.FullName BranchName = $BranchName CommitSha = $commitSha RootCommitSha = $rootCommitSha TreeSha = $treeSha GitLfs = $gitLfs ApprovedSourceState = $ApprovedSourceState ReleaseCheckpointPlan = $ReleaseCheckpointPlan GeneratedCommits = $generatedCommits.ToArray() ReleaseTags = $releaseTags.ToArray() FinalHeadCommitCreated = [bool] ($ReleaseCheckpointPlan -and $checkpoints.Count -gt 0 -and $finalHeadCheckpointRequired) Verified = $true } } finally { if (Test-Path -LiteralPath $workspacePath) { Remove-Item -LiteralPath $workspacePath -Recurse -Force -ErrorAction SilentlyContinue } } } |