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; +}