feat: Sign glyph images, anchor review flow, loop UX, and experiment ID consolidation #356
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main, develop] | |
| pull_request: | |
| branches: [main, develop] | |
| # Cancel stale runs on the same PR/branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| # ── Backend: pytest ────────────────────────────────────────────────────────── | |
| backend-tests: | |
| name: Backend tests (pytest) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: backend/pyproject.toml | |
| - name: Install backend dependencies | |
| working-directory: backend | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" || pip install -e "." || pip install -r requirements.txt || true | |
| # Fallback: install via pyproject.toml extras if available | |
| pip install pytest httpx anyio pytest-anyio 2>/dev/null || true | |
| - name: Run pytest (all tests, short tracebacks) | |
| working-directory: backend | |
| env: | |
| PYTHONPATH: ${{ github.workspace }}/backend | |
| GLOSSA_DATA_DIR: ${{ runner.temp }}/glossa_test_data | |
| GLOSSA_DEV_MODE: "1" | |
| run: | | |
| python -m pytest tests/ \ | |
| --tb=short \ | |
| -q \ | |
| --ignore=tests/test_pipelines_gpu.py \ | |
| --ignore=tests/test_install_gpu.py \ | |
| -x \ | |
| 2>&1 | tee pytest_output.txt | |
| exit ${PIPESTATUS[0]} | |
| - name: Upload pytest output on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pytest-output | |
| path: backend/pytest_output.txt | |
| retention-days: 7 | |
| # ── Frontend: build + Playwright ──────────────────────────────────────────────── | |
| frontend-tests: | |
| name: Frontend tests (Playwright) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 35 # includes 2-min research-loop SSE test | |
| needs: [] # run in parallel with backend-tests | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: backend/pyproject.toml | |
| - name: Install backend dependencies | |
| working-directory: backend | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" || pip install -e "." || true | |
| # Build the frontend FIRST so the backend can mount dist/ via StaticFiles | |
| # when it starts. If the frontend is built after the backend starts, the | |
| # StaticFiles mount is never registered and Playwright gets 404s. | |
| - name: Set up Node.js 20 | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Build frontend | |
| working-directory: frontend | |
| run: npm run build | |
| - name: Start backend in background | |
| working-directory: backend | |
| env: | |
| GLOSSA_DATA_DIR: ${{ runner.temp }}/glossa_playwright_data | |
| GLOSSA_DEV_MODE: "1" | |
| PYTHONPATH: ${{ github.workspace }}/backend | |
| run: | | |
| python -m uvicorn glossa_lab.main:app \ | |
| --host 127.0.0.1 --port 8001 \ | |
| --log-level warning & | |
| echo $! > /tmp/backend.pid | |
| # Wait for backend to be healthy (up to 30s) AND frontend to be served | |
| for i in $(seq 1 30); do | |
| if curl -sf http://127.0.0.1:8001/api/v1/health > /dev/null 2>&1; then | |
| echo "Backend healthy after ${i}s" | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| curl -sf http://127.0.0.1:8001/api/v1/health || echo "Backend may not be running" | |
| # Verify frontend is served | |
| curl -sf http://127.0.0.1:8001/ > /dev/null && echo "Frontend OK" || echo "Frontend not served" | |
| - name: Install Playwright browsers (Chromium only) | |
| working-directory: frontend | |
| run: npx playwright install chromium --with-deps | |
| - name: Run Playwright tests | |
| working-directory: frontend | |
| env: | |
| CI: "true" | |
| BACKEND_RUNNING: "true" | |
| # Backend at port 8001 serves both the built frontend (StaticFiles) and | |
| # the API on the same origin. No separate Vite preview server needed. | |
| PLAYWRIGHT_USE_BACKEND: "1" | |
| PLAYWRIGHT_BACKEND_URL: "http://127.0.0.1:8001" | |
| run: | | |
| npx playwright test \ | |
| --reporter=github \ | |
| 2>&1 | tee playwright_output.txt | |
| exit ${PIPESTATUS[0]} | |
| - name: Stop backend | |
| if: always() | |
| run: kill $(cat /tmp/backend.pid) 2>/dev/null || true | |
| - name: Upload Playwright report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: frontend/playwright-report/ | |
| retention-days: 7 | |
| - name: Upload Playwright traces (failures only) | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-traces | |
| path: frontend/test-results/ | |
| retention-days: 7 | |
| # ── Indus reproducibility: public validation suite (stdlib only) ───────── | |
| indus-public-checks: | |
| name: Indus public validation checks | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Run public validation suite (stdlib only, no corpus access needed) | |
| run: | | |
| python research/indus/indus-anchor-model/scripts/validation/run_all_public_checks.py \ | |
| --data-dir research/indus/indus-anchor-model/data/public \ | |
| --output-dir /tmp/indus-validation-outputs | |
| - name: Upload validation report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: indus-validation-report | |
| path: /tmp/indus-validation-outputs/ | |
| retention-days: 30 | |
| # ── Shell wrappers: syntax + functional checks on all 3 OS ───────────────── | |
| shell-scripts: | |
| name: Shell wrappers (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 15 | |
| needs: [] # run in parallel | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: backend/pyproject.toml | |
| # ── Bootstrap venv so wrappers can find it ──────────────────────────── | |
| - name: Bootstrap venv (POSIX) | |
| if: runner.os != 'Windows' | |
| working-directory: backend | |
| run: | | |
| python -m venv venv | |
| venv/bin/pip install --quiet --upgrade pip | |
| venv/bin/pip install --quiet -e ".[dev]" | |
| - name: Bootstrap venv (Windows) | |
| if: runner.os == 'Windows' | |
| working-directory: backend | |
| shell: cmd | |
| run: | | |
| python -m venv venv | |
| venv\Scripts\pip install --quiet --upgrade pip | |
| venv\Scripts\pip install --quiet -e ".[dev]" | |
| # ── POSIX: shell.sh ─────────────────────────────────────────────────── | |
| - name: shell.sh — syntax check (bash -n) | |
| if: runner.os != 'Windows' | |
| run: bash -n shell.sh | |
| - name: shell.sh — no-args shows usage (exit 0) | |
| if: runner.os != 'Windows' | |
| run: | | |
| output=$(./shell.sh 2>&1 || true) | |
| echo "$output" | |
| echo "$output" | grep -qi "usage" || { echo "FAIL: no usage line"; exit 1; } | |
| - name: shell.sh python — version | |
| if: runner.os != 'Windows' | |
| run: ./shell.sh python --version | |
| - name: shell.sh lint — backend passes ruff | |
| if: runner.os != 'Windows' | |
| run: ./shell.sh lint backend/ | |
| # ── POSIX: setup-os.sh ──────────────────────────────────────────────── | |
| - name: setup-os.sh — syntax check (bash -n) | |
| if: runner.os != 'Windows' | |
| run: bash -n setup-os.sh | |
| - name: setup-os.sh — no-args shows usage (exit 0) | |
| if: runner.os != 'Windows' | |
| run: | | |
| output=$(./setup-os.sh 2>&1 || true) | |
| echo "$output" | |
| echo "$output" | grep -qi "usage\|install\|start\|stop\|status" || \ | |
| { echo "FAIL: no usage line"; exit 1; } | |
| - name: setup-os.sh status — exits cleanly without backend running | |
| if: runner.os != 'Windows' | |
| run: ./setup-os.sh status 2>&1 || true | |
| # ── Windows: shell.cmd ──────────────────────────────────────────────── | |
| - name: shell.cmd — no-args shows usage (exit 0) | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: | | |
| shell.cmd 2>&1 && echo USAGE_OK | |
| exit 0 | |
| - name: shell.cmd python — version | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: shell.cmd python --version | |
| - name: shell.cmd lint — backend passes ruff | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: shell.cmd lint backend\ | |
| # ── Windows: setup-os.cmd ───────────────────────────────────────────── | |
| - name: setup-os.cmd — no-args shows usage (exit 0) | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: | | |
| setup-os.cmd 2>&1 && echo USAGE_OK | |
| exit 0 | |
| - name: setup-os.cmd status — exits cleanly without service registered | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: | | |
| setup-os.cmd status 2>&1 | |
| exit 0 | |
| # ── Evidence Graph: script smoke test ──────────────────────────────────────── | |
| indus-evidence-scripts: | |
| name: Evidence Graph script smoke tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [] # run in parallel | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: backend/pyproject.toml | |
| - name: Install pypdf for intake script | |
| run: pip install pypdf 2>/dev/null || true | |
| - name: Test indus_intake.py --status exits cleanly | |
| working-directory: glossa-indus | |
| run: python scripts/indus_intake.py --status || true | |
| - name: Test indus_claims.py exits cleanly | |
| working-directory: glossa-indus | |
| run: python scripts/indus_claims.py || true | |
| - name: Verify sweep.yaml is valid YAML | |
| run: | | |
| python -c " | |
| import yaml, sys | |
| with open('glossa-indus/config/sweep.yaml') as f: | |
| cfg = yaml.safe_load(f) | |
| assert 'sweep' in cfg, 'Missing sweep key' | |
| assert 'keywords' in cfg['sweep'], 'Missing keywords key' | |
| assert 'sources' in cfg['sweep'], 'Missing sources key' | |
| print('sweep.yaml OK:', cfg['sweep']['name']) | |
| " || python -c " | |
| # Fallback without PyYAML | |
| with open('glossa-indus/config/sweep.yaml') as f: | |
| content = f.read() | |
| assert 'schema_version' in content | |
| assert 'keywords' in content | |
| print('sweep.yaml basic check OK') | |
| " | |
| - name: Verify claim extraction output exists | |
| run: | | |
| python -c " | |
| import json, pathlib | |
| claims_dir = pathlib.Path('glossa-indus/claims/extracted_claims') | |
| files = list(claims_dir.glob('*.json')) | |
| assert len(files) >= 2, f'Expected >= 2 claim files, found {len(files)}' | |
| total = sum(json.loads(f.read_text())['total_claims'] for f in files) | |
| assert total >= 22, f'Expected >= 22 total claims, found {total}' | |
| print(f'Claims check OK: {len(files)} files, {total} total claims') | |
| " |