Public/Test-GitHubHealth.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Test-GitHubHealth {
    <#
    .SYNOPSIS
        Tests health of GitHub services used by the AI Triad project.
    .DESCRIPTION
        Checks GitHub platform status, repo accessibility, Actions workflow
        status, and API rate limits. Uses GITHUB_TOKEN env var for authenticated
        checks (higher rate limits, workflow status) when available.
    .PARAMETER Repo
        GitHub repo in owner/name format. Default: jpsnover/ai-triad-research.
    .PARAMETER TimeoutSec
        HTTP request timeout in seconds. Default: 10.
    .PARAMETER Branch
        Branch to scope the workflow-runs status check to. Default: main (the
        deployed line) — so a red PR/branch run does not false-FAIL the CI-health
        category in a prod smoke (t/1975).
    .EXAMPLE
        Test-GitHubHealth
    .EXAMPLE
        Test-GitHubHealth -Repo 'jpsnover/ai-triad-research'
    .LINK
        Show-AITriadHelp
    .LINK
        Get-GitHubWorkflowRun
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$Repo = 'jpsnover/ai-triad-research',

        [Parameter()]
        [ValidateRange(1, 120)]
        [int]$TimeoutSec = 10,

        [Parameter()]
        [string]$Branch = 'main'
    )

    Set-StrictMode -Version Latest

    $Checks = [System.Collections.Generic.List[PSObject]]::new()
    $Headers = @{ 'Accept' = 'application/json'; 'User-Agent' = 'AITriad-HealthCheck/1.0' }
    $Token = $env:GITHUB_TOKEN
    if ($Token) { $Headers['Authorization'] = "Bearer $Token" }

    # ── GitHub Status API ────────────────────────────────────────────────
    try {
        $Sw = [System.Diagnostics.Stopwatch]::StartNew()
        $StatusResp = Invoke-RestMethod -Uri 'https://www.githubstatus.com/api/v2/status.json' `
            -Headers @{ 'Accept' = 'application/json' } -TimeoutSec $TimeoutSec -ErrorAction Stop
        $Sw.Stop()

        $Indicator = $StatusResp.status.indicator
        $Description = $StatusResp.status.description

        $Checks.Add([PSCustomObject]@{
            Check      = 'GitHub Platform Status'
            Pass       = $Indicator -eq 'none'
            ResponseMs = $Sw.ElapsedMilliseconds
            Detail     = "$Indicator — $Description"
        })
    }
    catch {
        $Checks.Add([PSCustomObject]@{
            Check      = 'GitHub Platform Status'
            Pass       = $false
            ResponseMs = 0
            Detail     = $_.Exception.Message
        })
    }

    # ── Repository Accessibility ─────────────────────────────────────────
    try {
        $Sw = [System.Diagnostics.Stopwatch]::StartNew()
        $RepoResp = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo" `
            -Headers $Headers -TimeoutSec $TimeoutSec -ErrorAction Stop
        $Sw.Stop()

        $Checks.Add([PSCustomObject]@{
            Check      = 'Repository Accessible'
            Pass       = $true
            ResponseMs = $Sw.ElapsedMilliseconds
            Detail     = "visibility=$($RepoResp.visibility) | default_branch=$($RepoResp.default_branch)"
        })
    }
    catch {
        $Checks.Add([PSCustomObject]@{
            Check      = 'Repository Accessible'
            Pass       = $false
            ResponseMs = 0
            Detail     = $_.Exception.Message
        })
    }

    # ── API Rate Limits ──────────────────────────────────────────────────
    try {
        $RateResp = Invoke-RestMethod -Uri 'https://api.github.com/rate_limit' `
            -Headers $Headers -TimeoutSec $TimeoutSec -ErrorAction Stop

        $Core = $RateResp.resources.core
        $Remaining = $Core.remaining
        $Limit = $Core.limit
        $ResetEpoch = $Core.reset
        $ResetTime = ([DateTimeOffset]::FromUnixTimeSeconds($ResetEpoch)).LocalDateTime.ToString('HH:mm:ss')
        $Pct = if ($Limit -gt 0) { [math]::Round(($Remaining / $Limit) * 100, 0) } else { 0 }

        $Checks.Add([PSCustomObject]@{
            Check      = 'API Rate Limit'
            Pass       = $Remaining -gt 10
            ResponseMs = 0
            Detail     = "$Remaining/$Limit remaining ($Pct%) | resets $ResetTime | auth=$(if ($Token) {'token'} else {'anonymous'})"
        })
    }
    catch {
        $Checks.Add([PSCustomObject]@{
            Check      = 'API Rate Limit'
            Pass       = $false
            ResponseMs = 0
            Detail     = $_.Exception.Message
        })
    }

    # ── Latest Workflow Runs ─────────────────────────────────────────────
    $KeyWorkflows = @('ci.yml', 'deploy-azure.yml', 'health-monitor.yml')

    # t/1975 — scope the workflow-runs query to the deployed line (default: main)
    # so a red PR/branch run doesn't false-FAIL the CI-health category in a prod
    # smoke. GitHub filters by head_branch via the `branch` param; escape guards
    # branch names containing '/'.
    $BranchQ = [uri]::EscapeDataString($Branch)
    foreach ($Wf in $KeyWorkflows) {
        try {
            $RunsResp = Invoke-RestMethod `
                -Uri "https://api.github.com/repos/$Repo/actions/workflows/$Wf/runs?per_page=1&status=completed&branch=$BranchQ" `
                -Headers $Headers -TimeoutSec $TimeoutSec -ErrorAction Stop

            if ($RunsResp.total_count -gt 0) {
                $Run = $RunsResp.workflow_runs[0]
                $Conclusion = $Run.conclusion
                $RunDate = ([datetime]$Run.updated_at).ToString('yyyy-MM-dd HH:mm')

                $Checks.Add([PSCustomObject]@{
                    Check      = "Workflow: $Wf @$Branch"
                    Pass       = $Conclusion -eq 'success'
                    ResponseMs = 0
                    Detail     = "conclusion=$Conclusion | $RunDate | #$($Run.run_number)"
                })
            }
            else {
                $Checks.Add([PSCustomObject]@{
                    Check      = "Workflow: $Wf @$Branch"
                    Pass       = $true
                    ResponseMs = 0
                    Detail     = 'No completed runs found'
                })
            }
        }
        catch {
            $StatusCode = 0
            if ($_.Exception.PSObject.Properties['Response'] -and $_.Exception.Response) {
                $StatusCode = [int]$_.Exception.Response.StatusCode
            }
            $Checks.Add([PSCustomObject]@{
                Check      = "Workflow: $Wf @$Branch"
                Pass       = $false
                ResponseMs = 0
                Detail     = if ($StatusCode -eq 404) { 'Workflow not found' }
                             elseif ($StatusCode -eq 403) { 'Auth required — set GITHUB_TOKEN env var' }
                             else { $_.Exception.Message }
            })
        }
    }

    # ── GHCR Package Accessibility ───────────────────────────────────────
    try {
        $Sw = [System.Diagnostics.Stopwatch]::StartNew()
        $GhcrResp = Invoke-WebRequest -Uri 'https://ghcr.io/v2/' `
            -TimeoutSec $TimeoutSec -UseBasicParsing -ErrorAction Stop `
            -Headers @{ 'Accept' = 'application/json' }
        $Sw.Stop()

        $Checks.Add([PSCustomObject]@{
            Check      = 'GHCR Registry'
            Pass       = $true
            ResponseMs = $Sw.ElapsedMilliseconds
            Detail     = "Reachable (HTTP $($GhcrResp.StatusCode))"
        })
    }
    catch {
        $StatusCode = 0
        if ($_.Exception.PSObject.Properties['Response'] -and $_.Exception.Response) {
            $StatusCode = [int]$_.Exception.Response.StatusCode
        }
        # GHCR returns 401 for unauthenticated — that still means it's reachable
        $Reachable = $StatusCode -eq 401
        $Checks.Add([PSCustomObject]@{
            Check      = 'GHCR Registry'
            Pass       = $Reachable
            ResponseMs = 0
            Detail     = if ($Reachable) { "Reachable (HTTP 401 — auth required for pulls)" } else { $_.Exception.Message }
        })
    }

    # ── Summary ──────────────────────────────────────────────────────────
    $AllPass = @($Checks | Where-Object { -not $_.Pass }).Count -eq 0

    [PSCustomObject]@{
        Platform  = 'GitHub'
        Repo      = $Repo
        Healthy   = $AllPass
        Checks    = @($Checks)
        Timestamp = (Get-Date).ToString('o')
    }
}