name: CI on: push: branches: [main] pull_request: workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: # Keeps the ONNX cache in one place so actions/cache can hold it across runs. LEXORA_VAR_DIR: ${{ github.workspace }}/var jobs: api: name: API — lint, types, tests runs-on: ubuntu-latest timeout-minutes: 25 steps: - uses: actions/checkout@v7 # setup-uv publishes no floating major tag, so this is pinned exactly; # the others track a major that GitHub keeps pointed at a Node 24 release. - uses: astral-sh/setup-uv@v9.0.0 with: enable-cache: true - name: Install run: | uv venv --python 3.12 apps/api/.venv uv pip install --python apps/api/.venv/bin/python -e "apps/api[dev]" # The models are ~200 MB. Caching them turns a 3-minute download into seconds # and keeps CI from hammering the Hugging Face CDN on every push. - name: Cache ONNX models uses: actions/cache@v6 with: path: var/models key: onnx-${{ runner.os }}-${{ hashFiles('apps/api/pyproject.toml') }} - name: Fetch the corpus run: apps/api/.venv/bin/python corpus/download.py - name: Build the index run: apps/api/.venv/bin/python -m app.rag.index env: PYTHONPATH: apps/api - name: Verify the eval labels against the corpus run: apps/api/.venv/bin/python eval/build_questions.py - name: ruff run: | apps/api/.venv/bin/ruff check . apps/api/.venv/bin/ruff format --check . - name: mypy --strict run: apps/api/.venv/bin/mypy - name: pytest run: apps/api/.venv/bin/python -m pytest # A regression in retrieval quality fails the build like any other test. # The gate lives in scripts/quality_gate.py so CI and `make ci` run identical # code — a duplicated inline script is a gate that drifts. - name: Evaluate retrieval and gate on it run: | apps/api/.venv/bin/python eval/ragas_run.py apps/api/.venv/bin/python scripts/quality_gate.py - uses: actions/upload-artifact@v7 if: always() with: name: eval-results path: eval/results/ web: name: Web — types, lint, build runs-on: ubuntu-latest timeout-minutes: 15 defaults: run: working-directory: apps/web steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: "22" cache: npm cache-dependency-path: apps/web/package-lock.json - run: npm ci - run: npx tsc --noEmit - run: npx eslint . --max-warnings 0 - run: npm run build container: name: Container builds runs-on: ubuntu-latest timeout-minutes: 30 needs: api steps: - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v9.0.0 - name: Rebuild the portable index the image copies in run: | uv venv --python 3.12 apps/api/.venv uv pip install --python apps/api/.venv/bin/python -e "apps/api" apps/api/.venv/bin/python corpus/download.py PYTHONPATH=apps/api apps/api/.venv/bin/python -m app.rag.index # `shell: bash` is load-bearing: the default is `bash -e` without pipefail, so a # failing `docker build` piped into `tee` would exit 0 and pass this step. - name: Build the image shell: bash run: docker build --progress=plain -t lexora-api:ci . 2>&1 | tee build.log # A runner has gigabytes to spare, so a successful `docker build` says nothing # about a constrained builder. Two deploys were lost to a build-time OOM that CI # could not see. Gate on the peak RSS each bake stage reports instead. - name: Bake stages stayed within their memory budget run: apps/api/.venv/bin/python scripts/check_bake_budget.py build.log - name: Boot it and check readiness run: | docker run -d --name lexora -p 7860:7860 lexora-api:ci for _ in $(seq 1 60); do if curl -fsS http://127.0.0.1:7860/api/health | grep -q '"status":"ok"'; then echo "container healthy"; exit 0 fi sleep 5 done echo "container did not become healthy"; docker logs lexora; exit 1