Skip to content

Commit f13408f

Browse files
dovvnloadingclaude
andauthored
Run the quality gates locally, before they ever reach CI (#118)
Adds scripts/check.ps1, which runs the same checks quality.yml runs, on your own machine. The default "quick" tier -- ruff, pytest, contract drift, and frontend types/lint/unit tests -- finishes in about two minutes. "-Tier full" adds compileall, the Playwright browser tests, and the bundle build. Packaging and the qualification spikes are deliberately excluded from both tiers. They take 35+ minutes and need signing tooling, so there is no honest way to make them part of a local edit loop. A tracked .githooks/pre-push runs the quick tier and aborts the push if anything fails, so a red build is caught before it is published rather than minutes later in a browser tab. Enable it per clone with: git config core.hooksPath .githooks Bypass with --no-verify when you genuinely need to. quality.yml is split to match. A pull request now runs only the fast job (lint, tests, contracts, frontend). The Windows packaging, the recipe worker and coordinator qualifications, and the WebView2 signature check move to a heavy job that runs on main or on demand, and only after the fast job is green -- so nothing spends 35 minutes to discover a lint error. Every step from the original single job is preserved. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 68f1e0a commit f13408f

4 files changed

Lines changed: 265 additions & 2 deletions

File tree

.githooks/pre-push

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/sh
2+
#
3+
# Run Cortex's quick quality gates before anything leaves the machine.
4+
#
5+
# Bypass for a genuine emergency:
6+
# git push --no-verify
7+
# CORTEX_SKIP_HOOK=1 git push
8+
#
9+
# Enable (once per clone):
10+
# git config core.hooksPath .githooks
11+
12+
if [ "$CORTEX_SKIP_HOOK" = "1" ]; then
13+
echo "pre-push: skipped (CORTEX_SKIP_HOOK=1)"
14+
exit 0
15+
fi
16+
17+
repo_root=$(git rev-parse --show-toplevel)
18+
script="$repo_root/scripts/check.ps1"
19+
20+
if [ ! -f "$script" ]; then
21+
echo "pre-push: scripts/check.ps1 not found -- skipping."
22+
exit 0
23+
fi
24+
25+
if command -v pwsh >/dev/null 2>&1; then
26+
shell_bin="pwsh"
27+
elif command -v powershell >/dev/null 2>&1; then
28+
shell_bin="powershell"
29+
else
30+
echo "pre-push: no PowerShell found -- skipping local checks."
31+
exit 0
32+
fi
33+
34+
echo "pre-push: running quick quality checks (bypass with --no-verify)..."
35+
36+
"$shell_bin" -NoProfile -ExecutionPolicy Bypass -File "$script" -Tier quick
37+
status=$?
38+
39+
if [ $status -ne 0 ]; then
40+
echo ""
41+
echo "pre-push: checks failed -- push aborted."
42+
echo " Fix the failures above, or push anyway with: git push --no-verify"
43+
exit 1
44+
fi
45+
46+
exit 0

.github/workflows/quality.yml

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
name: Quality
22

3+
# Split into two jobs so a pull request gets fast feedback:
4+
#
5+
# fast Lint, tests, contracts, and the frontend. Runs on every PR and on
6+
# main. Mirrors ./scripts/check.ps1 -Tier full, which you can run
7+
# locally before pushing (see CONTRIBUTING.md).
8+
#
9+
# heavy Windows packaging and the long qualification spikes -- 35+ minutes
10+
# of work that a PR almost never invalidates. Runs only on main, or
11+
# on demand from the Actions tab, and only after `fast` is green.
12+
313
on:
414
push:
515
branches: [main]
616
pull_request:
717
branches: [main]
18+
workflow_dispatch:
819

920
permissions:
1021
contents: read
1122

1223
jobs:
13-
test:
24+
fast:
1425
runs-on: windows-latest
26+
timeout-minutes: 30
1527
steps:
1628
- name: Check out repository
1729
uses: actions/checkout@v4
@@ -84,6 +96,37 @@ jobs:
8496
working-directory: frontend
8597
run: npm run build
8698

99+
heavy:
100+
# Skipped on pull requests. Packaging and the durable-coordinator spikes are
101+
# slow and rarely affected by a PR, so they gate main rather than review.
102+
if: github.event_name != 'pull_request'
103+
needs: fast
104+
runs-on: windows-latest
105+
timeout-minutes: 90
106+
steps:
107+
- name: Check out repository
108+
uses: actions/checkout@v4
109+
110+
- name: Set up Python
111+
uses: actions/setup-python@v5
112+
with:
113+
python-version: "3.11"
114+
cache: pip
115+
116+
- name: Set up Node
117+
uses: actions/setup-node@v4
118+
with:
119+
node-version: "22"
120+
cache: npm
121+
cache-dependency-path: frontend/package-lock.json
122+
123+
- name: Install dependencies
124+
run: python -m pip install -r requirements.txt
125+
126+
- name: Install frontend dependencies
127+
working-directory: frontend
128+
run: npm ci
129+
87130
- name: Verify launcher-managed bundle
88131
shell: pwsh
89132
run: python main.py --build-frontend --data-dir "$env:RUNNER_TEMP\cortex-quality-data"

CONTRIBUTING.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,39 @@ data in tests or logs.
2323

2424
## Quality checks
2525

26-
Run the relevant checks before opening a pull request:
26+
One script runs the same gates CI does, on your machine:
27+
28+
```powershell
29+
./scripts/check.ps1
30+
```
31+
32+
That is the `quick` tier -- lint, backend tests, contract drift, and frontend
33+
types/lint/unit tests -- and takes roughly two minutes. Before opening a pull
34+
request, run the `full` tier, which adds `compileall`, the Playwright browser
35+
tests, and the bundle build:
36+
37+
```powershell
38+
./scripts/check.ps1 -Tier full
39+
```
40+
41+
Use `-SkipFrontend` or `-SkipBackend` to narrow the run while iterating.
42+
43+
Packaging (PyInstaller), the recipe-worker and coordinator qualification
44+
spikes, and WebView2 signature verification are deliberately left out of both
45+
tiers: they take 35+ minutes and need signing tooling. CI covers them.
46+
47+
### Run the checks automatically before a push
48+
49+
Point Git at the tracked hooks directory once per clone:
50+
51+
```powershell
52+
git config core.hooksPath .githooks
53+
```
54+
55+
The `pre-push` hook then runs the `quick` tier and aborts the push if anything
56+
fails. Bypass it in an emergency with `git push --no-verify`.
57+
58+
The individual commands, if you prefer to run them by hand:
2759

2860
```powershell
2961
python -m pytest

scripts/check.ps1

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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

Comments
 (0)