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
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
.git
**/__pycache__
**/.venv
**/.pytest_cache
**/.ruff_cache
**/*.log
.env*
backend/htmlcov
frontend/blob-report
frontend/dist
frontend/node_modules
frontend/playwright-report
frontend/test-results
node_modules
packages/react-email/.react-email
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.12
bun-version: 1.4.0
- name: Install frontend dependencies
run: bun ci
- name: Build frontend
Expand Down
22 changes: 20 additions & 2 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ permissions:
contents: read
pull-requests: read # dorny/paths-filter lists the PR's files

concurrency:
group: playwright-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
changes:
runs-on: ubuntu-latest
Expand All @@ -36,6 +40,9 @@ jobs:
changed:
- backend/**
- frontend/**
- pyproject.toml
- uv.lock
- bun.lock
- .env
- compose*.yml
- .github/workflows/playwright.yml
Expand All @@ -57,7 +64,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.12
bun-version: 1.4.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version-file: .python-version
Expand All @@ -75,7 +82,18 @@ jobs:
- run: bun ci
working-directory: frontend
- run: bash scripts/generate-client.sh
- name: Set up Docker Buildx
# The default "docker" buildx driver doesn't support the `gha` cache
# exporter/importer the compose build sections below rely on -- only
# docker-container (and a couple of others) do.
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
with:
driver: docker-container
- run: docker compose build
env:
# Routes the build through buildx bake so the backend/playwright
# services' cache_from/cache_to: type=gha fields actually apply.
COMPOSE_BAKE: "1"
- run: docker compose down -v --remove-orphans
- run: docker compose run --rm backend bash scripts/prestart.sh
- name: Run Playwright tests
Expand Down Expand Up @@ -104,7 +122,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.12
bun-version: 1.4.0
- name: Install dependencies
run: bun ci
- name: Download blob reports from GitHub Actions Artifacts
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
pull_request:
permissions: {}

concurrency:
group: pre-commit-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

env:
CAN_PUSH: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }}

Expand Down Expand Up @@ -40,7 +44,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.12
bun-version: 1.4.0
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/test-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,39 @@ on:
pull_request:
permissions:
contents: read
pull-requests: read # dorny/paths-filter lists the PR's files

concurrency:
group: test-backend-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
changed: ${{ steps.filter.outputs.changed }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
persist-credentials: false
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
filters: |
changed:
- backend/**
- pyproject.toml
- uv.lock
- .env
- compose*.yml
- .github/workflows/test-backend.yml

test-backend:
needs:
- changes
if: ${{ needs.changes.outputs.changed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/test-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,60 @@ on:
pull_request:
permissions:
contents: read
pull-requests: read # dorny/paths-filter lists the PR's files

concurrency:
group: test-docker-compose-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
changed: ${{ steps.filter.outputs.changed }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
persist-credentials: false
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
filters: |
changed:
- backend/**
- frontend/**
- pyproject.toml
- uv.lock
- bun.lock
- .env
- compose*.yml
- .github/workflows/test-docker-compose.yml

test-docker-compose:
needs:
- changes
if: ${{ needs.changes.outputs.changed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
# The default "docker" buildx driver doesn't support the `gha` cache
# exporter/importer the compose build section below relies on --
# only docker-container (and a couple of others) do.
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
with:
driver: docker-container
- run: docker compose build
env:
# Routes the build through buildx bake so the backend service's
# cache_from/cache_to: type=gha field actually applies.
COMPOSE_BAKE: "1"
- run: docker compose down -v --remove-orphans
- run: docker compose run --rm backend bash scripts/prestart.sh
- run: docker compose up -d --wait backend adminer
Expand Down
87 changes: 86 additions & 1 deletion PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ branch; only the phase branch opens a PR into `master`.
| 8 | First live mixed game (terminal + API) | done 2026-09-13 (PR #13) |
| 9 | Persistence and replay | done 2026-09-13 (PR #14) |
| 9.5 | Stabilize, document, rebrand | done 2026-09-13 (PR #15) |
| 9.6 | Upgrade to latest stable versions | done 2026-09-13 (branch phase-9.6-dependency-upgrade, PR pending) |
| 9.6 | Upgrade to latest stable versions | done 2026-09-13 (PR #17) |
| 9.7 | Docker, CI/CD, and server-start optimization | done 2026-09-14 (branch phase-9.7-infra-optimization, PR pending) |
| 10 | Frontend: play, spectate, admin, focus view | |
| 11 | Chat and personas | |
| 12 | Leaderboard | |
Expand Down Expand Up @@ -507,6 +508,90 @@ detail: `docs/development/dependency-audit.md`.
753 tests green, frontend `bun run build`/`bun run lint` clean,
`prek run --all-files` clean.

## Phase 9.7 — done 2026-09-14 (branch phase-9.7-infra-optimization)

Infrastructure-only pass: Docker image build, Compose, CI/CD workflows,
server startup, and test execution -- no application logic, API/websocket
behavior, or test content changed. Every Section 1 area got a documented
verdict (already fine, or changed with reasoning and, where possible, a
real before/after number). Full detail: `docs/development/ci-cd-and-docker.md`.

- Docker image: `python:3.14` -> `python:3.14-slim` in `backend/Dockerfile`,
measured **2.13GB -> 707MB (-67%)**; added a non-root `app` user (uid/gid
1000); worker count now configurable via `WEB_CONCURRENCY` (shell-expansion
default, not a persistent `ENV`, so it can't leak into the dev-only
`fastapi dev` command). The slim swap broke the `curl`-based healthcheck
(no `curl` in `-slim`) -- fixed via a `python3`/`urllib.request` one-liner
in `compose.yml` instead of re-adding `curl`. `.dockerignore` (root,
`backend/`) hardened with `.env*`, `.pytest_cache`, `.ruff_cache`, `*.log`,
`packages/react-email/.react-email/`.
- Compose: `compose.deploy.yml`'s `redis` service was missing
`restart: always` (present on every other service) -- added. Everything
else (named volumes, `condition: service_healthy` healthchecks, Compose
Watch) was already current best practice, confirmed and left alone.
- CI: added `concurrency` groups (cancel stale in-flight runs on a new push)
to `test-backend.yml`, `playwright.yml`, `pre-commit.yml`,
`test-docker-compose.yml` -- `deploy-docker-compose.yml` already had one.
Added `dorny/paths-filter` gating to `test-backend.yml`/
`test-docker-compose.yml`, mirroring `playwright.yml`'s existing pattern.
Added GHA-backed Docker layer caching (`cache_from`/`cache_to: type=gha`
+ `COMPOSE_BAKE=1`) for the `backend`/`playwright` builds, targeting the
4x-redundant independent build across Playwright's 4 parallel CI shards --
verified locally that the mechanism builds correctly; the real cross-run
cache-hit benefit needs confirming on the next CI push (no GHA cache
tokens exist locally to measure it honestly). Bumped stale
`bun-version: 1.3.12` -> `1.4.0` (matches local dev) in `playwright.yml`
(x2), `pre-commit.yml`, `deploy.yml`. Confirmed via empirical CI-log
inspection that uv's dependency cache (`setup-uv`'s `auto` default) was
already working correctly -- left alone rather than adding a redundant
`enable-cache: true`.
- Server startup: readiness (`app/main.py`'s lifespan LLM health gate,
`health.py`'s real DB/bus check) and graceful shutdown (sequential
`GameRunner`/`ModelRegistry`/bus/Redis close in the lifespan's `finally`)
were already correct -- confirmed, no changes. Deliberately deferred the
Gunicorn-vs-bare-Uvicorn production question: current guidance supports
either depending on the eventual deploy target, and Phase 15 (real
deployment) hasn't picked one yet.
- Test execution: checked `pytest-xdist` -- `tests/conftest.py`'s `db`
fixture is session-scoped against one real, shared Postgres database with
no per-worker isolation, so parallelizing would risk real cross-worker
collisions for a suite that's already fast (753 tests, ~53s). Not applied.
Checked `prestart.sh`'s lack of a wait-for-DB retry loop -- already
covered by `compose.yml`'s/CI's own `condition: service_healthy` /
`--wait` gating at the orchestration level. Not applied.
- No branch protection exists on this repo (private, free tier -- confirmed
via the GitHub API), so the new path-filter-gated jobs needed no
`alls-green`-style wrapper the way `playwright.yml` already has.
- Found, deliberately not fixed (Phase 14's job, matching the Phase 9.5/9.6
precedent for `.github/workflows/*` template logic): `deploy.yml`'s
leftover `if: github.repository != 'fastapi/full-stack-fastapi-template'`
template-org check.
- Full gate: `ruff check`/`ruff format --check`/`mypy app`/`ty check app`
clean, backend suite 753 tests green, `prek run --all-files` clean
(including `zizmor` on every workflow edit). No application code touched
this phase, so the suite result is identical to before -- confirming
nothing broke, not that anything was fixed.
- **Review fixes (PR #18):** `code-reviewer` and `python-reviewer` each
caught one real issue before merge. `python-reviewer`: the non-root
user's blanket `RUN chown -R app:app /app` forced overlay2 to copy up
every file from every earlier layer, quietly giving back ~430MB of the
size win (1.14GB with it, re-measured 707MB without) -- fixed by
chowning each layer's own output at creation time (`COPY --chown`,
`chown` appended inside the same `RUN` as `uv sync`) instead of one
blanket pass at the end. `code-reviewer`: the GHA Docker-cache
mechanism as first written would have been silently inert in CI, since
the `gha` cache backend needs buildx's `docker-container` driver and no
workflow provisioned it (confirmed locally: `docker buildx ls` shows
the default driver is plain `docker`) -- fixed by adding
`docker/setup-buildx-action` (`driver: docker-container`) before the
relevant `docker compose build` steps. Also fixed: a path-filter gap
(missing `pyproject.toml`/`uv.lock`/`bun.lock` in all three
`changes`-job filters, which could have let a dependency-only bump
skip its own test job) and an unquoted `${WEB_CONCURRENCY:-4}`
expansion in the Dockerfile's `CMD`. Re-verified after fixes: rebuilt
image, confirmed non-root `app` user can still import `app.main` and
the backend container reports healthy, full gate re-run clean.

## Phase 2 — done pending PR review

- `botpit.games.base`: `Game[State, Move]` contract, frozen pydantic state,
Expand Down
4 changes: 4 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ __pycache__
app.egg-info
*.pyc
.mypy_cache
.pytest_cache
.ruff_cache
.coverage
htmlcov
.venv
*.log
.env*
36 changes: 28 additions & 8 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ARG VITE_API_URL=
RUN bun run build


FROM python:3.14
FROM python:3.14-slim

ENV PYTHONUNBUFFERED=1

Expand All @@ -32,6 +32,14 @@ ENV UV_COMPILE_BYTECODE=1
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
ENV UV_LINK_MODE=copy

# Non-root user, created early so later steps can chown their own output as
# they create it. A blanket `chown -R /app` at the end (the first draft of
# this change) forces overlay2 to copy up every file from every earlier
# layer into a new one -- undoing a chunk of the slim-base size win. Owning
# each layer's output at creation time (COPY --chown, chown within the same
# RUN that populates .venv) avoids that.
RUN groupadd --gid 1000 app && useradd --uid 1000 --gid app --no-create-home app

WORKDIR /app/

# Place executables in the environment at the front of the path
Expand All @@ -43,23 +51,35 @@ ENV PATH="/app/.venv/bin:$PATH"
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-workspace --package botpit-backend
uv sync --frozen --no-install-workspace --package botpit-backend \
&& chown -R app:app /app/.venv

COPY ./backend/scripts /app/backend/scripts
COPY --chown=app:app ./backend/scripts /app/backend/scripts

COPY ./backend/pyproject.toml ./backend/alembic.ini /app/backend/
COPY --chown=app:app ./backend/pyproject.toml ./backend/alembic.ini /app/backend/

COPY ./backend/app /app/backend/app
COPY --chown=app:app ./backend/app /app/backend/app

COPY --from=frontend-build /app/backend/app/frontend /app/backend/app/frontend
COPY --chown=app:app --from=frontend-build /app/backend/app/frontend /app/backend/app/frontend

# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --package botpit-backend
uv sync --frozen --package botpit-backend \
&& chown -R app:app /app/.venv

WORKDIR /app/backend/

CMD ["fastapi", "run", "--workers", "4"]
USER app

# WEB_CONCURRENCY controls the worker count (default: 4, unchanged from
# before) -- overridable per deploy target instead of baked in, since the
# "right" number depends on the host's actual CPU count, which isn't known
# at build time. The default lives in the shell expansion, not a persistent
# ENV: an image-wide ENV would leak into compose.override.yml's `fastapi dev`
# too (dev's own command takes no --workers at all). `exec` replaces the
# shell so `fastapi run` receives SIGTERM directly for a clean graceful
# shutdown (see docs/development/ci-cd-and-docker.md).
CMD ["sh", "-c", "exec fastapi run --workers \"${WEB_CONCURRENCY:-4}\""]
3 changes: 3 additions & 0 deletions compose.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ services:
db:
restart: always

redis:
restart: always

adminer:
restart: always
labels:
Expand Down
Loading