merge: combine local dev updates into dev

This commit is contained in:
qzl
2026-02-25 17:05:04 +08:00
15 changed files with 724 additions and 464 deletions
+7 -69
View File
@@ -6,67 +6,16 @@
- Add dependencies: `uv add <package>`
- All dependencies declared in `pyproject.toml`
## Process Entrypoints
## Code Quality Checks
### Bootstrap Gate (REQUIRED)
**Git pre-commit hook enforces code quality before commit.**
**The bootstrap gate is the ONLY allowed entry point for deployment.**
Pre-commit hook automatically runs on backend/ directory:
- `ruff check` - code style and linting
- `basedpyright` - type checking with error level
```bash
# Using Makefile (recommended)
make runtime-bootstrap-gate
# Or directly using the script
bash infra/scripts/runtime-bootstrap-gate.sh
```
This gate:
1. Runs `init-job bootstrap` (migrate + init-data)
2. Starts web and worker services
3. Aborts if bootstrap fails (prevents web/worker startup)
**Deployment without passing the bootstrap gate is PROHIBITED.**
### New Entrypoints (Phase 1-2, 2026-02-24)
**Primary (recommended):** Use Docker Compose orchestration.
```bash
# Bootstrap gate (required before web/worker)
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
# Web
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web
# Worker (grouped)
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d \
worker-critical worker-default worker-bulk
```
**One-shot jobs:**
```bash
# Migrate only
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job migrate
# Init data only
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job init-data
# Full bootstrap (migrate + init-data)
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job bootstrap
```
### One-shot CLI (local development)
```bash
# Bootstrap (migrate + init-data)
PYTHONPATH=backend/src uv run python -m core.runtime.cli bootstrap
# Migrate only
PYTHONPATH=backend/src uv run python -m core.runtime.cli migrate
# Init data only
PYTHONPATH=backend/src uv run python -m core.runtime.cli init-data
```
If any error detected, commit is rejected. Fix errors before committing.
Do not bypass or weaken checks (no ignores, disables, or config relaxations). Resolve the underlying issues.
## Logging
@@ -94,17 +43,6 @@ PYTHONPATH=backend/src uv run python -m core.runtime.cli init-data
- Tests can set env vars via `monkeypatch.setenv`, and should read values via `Settings()` unless the test is explicitly validating env plumbing
- Canonical principle: one source of truth per setting; no duplicate/derived env vars in backend code
## Code Quality Checks
**Git pre-commit hook enforces code quality before commit.**
Pre-commit hook automatically runs on backend/ directory:
- `ruff check` - code style and linting
- `basedpyright` - type checking with error level
If any error detected, commit is rejected. Fix errors before committing.
Do not bypass or weaken checks (no ignores, disables, or config relaxations). Resolve the underlying issues.
## TDD First Policy
**Principle: tests before implementation.**
+13 -1
View File
@@ -9,12 +9,17 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
from core.config.settings import config
from core.http.models import HealthResponse
from core.http.response import build_problem_details
from core.logging import configure_logging, get_logger
from core.logging import configure_logging, get_logger, log_service_banner
from v1.router import router as mobile_router
configure_logging(config)
log_service_banner(
service_name=config.runtime.service_name,
environment=config.runtime.environment,
)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
@@ -26,6 +31,13 @@ app.add_middleware(
app.include_router(mobile_router)
logger = get_logger("api.app")
logger.info(
"Web application initialized",
environment=config.runtime.environment,
debug=config.runtime.debug,
log_level=config.runtime.log_level,
)
@app.get("/health", response_model=HealthResponse)
async def health() -> HealthResponse:
-2
View File
@@ -43,8 +43,6 @@ def create_celery_app() -> Celery:
worker_prefetch_multiplier=1,
)
app.autodiscover_tasks(["tasks"])
configure_celery_app(app, settings=config)
return app
+2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from core.logging import celery
from core.logging.banner import log_service_banner
from core.logging.config import configure_logging
from core.logging.context import bind_context, clear_context, get_context
from core.logging.logger import get_logger
@@ -12,4 +13,5 @@ __all__ = [
"configure_logging",
"get_context",
"get_logger",
"log_service_banner",
]
+21
View File
@@ -0,0 +1,21 @@
from __future__ import annotations
import structlog
def build_service_banner(service_name: str, environment: str) -> str:
service_upper = service_name.upper()
border = "=" * 50
lines = [
border,
f" {service_upper}",
f" Environment: {environment}",
border,
]
return "\n".join(lines)
def log_service_banner(service_name: str, environment: str) -> None:
logger = structlog.get_logger("banner")
banner = build_service_banner(service_name, environment)
logger.info(banner)
+7
View File
@@ -7,6 +7,7 @@ from typing import cast
from celery import Celery, signals
from core.config.settings import Settings
from core.logging.banner import log_service_banner
from core.logging.config import configure_logging
from core.logging.context import bind_context, clear_context
@@ -22,8 +23,14 @@ class CelerySignalHandlers:
def build_celery_signal_handlers(
settings: Settings | None = None,
) -> CelerySignalHandlers:
active_settings = settings or Settings()
def on_setup_logging(*_args: object, **_kwargs: object) -> None:
configure_logging(settings)
log_service_banner(
service_name=active_settings.runtime.service_name,
environment=active_settings.runtime.environment,
)
def on_after_setup_task_logger(*_args: object, **_kwargs: object) -> None:
configure_logging(settings)
+33
View File
@@ -0,0 +1,33 @@
from __future__ import annotations
from core.logging.banner import build_service_banner
def test_build_service_banner_contains_service_name() -> None:
banner = build_service_banner(
service_name="web",
environment="dev",
)
assert "WEB" in banner
assert "dev" in banner
def test_build_service_banner_uppercases_service_name() -> None:
banner = build_service_banner(
service_name="worker-critical",
environment="prod",
)
assert "WORKER-CCRITICAL" in banner.upper() or "WORKER" in banner
def test_build_service_banner_includes_border() -> None:
banner = build_service_banner(
service_name="web",
environment="dev",
)
lines = banner.strip().split("\n")
assert len(lines) >= 3
assert all(line.startswith("=") or "WEB" in line or "dev" in line for line in lines)