diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d29e4d..1b937b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install (locked deps + dev extras) - run: uv sync --extra dev --python ${{ matrix.python-version }} + run: uv sync --locked --extra dev --python ${{ matrix.python-version }} - name: Lint (ruff) run: uv run ruff check src tests diff --git a/Dockerfile b/Dockerfile index 1a270e7..47a5914 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,13 +7,19 @@ ENV PYTHONUNBUFFERED=1 \ WORKDIR /app -# uv for fast, reproducible installs. -RUN pip install --no-cache-dir uv - -# Install dependencies + the package (bundled idc-index-data Parquet comes along). -COPY pyproject.toml README.md ./ +# uv for fast, reproducible installs. Pinned so the lock export below behaves the same every build. +RUN pip install --no-cache-dir uv==0.12.24 + +# Install dependencies + the package (bundled idc-index-data Parquet comes along). Dependencies come +# from uv.lock, not pyproject.toml's lower bounds, so the image ships exactly the versions CI tested +# and pip-audit scanned, and rebuilding a commit is deterministic. --frozen fails rather than +# re-resolving; CI's `uv sync --locked` already guarantees the lock matches pyproject.toml. +COPY pyproject.toml uv.lock README.md ./ COPY src ./src -RUN uv pip install --system . +RUN uv export --frozen --no-dev --no-emit-project --format requirements-txt > /tmp/requirements.txt \ + && uv pip install --system -r /tmp/requirements.txt \ + && uv pip install --system --no-deps . \ + && rm /tmp/requirements.txt # Bake the read-only DuckDB file at build time so cold starts are instant. With no argument, # build_database_file includes ALL specialized indices (seg/ann/sm/ct/mr/pt, clinical, …), diff --git a/dev/deployment.md b/dev/deployment.md index 6fe5c54..09803a6 100644 --- a/dev/deployment.md +++ b/dev/deployment.md @@ -157,11 +157,13 @@ python3 dev/smoke_openapi_examples.py "$URL" ## 5. Updating for a new IDC release -The image bakes whatever `idc-index-data` resolves at build time. To publish a new IDC version, -**rebuild and redeploy** (steps 2–3). For reproducibility, pin the version in +The image installs dependencies from [uv.lock](../uv.lock) (see [Dockerfile](../Dockerfile)), so +it bakes exactly the `idc-index-data` the lock records and a rebuild of the same commit is +deterministic. To move IDC versions, bump the exact `idc-index` pin in [pyproject.toml](../pyproject.toml) (e.g. `idc-index==0.13.1`, which pulls a specific -`idc-index-data`) so a rebuild is deterministic; bump the pin to move IDC versions. The running -version is always reported at `/v3/version`. +`idc-index-data`), run `uv lock` and commit both files — CI's `uv sync --locked` fails if the lock +is stale — then **rebuild and redeploy** (steps 2–3). The running version is always reported at +`/v3/version`. ## Optional: remote MCP service (HTTP) @@ -246,8 +248,8 @@ boundary and never cross it the wrong way: - **dev** is *outside* the boundary and self-contained: it builds its own image into the **dev** project's registry and deploys it. Nothing dev produces is ever promoted onward. - **test** builds the **canonical** image into the **test** project's registry and deploys it - (test doubles as UAT). Pinning `idc-index` in [pyproject.toml](../pyproject.toml) keeps that - build deterministic. + (test doubles as UAT). Installing from [uv.lock](../uv.lock) keeps that build + deterministic. - **prod** deploys **test's exact image by immutable `@sha256:` digest**, referencing test's registry directly — no rebuild, no copy — so prod runs the bytes test validated. prod's deployer SA and Cloud Run service agent are granted read on test's Artifact Registry (the only diff --git a/dev/developer_guide.md b/dev/developer_guide.md index 953bee6..712c9b9 100644 --- a/dev/developer_guide.md +++ b/dev/developer_guide.md @@ -64,7 +64,8 @@ Fixtures live in [tests/conftest.py](../tests/conftest.py): `ctx` (the core [`.github/workflows/ci.yml`](../.github/workflows/ci.yml) runs on pushes to `main` (and manual dispatch) and on PRs that touch `src/idc_api/**`, `tests/**`, `pyproject.toml`, -or `uv.lock`. It installs locked deps with `uv sync --extra dev`, then lints, scans, and tests on +or `uv.lock`. It installs locked deps with `uv sync --locked --extra dev` — failing if `uv.lock` +is stale, so run `uv lock` after editing dependencies — then lints, scans, and tests on Python 3.11 and 3.12. CI needs no secrets or GCP; its first test run downloads the specialized indices (`IDC_API_INCLUDE_INDICES=all` by default — set `none` to run from bundled data only). Before pushing, run the same checks locally: diff --git a/pyproject.toml b/pyproject.toml index f77a654..179ef03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,10 @@ dependencies = [ # Query engine + data. # idc-index is pinned EXACTLY, unlike every other dependency here, because it transitively # pins idc-index-data (0.13.1 -> 25.0.1) — which *is* the IDC data release this server - # advertises at /v3/version and documents in the changelog. The production image installs - # from this file alone (the Dockerfile copies pyproject.toml and src, never uv.lock), so a - # lower bound would let a rebuild of this very commit bake a later IDC release than the one - # it claims to serve. Bump this pin to move IDC versions — see dev/deployment.md - # § Updating for a new IDC release. + # advertises at /v3/version and documents in the changelog. The image installs from uv.lock, + # so the lock alone already fixes it; the exact pin keeps the served IDC release explicit + # here and stops a routine `uv lock --upgrade` from silently moving it. Bump this pin (then + # `uv lock`) to move IDC versions — see dev/deployment.md § Updating for a new IDC release. "idc-index==0.13.1", "duckdb>=1.5.5", "pyarrow>=25.0.0",