fix(ci+docker): install from uv.lock — stop resolving dependencies at build time (#81)
CI & Build / Python lint (push) Successful in 7s
CI & Build / integration (push) Successful in 39s
CI & Build / TypeScript typecheck (push) Successful in 45s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 21s

This commit was merged in pull request #81.
This commit is contained in:
2026-07-28 13:04:24 -04:00
3 changed files with 110 additions and 70 deletions
+31 -19
View File
@@ -40,6 +40,9 @@ on:
- "frontend/**"
- "tests/**"
- "pyproject.toml"
# The lock now determines what gets installed, so a lock-only change has
# to trigger a run — otherwise a dependency bump lands untested.
- "uv.lock"
- "alembic/**"
- "alembic.ini"
- "Dockerfile"
@@ -126,20 +129,30 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ hashFiles('pyproject.toml') }}
# Keyed on the LOCK, not pyproject: the lock is what determines the
# installed set now, and a pyproject edit that doesn't change
# resolution shouldn't throw the cache away.
key: uv-${{ hashFiles('uv.lock') }}
restore-keys: uv-
- name: Create virtual environment
run: uv venv /opt/venv
- name: Install package with dev deps
run: |
# http-ece doesn't declare setuptools as a build dep, and uv
# creates bare venvs without it. Install setuptools first so
# --no-build-isolation can find it.
uv pip install --python /opt/venv/bin/python setuptools wheel
uv pip install --python /opt/venv/bin/python --no-build-isolation http-ece
uv pip install --python /opt/venv/bin/python -e ".[dev]"
# Installs exactly what uv.lock pins, and resolves nothing itself.
#
# This replaced `uv pip install -e ".[dev]"`, which resolved from the
# pyproject constraints and ignored the lock entirely. Every dependency
# floated: on 2026-07-28 mcp 2.0.0 shipped mid-session and turned `main`
# red with no repo change (issue #2194). Green CI has to mean "these exact
# versions passed", or it isn't evidence of anything.
#
# `--locked` also FAILS when uv.lock is stale against pyproject, so a
# dependency edit has to go through a deliberate `uv lock` — it can't
# arrive on its own. That check earned its place immediately: it caught
# that the lock had been missing `pgvector` entirely (added to pyproject,
# never re-locked), which the old install path had been silently papering
# over by resolving from pyproject instead.
- name: Install locked dependencies
env:
UV_PROJECT_ENVIRONMENT: /opt/venv
run: uv sync --locked --extra dev
- name: Run tests
# Integration tests (real Postgres) run in the `integration` job below.
@@ -179,13 +192,12 @@ jobs:
--health-retries 10
steps:
- uses: actions/checkout@v6
- name: Create virtual environment
run: uv venv /opt/venv
- name: Install package with dev deps
run: |
uv pip install --python /opt/venv/bin/python setuptools wheel
uv pip install --python /opt/venv/bin/python --no-build-isolation http-ece
uv pip install --python /opt/venv/bin/python -e ".[dev]"
# Same locked install as the unit lane — the two must agree on versions,
# or "unit green, integration red" stops being a signal about the code.
- name: Install locked dependencies
env:
UV_PROJECT_ENVIRONMENT: /opt/venv
run: uv sync --locked --extra dev
- name: Integration suite (resolve service IP, migrate, test)
run: |
set -eux
+20 -3
View File
@@ -12,10 +12,27 @@ RUN npm run build
FROM python:3.14-slim AS runtime
WORKDIR /app
COPY pyproject.toml .
COPY src/ src/
# Installed from uv.lock, exactly like CI (issue #2194). This used to be
# `COPY pyproject.toml .` + `pip install .`, which never even copied the lock:
# the shipped image resolved its own dependency set, so CI could be green on one
# set of versions while the published image ran another. On 2026-07-28 that
# class of drift turned `main` red when mcp 2.0.0 shipped mid-session.
RUN --mount=type=cache,target=/root/.cache/pip \
pip install .
pip install --no-cache-dir uv
# Dependencies before source, so the expensive layer is cached on every build
# that doesn't change the lock.
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project
COPY src/ src/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
# uv sync installs into a project venv rather than the system interpreter, so
# alembic and hypercorn in CMD have to be found there.
ENV PATH="/app/.venv/bin:$PATH"
COPY --from=build-frontend /build/dist/ src/scribe/static/
COPY alembic.ini .
Generated
+59 -48
View File
@@ -354,54 +354,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
[[package]]
name = "scribe"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "aiosmtplib" },
{ name = "alembic" },
{ name = "apscheduler" },
{ name = "asyncpg" },
{ name = "bcrypt" },
{ name = "caldav" },
{ name = "fastembed" },
{ name = "httpx" },
{ name = "hypercorn" },
{ name = "icalendar" },
{ name = "mcp", extra = ["cli"] },
{ name = "quart" },
{ name = "sqlalchemy", extra = ["asyncio"] },
]
[package.optional-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "aiosmtplib", specifier = ">=3.0" },
{ name = "alembic", specifier = ">=1.13" },
{ name = "apscheduler", specifier = ">=3.10,<4.0" },
{ name = "asyncpg", specifier = ">=0.29" },
{ name = "bcrypt", specifier = ">=4.0" },
{ name = "caldav", specifier = ">=1.3" },
{ name = "fastembed", specifier = ">=0.4" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "hypercorn", specifier = ">=0.17" },
{ name = "icalendar", specifier = ">=5.0" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" },
{ name = "quart", specifier = ">=0.19" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0" },
]
provides-extras = ["dev"]
[[package]]
name = "fastembed"
version = "0.8.0"
@@ -1038,6 +990,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pgvector"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/ec/6eb80aebc728200f95229219882994c1b0585b956ca47da5edb9d062627a/pgvector-0.5.0.tar.gz", hash = "sha256:07a9dcf735696879406983afc6eba9a787cef7c0cf6c367ca1a5779f036dee74", size = 35170, upload-time = "2026-07-06T18:27:27.767Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e4/a5573f2c579ca9ad133293bfb624148ba0893674ca4a6eeec85ced9a6a09/pgvector-0.5.0-py3-none-any.whl", hash = "sha256:fedc9800894e6da2be51358d7b7c574bf34f247ca741a5a09513622135f5964f", size = 30958, upload-time = "2026-07-06T18:27:26.797Z" },
]
[[package]]
name = "pillow"
version = "12.2.0"
@@ -1496,6 +1457,56 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
]
[[package]]
name = "scribe"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "aiosmtplib" },
{ name = "alembic" },
{ name = "apscheduler" },
{ name = "asyncpg" },
{ name = "bcrypt" },
{ name = "caldav" },
{ name = "fastembed" },
{ name = "httpx" },
{ name = "hypercorn" },
{ name = "icalendar" },
{ name = "mcp", extra = ["cli"] },
{ name = "pgvector" },
{ name = "quart" },
{ name = "sqlalchemy", extra = ["asyncio"] },
]
[package.optional-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "aiosmtplib", specifier = ">=3.0" },
{ name = "alembic", specifier = ">=1.13" },
{ name = "apscheduler", specifier = ">=3.10,<4.0" },
{ name = "asyncpg", specifier = ">=0.29" },
{ name = "bcrypt", specifier = ">=4.0" },
{ name = "caldav", specifier = ">=1.3" },
{ name = "fastembed", specifier = ">=0.4" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "hypercorn", specifier = ">=0.17" },
{ name = "icalendar", specifier = ">=5.0" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.0,<2" },
{ name = "pgvector", specifier = ">=0.3" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" },
{ name = "quart", specifier = ">=0.19" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0" },
]
provides-extras = ["dev"]
[[package]]
name = "shellingham"
version = "1.5.4"