GitHubActionVersioning.psm1
|
############################################################################## # GitHubActionVersioning.psm1 - PowerShell Module for CLI Usage ############################################################################# # This module provides a user-friendly PowerShell cmdlet for running the # GitHub Action SemVer Checker from the command line. ############################################################################# # Import CLI-specific logging module . "$PSScriptRoot/module/CliLogging.ps1" # Import core library modules . "$PSScriptRoot/lib/StateModel.ps1" . "$PSScriptRoot/lib/VersionParser.ps1" . "$PSScriptRoot/lib/GitHubApi.ps1" . "$PSScriptRoot/lib/RemediationActions.ps1" . "$PSScriptRoot/lib/Remediation.ps1" . "$PSScriptRoot/lib/ValidationRules.ps1" . "$PSScriptRoot/lib/InputValidation.ps1" . "$PSScriptRoot/lib/rules/releases/ReleaseRulesHelper.ps1" . "$PSScriptRoot/lib/rules/marketplace/MarketplaceRulesHelper.ps1" function Test-GitHubActionVersioning { <# .SYNOPSIS Validates semantic versioning tags and branches for GitHub Actions repositories. .DESCRIPTION Checks that version tags follow GitHub's immutable release strategy: - Patch versions (v1.0.0) have immutable GitHub Releases - Floating versions (v1, v1.0, latest) point to the latest compatible release .PARAMETER Repository Repository in format 'owner/repo'. If not provided, uses GITHUB_REPOSITORY environment variable. .PARAMETER Token GitHub token for API access. If not provided, tries gh auth token, then GITHUB_TOKEN environment variable. .PARAMETER CheckMinorVersion Check minor version tags. Values: error, warning, none. Default: error .PARAMETER CheckReleases Check that patch versions have GitHub Releases. Values: error, warning, none. Default: error .PARAMETER CheckReleaseImmutability Check that releases are immutable (published, not draft). Values: error, warning, none. Default: error .PARAMETER CheckMarketplace Check that the action has valid marketplace metadata (name, description, branding, README) and that the latest release is published to the GitHub Marketplace. Values: error, warning, none. Default: error .PARAMETER IgnorePreviewReleases Ignore preview/pre-release versions when calculating floating versions. Default: true .PARAMETER FloatingVersionsUse Use tags or branches for floating versions (v1, v1.0, latest). Values: tags, branches. Default: tags .PARAMETER AutoFix Automatically fix issues by updating tags/branches/releases. Requires write permissions. Default: false .PARAMETER IgnoreVersions Array of version patterns to ignore during validation (e.g., @('v1.0.0', 'v2.*')). .PARAMETER ApiUrl GitHub API URL. Default: https://api.github.com (or GITHUB_API_URL environment variable) .PARAMETER ServerUrl GitHub server URL. Default: https://github.com (or GITHUB_SERVER_URL environment variable) .PARAMETER Rules Array of rule names to run. If not specified, runs all rules. Use this to filter specific validation rules. .PARAMETER PassThru Return an object with detected issues and their statuses instead of just exit code. .EXAMPLE Test-GitHubActionVersioning -Repository 'owner/repo' Validates the specified repository using default settings. .EXAMPLE Test-GitHubActionVersioning -Repository 'owner/repo' -AutoFix Validates and automatically fixes any issues found. .EXAMPLE Test-GitHubActionVersioning -Repository 'owner/repo' -PassThru Returns an object with all validation issues and their statuses. .EXAMPLE Test-GitHubActionVersioning -Repository 'owner/repo' -Rules @('patch_release_required', 'major_tag_tracks_highest_patch') Runs only the specified validation rules. .EXAMPLE Test-GitHubActionVersioning -Repository 'owner/repo' -CheckMarketplace 'warning' Validates marketplace metadata and publication status, reporting issues as warnings. .OUTPUTS By default, returns exit code (0 = success, 1 = validation errors). With -PassThru, returns a hashtable with Issues, State, FixedCount, FailedCount, UnfixableCount, and ReturnCode. #> [CmdletBinding()] [OutputType([int], [hashtable])] param( [Parameter(Position = 0)] [string]$Repository, [Parameter()] [string]$Token, [Parameter()] [ValidateSet('error', 'warning', 'none')] [string]$CheckMinorVersion = 'error', [Parameter()] [ValidateSet('error', 'warning', 'none')] [string]$CheckReleases = 'error', [Parameter()] [ValidateSet('error', 'warning', 'none')] [string]$CheckReleaseImmutability = 'error', [Parameter()] [ValidateSet('error', 'warning', 'none')] [string]$CheckMarketplace = 'error', [Parameter()] [bool]$IgnorePreviewReleases = $true, [Parameter()] [ValidateSet('tags', 'branches')] [string]$FloatingVersionsUse = 'tags', [Parameter()] [switch]$AutoFix, [Parameter()] [string[]]$IgnoreVersions = @(), [Parameter()] [string]$ApiUrl, [Parameter()] [string]$ServerUrl, [Parameter()] [string[]]$Rules, [Parameter()] [switch]$PassThru ) ############################################################################# # Initialize State ############################################################################# # Filter out GitHub Actions workflow commands (::debug::, etc.) from output # The library modules emit these for GitHub Actions, but they're not appropriate for CLI $script:cliMode = $true # Create a wrapper for Write-Host to filter workflow commands in CLI mode function global:Write-Host { [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipeline)] [object]$Object, [switch]$NoNewline, [object]$Separator, [System.ConsoleColor]$ForegroundColor, [System.ConsoleColor]$BackgroundColor ) # Filter out GitHub Actions workflow commands in CLI mode # Matches patterns like ::debug::, ::warning::, ::error::, ::notice::, etc. if ($script:cliMode -and $Object -match '^::([a-z-]+)::') { # Convert ::debug:: to Write-Verbose, suppress others if ($Object -match '^::debug::') { $message = $Object -replace '^::debug::', '' Write-Verbose $message } # Suppress all other workflow commands (::warning::, ::error::, ::group::, etc.) return } # Pass through to original Write-Host using fully qualified name to avoid recursion $params = @{} if ($PSBoundParameters.ContainsKey('Object')) { $params['Object'] = $Object } if ($NoNewline) { $params['NoNewline'] = $true } if ($PSBoundParameters.ContainsKey('Separator')) { $params['Separator'] = $Separator } if ($PSBoundParameters.ContainsKey('ForegroundColor')) { $params['ForegroundColor'] = $ForegroundColor } if ($PSBoundParameters.ContainsKey('BackgroundColor')) { $params['BackgroundColor'] = $BackgroundColor } Microsoft.PowerShell.Utility\Write-Host @params } ############################################################################# # Resolve Token (CLI-specific: try gh auth token before environment) ############################################################################# $resolvedToken = $Token if (-not $resolvedToken) { # Try gh auth token first (CLI-specific) try { $ghToken = gh auth token 2>$null if ($LASTEXITCODE -eq 0 -and $ghToken) { $resolvedToken = $ghToken.Trim() Write-Verbose "Using token from 'gh auth token'" } } catch { Write-Verbose "gh auth token not available or failed: $($_.Exception.Message)" } } ############################################################################# # Initialize State using shared function ############################################################################# # Use the shared initialization function (no token masking in CLI mode) $state = Initialize-RepositoryState -Repository $Repository -Token $resolvedToken -ApiUrl $ApiUrl -ServerUrl $ServerUrl -MaskToken $false # Validate repository was provided or resolved if (-not $state.RepoOwner -or -not $state.RepoName) { # Try to provide helpful error based on whether Repository param was provided if (-not $Repository -and -not $env:GITHUB_REPOSITORY) { Write-ActionsError -Message "Repository not specified. Provide -Repository parameter or set GITHUB_REPOSITORY environment variable." -State $state } else { $repoValue = if ($Repository) { $Repository } else { $env:GITHUB_REPOSITORY } Write-ActionsError -Message "Invalid repository format. Expected 'owner/repo', got '$repoValue'" -State $state } if ($PassThru) { return New-ErrorResult -State $state } return 1 } # Warn if no token available if (-not $state.Token) { Write-ActionsWarning -Message "No GitHub token available. API rate limits will be restrictive. Consider providing -Token or running 'gh auth login'." } ############################################################################# # Configure State ############################################################################# $state.CheckMinorVersion = ($CheckMinorVersion -ne "none") $state.CheckReleases = $CheckReleases $state.CheckImmutability = $CheckReleaseImmutability $state.CheckMarketplace = $CheckMarketplace $state.IgnorePreviewReleases = $IgnorePreviewReleases $state.FloatingVersionsUse = $FloatingVersionsUse $state.AutoFix = $AutoFix.IsPresent $state.IgnoreVersions = $IgnoreVersions # Validate auto-fix requirements if ($AutoFix -and -not $Token) { Write-ActionsError -Message "Auto-fix mode requires a GitHub token. Provide -Token or ensure GITHUB_TOKEN is set." -State $state if ($PassThru) { return New-ErrorResult -State $state } return 1 } ############################################################################# # Fetch Repository Data ############################################################################# Write-Host "Fetching repository data for $Repository..." try { Initialize-RepositoryData -State $state ` -IgnoreVersions $IgnoreVersions ` -CheckMarketplace $CheckMarketplace ` -AutoFix $AutoFix.IsPresent ` -ScriptRoot "$PSScriptRoot" Write-Host "Found $($state.Tags.Count) version tags" if ($FloatingVersionsUse -eq 'branches') { Write-Host "Found $($state.Branches.Count) version branches" } Write-Host "Found $($state.Releases.Count) releases" if ($CheckMarketplace -ne 'none' -and $state.MarketplaceMetadata) { if ($state.MarketplaceMetadata.IsValid()) { Write-Host "Marketplace metadata: Valid (name=$($state.MarketplaceMetadata.Name))" } else { $missing = $state.MarketplaceMetadata.GetMissingRequirements() Write-Host "Marketplace metadata: Missing requirements - $($missing -join ', ')" } } } catch { Write-ActionsError -Message "Failed to fetch repository data: $_" -State $state if ($PassThru) { return New-ErrorResult -State $state } return 1 } ############################################################################# # Load and Run Validation Rules ############################################################################# Write-Host "" Write-Host "Running validation rules..." # Create config hashtable $config = @{ 'check-minor-version' = $CheckMinorVersion 'check-releases' = $CheckReleases 'check-release-immutability' = $CheckReleaseImmutability 'check-marketplace' = $CheckMarketplace 'ignore-preview-releases' = $IgnorePreviewReleases 'floating-versions-use' = $FloatingVersionsUse 'auto-fix' = $AutoFix.IsPresent } # Load all rules or filtered rules $allRules = Get-ValidationRule -Config $config if ($Rules -and $Rules.Count -gt 0) { Write-Host "Filtering to specified rules: $($Rules -join ', ')" $rulesToRun = $allRules | Where-Object { $_.Name -in $Rules } if ($rulesToRun.Count -eq 0) { Write-ActionsWarning -Message "No matching rules found. Available rules: $($allRules.Name -join ', ')" } } else { $rulesToRun = $allRules } # Run validation rules Invoke-ValidationRule -State $state -Config $config -Rules $rulesToRun | Out-Null Write-Host "Validation complete. Found $($state.Issues.Count) issue(s)." ############################################################################# # Auto-Fix ############################################################################# if ($AutoFix -and $state.Issues.Count -gt 0) { Write-Host "" Write-Host "Auto-fix enabled. Attempting to fix issues..." Invoke-Remediation -State $state $fixedCount = $state.GetFixedIssuesCount() $failedCount = $state.GetFailedFixesCount() $unfixableCount = $state.GetUnfixableIssuesCount() Write-Host "Auto-fix results: $fixedCount fixed, $failedCount failed, $unfixableCount unfixable" } ############################################################################# # Display Results ############################################################################# Write-Host "" if ($state.Issues.Count -eq 0) { Write-Host "✓ All validations passed!" -ForegroundColor Green } else { # Group issues by status $byStatus = $state.Issues | Group-Object -Property Status foreach ($group in $byStatus) { Write-Host "" Write-Host "Issues with status '$($group.Name)': $($group.Count)" foreach ($issue in $group.Group) { $prefix = if ($issue.Severity -eq 'error') { ' ERROR:' } else { ' WARNING:' } Write-Host "$prefix $($issue.Message)" } } # Show manual fix commands if available if ($AutoFix) { $pendingIssues = $state.Issues | Where-Object { $_.Status -in @('pending', 'failed', 'unfixable', 'manual_fix_required') } if ($pendingIssues.Count -gt 0) { Write-Host "" Write-Host "Manual fixes required:" Get-ManualFixCommands -State $state | ForEach-Object { Write-Host " $_" } } } } ############################################################################# # Return Results ############################################################################# # Restore original Write-Host Remove-Item Function:\Write-Host -ErrorAction SilentlyContinue $script:cliMode = $false # Calculate return code - if AutoFix is disabled, pending issues should cause failure # If AutoFix is enabled, only unresolved issues after fixing should cause failure $returnCode = $state.GetReturnCode() # When AutoFix is disabled, pending issues mean validation failed if (-not $AutoFix) { $pendingCount = ($state.Issues | Where-Object { $_.Status -eq "pending" }).Count if ($pendingCount -gt 0) { $returnCode = 1 } } if ($PassThru) { return @{ Issues = $state.Issues State = $state FixedCount = $state.GetFixedIssuesCount() FailedCount = $state.GetFailedFixesCount() UnfixableCount = $state.GetUnfixableIssuesCount() ReturnCode = $returnCode } } return $returnCode } # Export the cmdlets Export-ModuleMember -Function Test-GitHubActionVersioning, Get-ManualInstruction # Helper function for creating error result function New-ErrorResult { <# .SYNOPSIS Creates a standardized error result hashtable for PassThru output. .DESCRIPTION Helper function that generates a consistent error result structure containing the current issues and zero counts for fixes. Used when early validation fails before processing can begin. .PARAMETER State The RepositoryState object containing accumulated issues. .OUTPUTS Hashtable with Issues, FixedCount, FailedCount, UnfixableCount, and ReturnCode. #> param( [Parameter(Mandatory)] [RepositoryState]$State ) return @{ Issues = $State.Issues State = $State FixedCount = 0 FailedCount = 0 UnfixableCount = 0 ReturnCode = 1 } } |