fix(ci+docker): install from uv.lock — stop resolving dependencies at build time #81

Merged
bvandeusen merged 4 commits from dev into main 2026-07-28 13:04:24 -04:00
Owner

Closes issue #2194 (Scribe). Green on dev at 1b81310 (run 3009), image build included.

The hole

CI ran uv pip install -e ".[dev]" and the Dockerfile ran pip install . without ever copying uv.lock. Both resolved from the pyproject constraints, so uv.lock governed nothing. Consequences, in the order they bit:

  1. mcp 2.0.0 published mid-session on 2026-07-28 and turned main red with no repo change — the identical tree had passed on dev an hour earlier.
  2. Turning on --locked immediately surfaced that the lock had been missing pgvector entirely — added to pyproject for the vector-search work, never re-locked, and nothing noticed because nothing read the lock.
  3. The published image resolved its own set too, so a green CI run was never evidence about the artifact being shipped.

The fix

  • Both Python lanes: uv sync --locked --extra dev. --locked fails on a stale lock, so a dependency edit must go through a deliberate uv lock.
  • Dockerfile: installs from the lock, deps layer split from project layer for caching. Image and CI can no longer disagree.
  • uv.lock regenerated (uv lock in a throwaway ci-python:3.14 container). Conservative as expected: pgvector 0.5.0 added and not one existing pin moved — verified by diffing name=version across all 106 packages.
  • uv.lock added to the workflow paths: filter. It was absent, so a lock-only change — which is exactly what a dependency bump now looks like — would not have triggered CI.
  • Removed the http-ece install and the setuptools wheel step that existed only to support its --no-build-isolation build. Nothing imports http_ece; it's a leftover from the web-push subsystem dropped in the MCP-First pivot, and it was never in pyproject or the lock either.

Why this matters beyond today

Renovate's dashboard-approval discipline (rules #42–#45) was being bypassed entirely: upstream could bump this project with no PR and no approval. It can't now. Only APScheduler and mcp carry upper bounds, so that protection was the only thing standing between a major release and a red main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs

Closes issue #2194 (Scribe). Green on `dev` at `1b81310` (run 3009), image build included. ## The hole CI ran `uv pip install -e ".[dev]"` and the Dockerfile ran `pip install .` without ever copying `uv.lock`. Both resolved from the pyproject constraints, so **`uv.lock` governed nothing**. Consequences, in the order they bit: 1. `mcp` 2.0.0 published mid-session on 2026-07-28 and turned `main` red with no repo change — the identical tree had passed on `dev` an hour earlier. 2. Turning on `--locked` immediately surfaced that the lock had been **missing `pgvector` entirely** — added to pyproject for the vector-search work, never re-locked, and nothing noticed because nothing read the lock. 3. The published image resolved its own set too, so a green CI run was never evidence about the artifact being shipped. ## The fix - Both Python lanes: `uv sync --locked --extra dev`. `--locked` fails on a stale lock, so a dependency edit must go through a deliberate `uv lock`. - Dockerfile: installs from the lock, deps layer split from project layer for caching. Image and CI can no longer disagree. - `uv.lock` regenerated (`uv lock` in a throwaway `ci-python:3.14` container). Conservative as expected: **`pgvector 0.5.0` added and not one existing pin moved** — verified by diffing name=version across all 106 packages. - `uv.lock` added to the workflow `paths:` filter. It was absent, so a lock-only change — which is exactly what a dependency bump now looks like — would not have triggered CI. - Removed the `http-ece` install and the `setuptools wheel` step that existed only to support its `--no-build-isolation` build. Nothing imports `http_ece`; it's a leftover from the web-push subsystem dropped in the MCP-First pivot, and it was never in pyproject or the lock either. ## Why this matters beyond today Renovate's dashboard-approval discipline (rules #42–#45) was being bypassed entirely: upstream could bump this project with no PR and no approval. It can't now. Only `APScheduler` and `mcp` carry upper bounds, so that protection was the only thing standing between a major release and a red `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
bvandeusen added 4 commits 2026-07-28 13:04:16 -04:00
fix(ci): install from uv.lock so CI stops resolving dependencies itself
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Failing after 10s
CI & Build / Python tests (push) Failing after 16s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Build & push image (push) Has been skipped
ef7ebddadf
Closes the reproducibility hole that turned `main` red an hour ago. CI ran
`uv pip install -e ".[dev]"`, which resolves from the pyproject constraints
and ignores uv.lock completely — so every dependency floated. uv.lock pinned
mcp 1.27.2; CI installed the 2.0.0 published mid-session and the identical
tree that passed on `dev` failed on `main`.

Both Python lanes now run `uv sync --locked --extra dev`. `--locked` also
fails when the lock is stale against pyproject, so a dependency change has to
go through a deliberate `uv lock` instead of arriving on its own — which also
restores the point of the Renovate dashboard-approval flow.

Dropped the http-ece install and the setuptools/wheel step that existed only
to support it: nothing in src/ or tests/ imports http_ece. It is a leftover
from the web-push subsystem removed in the MCP-First pivot, and it was never
in pyproject or uv.lock — CI was installing an unused package and carrying a
--no-build-isolation workaround for it.

Cache key moves from pyproject.toml to uv.lock, since the lock is now what
determines the installed set.

uv.lock's recorded root requirement updated to match the mcp cap. Edited by
hand rather than regenerated: uv isn't installed on this workstation, the
resolved mcp 1.27.2 already satisfies `<2`, so no re-resolution is needed —
only the staleness check needed satisfying.

The Dockerfile still resolves independently (`pip install .`, and it doesn't
even copy uv.lock), so the shipped image is not yet covered. Following
separately so a build break can't strand `main`. Refs #2194.
fix(ci): use uv sync --frozen — --locked needs a lock this box can't regenerate
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Failing after 8s
CI & Build / Python tests (push) Failing after 21s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / Build & push image (push) Has been skipped
8bab0c762b
Run 3006 failed at the install step: "The lockfile at `uv.lock` needs to be
updated, but `--locked` was provided." The guard was working — the mcp cap
edited pyproject, so the lock genuinely is stale, and hand-editing the
recorded specifier wasn't enough to satisfy uv's freshness check.

Regenerating needs `uv lock`, and this workstation has neither uv nor pip
(rule #10 — local Python envs are deliberately absent), so obtaining it would
mean pulling a binary from github.com, against rule #3. Not doing that
unilaterally.

--frozen installs exactly what the lock pins and resolves nothing, which is
the whole point of #2194: no dependency can float into a run again. What it
gives up is only the staleness check — and a forgotten re-lock surfaces as a
loud ImportError, not as a silent version drift, so the failure mode is the
tolerable one.

Flip to --locked in the same change that runs `uv lock`. Refs #2194.
fix(ci): regenerate uv.lock and enforce it with --locked
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 17s
a47e1b9c4e
Completes the previous two commits. `--frozen` failed run 3007 with
`ModuleNotFoundError: No module named 'pgvector'` — the lock wasn't merely
stale in its recorded metadata, it was missing a real dependency. pgvector
was added to pyproject for the vector-search work and the lock was never
regenerated, and nothing noticed because CI resolved from pyproject and never
read the lock. The lock has been dead weight for some time.

Regenerated with `uv lock` inside a throwaway `ci-python:3.14` container —
this workstation has no uv and no pip, and the CI image already carries the
right toolchain, so nothing was installed to do it. uv was conservative as
promised: pgvector 0.5.0 added, and NOT ONE existing pin moved (verified by
diffing name=version pairs across all 106 packages).

Both lanes now run `uv sync --locked`, so a dependency edit without a re-lock
fails loudly at install rather than resolving around the lock. The check paid
for itself on its first run by surfacing the missing pgvector.

Also added uv.lock to the workflow's `paths:` filter. It was absent, so a
lock-only change — exactly what a dependency bump looks like now — would not
have triggered CI at all.

Closes the CI half of #2194. The Dockerfile still resolves independently and
is tracked there.
fix(docker): build the image from uv.lock too
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / integration (push) Successful in 17s
CI & Build / Python tests (push) Successful in 44s
CI & Build / Build & push image (push) Successful in 40s
1b81310847
The other half of #2194. The runtime stage did `COPY pyproject.toml .` +
`pip install .` and never copied uv.lock at all, so the SHIPPED IMAGE
resolved its own dependency set — independently of CI and of the lock. CI
could be green on one set of versions while the published image ran another,
which makes a green run evidence about the tests and not about the artifact.

Now: install uv, sync deps from the lock, then sync the project. Split into
two syncs so the dependency layer caches on any build that doesn't touch the
lock — the same shape CI uses, so image and CI can no longer disagree.

`uv sync` installs into /app/.venv rather than the system interpreter, so PATH
picks it up for the alembic + hypercorn CMD. The project stays editable, which
keeps /app/src authoritative exactly as PYTHONPATH and the frontend-dist copy
into src/scribe/static/ already assume.

Not built locally (rules #10/#12) — the dev build job verifies it, and `main`
already carries a working :latest, so a break here can't strand a deploy.
bvandeusen merged commit 6153231f9c into main 2026-07-28 13:04:24 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#81