From a1c55446949a5be890a3b86d461037b68e0a7e81 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Wed, 10 Jun 2026 23:31:20 -0700 Subject: [PATCH] =?UTF-8?q?feat(slice-4):=20VERSION=20file=20+=20versioned?= =?UTF-8?q?=20/healthz=20=E2=80=94=20the=20flotilla=20verify=20contract=20?= =?UTF-8?q?(=C2=A78.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- VERSION | 1 + backend/app/main.py | 14 ++++++++++++-- backend/tests/test_bootstrap.py | 2 +- backend/tests/test_healthz.py | 8 +++++++- 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..1d0ba9e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.4.0 diff --git a/backend/app/main.py b/backend/app/main.py index 84a43aa..46b2ceb 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -11,6 +11,7 @@ from __future__ import annotations import logging import sys from contextlib import asynccontextmanager +from pathlib import Path from typing import Any import psycopg @@ -26,6 +27,15 @@ from app.platform.mailer import Mailer from app.platform import session as session_mod +# The repo-root VERSION file is the single version source: the deploy pin checks out its +# tag, and /healthz must report it back (flotilla-core's verify gate compares them). +_REPO_ROOT = Path(__file__).resolve().parents[2] +try: + _APP_VERSION = (_REPO_ROOT / "VERSION").read_text().strip() +except OSError: + _APP_VERSION = "0.0.0" + + class RequestCodeBody(BaseModel): email: str @@ -94,7 +104,7 @@ def create_app(database_url: str | None = None) -> FastAPI: finally: app.state.pool.close() - app = FastAPI(title="ecomm", version="0.3", lifespan=lifespan) + app = FastAPI(title="ecomm", version=_APP_VERSION, lifespan=lifespan) @app.get("/healthz") def healthz(response: Response, conn: psycopg.Connection = Depends(get_conn)): @@ -108,7 +118,7 @@ def create_app(database_url: str | None = None) -> FastAPI: if pending: response.status_code = 503 return {"status": "unavailable", "reason": "migrations_pending"} - return {"status": "ok"} + return {"status": "ok", "version": _APP_VERSION} @app.post("/api/auth/request-code") def request_code( diff --git a/backend/tests/test_bootstrap.py b/backend/tests/test_bootstrap.py index 19ffac6..ec1db53 100644 --- a/backend/tests/test_bootstrap.py +++ b/backend/tests/test_bootstrap.py @@ -12,7 +12,7 @@ def test_inv_1_bootstrap_whole_flow_from_empty(fresh_db_url): # fresh_db_url is a brand-new empty database; create_app() self-migrates (INV-7). with TestClient(create_app(database_url=fresh_db_url)) as client: # a fresh deployment serves healthz green before any row exists - assert client.get("/healthz").json() == {"status": "ok"} + assert client.get("/healthz").json()["status"] == "ok" # PUC-2: first visitor requests a code; it reaches them via the dev channel assert client.post( diff --git a/backend/tests/test_healthz.py b/backend/tests/test_healthz.py index 0dc58ba..c0a8871 100644 --- a/backend/tests/test_healthz.py +++ b/backend/tests/test_healthz.py @@ -1,14 +1,20 @@ +from pathlib import Path + from fastapi.testclient import TestClient from app.main import create_app +# /healthz reports the VERSION file's value — the flotilla deploy gate compares +# body.version against the pinned target (flotilla-core SPEC §8.1 phase 8). +_VERSION = (Path(__file__).resolve().parents[2] / "VERSION").read_text().strip() + def test_healthz_ok_on_migrated_empty_db(fresh_db_url): app = create_app(database_url=fresh_db_url) with TestClient(app) as client: resp = client.get("/healthz") assert resp.status_code == 200 - assert resp.json() == {"status": "ok"} + assert resp.json() == {"status": "ok", "version": _VERSION} def test_startup_migrates_from_empty(fresh_db_url):