|
| 1 | +/** |
| 2 | + * @file Probe githubstatus.com to detect platform degradation before making |
| 3 | + * GitHub API calls. Useful in scripts, release tooling, and CI pre-flight |
| 4 | + * checks where a cryptic "operation was canceled" error would otherwise mask |
| 5 | + * an upstream GitHub outage. |
| 6 | + * |
| 7 | + * Component IDs are stable GitHub-assigned identifiers from |
| 8 | + * githubstatus.com/api/v2/components.json. The probe adds at most 8 seconds |
| 9 | + * to startup (configurable timeout) and fails open on network error so a |
| 10 | + * down status page never blocks a healthy workflow. |
| 11 | + */ |
| 12 | + |
| 13 | +// oxlint-disable-next-line socket/no-platform-specific-http-import -- server-only module; node platform is intentional. |
| 14 | +import { httpJson } from '../http-request/node' |
| 15 | + |
| 16 | +export type GitHubComponentStatus = |
| 17 | + | 'degraded_performance' |
| 18 | + | 'major_outage' |
| 19 | + | 'operational' |
| 20 | + | 'partial_outage' |
| 21 | + | 'under_maintenance' |
| 22 | + |
| 23 | +export type GitHubStatusResult = { |
| 24 | + /** Worst-case status across all monitored components. */ |
| 25 | + status: GitHubComponentStatus | 'unknown' |
| 26 | + /** Whether any monitored component is not fully operational. */ |
| 27 | + degraded: boolean |
| 28 | + /** Human-readable summary, e.g. "Actions: degraded_performance". */ |
| 29 | + summary: string |
| 30 | + /** Per-component breakdown for the monitored set. */ |
| 31 | + components: Array<{ |
| 32 | + id: string |
| 33 | + name: string |
| 34 | + status: GitHubComponentStatus |
| 35 | + }> |
| 36 | +} |
| 37 | + |
| 38 | +// Component IDs that matter for CI / GitHub API call workflows. |
| 39 | +// Stable identifiers from githubstatus.com; the names are display-only. |
| 40 | +const MONITORED_COMPONENT_IDS: ReadonlyMap<string, string> = new Map([ |
| 41 | + ['br0l2tvcx85d', 'Actions'], |
| 42 | + ['8l4ygp009s5s', 'Git Operations'], |
| 43 | + ['brv1bkgrwx7q', 'API Requests'], |
| 44 | +]) |
| 45 | + |
| 46 | +const SEVERITY: ReadonlyMap<string, number> = new Map([ |
| 47 | + ['major_outage', 4], |
| 48 | + ['partial_outage', 3], |
| 49 | + ['degraded_performance', 2], |
| 50 | + ['under_maintenance', 1], |
| 51 | + ['operational', 0], |
| 52 | +]) |
| 53 | + |
| 54 | +const STATUS_API_URL = 'https://www.githubstatus.com/api/v2/components.json' |
| 55 | + |
| 56 | +/** |
| 57 | + * Probe githubstatus.com and return the health of GitHub Actions, Git |
| 58 | + * Operations, and API Requests. Fails open (returns `{ status: 'unknown' }`) |
| 59 | + * when the probe itself fails — so a down status page never blocks a healthy |
| 60 | + * workflow. |
| 61 | + * |
| 62 | + * @param timeoutMs - Maximum milliseconds to wait for the probe. Default 8000. |
| 63 | + * |
| 64 | + * @example |
| 65 | + * ;```typescript |
| 66 | + * import { probeGitHubStatus } from '@socketsecurity/lib/env/github-status' |
| 67 | + * |
| 68 | + * const health = await probeGitHubStatus() |
| 69 | + * if (health.degraded) { |
| 70 | + * console.warn(`GitHub degraded: ${health.summary}`) |
| 71 | + * } |
| 72 | + * ``` |
| 73 | + */ |
| 74 | +export async function probeGitHubStatus( |
| 75 | + timeoutMs = 8000, |
| 76 | +): Promise<GitHubStatusResult> { |
| 77 | + let body: unknown |
| 78 | + try { |
| 79 | + body = await httpJson(STATUS_API_URL, { |
| 80 | + timeout: timeoutMs, |
| 81 | + }) |
| 82 | + } catch { |
| 83 | + return { |
| 84 | + status: 'unknown', |
| 85 | + degraded: false, |
| 86 | + summary: 'githubstatus.com unreachable — cannot confirm GitHub health', |
| 87 | + components: [], |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + const raw = body as { |
| 92 | + components?: Array<{ id: string; name: string; status: string }> |
| 93 | + } |
| 94 | + const allComponents = raw.components ?? [] |
| 95 | + |
| 96 | + const monitored: GitHubStatusResult['components'] = [] |
| 97 | + let worstSeverity = 0 |
| 98 | + let worstStatus: GitHubComponentStatus = 'operational' |
| 99 | + |
| 100 | + for (const c of allComponents) { |
| 101 | + const name = MONITORED_COMPONENT_IDS.get(c.id) |
| 102 | + if (!name) { |
| 103 | + continue |
| 104 | + } |
| 105 | + const status = c.status as GitHubComponentStatus |
| 106 | + const sev = SEVERITY.get(status) ?? 0 |
| 107 | + if (sev > worstSeverity) { |
| 108 | + worstSeverity = sev |
| 109 | + worstStatus = status |
| 110 | + } |
| 111 | + monitored.push({ id: c.id, name, status }) |
| 112 | + } |
| 113 | + |
| 114 | + const degradedComponents = monitored.filter(c => c.status !== 'operational') |
| 115 | + const degraded = degradedComponents.length > 0 |
| 116 | + const summary = |
| 117 | + degraded |
| 118 | + ? degradedComponents.map(c => `${c.name}: ${c.status}`).join(', ') |
| 119 | + : 'All monitored GitHub components operational' |
| 120 | + |
| 121 | + return { |
| 122 | + status: worstStatus, |
| 123 | + degraded, |
| 124 | + summary, |
| 125 | + components: monitored, |
| 126 | + } |
| 127 | +} |
0 commit comments