Private/Migration/Invoke-CgrApprovedSnapshotReleaseVerification.ps1
|
function Invoke-CgrApprovedSnapshotReleaseVerification { <# .SYNOPSIS Verifies generated Snapshot release checkpoints from immutable reviewed evidence. .DESCRIPTION Reads only the destination Git repository and compares its generated linear Snapshot history with the reviewed release-checkpoint plan. Source commit identities are not expected to survive Snapshot publication; verification instead proves checkpoint order, parent relationships, tree equivalence, release-tag targets, and reviewed HEAD equivalence from immutable planning evidence. #> [CmdletBinding()] param( [Parameter(Mandatory)] [psobject] $SourceRepository, [Parameter(Mandatory)] [psobject] $DestinationRepository, [Parameter(Mandatory)] [psobject] $ReleaseCheckpointPlan ) $workspacePath = Join-Path ([System.IO.Path]::GetTempPath()) "copy-github-repo-snapshot-release-verify-$([guid]::NewGuid().ToString('N'))" $destinationPath = Join-Path $workspacePath 'destination.git' try { New-Item -Path $workspacePath -ItemType Directory -Force | Out-Null $destinationHostName = if ($DestinationRepository.PSObject.Properties.Name -contains 'HostName') { [string] $DestinationRepository.HostName } else { ([uri] $DestinationRepository.CloneUrl).Host } $cloneResult = Invoke-CgrGitCommand ` -HostName $destinationHostName ` -ArgumentList @('clone', '--bare', $DestinationRepository.CloneUrl, $destinationPath) if ($cloneResult.ExitCode -ne 0) { $message = "Git failed to clone destination repository '$($DestinationRepository.FullName)' for Snapshot release verification. $($cloneResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new( $exception, 'SnapshotReleaseVerificationDestinationCloneFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $DestinationRepository.FullName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } $sourceHead = Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'SourceHead' $checkpoints = @(Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'Checkpoints') $releaseEvidence = @(Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'ReleaseEvidence') $expectedCommitCount = [int] (Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'PlannedSnapshotCommitCount') $finalHeadCheckpointRequired = [bool] (Get-CgrObjectProperty -InputObject $ReleaseCheckpointPlan -Name 'FinalHeadCheckpointRequired') $expectedBranch = [string] (Get-CgrObjectProperty -InputObject $sourceHead -Name 'DefaultBranch') $expectedHeadTree = [string] (Get-CgrObjectProperty -InputObject $sourceHead -Name 'TreeSha') $historyResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $destinationPath, 'rev-list', '--reverse', "refs/heads/$expectedBranch") if ($historyResult.ExitCode -ne 0) { $message = "Git failed to read generated Snapshot history for destination '$($DestinationRepository.FullName)'. $($historyResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new( $exception, 'SnapshotReleaseVerificationHistoryReadFailed', [System.Management.Automation.ErrorCategory]::ReadError, $DestinationRepository.FullName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } $destinationCommits = @($historyResult.Output | ForEach-Object { [string] $_ }) $checks = [System.Collections.Generic.List[object]]::new() $checkpointResults = [System.Collections.Generic.List[object]]::new() $releaseTagResults = [System.Collections.Generic.List[object]]::new() $checks.Add([pscustomobject] @{ Name = 'DestinationExists' Passed = $true Expected = $DestinationRepository.FullName Actual = $DestinationRepository.FullName }) $checks.Add([pscustomobject] @{ Name = 'DefaultBranchMatches' Passed = [string] $DestinationRepository.DefaultBranch -eq $expectedBranch Expected = $expectedBranch Actual = [string] $DestinationRepository.DefaultBranch }) $checks.Add([pscustomobject] @{ Name = 'SnapshotCommitCountMatchesReviewedPlan' Passed = $destinationCommits.Count -eq $expectedCommitCount Expected = $expectedCommitCount Actual = $destinationCommits.Count }) for ($index = 0; $index -lt $destinationCommits.Count; $index++) { $commitSha = [string] $destinationCommits[$index] $parentsResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $destinationPath, 'rev-list', '--parents', '-n', '1', $commitSha) if ($parentsResult.ExitCode -ne 0) { $message = "Git failed to read parents for generated Snapshot commit '$commitSha'. $($parentsResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new( $exception, 'SnapshotReleaseVerificationParentReadFailed', [System.Management.Automation.ErrorCategory]::ReadError, $commitSha ) $PSCmdlet.ThrowTerminatingError($errorRecord) } $parts = @(([string] @($parentsResult.Output)[0]).Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries)) $expectedParents = if ($index -eq 0) { @() } else { @([string] $destinationCommits[$index - 1]) } $actualParents = if ($parts.Count -gt 1) { @($parts[1..($parts.Count - 1)]) } else { @() } $checks.Add([pscustomobject] @{ Name = "SnapshotCommitParent:$($index + 1)" Passed = ($actualParents -join "`n") -eq ($expectedParents -join "`n") Expected = $expectedParents Actual = $actualParents }) } foreach ($checkpoint in $checkpoints) { $order = [int] (Get-CgrObjectProperty -InputObject $checkpoint -Name 'Order') $expectedTree = [string] (Get-CgrObjectProperty -InputObject $checkpoint -Name 'SourceTreeSha') $sourceCommitSha = [string] (Get-CgrObjectProperty -InputObject $checkpoint -Name 'SourceCommitSha') $destinationCommitSha = if ($order -ge 1 -and $order -le $destinationCommits.Count) { [string] $destinationCommits[$order - 1] } else { $null } $actualTree = $null if (-not [string]::IsNullOrWhiteSpace($destinationCommitSha)) { $treeResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $destinationPath, 'rev-parse', "$destinationCommitSha^{tree}") if ($treeResult.ExitCode -ne 0) { $message = "Git failed to read tree for generated Snapshot checkpoint $order ('$destinationCommitSha'). $($treeResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new( $exception, 'SnapshotReleaseVerificationCheckpointTreeReadFailed', [System.Management.Automation.ErrorCategory]::ReadError, $destinationCommitSha ) $PSCmdlet.ThrowTerminatingError($errorRecord) } $actualTree = [string] @($treeResult.Output)[0] } $checkpointPassed = -not [string]::IsNullOrWhiteSpace($destinationCommitSha) -and $actualTree -eq $expectedTree $checks.Add([pscustomobject] @{ Name = "SnapshotCheckpointTree:$order" Passed = $checkpointPassed Expected = $expectedTree Actual = $actualTree }) $checkpointResults.Add([pscustomobject] @{ Order = $order SourceCommitSha = $sourceCommitSha ExpectedTreeSha = $expectedTree DestinationCommitSha = $destinationCommitSha DestinationTreeSha = $actualTree TagNames = @(Get-CgrObjectProperty -InputObject $checkpoint -Name 'TagNames') IsSuccessful = $checkpointPassed }) } $destinationHeadCommit = if ($destinationCommits.Count -gt 0) { [string] $destinationCommits[-1] } else { $null } $destinationHeadTree = $null if ($destinationHeadCommit) { $headTreeResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $destinationPath, 'rev-parse', "$destinationHeadCommit^{tree}") if ($headTreeResult.ExitCode -ne 0) { $message = "Git failed to read destination Snapshot HEAD tree. $($headTreeResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new( $exception, 'SnapshotReleaseVerificationHeadTreeReadFailed', [System.Management.Automation.ErrorCategory]::ReadError, $DestinationRepository.FullName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } $destinationHeadTree = [string] @($headTreeResult.Output)[0] } $checks.Add([pscustomobject] @{ Name = 'SnapshotHeadTreeMatchesReviewedSourceHead' Passed = $destinationHeadTree -eq $expectedHeadTree Expected = $expectedHeadTree Actual = $destinationHeadTree }) $expectedFinalCommitKind = if ($finalHeadCheckpointRequired) { 'CurrentStateCheckpoint' } else { 'LatestReleaseCheckpoint' } $actualFinalCommitKind = if ($destinationCommits.Count -ne $expectedCommitCount) { 'UnexpectedCommitCount' } elseif ($finalHeadCheckpointRequired -and $destinationCommits.Count -gt $checkpoints.Count) { 'CurrentStateCheckpoint' } elseif (-not $finalHeadCheckpointRequired -and $destinationCommits.Count -eq $checkpoints.Count) { 'LatestReleaseCheckpoint' } else { 'UnexpectedHistoryShape' } $checks.Add([pscustomobject] @{ Name = 'SnapshotFinalHeadCheckpointShape' Passed = $actualFinalCommitKind -eq $expectedFinalCommitKind Expected = $expectedFinalCommitKind Actual = $actualFinalCommitKind }) foreach ($evidence in $releaseEvidence) { $tagName = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'TagName') $sourceCommitSha = [string] (Get-CgrObjectProperty -InputObject $evidence -Name 'PeeledCommitSha') $mappedCheckpoint = @($checkpointResults | Where-Object SourceCommitSha -EQ $sourceCommitSha) $expectedDestinationCommit = if ($mappedCheckpoint.Count -eq 1) { [string] $mappedCheckpoint[0].DestinationCommitSha } else { $null } $tagResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $destinationPath, 'rev-parse', "refs/tags/$tagName^{commit}") $actualDestinationCommit = if ($tagResult.ExitCode -eq 0) { [string] @($tagResult.Output)[0] } else { $null } $tagPassed = -not [string]::IsNullOrWhiteSpace($expectedDestinationCommit) -and $actualDestinationCommit -eq $expectedDestinationCommit $checks.Add([pscustomobject] @{ Name = "SnapshotReleaseTagTarget:$tagName" Passed = $tagPassed Expected = $expectedDestinationCommit Actual = $actualDestinationCommit }) $releaseTagResults.Add([pscustomobject] @{ TagName = $tagName SourceCommitSha = $sourceCommitSha DestinationCommitSha = $actualDestinationCommit ExpectedDestinationCommitSha = $expectedDestinationCommit IsSuccessful = $tagPassed }) } $tagListResult = Invoke-CgrNativeCommand -FilePath 'git' -ArgumentList @('-C', $destinationPath, 'for-each-ref', '--format=%(refname:strip=2)', 'refs/tags') if ($tagListResult.ExitCode -ne 0) { $message = "Git failed to enumerate destination Snapshot release tags. $($tagListResult.ErrorText)" $exception = [System.InvalidOperationException]::new($message.Trim()) $errorRecord = [System.Management.Automation.ErrorRecord]::new( $exception, 'SnapshotReleaseVerificationTagEnumerationFailed', [System.Management.Automation.ErrorCategory]::ReadError, $DestinationRepository.FullName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } $expectedTags = @($releaseEvidence | ForEach-Object { [string] (Get-CgrObjectProperty -InputObject $_ -Name 'TagName') } | Sort-Object) $actualTags = @($tagListResult.Output | ForEach-Object { [string] $_ } | Sort-Object) $checks.Add([pscustomobject] @{ Name = 'SnapshotReleaseTagsMatchReviewedSelection' Passed = ($actualTags -join "`n") -eq ($expectedTags -join "`n") Expected = $expectedTags Actual = $actualTags }) [pscustomobject] @{ PSTypeName = 'CopyGitHubRepo.MigrationVerificationResult' SchemaVersion = 1 ContentMode = 'Snapshot' IncludeReleases = $true SourceRepository = $SourceRepository.FullName DestinationRepository = $DestinationRepository.FullName ApprovedReleaseCheckpointPlan = $ReleaseCheckpointPlan BranchName = $expectedBranch ExpectedCommitCount = $expectedCommitCount DestinationCommitCount = $destinationCommits.Count FinalHeadCheckpointRequired = $finalHeadCheckpointRequired DestinationHeadCommitSha = $destinationHeadCommit SourceHeadTreeSha = $expectedHeadTree DestinationHeadTreeSha = $destinationHeadTree Checkpoints = $checkpointResults.ToArray() ReleaseTags = $releaseTagResults.ToArray() Checks = $checks.ToArray() IsSuccessful = -not ($checks | Where-Object { -not $_.Passed }) } } finally { if (Test-Path -LiteralPath $workspacePath) { Remove-Item -LiteralPath $workspacePath -Recurse -Force -ErrorAction SilentlyContinue } } } |