Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e8f403e
Centralize API base path and introduce /api/v1 versioning
tomvothecoder Jan 3, 2026
ceed352
Fix env variable setting in frontend with vite
tomvothecoder Jan 3, 2026
811915e
Update pre-commit hooks
tomvothecoder Jan 5, 2026
95339fa
Rename constants.py to version.py and add meta.py
tomvothecoder Jan 5, 2026
4fa05b7
Fix tests
tomvothecoder Jan 5, 2026
b4be551
Update uv.lock
tomvothecoder Jan 5, 2026
82f6833
Update pre-commit to correctly read pyproject.toml
tomvothecoder Jan 5, 2026
d309d99
Make pre-commit use correct config
tomvothecoder Jan 5, 2026
912910a
Fix pyproject.toml config
tomvothecoder Jan 5, 2026
e86f826
Cache only uv-cache
tomvothecoder Jan 5, 2026
7733920
Run pre-commit from root
tomvothecoder Jan 5, 2026
5d9c689
Use --project backend
tomvothecoder Jan 5, 2026
112344f
Run pre-commit on backend
tomvothecoder Jan 5, 2026
201ab6d
Rename workflow
tomvothecoder Jan 5, 2026
d77b4ff
Minor updates to build workflow
tomvothecoder Jan 5, 2026
399c274
Skip frontend hooks
tomvothecoder Jan 5, 2026
82975f8
Fix pytest cwd
tomvothecoder Jan 5, 2026
923b5d5
Update root README.md to describe pre-commit
tomvothecoder Jan 5, 2026
f04aadc
Update triggering mechanism for backend-ci.yml
tomvothecoder Jan 5, 2026
05da79b
Add suggestions from copilot
tomvothecoder Jan 5, 2026
9b9b6ec
Fix test
tomvothecoder Jan 5, 2026
d9cf597
Fix path specified for alembic.ini
tomvothecoder Jan 5, 2026
729e7da
Fix env file check for CI
tomvothecoder Jan 5, 2026
a69273f
Add print statement for debugging
tomvothecoder Jan 5, 2026
f387f27
Show lines that are not hit by pytest in build
tomvothecoder Jan 5, 2026
44089d6
Update coverage settings in pyproject.toml
tomvothecoder Jan 5, 2026
23ed76f
Update config.py
tomvothecoder Jan 5, 2026
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 .envs/dev/backend.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret

# Must match the callback URL in your GitHub App configuration
# This is the backend OAuth callback endpoint that GitHub calls with the authorization code.
GITHUB_REDIRECT_URL=https://127.0.0.1/auth/github/callback
GITHUB_REDIRECT_URL=https://127.0.0.1/api/v1/auth/github/callback

# Secret used to sign OAuth "state" parameter (prevents CSRF)
# Generate securely with:
Expand Down
2 changes: 1 addition & 1 deletion .envs/dev/frontend.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

# Backend configuration
# -------------------------------------------------------------------
VITE_API_BASE_URL=https://127.0.0.1:8000/api
VITE_API_BASE_URL=https://127.0.0.1:8000/api/v1
2 changes: 1 addition & 1 deletion .envs/dev_docker/backend.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret

# Must match the callback URL in your GitHub App configuration
# This is the backend OAuth callback endpoint that GitHub calls with the authorization code.
GITHUB_REDIRECT_URL=https://127.0.0.1/auth/github/callback
GITHUB_REDIRECT_URL=https://127.0.0.1/api/v1/auth/github/callback

# Secret used to sign OAuth "state" parameter (prevents CSRF)
# Generate securely with:
Expand Down
2 changes: 1 addition & 1 deletion .envs/dev_docker/frontend.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

# Backend configuration
# -------------------------------------------------------------------
VITE_API_BASE_URL=https://127.0.0.1:8000/api
VITE_API_BASE_URL=https://127.0.0.1:8000/api/v1
4 changes: 2 additions & 2 deletions .envs/prod/backend.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret

# Must match the callback URL in your GitHub App configuration
# This is the backend OAuth callback endpoint that GitHub calls with the authorization code.
GITHUB_REDIRECT_URL=https://127.0.0.1/auth/github/callback
GITHUB_REDIRECT_URL=https://127.0.0.1/api/v1/auth/github/callback
# For production, this must also point to the backend's OAuth callback endpoint
# GITHUB_REDIRECT_URL=https://app.${DOMAIN}/auth/callback
# GITHUB_REDIRECT_URL=https://app.${DOMAIN}/api/v1/auth/callback

# Secret used to sign OAuth "state" parameter (prevents CSRF)
# Generate securely with:
Expand Down
2 changes: 1 addition & 1 deletion .envs/prod/frontend.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

# Backend configuration
# -------------------------------------------------------------------
VITE_API_BASE_URL=https://127.0.0.1:8000/api
VITE_API_BASE_URL=https://127.0.0.1:8000/api/v1
38 changes: 24 additions & 14 deletions .github/workflows/build.yml → .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
name: CI
name: Backend CI

on:
pull_request:
branches: [main]
paths:
- backend/**
- .pre-commit-config.yaml
- .github/workflows/backend-ci.yml
push:
branches: [main]
paths:
- backend/**
- .pre-commit-config.yaml
- .github/workflows/backend-ci.yml
workflow_dispatch: {}

jobs:
backend:
name: Backend checks
runs-on: ubuntu-latest
timeout-minutes: 15

services:
postgres:
Expand Down Expand Up @@ -60,7 +69,7 @@ jobs:
# --------------------------------------------------
GITHUB_CLIENT_ID: dummy
GITHUB_CLIENT_SECRET: dummy
GITHUB_REDIRECT_URL: http://localhost/api/auth/github/callback
GITHUB_REDIRECT_URL: http://localhost/api/v1/auth/github/callback
GITHUB_STATE_SECRET_KEY: dummy

# --------------------------------------------------
Expand Down Expand Up @@ -108,15 +117,13 @@ jobs:
# -------------------------
# Cache uv + dependencies
# -------------------------
- name: Cache uv
- name: Cache uv cache
uses: actions/cache@v4
with:
path: |
~/.cache/uv
backend/.venv
key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }}
path: ~/.cache/uv
key: uv-cache-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
uv-cache-${{ runner.os }}-

# -------------------------
# Install backend deps
Expand All @@ -135,21 +142,24 @@ jobs:
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-${{ runner.os }}-

# -------------------------
# Run pre-commit
# -------------------------
- name: Run pre-commit
working-directory: backend
- name: Run pre-commit (backend only)
env:
SKIP: frontend-eslint,frontend-prettier
run: |
uv run pre-commit run --all-files
uv run --project backend pre-commit run --all-files

# -------------------------
# Run pytest
# -------------------------
- name: Run pytest
# NOTE:
# Pytest (and Alembic) must run from the backend directory so relative paths
# like alembic.ini and script_location resolve correctly. `--project` only
# selects the virtualenv; it does not change the working directory.
working-directory: backend
run: |
uv run pytest
uv run pytest --strict-markers
20 changes: 15 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ repos:
# General sanity checks
# ------------------------
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand All @@ -17,16 +17,26 @@ repos:
rev: v0.14.10
hooks:
- id: ruff-check
args: [--fix]
files: ^backend/
name: ruff (lint)
args:
- --config=backend/pyproject.toml
- --fix
files: ^backend/.*\.py$

- id: ruff-format
files: ^backend/
name: ruff (format)
args:
- --config=backend/pyproject.toml
files: ^backend/.*\.py$


- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
rev: v1.19.1
hooks:
- id: mypy
name: mypy (type checking)
args:
- --config-file=backend/pyproject.toml
files: ^backend/.*\.py$
additional_dependencies:
- fastapi
Expand Down
41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,19 +251,49 @@ Pre-commit runs automatically on `git commit` and will block commits if checks f

---

### ⚠️ Important: Where to run pre-commit

**Pre-commit must always be run from the repository root.**

Some hooks (notably `mypy`) reference configuration files using paths relative to the repo root (for example, `backend/pyproject.toml`). Running pre-commit from a subdirectory such as `backend/` can cause those configurations to be missed, leading to inconsistent or misleading results.

✅ Correct:

```bash
pre-commit run --all-files
```

❌ Incorrect:

```bash
cd backend
pre-commit run --all-files
```

In CI, pre-commit is also executed from the repository root for this reason.

> **Note:** When using `uv`, CI runs pre-commit via
> `uv run --project backend pre-commit run --all-files`.
> The `--project` flag selects the backend virtual environment, but **does not change the working directory**. Pre-commit itself must still be invoked from the repo root so configuration paths resolve correctly.

---

### What pre-commit checks

- **Backend**

- Ruff linting and formatting
- Python style and correctness checks
- Python style and correctness checks (mypy)

- **Frontend**

- ESLint (auto-fix on staged files)
- Prettier formatting (staged files only)

All hooks are configured in the root `.pre-commit-config.yaml`.

> **Note:** Git hooks run in a non-interactive shell
> Make sure that Node.js tools (such as `pnpm`) are available in your system `PATH` so pre-commit hooks can execute successfully.
> **Note:** Git hooks run in a non-interactive shell.
> Make sure that Node.js tools (such as `pnpm`) are available in your system `PATH` so frontend hooks can execute successfully.

---

Expand Down Expand Up @@ -293,7 +323,7 @@ make pre-commit-install

### Running pre-commit manually

To run all hooks against all files:
To run all hooks against all files (from the repo root):

```bash
make pre-commit-run
Expand All @@ -302,12 +332,9 @@ make pre-commit-run
Or directly:

```bash
cd backend
uv run pre-commit run --all-files
```

You can also run pre-commit from **any directory inside the repo**; it always resolves the repo root configuration.

---

### Notes & expectations
Expand Down
8 changes: 8 additions & 0 deletions backend/app/api/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import APIRouter

router = APIRouter(tags=["health"])


@router.get("/health")
async def health():
return {"status": "ok"}
20 changes: 20 additions & 0 deletions backend/app/api/meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi import APIRouter

from app.api.version import API_VERSION

router = APIRouter(tags=["meta"])


@router.get("/meta")
def api_meta():
return {
"version": API_VERSION,
"status": "internal",
"breaking_changes": (
"No breaking changes are currently declared. This field will describe "
"required upgrade paths when incompatible API versions are introduced."
),
# Build identifier injected at deploy time (e.g., short git SHA).
# Intentionally None until CI/CD or multi-environment deployments exist.
"build": None,
}
8 changes: 8 additions & 0 deletions backend/app/api/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
API_PREFIX = "/api"

# NOTE: Any backward-incompatible response shape change requires a new API version (v2+).
API_VERSION = "v1"

API_BASE = f"{API_PREFIX}/{API_VERSION}"

__all__ = ["API_BASE", "API_VERSION"]
7 changes: 4 additions & 3 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def get_env_file(project_root: Path | None = None) -> str | None:

Parameters
----------
project_root : Path or None, optional
project_root : str or Path or None, optional
The root directory of the project. If None, it is inferred from the file
location.

Expand All @@ -31,12 +31,13 @@ def get_env_file(project_root: Path | None = None) -> str | None:
If the required environment file does not exist.
"""
# In CI, do not require an env file
if os.getenv("CI", "").lower() == "true":
if os.getenv("CI"):
return None

app_env = os.getenv("APP_ENV", "dev")

if project_root is None:
# project_root is only specified during testing to point to temp dirs.
if project_root is None: # pragma: no cover
project_root = Path(__file__).resolve().parents[3]

env_file = project_root / ".envs" / app_env / "backend.env"
Expand Down
17 changes: 9 additions & 8 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.api.health import router as health_router
from app.api.meta import router as meta_router
from app.api.version import API_BASE
from app.core.config import settings
from app.core.exceptions import register_exception_handlers
from app.core.logger import _setup_root_logger
Expand Down Expand Up @@ -28,14 +31,12 @@ def create_app() -> FastAPI:
)

# Register routers.
app.include_router(simulations_router, prefix="/api")
app.include_router(machine_router, prefix="/api")
app.include_router(user_router, prefix="/api")
app.include_router(auth_router, prefix="/api")

@app.get("/health")
async def health():
return {"status": "ok"}
app.include_router(simulations_router, prefix=API_BASE)
app.include_router(machine_router, prefix=API_BASE)
app.include_router(user_router, prefix=API_BASE)
app.include_router(auth_router, prefix=API_BASE)
app.include_router(meta_router, prefix=API_BASE)
app.include_router(health_router, prefix=API_BASE)

return app

Expand Down
17 changes: 13 additions & 4 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ dev = [
# --- Development Tools ---
"ruff==0.14.1",
"pre-commit==4.3.0",
"mypy==1.18.2",
"mypy==1.19.1",

# --- Runtime / ML ---
"torch==2.7.1",
Expand Down Expand Up @@ -122,13 +122,22 @@ convention = "numpy"

[tool.ruff.lint.per-file-ignores]
# FastAPI’s use of Depends(...) in defaults is safe and idiomatic. Ignore B008 (do not call a method in argument defaults) in router files.
"app/features/*/api.py" = ["B008"]
"app/features/*/api/*.py" = ["B008"]
"**/app/features/*/api.py" = ["B008"]
"**/app/features/*/api/*.py" = ["B008"]

[tool.ruff.lint.isort]
known-first-party = ["app"]

[tool.pytest.ini_options]
# Docs: https://docs.pytest.org/en/7.2.x/reference/customize.html#configuration
junit_family = "xunit2"
addopts = "--cov=app --cov-report term --cov-report html:tests_coverage_reports/htmlcov --cov-report xml:tests_coverage_reports/coverage.xml -s"
addopts = [
"--cov=app",
"--cov-report=term-missing:skip-covered",
"--cov-report=html:tests_coverage_reports/htmlcov",
"--cov-report=xml:tests_coverage_reports/coverage.xml",
"-s",
]
python_files = ["tests.py", "test_*.py"]

[tool.mypy]
Expand Down
5 changes: 4 additions & 1 deletion backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import uuid
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Generator
from urllib.parse import urlparse

Expand All @@ -26,7 +27,9 @@

logger = _setup_custom_logger(__name__)

ALEMBIC_INI_PATH = "alembic.ini"

BASE_DIR = Path(__file__).resolve().parents[1]
ALEMBIC_INI_PATH = BASE_DIR / "alembic.ini"


# -----------------------------------------------
Expand Down
Loading