Payload/scripts/kaden/harness/quality-gate/RepositoryQualityPrePush.psm1
|
#Requires -Version 5.1 <# .SYNOPSIS Helpers for pre-push comprehensive-pester evidence reuse (ADR-0009). .DESCRIPTION Builds evaluate-check evidence for comprehensive-pester, resolves the live Pester target set from tests/harness/run-harness-tests.ps1, and reads/writes the local .harness/quality-gate ledger. Does not run Pester by itself. #> Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $script:PesterRuleIdentity = 'pester-runner-v1' $script:EvidenceRelativePath = '.harness/quality-gate/comprehensive-pester.json' function Get-RepositoryQualityRepoRoot { param([string]$StartPath = $PSScriptRoot) $probe = $StartPath for ($i = 0; $i -lt 8; $i++) { $gitDir = Join-Path $probe '.git' $invoke = Join-Path $probe (Join-Path 'scripts' (Join-Path 'kaden' 'harness-invoke.ps1')) if ((Test-Path -LiteralPath $gitDir) -or (Test-Path -LiteralPath $invoke)) { return (Resolve-Path -LiteralPath $probe).Path } $parent = Split-Path $probe -Parent if ([string]::IsNullOrWhiteSpace($parent) -or ($parent -eq $probe)) { break } $probe = $parent } throw "Unable to resolve repository root from $StartPath" } function Get-RepositoryQualityPesterEvidencePath { param([Parameter(Mandatory = $true)][string]$RepoRoot) return [System.IO.Path]::GetFullPath((Join-Path $RepoRoot ($script:EvidenceRelativePath -replace '/', [IO.Path]::DirectorySeparatorChar))) } function Get-RepositoryQualityPesterTargetSet { <# .SYNOPSIS Relative test paths registered in tests/harness/run-harness-tests.ps1. #> param([Parameter(Mandatory = $true)][string]$RepoRoot) $runner = Join-Path $RepoRoot (Join-Path 'tests' (Join-Path 'harness' 'run-harness-tests.ps1')) if (-not (Test-Path -LiteralPath $runner)) { throw "Pester runner not found: $runner" } $raw = Get-Content -LiteralPath $runner -Raw $regexHits = [regex]::Matches( $raw, 'Join-Path\s+\$PSScriptRoot\s+''(?<file>[^'']+\.Tests\.ps1)''' ) $targets = [System.Collections.Generic.List[string]]::new() foreach ($m in $regexHits) { $file = [string]$m.Groups['file'].Value $rel = ('tests/harness/' + $file).Replace('\', '/') if (-not ($targets -contains $rel)) { [void]$targets.Add($rel) } } if ($targets.Count -eq 0) { throw "No Pester test paths parsed from $runner" } return @($targets.ToArray()) } function Get-RepositoryQualityPushChangedPath { <# .SYNOPSIS Paths changed in the unpushed range (upstream..HEAD), or HEAD vs empty tree. #> param( [Parameter(Mandatory = $true)][string]$RepoRoot, [string]$UpstreamRef = '' ) $git = Get-Command git.exe -ErrorAction SilentlyContinue if (-not $git) { $git = Get-Command git -ErrorAction Stop } Push-Location $RepoRoot try { $range = $UpstreamRef if ([string]::IsNullOrWhiteSpace($range)) { $upstream = & $git.Source rev-parse --abbrev-ref '@{upstream}' 2>$null if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($upstream)) { $range = '@{upstream}..HEAD' } else { # No upstream: compare against the empty tree so first push still sees commit trees. $empty = '4b825dc642cb6eb9a060e54bf8d6927bfc89f564' $range = "$empty..HEAD" } } $names = & $git.Source -c core.quotepath=false diff --name-only $range if ($LASTEXITCODE -ne 0) { throw "git diff --name-only failed for range $range" } $result = [System.Collections.Generic.List[string]]::new() foreach ($line in @($names)) { $text = [string]$line if ([string]::IsNullOrWhiteSpace($text)) { continue } [void]$result.Add($text.Replace('\', '/')) } return @($result.ToArray()) } finally { Pop-Location } } function Get-RepositoryQualityHeadRevision { param([Parameter(Mandatory = $true)][string]$RepoRoot) $git = Get-Command git.exe -ErrorAction SilentlyContinue if (-not $git) { $git = Get-Command git -ErrorAction Stop } Push-Location $RepoRoot try { $sha = (& $git.Source rev-parse HEAD).Trim() if ([string]::IsNullOrWhiteSpace($sha)) { throw 'git rev-parse HEAD returned empty' } return $sha } finally { Pop-Location } } function Read-RepositoryQualityPesterEvidence { param([Parameter(Mandatory = $true)][string]$RepoRoot) $path = Get-RepositoryQualityPesterEvidencePath -RepoRoot $RepoRoot if (-not (Test-Path -LiteralPath $path)) { return $null } $doc = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json $targets = @() if ($null -ne $doc.TargetSet) { $targets = @($doc.TargetSet | ForEach-Object { [string]$_ }) } $completed = $null if ($doc.CompletedAt) { $completed = [datetime]::Parse([string]$doc.CompletedAt, $null, [System.Globalization.DateTimeStyles]::RoundtripKind) if ($completed.Kind -eq [DateTimeKind]::Unspecified) { $completed = [datetime]::SpecifyKind($completed, [DateTimeKind]::Utc) } else { $completed = $completed.ToUniversalTime() } } return @{ RuleIdentity = [string]$doc.RuleIdentity TargetSet = $targets FailedCount = [int]$doc.FailedCount CompletedAt = $completed ProtectedRevision = [string]$doc.ProtectedRevision RecordedRevision = [string]$doc.ProtectedRevision } } function Write-RepositoryQualityPesterEvidence { param( [Parameter(Mandatory = $true)][string]$RepoRoot, [Parameter(Mandatory = $true)][string[]]$TargetSet, [Parameter(Mandatory = $true)][datetime]$CompletedAt, [Parameter(Mandatory = $true)][string]$ProtectedRevision, [int]$FailedCount = 0 ) $path = Get-RepositoryQualityPesterEvidencePath -RepoRoot $RepoRoot $dir = Split-Path $path -Parent if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $payload = [ordered]@{ schema = 'kaden.quality-gate.comprehensive-pester/v1' RuleIdentity = $script:PesterRuleIdentity TargetSet = @($TargetSet) FailedCount = $FailedCount CompletedAt = $CompletedAt.ToUniversalTime().ToString('o') ProtectedRevision = $ProtectedRevision } $json = ($payload | ConvertTo-Json -Depth 6) [System.IO.File]::WriteAllText($path, $json, [System.Text.UTF8Encoding]::new($false)) return $path } function New-RepositoryQualityPesterCheckEvidence { <# .SYNOPSIS Build CheckEvidence hashtable for evaluate-check comprehensive-pester. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory evidence hashtable for the decision port; no durable side effects.' )] param( [Parameter(Mandatory = $true)][string[]]$CurrentTargetSet, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[]]$ChangedPaths, [Parameter(Mandatory = $true)][datetime]$RequestedAt, [Parameter(Mandatory = $true)][string]$ProtectedRevision, $PriorEvidence = $null ) $hasPrior = ($null -ne $PriorEvidence) -and ($null -ne $PriorEvidence.CompletedAt) $targetSet = @($CurrentTargetSet) $failedCount = 0 $completedAt = $RequestedAt $recordedRevision = $ProtectedRevision $presentedAgain = $false if ($hasPrior) { $targetSet = @($PriorEvidence.TargetSet) if (@($targetSet).Count -eq 0) { $targetSet = @($CurrentTargetSet) } $failedCount = [int]$PriorEvidence.FailedCount $completedAt = [datetime]$PriorEvidence.CompletedAt $recordedRevision = [string]$PriorEvidence.ProtectedRevision $presentedAgain = $true } return @{ PresentedAgain = $presentedAgain TargetSet = @($targetSet) ScannedTargetSet = @($CurrentTargetSet) CurrentTargetSet = @($CurrentTargetSet) RuleIdentity = $script:PesterRuleIdentity CurrentRuleIdentity = $script:PesterRuleIdentity CompletedAt = $completedAt RequestedAt = $RequestedAt TargetCount = @($CurrentTargetSet).Count FailedCount = $failedCount BlockingFindingCount = 0 ErrorFindingCount = 0 SecretFindingCount = 0 AnalyzerFindingCount = 0 EncodingFaultCount = 0 ChangedPaths = @($ChangedPaths) ProtectedRevision = $ProtectedRevision RecordedRevision = $recordedRevision AffectedTarget = $null Condition = $null } } function Test-RepositoryQualityPesterReuse { <# .SYNOPSIS Ask the decision port whether comprehensive-pester evidence may be reused. #> param( [Parameter(Mandatory = $true)]$CheckEvidence ) return Invoke-RepositoryQualityDecision -Request evaluate-check -Check comprehensive-pester -CheckEvidence $CheckEvidence } Export-ModuleMember -Function @( 'Get-RepositoryQualityRepoRoot' 'Get-RepositoryQualityPesterEvidencePath' 'Get-RepositoryQualityPesterTargetSet' 'Get-RepositoryQualityPushChangedPath' 'Get-RepositoryQualityHeadRevision' 'Read-RepositoryQualityPesterEvidence' 'Write-RepositoryQualityPesterEvidence' 'New-RepositoryQualityPesterCheckEvidence' 'Test-RepositoryQualityPesterReuse' ) |