Skip to content

feat(windows): add install.ps1 / run.ps1 + Windows CI smoke #25

feat(windows): add install.ps1 / run.ps1 + Windows CI smoke

feat(windows): add install.ps1 / run.ps1 + Windows CI smoke #25

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
backend:
name: Backend tests (pytest)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
backend/requirements.txt
backend/requirements-dev.txt
- name: Install backend dependencies
working-directory: backend
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run pytest
working-directory: backend
run: pytest -q
frontend:
name: Frontend JS syntax check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Syntax-check frontend JS
run: |
set -e
for f in frontend/*.js; do
echo "node --check $f"
node --check "$f"
done
- name: Validate JSON content
run: |
set -e
python3 -c "
import json, pathlib, sys
errors = 0
for p in list(pathlib.Path('frontend').rglob('*.json')) + list(pathlib.Path('curriculum').rglob('*.json')):
try:
json.loads(p.read_text())
print(f'ok {p}')
except Exception as e:
print(f'ERR {p}: {e}', file=sys.stderr)
errors += 1
sys.exit(1 if errors else 0)
"
- name: Check hero landing site
run: |
if [ -f scripts/check_site.sh ]; then
chmod +x scripts/check_site.sh
./scripts/check_site.sh
else
echo "scripts/check_site.sh missing; skipping"
fi
scripts:
name: Shell script lint + smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: shellcheck install/run scripts
run: |
if [ -f install.sh ] || [ -f run.sh ]; then
sudo apt-get update -y
sudo apt-get install -y shellcheck
for f in install.sh run.sh; do
[ -f "$f" ] && shellcheck -x "$f"
done
else
echo "no install.sh / run.sh yet; skipping"
fi
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Smoke-test install.sh (skip model pull, skip Ollama)
env:
TUTOR_SKIP_OLLAMA: "1"
TUTOR_SKIP_MODEL_PULL: "1"
TUTOR_NONINTERACTIVE: "1"
run: |
if [ -f install.sh ]; then
chmod +x install.sh run.sh scripts/smoke_run.sh
./install.sh
test -d backend/.venv
backend/.venv/bin/python -c "import fastapi, uvicorn, httpx, pydantic"
else
echo "install.sh missing; skipping smoke test"
fi
- name: Smoke-test run.sh (no Ollama; check /api/health and /)
env:
TUTOR_SKIP_OLLAMA: "1"
TUTOR_PORT: "8801"
run: |
if [ -f scripts/smoke_run.sh ]; then
chmod +x scripts/smoke_run.sh
./scripts/smoke_run.sh
else
echo "scripts/smoke_run.sh missing; skipping run.sh smoke"
fi
- name: Smoke-test install.sh y/N prompts (noninteractive)
run: |
if [ -f scripts/smoke_prompts.sh ]; then
chmod +x scripts/smoke_prompts.sh
./scripts/smoke_prompts.sh
else
echo "scripts/smoke_prompts.sh missing; skipping prompt smoke"
fi
- name: Smoke-test CLI flags (--help, --no-launch, port-in-use)
env:
TUTOR_SKIP_OLLAMA: "1"
run: |
if [ -f scripts/smoke_flags.sh ]; then
chmod +x scripts/smoke_flags.sh
./scripts/smoke_flags.sh
else
echo "scripts/smoke_flags.sh missing; skipping flags smoke"
fi
windows:
name: Windows PowerShell install + run smoke
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: PowerShell syntax check (install.ps1 / run.ps1)
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
foreach ($f in @('install.ps1','run.ps1')) {
if (-not (Test-Path $f)) { throw "missing $f" }
$tokens = $null; $parseErrors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path $f), [ref]$tokens, [ref]$parseErrors) | Out-Null
if ($parseErrors -and $parseErrors.Count -gt 0) {
$parseErrors | ForEach-Object { Write-Host $_ }
throw "parse errors in $f"
}
Write-Host "ok $f"
}
- name: Help text (-Help) for both scripts
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
.\install.ps1 -Help | Select-Object -First 5
.\run.ps1 -Help | Select-Object -First 5
- name: Smoke-test install.ps1 (noninteractive, skip Ollama, skip pull)
shell: pwsh
env:
TUTOR_SKIP_OLLAMA: "1"
TUTOR_SKIP_MODEL_PULL: "1"
TUTOR_NONINTERACTIVE: "1"
run: |
$ErrorActionPreference = 'Stop'
.\install.ps1 -NoLaunch
if (-not (Test-Path 'backend\.venv\Scripts\python.exe')) {
throw 'venv was not created'
}
& 'backend\.venv\Scripts\python.exe' -c "import fastapi, uvicorn, httpx, pydantic; print('imports ok')"
- name: Smoke-test run.ps1 -NoLaunch (preflight only, skip Ollama)
shell: pwsh
env:
TUTOR_SKIP_OLLAMA: "1"
TUTOR_PORT: "8801"
run: |
$ErrorActionPreference = 'Stop'
.\run.ps1 -NoLaunch -SkipOllama -Port 8801
- name: Smoke-test run.ps1 actually serves /api/health (no Ollama)
shell: pwsh
env:
TUTOR_SKIP_OLLAMA: "1"
run: |
$ErrorActionPreference = 'Stop'
$outLog = Join-Path $env:RUNNER_TEMP 'run-ps1-smoke.out.log'
$errLog = Join-Path $env:RUNNER_TEMP 'run-ps1-smoke.err.log'
New-Item -ItemType File -Force -Path $outLog | Out-Null
New-Item -ItemType File -Force -Path $errLog | Out-Null
# Launch run.ps1 in a detached pwsh so the parent job can poll
# /api/health without waiting on Start-Job module init.
$procArgs = @(
'-NoProfile','-NoLogo','-File','run.ps1',
'-SkipOllama','-Port','8802'
)
$proc = Start-Process -FilePath 'pwsh' -ArgumentList $procArgs `
-RedirectStandardOutput $outLog -RedirectStandardError $errLog `
-PassThru -WorkingDirectory (Get-Location).Path
try {
$ok = $false
for ($i = 0; $i -lt 120; $i++) {
Start-Sleep -Seconds 1
try {
$r = Invoke-WebRequest -Uri 'http://127.0.0.1:8802/api/health' `
-UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
if ($r.StatusCode -eq 200) { $ok = $true; break }
} catch { }
if ($proc.HasExited) { break }
}
if (-not $ok) {
Write-Host '--- run.ps1 stdout ---'
if (Test-Path $outLog) { Get-Content -LiteralPath $outLog }
Write-Host '--- run.ps1 stderr ---'
if (Test-Path $errLog) { Get-Content -LiteralPath $errLog }
throw "/api/health did not return 200 within 120s (proc exited=$($proc.HasExited))"
}
Write-Host 'ok /api/health -> 200'
} finally {
if (-not $proc.HasExited) {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
}
- name: Reject unknown parameter
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'
$err = $null
try {
& .\install.ps1 -DoesNotExist 2>&1 | Out-Null
} catch {
$err = $_
}
# PowerShell raises a ParameterBindingException before the script
# body runs; either $err is set or the call wrote a non-terminating
# error to $Error[0].
if (-not $err -and -not $Error[0]) {
throw 'unknown parameter should have been rejected'
}
Write-Host 'ok unknown parameter rejected'