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
118 changes: 118 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: CI

# Build + static checks (lint, typecheck, unit) run on every push/PR. The dynamic
# Playwright suite (integration + e2e) runs against the dockerized stack and is
# gated behind `static`. See docs/ci.md and docs/adr/0008-github-actions-ci.md.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# Supersede in-flight runs on the same ref.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
static:
name: Build & static checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

# Node version comes from .nvmrc (single source of truth, kept in lockstep
# with the Dockerfiles' digest-pinned NODE_IMAGE).
- uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci

# Next.js build cache — makes `next build` incremental across runs.
- name: Cache Next.js build
uses: actions/cache@v5
with:
path: frontend/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-${{ hashFiles('frontend/**/*.[jt]s', 'frontend/**/*.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-
${{ runner.os }}-nextjs-

# Build the shared contract first, then the two apps (mirrors the images).
- name: Build
run: |
npm run build:shared
npm run build --workspace backend
npm run build --workspace frontend

- name: Lint
run: npm run lint

- name: Typecheck
run: npm run typecheck

# Pure unit suites (slot engine determinism/RTP + economy helpers) — no DB.
- name: Unit tests
run: npm test --workspace backend

# Advisory: surface HIGH/CRITICAL CVEs without failing the run (mirrors `make audit`).
- name: Audit (advisory)
run: npm audit --audit-level=high
continue-on-error: true

e2e:
name: Dynamic (integration + e2e)
needs: static
runs-on: ubuntu-latest
env:
# Layer the CI-only Bake cache override on top of the prod + test compose files.
# docker-compose.override.yml is NOT included (CI must not get the dev target).
COMPOSE_FILES: -f docker-compose.yml -f docker-compose.test.yml -f docker-compose.ci.yml
steps:
- uses: actions/checkout@v5

# Buildx enables the gha layer-cache backend used by Compose Bake.
- uses: docker/setup-buildx-action@v4

# Compose reads .env for variable substitution; the example values are
# self-consistent and safe for the ephemeral test stack.
- name: Create .env
run: cp .env.example .env

# Build the images (Compose Bake + gha layer cache) and run the in-container
# Playwright suite against fresh db/api/web. The `tests` container's exit code
# is the suite result. Mirrors `make test`.
- name: Run integration + e2e
env:
COMPOSE_BAKE: "1"
run: docker compose ${{ env.COMPOSE_FILES }} up --build --abort-on-container-exit --exit-code-from tests tests

- name: Dump stack logs on failure
if: failure()
run: docker compose ${{ env.COMPOSE_FILES }} logs --no-color

# The HTML report is written inside the tests container; copy it out before teardown.
- name: Extract Playwright report
if: ${{ !cancelled() }}
continue-on-error: true
run: docker compose ${{ env.COMPOSE_FILES }} cp tests:/app/tests/playwright-report ./playwright-report

- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report
if-no-files-found: ignore
retention-days: 7

- name: Tear down stack
if: always()
run: docker compose ${{ env.COMPOSE_FILES }} down -v
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.16.0
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ fmt: ## Format with Prettier
audit: ## Fail on high/critical dependency CVEs
npm audit --audit-level=high

.PHONY: scan
scan: ## Fail on high/critical CVEs in built runtime images (needs trivy)
docker build -t lunaland-api:scan -f backend/Dockerfile --target runner .
docker build -t lunaland-web:scan -f frontend/Dockerfile --target runner .
trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed lunaland-api:scan
trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed lunaland-web:scan

## ─── Repo ────────────────────────────────────────────────────────────────
.PHONY: push
push: ## Push main to origin (github.com/mitekk/casino-land)
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ make psql # psql shell make help # list everything
## Architecture & docs

- **Product:** [`docs/prd/PRD-current.md`](docs/prd/PRD-current.md)
- **Decisions:** [`docs/adr/`](docs/adr/) (ADR 0001–0007)
- **Decisions:** [`docs/adr/`](docs/adr/) (ADR 0001–0008)
- **API contract (BE↔FE):** [`docs/api-contract.md`](docs/api-contract.md)
- **DB schema (DB↔BE):** [`docs/db-schema.md`](docs/db-schema.md)
- **CI (GitHub Actions):** [`docs/ci.md`](docs/ci.md)

### Repository layout & track ownership

Expand Down
19 changes: 16 additions & 3 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# syntax=docker/dockerfile:1
# Build context is the repo root (see docker-compose.yml).

# Pinned Node 24 LTS on Alpine 3.23, digest-locked for reproducibility. Bump via
# `docker pull node:24-alpine && docker inspect --format '{{index .RepoDigests 0}}' node:24-alpine`
# then re-run `make scan` to confirm the new base is free of HIGH/CRITICAL CVEs.
# Keep the major/minor in lockstep with `.nvmrc` (the CI host + local Node version).
ARG NODE_IMAGE=node:24.16.0-alpine3.23@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14

# ─── deps (cached install from manifests) ────────────────────────────────
FROM node:22-alpine AS base
FROM ${NODE_IMAGE} AS base
WORKDIR /app
COPY package.json package-lock.json tsconfig.base.json ./
COPY packages/shared/package.json packages/shared/
Expand All @@ -26,11 +32,18 @@ COPY backend backend
RUN npm run build --workspace @casino/shared \
&& npm run build --workspace backend

# ─── prod-deps (runtime-only node_modules) ───────────────────────────────
# Drop devDependencies so the production image doesn't ship build tooling.
# This also removes the vendored esbuild Go binary (drizzle-kit/tsx), whose
# embedded Go stdlib otherwise trips the `make scan` HIGH/CRITICAL gate.
FROM build AS prod-deps
RUN npm prune --omit=dev

# ─── runner (production) ─────────────────────────────────────────────────
FROM node:22-alpine AS runner
FROM ${NODE_IMAGE} AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/packages/shared/package.json ./packages/shared/package.json
COPY --from=build /app/packages/shared/dist ./packages/shared/dist
Expand Down
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@nestjs/testing": "^11.1.0",
"@types/bcryptjs": "^2.4.6",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.0",
"@types/node": "^24.0.0",
"@types/passport-jwt": "^4.0.1",
"@types/pg": "^8.11.10",
"@types/supertest": "^6.0.2",
Expand Down
39 changes: 39 additions & 0 deletions docker-compose.ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# CI-ONLY override — layered LAST, after docker-compose.yml + docker-compose.test.yml:
#
# COMPOSE_BAKE=1 docker compose \
# -f docker-compose.yml -f docker-compose.test.yml -f docker-compose.ci.yml \
# up --build --abort-on-container-exit --exit-code-from tests tests
#
# Purpose: wire the GitHub Actions layer cache into the image builds so repeated
# CI runs reuse Docker layers instead of rebuilding from scratch. We stay
# compose-native by letting Compose Bake (COMPOSE_BAKE=1, backed by Buildx) read
# the `x-bake` cache directives below. This file makes NO runtime/service changes
# and is never used outside CI, keeping the production compose files clean.
#
# `type=gha` uses the Actions cache API directly (no registry needed); `mode=max`
# caches intermediate stage layers too (worthwhile for the multi-stage Dockerfiles).
# Distinct `scope=` per service keeps their caches from clobbering each other.
services:
api:
build:
x-bake:
cache-from:
- type=gha,scope=api
cache-to:
- type=gha,mode=max,scope=api

web:
build:
x-bake:
cache-from:
- type=gha,scope=web
cache-to:
- type=gha,mode=max,scope=web

tests:
build:
x-bake:
cache-from:
- type=gha,scope=tests
cache-to:
- type=gha,mode=max,scope=tests
56 changes: 56 additions & 0 deletions docs/adr/0008-github-actions-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ADR 0008 — Continuous Integration via GitHub Actions

- Status: Accepted
- Date: 2026-06-03

## Context

The repo already has a full local quality gate — build, lint, typecheck, unit tests, and a
dockerized integration/e2e suite — wired through the root scripts and the `Makefile`.
Nothing ran it automatically on push/PR. ADR‑0006 anticipated that tests "exercise real
containers"; CI can reuse the same compose flow rather than re‑implement it.

## Decision

- A single workflow, [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml), runs on
push to `main`, on every PR, and via manual dispatch, with a `concurrency` group that
cancels superseded runs.
- **Two jobs that mirror the `Makefile`, not re‑implementations:**
- **`static`** — `npm ci` → build (`build:shared` → backend → frontend) → `lint` →
`typecheck` → `npm test --workspace backend` (the pure unit suites). Equivalent to
`make lint` + `make typecheck` + `make test-be`.
- **`e2e`** — builds & boots the dockerized stack and runs the **in‑container** Playwright
integration + e2e suite against fresh `db`/`api`/`web`, then tears it down. Equivalent
to `make test`. The explicit `-f docker-compose.yml -f docker-compose.test.yml
-f docker-compose.ci.yml` set keeps `docker-compose.override.yml` (dev target) out.
- **`e2e` is gated behind `static`** (`needs: static`): don't spend Docker build/boot time
when the cheap static checks already failed.
- **Node is single‑sourced** via [`.nvmrc`](../../.nvmrc) (`24.16.0`): the `static` job reads
it with `setup-node`'s `node-version-file`, and the images pin the same major/minor through
their digest‑locked `NODE_IMAGE`, kept in lockstep by comment.
- **Caching is mandatory** so installs/builds are reused across runs: the npm download cache,
the Next.js build cache (`frontend/.next/cache`), and Docker build layers. The last uses
**Compose Bake + Buildx** with a CI‑only override,
[`docker-compose.ci.yml`](../../docker-compose.ci.yml), that adds `x-bake`
`cache-from`/`cache-to: type=gha` to the `api`/`web`/`tests` builds — keeping the production
compose files untouched. See [`docs/ci.md`](../ci.md).
- **`npm audit`** runs as an advisory (non‑blocking) step. **Prettier** is not a CI gate —
there is no shared config and `make fmt` is `--write`‑only.

## Alternatives considered

- **Run e2e on every PR ungated** — wastes Docker build/boot minutes on PRs that already
fail lint/typecheck/unit; gating gives the same signal cheaper.
- **Run Playwright on the host** (boot `db`/`api`/`web`, drive published ports) — viable, but
diverges from our committed container‑targeted suite (`make test`); the in‑container run
keeps CI identical to the proven local flow.
- **No caching** — simpler YAML, but every run reinstalls deps and rebuilds all images from
scratch; unacceptably slow for a Docker‑building e2e job.

## Consequences

- Push/PR now gets the same guarantees as the local `make` flow, automatically.
- Repeat runs are fast: Buildx logs `CACHED` layers when image inputs are unchanged, and the
npm/Next caches skip redundant work.
- One CI‑only compose file (`docker-compose.ci.yml`) exists purely for build‑cache wiring; it
makes no runtime changes and is never used locally.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ MADR‑style records of significant decisions for the Lunaland Casino POC.
| [0005](./0005-jwt-auth.md) | Email/password + JWT authentication |
| [0006](./0006-docker-compose-topology.md) | Docker Compose topology (db/api/web) |
| [0007](./0007-parallel-subagent-build.md) | Contract‑first, parallel sub‑agent build |
| [0008](./0008-github-actions-ci.md) | GitHub Actions CI pipeline (build + static/dynamic, cached) |
Loading