Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Shared local dev environment for all apps. Not committed (see .gitignore).
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/combateone?schema=public"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/spidder?schema=public"
REDIS_URL="redis://localhost:6379"
JWT_SECRET="replace-with-a-long-random-string"
CORS_ORIGINS="http://localhost:3001,http://localhost:3002"
Expand Down
85 changes: 85 additions & 0 deletions .env.prod.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Spidder — production environment for docker-compose.prod.yml.
#
# cp .env.prod.example .env # on the VPS, next to the compose file
# chmod 600 .env
#
# Compose reads `.env` automatically. This file holds real credentials once
# filled in, so it is NEVER committed — .gitignore already excludes `.env`.
#
# Every value without a default below is marked required in the compose file:
# leave one out and `docker compose up` refuses to start rather than silently
# booting against the wrong database.

# ---------------------------------------------------------------------------
# Host services
# ---------------------------------------------------------------------------
# Postgres and Redis run on the VPS host, not in Docker. Containers reach them
# through `host.docker.internal`, which docker-compose.prod.yml maps to the
# host gateway. Do NOT use `localhost` here — inside a container that is the
# container itself, and the connection will be refused.
DATABASE_URL="postgresql://spidder:CHANGE_ME@host.docker.internal:5432/spidder?schema=public"

# Include the password you set with `requirepass` in redis.conf.
REDIS_URL="redis://:CHANGE_ME@host.docker.internal:6379"

# ---------------------------------------------------------------------------
# Secrets
# ---------------------------------------------------------------------------
# Signs session and guest JWTs. Generate with: openssl rand -hex 32
# Changing it invalidates every existing session, which is the correct
# response to a suspected leak.
JWT_SECRET="CHANGE_ME"

# Comma-separated origins the API and ws-server accept cross-origin requests
# from. This is the public site origin, NOT the container port.
CORS_ORIGINS="https://spidder.example.com"

# ---------------------------------------------------------------------------
# Public URLs — baked into the browser bundle at IMAGE BUILD time
# ---------------------------------------------------------------------------
# These are what the visitor's browser calls, so they must be the public
# origins served by your reverse proxy.
#
# Because they are compiled into the client bundle, changing them here and
# restarting does NOTHING — the web image has to be rebuilt. In the CI/CD flow
# they come from repository secrets of the same name and are passed as Docker
# build args; these entries matter only if you build on the server by hand.
NEXT_PUBLIC_API_URL="https://api.spidder.example.com"
NEXT_PUBLIC_WS_URL="wss://api.spidder.example.com/ws"

# ---------------------------------------------------------------------------
# Images (Docker Hub)
# ---------------------------------------------------------------------------
# The Docker Hub ACCOUNT name the five spidder-* images live under. Defaults
# to `codeheist` in the compose file, so leaving this commented out is fine for
# this project — set it only if you publish under a different account.
#
# Write it lowercase if you do change it. Docker Hub stores account names
# lowercase, and while `docker tag` accepts the capitals you typed, the pull
# then targets a namespace that does not exist.
# REGISTRY="codeheist"

# Which build to run. The deploy workflow sets this to the commit SHA, which
# is what makes a rollback a one-line change. Unset means `latest`, which is
# what you want when starting the stack by hand.
# IMAGE_TAG="latest"

# ---------------------------------------------------------------------------
# Optional
# ---------------------------------------------------------------------------
# Host-side ports the reverse proxy forwards to. Only change these if
# something else on the VPS already owns the port.
# WEB_PORT=3001
# HTTP_API_PORT=4001
# WS_SERVER_PORT=4002

# How many submissions the judge runs in parallel. Raise it only if the VPS
# has the cores to spare — each one is a sandboxed process.
# JUDGE_CONCURRENCY=4

# Creates the first admin account on boot when it does not exist. Set these
# for the initial deploy, then remove them and restart so the credentials stop
# living in a file on disk.
# SUPER_ADMIN_EMAIL="you@example.com"
# SUPER_ADMIN_PASSWORD="a-strong-password"
# SUPER_ADMIN_USERNAME="admin"
133 changes: 133 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
###############################################################################
# CI — runs on every push and pull request.
#
# Three jobs in parallel (quality, test, build) rather than one long chain, so
# a type error and a failing test surface together instead of one hiding the
# other behind a 6-minute queue.
#
# Docker images are NOT built here. Building four images on every branch push
# would dominate the run time and produce artifacts nobody installs. The deploy
# workflow builds them, once, when something is actually shipping.
###############################################################################

name: CI

on:
push:
branches: ["**"]
pull_request:
branches: [main]

# A second push to the same branch cancels the first. Nobody is waiting on the
# results of a commit that has already been replaced.
#
# `main` is deliberately excluded from cancellation: its runs gate the deploy,
# and a cancelled run there would leave a commit with no verdict at all.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

env:
# Keep in lockstep with the root package.json "packageManager" field and the
# PNPM_VERSION arg in every Dockerfile.prod.
PNPM_VERSION: 10.19.0
NODE_VERSION: 22

jobs:
quality:
name: Lint & types
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

# --frozen-lockfile fails if pnpm-lock.yaml does not match the manifests.
# That is the point: a lockfile drifting from package.json is how a build
# that passes CI installs different versions in production.
- run: pnpm install --frozen-lockfile

# @repo/db's build runs `prisma generate`. Without it the generated client
# does not exist and every downstream typecheck fails on missing imports,
# which looks like a hundred unrelated errors rather than one missing step.
- name: Generate Prisma client
run: pnpm --filter @repo/db build

- name: Typecheck
run: pnpm check-types

- name: Lint
run: pnpm lint

test:
name: Unit tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

- run: pnpm install --frozen-lockfile

# @repo/game imports @repo/protocol, whose types resolve to its dist/.
# A fresh checkout has no dist/, so `tsc` inside the test script fails
# with "Cannot find module '@repo/protocol'" — a missing build step that
# reads like a broken import. Locally it passes only because dist/ is
# left over from a previous build.
- name: Build workspace dependencies
run: pnpm --filter @repo/game^... build

# @repo/game is the only package with tests, and it is deliberately
# database-free so it needs no services here. Called by filter rather than
# `turbo run test` because turbo.json defines no `test` task — adding one
# just to reach a single package would be indirection for its own sake.
- name: Game logic tests
run: pnpm --filter @repo/game test

build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

- run: pnpm install --frozen-lockfile

# Compiles every app and package exactly as the Dockerfiles do. Catches
# the class of failure that only appears in a real build — a bad import
# path, a Next.js page that throws while being prerendered — before the
# deploy workflow spends ten minutes discovering it inside Docker.
#
# NEXT_PUBLIC_* are inlined into the client bundle at build time. The
# placeholders here are never shipped: the deploy workflow rebuilds the
# web image with the real public origins as build args.
- name: Build all packages
env:
NEXT_PUBLIC_API_URL: http://localhost:4001
NEXT_PUBLIC_WS_URL: ws://localhost:4002/ws
run: pnpm build
Loading
Loading