Skip to content

Commit 420b02c

Browse files
author
Ryan Winkler
authored
build: cut over locked uv consumers (#672)
Build one locked application and MCP virtual environment in a pinned uv 0.12.3 multi-stage image. Route Docker, local tooling, audits, tests, upgrade replay, pre-commit, Dependabot, benchmark tooling, and source-install documentation through that lock. Remove both hand-maintained requirements files after every supported consumer is converted. Includes aiohttp 3.14.1→3.14.3 security patch from #671. Closes #660.
1 parent ad13eff commit 420b02c

23 files changed

Lines changed: 415 additions & 166 deletions

.claude/skills/run-floppy/SKILL.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ misleading. This skill exists mostly to save you from rediscovering those.
1515
```bash
1616
cd <repo>/src
1717
redis-server --port 6379 --daemonize yes --save '' --dir /tmp
18-
python manage.py migrate --noinput
19-
python manage.py collectstatic --noinput --settings=local_serve # see below
20-
bash .claude/skills/run-floppy/scripts/serve.sh start
21-
python .claude/skills/run-floppy/scripts/smoke.py --base http://localhost:8299
22-
bash .claude/skills/run-floppy/scripts/serve.sh stop
18+
uv run --project .. --no-sync python manage.py migrate --noinput
19+
uv run --project .. --no-sync python manage.py collectstatic --noinput --settings=local_serve # see below
20+
uv run --project .. --no-sync bash ../.claude/skills/run-floppy/scripts/serve.sh start
21+
uv run --project .. --no-sync python ../.claude/skills/run-floppy/scripts/smoke.py --base http://localhost:8299
22+
uv run --project .. --no-sync bash ../.claude/skills/run-floppy/scripts/serve.sh stop
2323
```
2424

2525
`serve.sh` and `smoke.py` encode everything below. Read on when you need to
@@ -67,8 +67,13 @@ from config.settings import * # noqa: F403
6767
IS_PROD = False
6868
```
6969

70-
Then `python manage.py collectstatic --noinput --settings=local_serve` and run
71-
gunicorn with `DJANGO_SETTINGS_MODULE=local_serve`.
70+
Then run the collectstatic command through uv:
71+
72+
```bash
73+
uv run --project .. --no-sync python manage.py collectstatic --noinput --settings=local_serve
74+
```
75+
76+
Start gunicorn through uv with `DJANGO_SETTINGS_MODULE=local_serve`.
7277

7378
The stylesheet is `static/css/main.css` and it is committed pre-built — there is
7479
no npm build step, and `package.json` has no scripts. If you find yourself
@@ -104,7 +109,7 @@ tier that determines how many gunicorn workers and Celery workers run.
104109
`entrypoint.sh` probes it once and exports the result; reproduce that with:
105110

106111
```bash
107-
eval "$(python -c 'from config.runtime_profile import emit_env; emit_env()')"
112+
eval "$(uv run --project .. --no-sync python -c 'from config.runtime_profile import emit_env; emit_env()')"
108113
```
109114

110115
That sets `WEB_CONCURRENCY`, `GUNICORN_THREADS`, `FLOPPY_CELERY_QUEUES`,
@@ -123,7 +128,7 @@ hunting for one.
123128
Start a worker when you need that path:
124129

125130
```bash
126-
FLOPPY_PROCESS_ROLE=background celery --app config worker \
131+
FLOPPY_PROCESS_ROLE=background uv run --project .. --no-sync celery --app config worker \
127132
--queues "${FLOPPY_CELERY_QUEUES:-celery}" --loglevel INFO \
128133
--without-mingle --without-gossip &
129134
```
@@ -157,7 +162,7 @@ Registration is off by default and the demo account only exists when
157162
`DEMO_ACCOUNT_ENABLED` is set. Make one directly:
158163

159164
```bash
160-
python -c "
165+
uv run --project .. --no-sync python -c "
161166
import os, django
162167
os.environ.setdefault('DJANGO_SETTINGS_MODULE','local_serve')
163168
django.setup()

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ __pycache__
33
.env
44
.coverage*
55
venv
6+
.venv/
67
src/staticfiles
78
src/db/db.sqlite3
89
db.sqlite3-shm

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
## Validation
88
- List commands run and outcomes.
99

10+
## Human Review
11+
- [ ] Pending human review.
12+
- [ ] Completed — reviewer/evidence: <!-- link or concise evidence -->
13+
14+
## Gstack QA
15+
- [ ] Pending `/gstack-qa`.
16+
- [ ] Completed — report/outcome: <!-- link or concise outcome -->
17+
1018
## Migration Sync Gate (Required for `upstream` -> `latest` sync PRs)
1119
- [ ] Conflicts resolved with upstream files preserved and fork behavior merged intentionally.
1220
- [ ] Migration conflicts handled per policy (no rewrite of shared/released migrations).
13-
- [ ] `cd src && python manage.py makemigrations --merge` run for affected apps.
14-
- [ ] `cd src && python manage.py check_migration_hygiene --strict` passed.
21+
- [ ] `uv run --no-sync python src/manage.py makemigrations --merge` run for affected apps.
22+
- [ ] `uv run --no-sync python src/manage.py check_migration_hygiene --strict` passed.
1523
- [ ] `scripts/replay_upgrade_matrix.sh --from-tag <previous_release_tag> --to-ref latest --db sqlite,postgres --with-drift-scenarios` passed.
16-
- [ ] `coverage run src/manage.py test app users integrations lists events --parallel` passed.
24+
- [ ] `uv run --no-sync coverage run src/manage.py test app users integrations lists events --parallel` passed.
1725

1826
## Notes
1927
- Link relevant issues (for example: `Refs #101`).

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ updates:
1010
patterns:
1111
- "*"
1212

13-
- package-ecosystem: "pip"
13+
- package-ecosystem: "uv"
1414
directory: "/"
1515
schedule:
1616
interval: "daily"

.pre-commit-config.yaml

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,52 @@ repos:
44
hooks:
55
- id: detect-secrets
66
args: ["--baseline", ".secrets.baseline"]
7-
- repo: https://github.com/pypa/pip-audit
8-
rev: v2.10.1
9-
hooks:
10-
- id: pip-audit
11-
args: ["-r", "requirements.txt"]
12-
files: ^requirements(-dev)?\.txt$
137
- repo: local
148
hooks:
9+
- id: pip-audit
10+
name: pip-audit
11+
entry: scripts/audit_dependencies.sh
12+
language: system
13+
pass_filenames: false
14+
files: ^(pyproject\.toml|uv\.lock|mcp_server/pyproject\.toml)$
15+
- id: uv-lock-check
16+
name: uv-lock-check
17+
entry: uv lock --check
18+
language: system
19+
pass_filenames: false
20+
files: ^(pyproject\.toml|uv\.lock|mcp_server/pyproject\.toml)$
1521
- id: check-migrations
1622
name: check-migrations
17-
entry: python src/manage.py makemigrations --check
23+
entry: uv run --no-sync python src/manage.py makemigrations --check
1824
language: system
1925
pass_filenames: false
2026
always_run: true
2127
- id: django-upgrade
2228
name: django-upgrade
23-
entry: django-upgrade
24-
language: system
25-
types: [python]
26-
- id: pyflakes
27-
name: pyflakes
28-
entry: python -m pyflakes
29+
entry: uv run --no-sync django-upgrade
2930
language: system
3031
types: [python]
31-
require_serial: true
3232
- id: ruff
3333
name: ruff
34-
entry: ruff check --fix
34+
entry: uv run --no-sync ruff check --fix
3535
language: system
3636
types: [python]
3737
require_serial: true
3838
- id: ruff-format
3939
name: ruff-format
40-
entry: ruff format
40+
entry: uv run --no-sync ruff format
4141
language: system
4242
types: [python]
4343
require_serial: true
4444
- id: djlint-lint
4545
name: djlint-lint
46-
entry: djlint --lint
46+
entry: uv run --no-sync djlint --lint
4747
language: system
4848
types: [html]
4949
require_serial: true
5050
- id: djlint-reformat
5151
name: djlint-reformat
52-
entry: djlint --reformat
52+
entry: uv run --no-sync djlint --reformat
5353
language: system
5454
types: [html]
55-
require_serial: true
55+
require_serial: true

AGENTS.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ Also validate when:
144144
- the user asked for validation
145145
- the change affects multiple routes, components, or packages
146146

147-
Prefer targeted tests first (`scripts/test.sh <dotted.label>`), then `ruff check src`, then the fast suite (`scripts/test.sh`) only when risk justifies it. See the Testing section.
147+
Prefer targeted tests first (`scripts/test.sh <dotted.label>`), then `uv run --no-sync ruff check src`, then the fast suite (`scripts/test.sh`) only when risk justifies it. See the Testing section.
148148

149149
Prefer the cheapest useful check:
150150

@@ -303,13 +303,13 @@ Models/migrations and divergent UI normally require manual adaptation. Provider
303303
- `docs/agents/migration_sync_playbook.md`: hard-gate flow for adapting accepted upstream migration outcomes to Floppy's current graph.
304304
305305
## Local Commands
306-
- Install dev dependencies: `python -m pip install -U -r requirements-dev.txt`
307-
- Run migrations: `cd src && python manage.py migrate`
308-
- Run the app: `cd src && python manage.py runserver`
306+
- Install locked dev dependencies: `uv sync --locked`
307+
- Run migrations: `uv run --no-sync python src/manage.py migrate`
308+
- Run the app: `uv run --no-sync python src/manage.py runserver`
309309
- Run Celery (two workers in one command, mirrors production):
310310
```bash
311-
PYTHONPATH=src celery -A config worker --queues interactive --hostname celery-interactive@%h --loglevel DEBUG &
312-
PYTHONPATH=src celery -A config worker --queues celery --beat --scheduler django --hostname celery@%h --loglevel DEBUG
311+
PYTHONPATH=src uv run --no-sync celery -A config worker --queues interactive --hostname celery-interactive@%h --loglevel DEBUG &
312+
PYTHONPATH=src uv run --no-sync celery -A config worker --queues celery --beat --scheduler django --hostname celery@%h --loglevel DEBUG
313313
```
314314
The interactive worker must be dedicated — **never add `celery` to its `--queues`** or long-running background tasks (Reload calendar, imports) will block user-triggered refreshes.
315315
- Run Tailwind: `cd src && tailwindcss -i ./static/css/input.css -o ./static/css/main.css --watch`
@@ -330,8 +330,8 @@ Run tests through `scripts/test.sh`, in this priority order:
330330
3. **Full suite (rarely needed locally):** `scripts/test.sh --full` — all tags, including slow benchmarks/Playwright and live-provider `network` tests. Takes 20+ minutes and produces huge output. Only run it when the user asks or the risk clearly justifies it. Application-impacting PRs run the CI application suite, which excludes `network` tests; documentation-only trigger filtering is owned by `.github/workflows/app-tests.yml`.
331331
332332
Notes:
333-
- Quick confidence: `ruff check src`
334-
- Migration sync confidence: `cd src && python manage.py check_migration_hygiene --strict`
333+
- Quick confidence: `uv run --no-sync ruff check src`
334+
- Migration sync confidence: `uv run --no-sync python src/manage.py check_migration_hygiene --strict`
335335
- Migration upgrade replay: `scripts/replay_upgrade_matrix.sh --from-tag <previous_release_tag> --to-ref latest --db sqlite,postgres --with-drift-scenarios`
336336
- Tag vocabulary: `slow` and `network` are the exclusion tags used by the fast
337337
suite. `network` marks tests that call a live provider API: they need keys and
@@ -347,7 +347,7 @@ Notes:
347347
- Python target is 3.12 (see `Dockerfile` and CI).
348348
- Ruff config lives in `pyproject.toml` and excludes `migrations/`.
349349
- Djlint config is in `pyproject.toml`; Stylelint config is in `.stylelintrc`.
350-
- After model changes, keep migration files under `src/*/migrations/` and run `cd src && python manage.py migrate`.
350+
- After model changes, keep migration files under `src/*/migrations/` and run `uv run --no-sync python src/manage.py migrate`.
351351
- Media type changes follow `docs/agents/media_type_integration.md` (MediaTypes enum + `media_type_config` wiring).
352352
353353
## PR / Commit Expectations

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ Match validation to risk. See `AGENTS.md` for the full matrix. Short version:
9595
|---|---|
9696
| Copy, labels, static content | None required |
9797
| CSS / Tailwind spacing | Visual screenshot |
98-
| Template or UI logic | Screenshot + `ruff check src` |
99-
| Python behavior | `ruff check src` + targeted test |
100-
| Model or migration change | `python manage.py check_migration_hygiene --strict` + full test suite |
98+
| Template or UI logic | Screenshot + `uv run --no-sync ruff check src` |
99+
| Python behavior | `uv run --no-sync ruff check src` + targeted test |
100+
| Model or migration change | `uv run --no-sync python src/manage.py check_migration_hygiene --strict` + full test suite |
101101
| `upstream``latest` upstream sync | Full migration sync gate (see PR template) |
102102

103103
Never skip validation for migrations, models, auth, permissions, webhooks, Celery tasks, or cache behavior.
@@ -107,6 +107,7 @@ Never skip validation for migrations, models, auth, permissions, webhooks, Celer
107107
## Style
108108

109109
- Python 3.12. Ruff configured in `pyproject.toml` (88-char line limit, migrations excluded).
110+
- Python dependencies are declared in `pyproject.toml` and locked in `uv.lock`; use uv 0.12.3 and `uv sync --locked` rather than maintaining requirements files.
110111
- Templates: djlint config in `pyproject.toml`. CSS: Stylelint config in `.stylelintrc`.
111112
- Tailwind output is committed at `src/static/css/main.css`. Run `npx @tailwindcss/cli -i ./src/static/css/input.css -o ./src/static/css/main.css` after any template or class changes — this uses the version pinned in `package.json`, so it always matches the committed output. Do not use a bare/global `tailwindcss` binary or unpinned `npx`; a version mismatch regenerates the file with a different (larger) utility set and produces spurious diffs.
112113
- Commit messages: short imperative title, optional 1–3 bullet body, then issue lines.

Dockerfile

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
FROM python:3.12-alpine3.21 AS repo_meta
1+
ARG PYTHON_VERSION=3.12
2+
ARG ALPINE_VERSION=3.21
3+
4+
FROM python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} AS repo_meta
25

36
WORKDIR /repo
47
COPY . .
@@ -39,10 +42,27 @@ if config_path.exists():
3942
Path("/repo_owner").write_text(owner)
4043
PY
4144

42-
FROM python:3.12-alpine3.21
45+
FROM python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} AS builder
46+
47+
COPY --from=ghcr.io/astral-sh/uv:0.12.3 /uv /uvx /bin/
48+
ENV UV_LINK_MODE=copy
49+
50+
WORKDIR /floppy
51+
52+
COPY ./pyproject.toml ./uv.lock ./
53+
COPY ./mcp_server/pyproject.toml ./mcp_server/pyproject.toml
54+
RUN --mount=type=cache,target=/root/.cache/uv \
55+
uv sync --locked --no-default-groups --no-install-workspace
56+
57+
COPY ./mcp_server ./mcp_server
58+
RUN --mount=type=cache,target=/root/.cache/uv \
59+
uv sync --locked --no-default-groups --no-editable
60+
61+
FROM python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}
4362

4463
# https://stackoverflow.com/questions/58701233/docker-logs-erroneously-appears-empty-until-container-stops
4564
ENV PYTHONUNBUFFERED=1
65+
ENV PATH="/floppy/.venv/bin:$PATH"
4666

4767
# Define build argument with default value
4868
ARG VERSION=dev
@@ -63,7 +83,6 @@ ENV FLOPPY_CELERY_QUEUES=celery
6383
ENV FLOPPY_START_INTERACTIVE_WORKER=true
6484
ENV FLOPPY_START_DISCOVER_WORKER=true
6585

66-
COPY ./requirements.txt /requirements.txt
6786
COPY ./entrypoint.sh /entrypoint.sh
6887
COPY ./supervisord.conf /etc/supervisord.conf
6988
COPY ./nginx.conf /etc/nginx/nginx.conf
@@ -78,10 +97,6 @@ WORKDIR /floppy
7897
RUN ln -s /floppy /yamtrack
7998

8099
RUN apk add --no-cache nginx shadow \
81-
&& pip install --no-cache-dir -r /requirements.txt \
82-
&& pip install --no-cache-dir supervisor==4.3.0 \
83-
&& rm -rf /root/.cache /tmp/* \
84-
&& find /usr/local -type d -name __pycache__ -exec rm -rf {} + \
85100
&& chmod +x /entrypoint.sh \
86101
# create user abc for later PUID/PGID mapping
87102
&& useradd -U -M -s /bin/sh abc \
@@ -90,6 +105,7 @@ RUN apk add --no-cache nginx shadow \
90105
&& mkdir -p /var/lib/nginx/body
91106

92107
COPY --from=repo_meta /repo_owner /etc/floppy/fork_owner
108+
COPY --from=builder /floppy/.venv /floppy/.venv
93109

94110
# Django app
95111
COPY src ./
@@ -99,8 +115,6 @@ RUN SECRET=build-time-placeholder python manage.py collectstatic --noinput
99115
# version shipped with this image, instead of a user's local checkout that
100116
# can silently drift from the app version.
101117
COPY mcp_server ./mcp_server
102-
RUN pip install --no-cache-dir ./mcp_server \
103-
&& rm -rf /root/.cache
104118

105119
EXPOSE 8000
106120

README.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ host-derived worker settings and database/Redis fork-safety hooks are active:
389389

390390
```bash
391391
cd /path/to/floppy/src
392-
gunicorn --config python:config.gunicorn config.wsgi:application
392+
uv run --no-sync gunicorn --config python:config.gunicorn config.wsgi:application
393393
```
394394

395395
Do not start the service with bare `gunicorn config.wsgi:application`; that
@@ -474,31 +474,37 @@ For contributing or customizing locally:
474474
git clone https://github.com/dannyvfilms/Floppy.git
475475
cd Floppy
476476
docker run -d --name redis -p 6379:6379 --restart unless-stopped redis:8-alpine
477-
python -m pip install -U -r requirements-dev.txt
477+
uv sync --locked
478478
```
479479

480480
Create a `.env` with at least `SECRET`, `DEBUG=True`, and whichever API keys you need (same names as the Docker list above), then:
481481

482482
```bash
483-
cd src
484-
python manage.py migrate
485-
python manage.py createsuperuser
486-
python manage.py runserver
483+
uv run --no-sync python src/manage.py migrate
484+
uv run --no-sync python src/manage.py createsuperuser
485+
uv run --no-sync python src/manage.py runserver
487486
```
488487

489488
Celery and Tailwind run in separate terminals:
490489

491490
```bash
492-
celery -A config worker --queues interactive --hostname celery-interactive@%h --loglevel DEBUG
493-
celery -A config worker --queues celery --beat --scheduler django --hostname celery@%h --loglevel DEBUG
491+
PYTHONPATH=src uv run --no-sync celery -A config worker --queues interactive --hostname celery-interactive@%h --loglevel DEBUG
492+
PYTHONPATH=src uv run --no-sync celery -A config worker --queues celery --beat --scheduler django --hostname celery@%h --loglevel DEBUG
494493
```
495494

496495
```bash
497-
npx @tailwindcss/cli -i ./static/css/input.css -o ./static/css/main.css --watch
496+
npx @tailwindcss/cli -i ./src/static/css/input.css -o ./src/static/css/main.css --watch
498497
```
499498

500499
Visit `http://localhost:8000`. A `demo` / `demodemo` account is provisioned after migrations; set `DEMO_ACCOUNT_ENABLED=False` to disable it. See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
501500

501+
`pyproject.toml` and `uv.lock` are the dependency source of truth for the app
502+
and bundled MCP workspace. Use uv 0.12.3 and keep the lockfile in sync; the
503+
removed requirements files are not maintained in parallel. The MCP package's
504+
`setuptools>=68` isolated build-backend range is the only build-time resolver
505+
exception to the runtime lock and is intentionally not pinned as an application
506+
dependency.
507+
502508
## Support the project
503509

504510
- Star the repository if you want to help more people find Floppy.

0 commit comments

Comments
 (0)