From 16370a2e547b7851ba1127a0c42a9d173afd2e7c Mon Sep 17 00:00:00 2001 From: ghosteau Date: Wed, 2 Sep 2026 22:43:35 -0400 Subject: [PATCH] ci: add GitHub Actions workflow and an nginx deploy example Runs pytest from backend/ (pyproject sets testpaths relative to it and the suite imports app as a top-level package), and oxlint plus the production build for the frontend -- npm run build is 'tsc -b && vite build', so it covers type errors and the bundle the image ships. Python and Node versions match the two stages of the Dockerfile. torch is installed from PyTorch's CPU index for the same reason the image does it: default PyPI resolves to the CUDA build and drags ~2.5 GB of NVIDIA wheels onto a runner that cannot use them. The only deploy example was a Caddyfile; this adds nginx for hosts already running it. It carries proxy_read_timeout 300s -- nginx defaults to 60s, which would cut off a first-time model download that the app allows 300s to finish and report it as a confusing 504. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 71 +++++++++++++++++++++++++++++++++++++++ deploy/nginx.conf.example | 50 +++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 deploy/nginx.conf.example diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..452ff38 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: CI + +on: + push: + branches: [master, dev] + pull_request: + branches: [master, dev] + +# A new push to a branch makes an in-flight run for that branch obsolete. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: Backend (pytest) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + # Matches the runtime stage of the production Dockerfile. + python-version: "3.12" + cache: pip + cache-dependency-path: | + backend/requirements.txt + backend/requirements-dev.txt + + - name: Install torch (CPU build) + # Default PyPI resolves torch to the CUDA build: ~2.5 GB of NVIDIA + # wheels a CI runner can never use. Installing from the CPU index first + # means the requirements step below finds it already satisfied. + run: pip install --index-url https://download.pytorch.org/whl/cpu "torch>=2.2,<3.0" + + - name: Install dependencies + working-directory: backend + run: pip install -r requirements-dev.txt + + - name: Run tests + # pyproject.toml sets testpaths relative to this directory, and the + # suite imports "app" as a top-level package, so cwd matters. + working-directory: backend + run: pytest + + frontend: + name: Frontend (lint, types, build) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + # Matches the build stage of the production Dockerfile. + node-version: "24" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Lint + working-directory: frontend + run: npm run lint + + - name: Type-check and build + # "build" is `tsc -b && vite build`, so this covers type errors and + # verifies the bundle the production image actually ships. + working-directory: frontend + run: npm run build diff --git a/deploy/nginx.conf.example b/deploy/nginx.conf.example new file mode 100644 index 0000000..7ba2e6d --- /dev/null +++ b/deploy/nginx.conf.example @@ -0,0 +1,50 @@ +# nginx equivalent of Caddyfile.example, for hosts already running nginx. +# +# Install as /etc/nginx/sites-available/embeddings, then: +# ln -s /etc/nginx/sites-available/embeddings /etc/nginx/sites-enabled/ +# nginx -t && systemctl reload nginx +# certbot --nginx -d embeddings.mannymcgrail.com +# +# certbot rewrites this file in place to add the TLS listener and to redirect +# port 80, so apply it as plain HTTP first and let certbot do the rest. Point +# the DNS A record at this server before running certbot: the HTTP-01 +# challenge has to reach port 80. Behind Cloudflare, leave the record +# unproxied ("DNS only") until the certificate is issued. + +server { + listen 80; + listen [::]:80; + server_name embeddings.mannymcgrail.com; + + # The container publishes on loopback only, so nginx is the sole way in. + location / { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # uvicorn runs with --proxy-headers; this is what tells it the original + # request was HTTPS, so generated URLs do not downgrade to http://. + proxy_set_header X-Forwarded-Proto $scheme; + + # The first request for an uncached model downloads weights and then + # computes a UMAP projection, which the app allows up to + # MODEL_LOAD_TIMEOUT_SECONDS (300) to finish. nginx defaults to a 60s + # read timeout, which would sever that request and surface as a + # confusing 504 well before the backend gave up. Keep this at or above + # the application timeout. + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # Projection payloads are large, highly compressible JSON. + gzip on; + gzip_proxied any; + gzip_min_length 1000; + gzip_types application/json application/javascript text/css text/plain image/svg+xml; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; +}