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
45 changes: 45 additions & 0 deletions .github/workflows/backend-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Backend Test

on:
push:
branches: [main]
paths:
- "backend/**"
- "src/fall_in/core/**"
- "src/fall_in/net/**"
pull_request:
branches: [main]
paths:
- "backend/**"
- "src/fall_in/core/**"
- "src/fall_in/net/**"

defaults:
run:
shell: bash
working-directory: backend

jobs:
test:
name: Lint & Test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Install dependencies
run: uv sync --extra dev

- name: Lint (ruff)
run: uv run ruff check app/ tests/

- name: Test (pytest)
run: uv run pytest --cov=app --cov-report=term-missing
120 changes: 120 additions & 0 deletions .github/workflows/deploy-backend.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Deploy Backend

on:
workflow_dispatch:
push:
branches: [main]
paths:
- "backend/**"
- "src/fall_in/core/**"
- "src/fall_in/ai/**"
- "src/fall_in/net/**"
- "src/fall_in/multiplayer/models.py"

# Only one deployment at a time.
concurrency:
group: deploy-backend
cancel-in-progress: true

jobs:
test:
name: Pre-deploy Test
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: backend

steps:
- uses: actions/checkout@v6

- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Install dependencies
run: uv sync --extra dev

- name: Lint
run: uv run ruff check app/ tests/

- name: Test
run: uv run pytest -x -q

deploy:
name: Deploy to OCI
needs: test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.OCI_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "${{ secrets.OCI_HOST }}" >> ~/.ssh/known_hosts

- name: Sync source to server
run: |
rsync -azP --delete \
--include='backend/***' \
--include='src/fall_in/core/***' \
--include='src/fall_in/ai/***' \
--include='src/fall_in/net/***' \
--include='src/fall_in/multiplayer/models.py' \
--include='src/fall_in/__init__.py' \
--include='src/fall_in/multiplayer/__init__.py' \
--include='src/' \
--include='src/fall_in/' \
--include='src/fall_in/multiplayer/' \
--include='pyproject.toml' \
--include='uv.lock' \
--include='data/***' \
--exclude='*' \
./ "${{ secrets.OCI_USER }}@${{ secrets.OCI_HOST }}:~/fall-in/"

- name: Build & restart on server
run: |
ssh "${{ secrets.OCI_USER }}@${{ secrets.OCI_HOST }}" << 'DEPLOY_SCRIPT'
set -e
cd ~/fall-in

# Build Docker image (ARM-native on Ampere A1).
docker build -t fall-in-backend -f backend/Dockerfile .

# Stop existing container (if any) and start fresh.
docker stop fall-in-backend 2>/dev/null || true
docker rm fall-in-backend 2>/dev/null || true

docker run -d \
--name fall-in-backend \
--restart unless-stopped \
--env-file ~/fall-in/backend/.env \
--network host \
fall-in-backend

# Wait for health check.
echo "Waiting for health check..."
for i in $(seq 1 15); do
if curl -sf http://localhost:8000/healthz > /dev/null 2>&1; then
echo "Health check passed."
exit 0
fi
sleep 2
done
echo "Health check failed after 30s"
docker logs fall-in-backend --tail 30
exit 1
DEPLOY_SCRIPT

- name: Clean up old Docker images
if: success()
run: |
ssh "${{ secrets.OCI_USER }}@${{ secrets.OCI_HOST }}" \
'docker image prune -f'
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ Thumbs.db
ingame_base.png

# Logs
*.log
*.log

# DB
*.db

# env
.env
8 changes: 8 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__
*.pyc
.venv
.env
*.db
.ruff_cache
.pytest_cache
tests/
43 changes: 43 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# ============================================================
# Fall-In Backend — local development environment
# Copy to .env and fill in real values before running.
# ============================================================

# --- Database ---
# SQLite for local dev (no Docker needed):
DATABASE_URL=sqlite:///./fall_in_dev.db

# PostgreSQL example (for staging/prod):
# DATABASE_URL=postgresql://fall_in:secret@localhost:5432/fall_in

# --- JWT ---
# Generate a strong random key: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=dev-secret-key-change-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=7
GUEST_TOKEN_EXPIRE_HOURS=24

# --- Startup behaviour ---
# Set true to auto-create tables on startup (dev only).
# Production should use: uv run alembic upgrade head
CREATE_TABLES_ON_STARTUP=true

# --- Redis (optional) ---
# Leave unset for local dev — in-memory fallback is used automatically.
# Only covers the quick-match queue and reconnect token TTLs.
# NOTE: Redis does NOT make the stack multi-worker-safe. Room, match, and
# connection state are still in-process singletons. Run a single uvicorn
# worker until a future PR moves those stores to a shared backend.
# REDIS_URL=redis://localhost:6379/0

# --- Logging ---
# INFO for normal beta use; DEBUG for step-through local debugging.
LOG_LEVEL=INFO

# --- Admin API ---
# Static bearer token that gates /admin/* endpoints.
# Leave empty to disable admin endpoints (safe default for local dev).
# Set to a strong random string before sharing with moderators:
# python -c "import secrets; print(secrets.token_hex(32))"
ADMIN_TOKEN=
54 changes: 54 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# --- Fall-In Backend ---
# Multi-stage build optimised for ARM64 (Oracle Cloud Ampere A1).
# Build (from repo root):
# docker build -t fall-in-backend -f backend/Dockerfile .
# Run:
# docker run -p 8000:8000 --env-file backend/.env fall-in-backend

FROM python:3.12-slim AS base

# Prevent Python from writing .pyc files and enable unbuffered output.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

# --- Dependencies stage ---
FROM base AS deps

# Install uv for fast, reproducible dependency resolution.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Copy only dependency metadata first (cache-friendly layer).
COPY backend/pyproject.toml backend/uv.lock ./backend/
COPY pyproject.toml uv.lock ./

# Install backend deps (prod extras include redis).
# The client package (fall_in.core, fall_in.ai, etc.) is needed at runtime.
RUN cd backend && uv sync --no-dev --extra prod --no-install-project

# --- Runtime stage ---
FROM base AS runtime

# Copy the virtual environment from deps stage.
COPY --from=deps /app/backend/.venv /app/backend/.venv

# Copy source code.
COPY backend/app ./backend/app
COPY backend/migrations ./backend/migrations
COPY backend/alembic.ini ./backend/
COPY src/fall_in ./src/fall_in
COPY data ./data

# Make fall_in package importable (backend pythonpath includes ../src).
ENV PYTHONPATH="/app/src:/app/backend"
ENV PATH="/app/backend/.venv/bin:$PATH"

WORKDIR /app/backend

EXPOSE 8000

# Run migrations then start uvicorn.
# --host 0.0.0.0 binds to all interfaces inside the container.
# --workers 1 is correct for the in-memory singleton architecture.
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"]
42 changes: 42 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Alembic configuration for Fall-In backend.
# The DATABASE_URL is loaded from app.config (via .env) in migrations/env.py.
# Do NOT hard-code credentials here.

[alembic]
script_location = migrations
prepend_sys_path = .

# Logging
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
1 change: 1 addition & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Fall-In Multiplayer API
Empty file added backend/app/api/__init__.py
Empty file.
Loading
Loading