Public/Test-PCReleaseReady.ps1
|
<# .SYNOPSIS Validates that the repository is ready for a release. .DESCRIPTION Performs pre-release validation checks including git status, branch, remote sync, optional test and changelog validation. .PARAMETER RequireBranch Branch name(s) that releases must be created from. Default: main, master .PARAMETER RequireTests Require Pester tests to pass before release. .PARAMETER RequireChangelog Require CHANGELOG.md to exist and be updated. .PARAMETER Path Project directory. Defaults to current directory. .EXAMPLE Test-PCReleaseReady # Basic validation: git repo, branch, clean, remote sync .EXAMPLE Test-PCReleaseReady -RequireTests -RequireChangelog # Full validation including tests and changelog .OUTPUTS [PSCustomObject] with Pass, Errors, Warnings, and Checks properties. #> function Test-PCReleaseReady { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [string[]]$RequireBranch = @('main', 'master'), [switch]$RequireTests, [switch]$RequireChangelog, [Parameter()] [string]$Path = (Get-Location).Path ) Push-Location $Path try { $result = [PSCustomObject]@{ Pass = $true Errors = [System.Collections.Generic.List[string]]::new() Warnings = [System.Collections.Generic.List[string]]::new() Checks = [System.Collections.Generic.List[PSCustomObject]]::new() } # Check 1: Git repository if (-not (Test-GitRepository -Path $Path)) { $result.Pass = $false $result.Errors.Add("Not in a git repository") $result.Checks.Add([PSCustomObject]@{ Check = "Git Repository"; Status = "FAIL"; Message = "Not in a git repository" }) return $result } $result.Checks.Add([PSCustomObject]@{ Check = "Git Repository"; Status = "PASS"; Message = "Valid git repository" }) # Check 2: Current branch try { $currentBranch = Get-CurrentBranch if ($currentBranch -notin $RequireBranch) { $result.Pass = $false $result.Errors.Add("Must be on branch: $($RequireBranch -join ' or '). Currently on: $currentBranch") $result.Checks.Add([PSCustomObject]@{ Check = "Branch"; Status = "FAIL"; Message = "On '$currentBranch', expected: $($RequireBranch -join '/')" }) } else { $result.Checks.Add([PSCustomObject]@{ Check = "Branch"; Status = "PASS"; Message = "On valid branch: $currentBranch" }) } } catch { $result.Pass = $false $result.Errors.Add("Failed to get current branch: $_") $result.Checks.Add([PSCustomObject]@{ Check = "Branch"; Status = "FAIL"; Message = $_.Exception.Message }) } # Check 3: Working directory clean if (-not (Test-WorkingDirectoryClean)) { $result.Pass = $false $result.Errors.Add("Working directory has uncommitted changes") $result.Checks.Add([PSCustomObject]@{ Check = "Working Directory"; Status = "FAIL"; Message = "Uncommitted changes detected" }) } else { $result.Checks.Add([PSCustomObject]@{ Check = "Working Directory"; Status = "PASS"; Message = "Clean working directory" }) } # Check 4: Remote sync try { $branchStatus = Test-BranchUpToDate if (-not $branchStatus.IsUpToDate) { if ($branchStatus.BehindBy -gt 0) { $result.Pass = $false $result.Errors.Add("Branch is $($branchStatus.BehindBy) commit(s) behind remote") } if ($branchStatus.AheadBy -gt 0) { $result.Pass = $false $result.Errors.Add("Branch is $($branchStatus.AheadBy) commit(s) ahead of remote. Push first.") } $result.Checks.Add([PSCustomObject]@{ Check = "Remote Sync"; Status = "FAIL"; Message = $branchStatus.Message }) } else { $result.Checks.Add([PSCustomObject]@{ Check = "Remote Sync"; Status = "PASS"; Message = "Up to date with remote" }) } } catch { $result.Warnings.Add("Could not verify remote sync: $_") $result.Checks.Add([PSCustomObject]@{ Check = "Remote Sync"; Status = "WARN"; Message = "Could not verify: $($_.Exception.Message)" }) } # Check 5: Version detectable try { $versionInfo = Get-PCProjectVersion -Path $Path $result.Checks.Add([PSCustomObject]@{ Check = "Version"; Status = "PASS"; Message = "Current version: $($versionInfo.Version)" }) } catch { $result.Pass = $false $result.Errors.Add("Cannot detect project version: $_") $result.Checks.Add([PSCustomObject]@{ Check = "Version"; Status = "FAIL"; Message = $_.Exception.Message }) } # Check 6: Tests (if required) if ($RequireTests) { $testFiles = Get-ChildItem -Path $Path -Filter '*.Tests.ps1' -Recurse if ($testFiles) { try { $testResult = Invoke-Pester -Path $Path -PassThru -Show None if ($testResult.FailedCount -gt 0) { $result.Pass = $false $result.Errors.Add("$($testResult.FailedCount) test(s) failed") $result.Checks.Add([PSCustomObject]@{ Check = "Tests"; Status = "FAIL"; Message = "$($testResult.FailedCount) failed, $($testResult.PassedCount) passed" }) } else { $result.Checks.Add([PSCustomObject]@{ Check = "Tests"; Status = "PASS"; Message = "$($testResult.PassedCount) test(s) passed" }) } } catch { $result.Pass = $false $result.Errors.Add("Test execution failed: $_") $result.Checks.Add([PSCustomObject]@{ Check = "Tests"; Status = "FAIL"; Message = $_.Exception.Message }) } } else { $result.Warnings.Add("No Pester test files found") $result.Checks.Add([PSCustomObject]@{ Check = "Tests"; Status = "WARN"; Message = "No .Tests.ps1 files found" }) } } # Check 7: CHANGELOG (if required) if ($RequireChangelog) { $changelogPath = Join-Path $Path 'CHANGELOG.md' if (-not (Test-Path $changelogPath)) { $result.Pass = $false $result.Errors.Add("CHANGELOG.md not found") $result.Checks.Add([PSCustomObject]@{ Check = "CHANGELOG"; Status = "FAIL"; Message = "File not found" }) } else { $changelogContent = Get-Content $changelogPath -Raw if ($changelogContent -match '## \[Unreleased\]' -and -not ($changelogContent -match '## \[\d+\.\d+\.\d+\]')) { $result.Warnings.Add("CHANGELOG.md only has [Unreleased] section — no versioned entries") $result.Checks.Add([PSCustomObject]@{ Check = "CHANGELOG"; Status = "WARN"; Message = "Only [Unreleased] section present" }) } else { $result.Checks.Add([PSCustomObject]@{ Check = "CHANGELOG"; Status = "PASS"; Message = "CHANGELOG.md exists with versioned entries" }) } } } return $result } finally { Pop-Location } } |