Skip to content

Production image installs from pyproject.toml, not uv.lock #57

Description

@fedorov

Context: surfaced by Copilot review on #56 (IDC v25), where it caused a concrete, user-visible drift risk. That PR fixed the symptom for one dependency; this issue is the root cause.


Summary

uv.lock constrains CI but not the shipped image. The Docker build resolves dependencies fresh from the lower bounds in pyproject.toml, so the same git commit can produce different images at different times, and the versions CI tested and audited are not necessarily the versions that ship.

Mechanism

The two install paths diverge:

Command Resolves from
CI uv sync --extra dev (.github/workflows/ci.yml:49) uv.lock
Image uv pip install --system . (Dockerfile:14-16) pyproject.toml bounds

The Dockerfile copies pyproject.toml, README.md and src — never uv.lock. Every runtime dependency except idc-index is a lower bound (duckdb>=1.5.5, pyarrow>=25.0.0, pydantic>=2.7, fastapi>=0.141.1, uvicorn[standard]>=0.52.1, mcp>=1.29.0,<2), so each rebuild re-resolves to whatever is newest at that moment.

Why it matters

  1. CI's guarantees do not transfer to the artifact. pytest, bandit and — most importantly — pip-audit all run against the locked set. A clean vulnerability scan says nothing about the image, which may contain a different, never-audited version of any dependency.
  2. Rebuilds are not reproducible. Per dev/deployment.md, prod runs test's exact bytes and never rebuilds, so the exposure window is the image build in test. But "build the canonical image from this commit" is not currently a deterministic operation, which is the property the promotion model assumes.
  3. Data-version drift was the visible case. idc-index transitively pins idc-index-data, which is the IDC release advertised at /v3/version. Under idc-index>=0.13.0, rebuilding the v25 commit after a later release shipped would have served a different dataset than the changelog claimed. feat: serve IDC v25 (idc-index 0.13.0) #56 pins idc-index==0.13.0 to stop that — a point fix for the one dependency where drift is user-visible, not a general solution.

Options

A. uv sync --frozen in the image. Most direct, but uv sync builds a project .venv rather than installing system-wide, so the DuckDB bake step (RUN python -c ...) and the CMD both need repointing at /app/.venv/bin/. Larger diff.

B. Export the lock to a requirements file, then install as today.

COPY pyproject.toml uv.lock README.md ./
COPY src ./src
RUN uv export --frozen --no-dev --format requirements-txt > /tmp/requirements.txt \
 && uv pip install --system -r /tmp/requirements.txt \
 && uv pip install --system --no-deps .

Keeps --system and leaves the rest of the Dockerfile untouched. Probably the smaller change.

C. Status quo — pin each dependency exactly in pyproject.toml. Works, but duplicates the lockfile by hand and drifts from it silently.

Preference is B unless the .venv layout is wanted for other reasons.

Notes

  • Whichever option lands, Dockerfile needs uv.lock in its build context, and the uv.lock path filters already present in ci.yml and build-and-deploy-dev.yml mean lock changes will correctly trigger a rebuild.
  • Worth deciding at the same time whether the idc-index==0.13.0 pin from feat: serve IDC v25 (idc-index 0.13.0) #56 stays. It remains useful as an explicit statement of the advertised data release even once the lock is authoritative, and dev/deployment.md § Updating for a new IDC release already describes bumping it as the way to move IDC versions.
  • Verify after the change that the image still reports the expected /v3/version and that the baked DuckDB build step runs in the same environment the deps landed in.

Drafted with Claude Code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions