From ce1baaa70672f293b08e830d83aae871a94632bc Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 29 Oct 2026 18:40:58 +0100 Subject: [PATCH] feat: deploy (Dockerfile, compose) and Gitea Actions CI Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 5 +++ .gitea/workflows/ci.yml | 80 +++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + deploy/.env.example | 5 +++ deploy/Dockerfile | 14 +++++++ deploy/README.md | 38 +++++++++++++++++++ deploy/docker-compose.yml | 21 ++++++++++ site/astro.config.mjs | 8 ++-- 8 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yml create mode 100644 deploy/.env.example create mode 100644 deploy/Dockerfile create mode 100644 deploy/README.md create mode 100644 deploy/docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1e1b03f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +**/node_modules +**/target +**/dist +.git +.superpowers diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..9837587 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [main, "build/**"] + pull_request: + +jobs: + tools: + name: Tool tests and sync-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm run test:tools + - run: npm run check + - run: npm run test:site + + verify-references: + name: Verify references (all modules, Rust and TS) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: dtolnay/rust-toolchain@1.85.1 + - run: node tools/verify-references.mjs + + coupling-audit: + name: Coupling audit on references + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - name: Audit every reference engine + run: | + set -e + for d in course/*/reference/*/; do + echo "== $d" + node tools/coupling-audit.mjs "$d" + done + + site: + name: Site build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm --prefix site ci + - run: npm --prefix site run build + env: + COURSE_HOST: ci.example.invalid + + naive-engine: + name: Naive engine tests (Module 0) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: dtolnay/rust-toolchain@1.85.1 + # test_00_real_rules_whirlwind_order is expected to fail: it is marked + # #[should_panic] (Rust) / test.fails (TS), so the suite passes. + - run: cargo test + working-directory: naive-engine/rust + - run: npm ci + working-directory: naive-engine/ts + - run: npm test + working-directory: naive-engine/ts + - run: npm run typecheck + working-directory: naive-engine/ts diff --git a/.gitignore b/.gitignore index f9db400..e62fc66 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target/ dist/ .DS_Store *.log +deploy/.env diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..085a54d --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,5 @@ +# Copy to deploy/.env and fill in. +# Public hostname of the course (a DNS A/AAAA record must point at this server). +COURSE_HOST=course.example.com +# Name of the Docker network nginx-proxy is attached to. +PROXY_NETWORK=nginx-proxy diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..ea7c71b --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,14 @@ +# Build context is the repository root: +# docker build -f deploy/Dockerfile --build-arg COURSE_HOST=course.example.com . +FROM node:22-alpine AS build +ARG COURSE_HOST +RUN test -n "$COURSE_HOST" || (echo "COURSE_HOST build arg is required" >&2; exit 1) +ENV COURSE_HOST=$COURSE_HOST +WORKDIR /app +COPY . ./ +RUN npm --prefix site ci --no-audit --no-fund +RUN npm --prefix site run build + +FROM nginx:alpine +COPY --from=build /app/site/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..ec7f279 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,38 @@ +# Deploying the course site + +The site (`site/`, Astro Starlight) is built into static files and served by +`nginx:alpine`. It is meant to run behind a `jwilder/nginx-proxy` container with +`acme-companion` (Let's Encrypt), which routes by the `VIRTUAL_HOST` variable. + +## Deploy + +```sh +git clone https://gitea.lucaswinther.info/lucasw89/systemic-game-design-course.git +cd systemic-game-design-course/deploy +cp .env.example .env # set COURSE_HOST (and PROXY_NETWORK if yours differs) +docker compose up -d --build +``` + +- `COURSE_HOST` is the public hostname; point its DNS record at the server first. + It is used as the site URL at build time, as `VIRTUAL_HOST`, and as `LETSENCRYPT_HOST`. +- The build fails if `COURSE_HOST` is unset, so the placeholder URL never ships. +- The container joins the external Docker network `${PROXY_NETWORK:-nginx-proxy}`; + it must be the network your nginx-proxy container is on. +- To update: `git pull && docker compose up -d --build`. + +Manual build: `docker build -f deploy/Dockerfile --build-arg COURSE_HOST=course.example.com .` +(run from the repository root). + +## CI + +`.gitea/workflows/ci.yml` runs on Gitea Actions (needs a runner with the `ubuntu-latest` label): + +| Job | Runs | +| --- | --- | +| `tools` | `npm run test:tools`, `npm run check` (sync-check), `npm run test:site` | +| `verify-references` | `node tools/verify-references.mjs`: every module's reference, Rust and TS, with cumulative tests (needs network for `npm install`; Rust pinned to 1.85.1) | +| `coupling-audit` | `tools/coupling-audit.mjs` on every `course/*/reference/*` | +| `site` | `npm --prefix site ci && npm --prefix site run build` with a dummy `COURSE_HOST` | +| `naive-engine` | Module 0's engine: `cargo test`, `npm test`, `npm run typecheck`. The whirlwind-order test is expected to fail and is marked `should_panic` / `test.fails`, so the suite passes | + +There is no clippy step. diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..b56f217 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,21 @@ +services: + course: + build: + context: .. + dockerfile: deploy/Dockerfile + args: + COURSE_HOST: ${COURSE_HOST:?set COURSE_HOST in deploy/.env} + restart: unless-stopped + expose: + - "80" + environment: + VIRTUAL_HOST: ${COURSE_HOST} + LETSENCRYPT_HOST: ${COURSE_HOST} + VIRTUAL_PORT: "80" + networks: + - proxy + +networks: + proxy: + name: ${PROXY_NETWORK:-nginx-proxy} + external: true diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 0533b78..93160f8 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -1,9 +1,11 @@ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; -// The site is served at the root of its own subdomain. Set COURSE_HOST at build time -// (e.g. COURSE_HOST=course.example.com); the default is a placeholder. -const host = process.env.COURSE_HOST ?? "course.example.com"; +// The site is served at the root of its own subdomain. COURSE_HOST is required at build +// time (e.g. COURSE_HOST=course.example.com); there is deliberately no default, so a +// deploy can never ship with a placeholder URL. +const host = process.env.COURSE_HOST; +if (!host) throw new Error("COURSE_HOST is not set (e.g. COURSE_HOST=course.example.com)"); export default defineConfig({ site: `https://${host}`,