feat: deploy (Dockerfile, compose) and Gitea Actions CI

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 18:40:58 +01:00
co-authored by Claude Opus 5.5
parent 699d038d1c
commit ce1baaa706
8 changed files with 169 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
**/node_modules
**/target
**/dist
.git
.superpowers
+80
View File
@@ -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/[email protected]
- 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/[email protected]
# 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
+1
View File
@@ -3,3 +3,4 @@ target/
dist/
.DS_Store
*.log
deploy/.env
+5
View File
@@ -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
+14
View File
@@ -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
+38
View File
@@ -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.
+21
View File
@@ -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
+5 -3
View File
@@ -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}`,