Private/Helpers.ps1
|
# Built-in standard profiles and helpers for CodeCompass.GitHub #region Standard Profiles $script:Standards = @{ core = @{ branch_protection = @{ require = $true min_reviewers = 1 dismiss_stale_reviews = $true require_status_checks = $true require_up_to_date = $true enforce_admins = $false restrict_force_pushes = $true restrict_deletions = $true require_linear_history = $false require_signed_commits = $false } security = @{ dependabot_security = $true dependabot_versions = $true secret_scanning = $true push_protection = $true vulnerability_alerts = $true } settings = @{ require_description = $true require_topics = $true issues_enabled = $true default_branch_name = 'main' delete_branch_on_merge = $true allow_squash_merge = $true wiki_disabled = $true } collaboration = @{ max_stale_branch_days = 90 max_open_prs = 10 } } active = @{ branch_protection = @{ require = $true min_reviewers = 1 dismiss_stale_reviews = $false require_status_checks = $false require_up_to_date = $false enforce_admins = $false restrict_force_pushes = $true restrict_deletions = $true require_linear_history = $false require_signed_commits = $false } security = @{ dependabot_security = $true dependabot_versions = $false secret_scanning = $true push_protection = $false vulnerability_alerts = $true } settings = @{ require_description = $true require_topics = $true issues_enabled = $true default_branch_name = $null # Don't enforce delete_branch_on_merge = $true allow_squash_merge = $false # Don't enforce wiki_disabled = $false # Don't enforce } collaboration = @{ max_stale_branch_days = 90 max_open_prs = 10 } } minimal = @{ branch_protection = @{ require = $false min_reviewers = 0 dismiss_stale_reviews = $false require_status_checks = $false require_up_to_date = $false enforce_admins = $false restrict_force_pushes = $false restrict_deletions = $false require_linear_history = $false require_signed_commits = $false } security = @{ dependabot_security = $false dependabot_versions = $false secret_scanning = $false push_protection = $false vulnerability_alerts = $true } settings = @{ require_description = $true require_topics = $false issues_enabled = $false default_branch_name = $null delete_branch_on_merge = $false allow_squash_merge = $false wiki_disabled = $false } collaboration = @{ max_stale_branch_days = $null max_open_prs = $null } } } #endregion #region API Helpers function Resolve-CCToken { <# .SYNOPSIS Resolves a GitHub token from parameter, environment, or gh CLI. #> [CmdletBinding()] [OutputType([string])] param( [Parameter()] [string]$Token ) if ($Token) { return $Token } if ($env:GITHUB_TOKEN) { return $env:GITHUB_TOKEN } if ($env:GH_TOKEN) { return $env:GH_TOKEN } # Try gh CLI try { $ghToken = gh auth token 2>$null if ($LASTEXITCODE -eq 0 -and $ghToken) { return $ghToken.Trim() } } catch { } return $null } function Invoke-CCGitHubApi { <# .SYNOPSIS Makes an authenticated GitHub API call with error handling. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Endpoint, [Parameter(Mandatory)] [string]$Token, [Parameter()] [ValidateSet('Get', 'Put', 'Post', 'Patch', 'Delete')] [string]$Method = 'Get', [Parameter()] [object]$Body, [Parameter()] [switch]$AllowNotFound ) $uri = if ($Endpoint -match '^https://') { $Endpoint } else { "https://api.github.com/$Endpoint" } $headers = @{ 'Authorization' = "Bearer $Token" 'Accept' = 'application/vnd.github+json' 'X-GitHub-Api-Version' = '2022-11-28' } $params = @{ Uri = $uri Headers = $headers Method = $Method } if ($Body) { $params['Body'] = ($Body | ConvertTo-Json -Depth 10) $params['ContentType'] = 'application/json' } try { return Invoke-RestMethod @params } catch { $statusCode = $_.Exception.Response.StatusCode.value__ if ($AllowNotFound -and $statusCode -eq 404) { return $null } if ($statusCode -eq 403 -and $_.Exception.Response.Headers['X-RateLimit-Remaining'] -eq '0') { $reset = $_.Exception.Response.Headers['X-RateLimit-Reset'] $resetTime = [DateTimeOffset]::FromUnixTimeSeconds([int]$reset).LocalDateTime Write-Warning "GitHub API rate limit exceeded. Resets at $resetTime" } throw } } function Resolve-CCRepository { <# .SYNOPSIS Resolves repository owner/name from parameter or git remote. #> [CmdletBinding()] [OutputType([string])] param( [Parameter()] [string]$Repository, [Parameter()] [string]$Path = $PWD.Path ) if ($Repository) { return $Repository } try { Push-Location $Path $remote = git remote get-url origin 2>$null Pop-Location if ($remote -match 'github\.com[:/](.+?)(?:\.git)?$') { return $Matches[1] } } catch { } return $null } function Get-CCStandardConfig { <# .SYNOPSIS Loads and merges standard configuration with per-repo overrides. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [string]$Standard, [Parameter()] [string]$ConfigPath ) if (-not $script:Standards.ContainsKey($Standard)) { throw "Unknown standard '$Standard'. Available: $($script:Standards.Keys -join ', ')" } $config = $script:Standards[$Standard] # Load per-repo override if available if ($ConfigPath -and (Test-Path $ConfigPath)) { try { # Support YAML-like or JSON config $content = Get-Content $ConfigPath -Raw if ($ConfigPath -match '\.json$') { $override = $content | ConvertFrom-Json -AsHashtable } else { # Simple YAML parser for our config subset $override = ConvertFrom-CCYaml -Content $content } if ($override -and $override['github']) { $config = Merge-CCConfig -Base $config -Override $override['github'] } } catch { Write-Warning "Failed to load config override from $ConfigPath : $_" } } return $config } function Merge-CCConfig { <# .SYNOPSIS Deep merges two config hashtables (override wins). #> param( [hashtable]$Base, [hashtable]$Override ) $result = @{} foreach ($key in $Base.Keys) { if ($Base[$key] -is [hashtable] -and $Override.ContainsKey($key) -and $Override[$key] -is [hashtable]) { $result[$key] = Merge-CCConfig -Base $Base[$key] -Override $Override[$key] } elseif ($Override.ContainsKey($key)) { $result[$key] = $Override[$key] } else { $result[$key] = $Base[$key] } } # Add keys from override that aren't in base foreach ($key in $Override.Keys) { if (-not $result.ContainsKey($key)) { $result[$key] = $Override[$key] } } return $result } function ConvertFrom-CCYaml { <# .SYNOPSIS Minimal YAML parser for CodeCompass config files (key: value, nested with indentation). #> param([string]$Content) $result = @{} $stack = @(@{ Indent = -1; Dict = $result }) foreach ($line in $Content -split "`n") { if ($line -match '^\s*#' -or $line -match '^\s*$') { continue } if ($line -match '^(\s*)(\S+):\s*$') { # Section header (no value) $indent = $Matches[1].Length $key = $Matches[2] $newDict = @{} while ($stack.Count -gt 1 -and $stack[-1].Indent -ge $indent) { $stack = $stack[0..($stack.Count - 2)] } $stack[-1].Dict[$key] = $newDict $stack += @{ Indent = $indent; Dict = $newDict } } elseif ($line -match '^(\s*)(\S+):\s+(.+)$') { # Key: value $indent = $Matches[1].Length $key = $Matches[2] $rawValue = $Matches[3].Trim() while ($stack.Count -gt 1 -and $stack[-1].Indent -ge $indent) { $stack = $stack[0..($stack.Count - 2)] } # Type coercion $value = switch -Regex ($rawValue) { '^true$' { $true } '^false$' { $false } '^\d+$' { [int]$rawValue } '^null$' { $null } default { $rawValue.Trim('"', "'") } } $stack[-1].Dict[$key] = $value } } return $result } function New-CCCheckResult { <# .SYNOPSIS Creates a standardized check result object. #> param( [string]$CheckId, [string]$Category, [string]$Item, [ValidateSet('Pass', 'Fail', 'Warning', 'Skipped')] [string]$Status, [ValidateSet('Error', 'Warning', 'Info')] [string]$Severity = 'Error', [string]$Message, [bool]$FixAvailable = $false, [string]$FixId, [object]$Current, [object]$Expected ) [PSCustomObject]@{ CheckId = $CheckId Category = $Category Item = $Item Status = $Status Severity = $Severity Message = $Message FixAvailable = $FixAvailable FixId = $FixId Current = $Current Expected = $Expected } } #endregion |