|
| 1 | +<# |
| 2 | +.SYNOPSIS |
| 3 | + Run Cortex's quality gates locally, the same ones .github/workflows/quality.yml runs. |
| 4 | +
|
| 5 | +.DESCRIPTION |
| 6 | + Catches failures on your machine in ~2 minutes instead of waiting on CI. |
| 7 | +
|
| 8 | + Tiers: |
| 9 | + quick (default) Lint, backend tests, contract drift, frontend types/lint/unit tests. |
| 10 | + This is what the pre-push hook runs. |
| 11 | + full Everything in quick, plus compileall, Playwright e2e, and the |
| 12 | + frontend bundle build. |
| 13 | +
|
| 14 | + Deliberately NOT included at any tier: PyInstaller packaging, the recipe-worker / |
| 15 | + coordinator qualification spikes, and WebView2 signature verification. Those take |
| 16 | + 35+ minutes and need signing tooling -- leave them to CI or run them by hand. |
| 17 | +
|
| 18 | +.EXAMPLE |
| 19 | + ./scripts/check.ps1 |
| 20 | + ./scripts/check.ps1 -Tier full |
| 21 | + ./scripts/check.ps1 -SkipFrontend |
| 22 | +#> |
| 23 | +[CmdletBinding()] |
| 24 | +param( |
| 25 | + [ValidateSet('quick', 'full')] |
| 26 | + [string]$Tier = 'quick', |
| 27 | + [switch]$SkipBackend, |
| 28 | + [switch]$SkipFrontend |
| 29 | +) |
| 30 | + |
| 31 | +$ErrorActionPreference = 'Continue' |
| 32 | +$repoRoot = Split-Path -Parent $PSScriptRoot |
| 33 | +$frontend = Join-Path $repoRoot 'frontend' |
| 34 | + |
| 35 | +$results = [System.Collections.Generic.List[object]]::new() |
| 36 | + |
| 37 | +function Invoke-Step { |
| 38 | + param( |
| 39 | + [Parameter(Mandatory)][string]$Name, |
| 40 | + [Parameter(Mandatory)][scriptblock]$Body, |
| 41 | + [string]$WorkingDirectory = $repoRoot |
| 42 | + ) |
| 43 | + |
| 44 | + Write-Host '' |
| 45 | + Write-Host "-> $Name" -ForegroundColor Cyan |
| 46 | + |
| 47 | + $started = Get-Date |
| 48 | + Push-Location $WorkingDirectory |
| 49 | + try { |
| 50 | + $global:LASTEXITCODE = 0 |
| 51 | + & $Body |
| 52 | + $code = $LASTEXITCODE |
| 53 | + } catch { |
| 54 | + Write-Host $_.Exception.Message -ForegroundColor Red |
| 55 | + $code = 1 |
| 56 | + } finally { |
| 57 | + Pop-Location |
| 58 | + } |
| 59 | + |
| 60 | + $elapsed = [math]::Round(((Get-Date) - $started).TotalSeconds, 1) |
| 61 | + $ok = ($code -eq 0) |
| 62 | + |
| 63 | + if ($ok) { |
| 64 | + Write-Host " ok ($elapsed s)" -ForegroundColor Green |
| 65 | + } else { |
| 66 | + Write-Host " FAILED (exit $code, $elapsed s)" -ForegroundColor Red |
| 67 | + } |
| 68 | + |
| 69 | + $results.Add([pscustomobject]@{ Name = $Name; Ok = $ok; Seconds = $elapsed }) |
| 70 | +} |
| 71 | + |
| 72 | +Write-Host "Cortex local quality check -- tier: $Tier" -ForegroundColor White |
| 73 | + |
| 74 | +if (-not $SkipBackend) { |
| 75 | + Invoke-Step 'Lint Python (ruff)' { |
| 76 | + python -m ruff check backend tests tools main.py |
| 77 | + } |
| 78 | + |
| 79 | + Invoke-Step 'Backend tests (pytest)' { |
| 80 | + python -m pytest -q |
| 81 | + } |
| 82 | + |
| 83 | + Invoke-Step 'API contracts are up to date' { |
| 84 | + # Regenerates in place, then fails if that produced a diff -- the same |
| 85 | + # check CI runs, and the usual cause of a red build after touching a |
| 86 | + # Pydantic model. |
| 87 | + python tools/generate_contracts.py |
| 88 | + if ($LASTEXITCODE -ne 0) { return } |
| 89 | + git diff --exit-code -- contracts/openapi.json contracts/cortex-api.ts |
| 90 | + if ($LASTEXITCODE -ne 0) { |
| 91 | + Write-Host ' Contracts were stale and have been regenerated. Commit them.' -ForegroundColor Yellow |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + if ($Tier -eq 'full') { |
| 96 | + Invoke-Step 'Compile application modules' { |
| 97 | + python -m compileall -q main.py backend |
| 98 | + } |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +if (-not $SkipFrontend) { |
| 103 | + if (-not (Test-Path (Join-Path $frontend 'node_modules'))) { |
| 104 | + Write-Host '' |
| 105 | + Write-Host 'frontend/node_modules missing -- running npm ci first.' -ForegroundColor Yellow |
| 106 | + Invoke-Step 'Install frontend dependencies' { npm ci } $frontend |
| 107 | + } |
| 108 | + |
| 109 | + Invoke-Step 'Frontend types (tsc)' { npm run typecheck } $frontend |
| 110 | + Invoke-Step 'Lint frontend (eslint)' { npm run lint } $frontend |
| 111 | + Invoke-Step 'Frontend unit tests (vitest)' { npm test -- --run } $frontend |
| 112 | + |
| 113 | + if ($Tier -eq 'full') { |
| 114 | + Invoke-Step 'Frontend browser tests (playwright)' { |
| 115 | + npm run e2e -- --workers=1 |
| 116 | + } $frontend |
| 117 | + |
| 118 | + Invoke-Step 'Build frontend bundle' { npm run build } $frontend |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +Write-Host '' |
| 123 | +Write-Host ('-' * 58) |
| 124 | + |
| 125 | +$failed = @($results | Where-Object { -not $_.Ok }) |
| 126 | +$total = [math]::Round(($results | Measure-Object -Property Seconds -Sum).Sum, 1) |
| 127 | + |
| 128 | +foreach ($r in $results) { |
| 129 | + $mark = if ($r.Ok) { 'ok ' } else { 'FAIL' } |
| 130 | + $color = if ($r.Ok) { 'Green' } else { 'Red' } |
| 131 | + Write-Host (" {0} {1,-42} {2,6}s" -f $mark, $r.Name, $r.Seconds) -ForegroundColor $color |
| 132 | +} |
| 133 | + |
| 134 | +Write-Host ('-' * 58) |
| 135 | + |
| 136 | +if ($failed.Count -gt 0) { |
| 137 | + Write-Host "$($failed.Count) of $($results.Count) checks failed in ${total}s." -ForegroundColor Red |
| 138 | + exit 1 |
| 139 | +} |
| 140 | + |
| 141 | +Write-Host "All $($results.Count) checks passed in ${total}s." -ForegroundColor Green |
| 142 | +exit 0 |
0 commit comments