feat: Sign glyph images, anchor review flow, loop UX, and experiment ID consolidation #348
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: 25 | |
| 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 | |
| - 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) | |
| 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" | |
| - 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: 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" | |
| 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') | |
| " |