Skip to content

Migrate to uv, update dependencies, harden login flow - #83

Merged
andrewyager merged 18 commits into
developfrom
chore/uv-migration-and-login-review
Sep 17, 2026
Merged

andrewyager merged 18 commits into
developfrom
chore/uv-migration-and-login-review

Conversation

@andrewyager

Copy link
Copy Markdown
Member

Summary

Maintenance pass after a few months without changes: move dependency management from pip-tools to uv, upgrade all Python and frontend build dependencies, and fix the login flow defects behind the reported "intermittent password failures".

Spec Alignment

  • Extends the spec (new feature or behaviour not currently specified)
  • Fixes a bug (deviation from expected spec behaviour)

Section 4.13 and 6.1.6 mandate pip-tools. The matching spec change is on the spec repo branch spec/uv-dependency-management and must be approved with this PR.

Changes

  • Login flow. Rate limiting keyed on the proxy address, so all users shared one 5-per-minute bucket; it now keys on the proxy-appended client hop. The rate-limited page rendered no message. Username lookup was case-sensitive while mobile keyboards capitalise. The next parameter allowed an open redirect.
  • uv migration. pyproject.toml and uv.lock replace requirements.in/.txt. Image is python:3.13-slim, installs with uv sync --frozen into /usr/local; dev tooling only with INSTALL_DEV=true. CI runs uv lock --check and pip-audit. Dependabot config for uv, Actions and Docker.
  • Dependency upgrades. Everything to latest within Django 5.2 LTS (Django 5.2.17, anthropic 1.6, django-unfold 0.106, gunicorn 26, weasyprint 70, Pillow 12.3). pip-audit: no known vulnerabilities. libgobject-2.0-0 no longer exists on trixie; replaced with libglib2.0-0t64.
  • Frontend toolchain. Node 24 LTS, Tailwind CLI 4.3.3, mjml pinned to 5.4.1.
  • Cache default. CACHE_URL was undocumented and defaulted to localhost, which 500s every page in a container. It now follows the Celery broker host.
  • Test isolation. Test media goes to a temp directory; pytest no longer recurses into generated directories (suite time 167s to 97s locally, and no longer stalls in Docker).

Test Plan

  • New tests written for this change
  • Tests fail before implementation, pass after (TDD red-green verified)
  • All tests pass locally (pytest) — 2775 passed, 6 skipped, 83 xfailed
  • All tests pass in Docker (docker compose exec web pytest) — 2775 passed
  • Code formatted (black src/ && isort src/ && flake8 src/)

Notes

  • Django 6.x is available but is a major upgrade; kept on 5.2 LTS (supported to April 2028). Suggest a separate issue.
  • The 11 open Dependabot pip PRs are superseded by the new lockfile and can be closed.
  • Production .env should gain CACHE_URL=redis://redis:6379/1 for clarity, although the new fallback makes it optional.

🤖 Generated with Claude Code

andrewyager and others added 18 commits June 9, 2026 22:53
Add an operator guide covering both PROPS data stores: the PostgreSQL
database (pg_dump custom-format dumps or managed-Postgres snapshots, with
cron automation and pg_restore recovery) and the object/media store
(rclone offsite mirror, S3 versioning, or garage_data volume snapshot).

The generic guidance is dependency-free for open-source/self-hosted
deployments; ARK (rwts-backup) is documented separately as the operational,
no-repo-dependency option for RWTS-managed deployments. Covers cadence,
retention, restore drills, and the DB<->media consistency trade-off.

Link the guide from the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Users reported intermittent password failures with no clear cause.
Four defects in the login flow explain them:

- Rate limiting keyed on REMOTE_ADDR. Behind Traefik that is the proxy
  container, so all users shared one 5-per-minute bucket. Add a
  RATELIMIT_IP_META_KEY callable that reads the first X-Forwarded-For
  hop only when the proxy is trusted (SECURE_PROXY_SSL_HEADER is set).
- The rate-limited response queued a message the login template never
  rendered, so the user saw an empty form and assumed a bad password.
  Render messages on the login page.
- Username lookup was case-sensitive while usernames are generated in
  lowercase. Mobile keyboards capitalise the first letter of a text
  field. Fall back to a unique case-insensitive match and set
  autocapitalize="none" on the input.
- The next parameter was followed without validation (open redirect).
  Accept only same-host URLs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyproject.toml now declares runtime dependencies and a dev group;
uv.lock replaces requirements.txt. The image installs with
uv sync --frozen into /usr/local on python:3.13-slim (no venv), and
only includes dev tooling when INSTALL_DEV=true (dev compose profile
and CI). CI checks the lockfile with uv lock --check and runs
pip-audit. Dependabot now tracks the uv, GitHub Actions and Docker
ecosystems. libgobject-2.0-0 no longer exists on Debian trixie; use
libglib2.0-0t64.

All dependencies were upgraded to their latest compatible releases
within Django 5.2 LTS. pip-audit reports no known vulnerabilities.

Spec §4.13 and §6.1.6 are updated on the spec branch
spec/uv-dependency-management.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Node 22 to 24 LTS, Tailwind standalone CLI 4.1.18 to 4.3.3, and pin
mjml to 5.4.1 so the email build is reproducible.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CACHE_URL was never documented in .env.example, and the fallback
pointed at localhost. Inside a container that is unreachable, and
because every page reads the site branding from the cache, each
request failed with a 500. Fall back to the broker's Redis host on
database 1 and document CACHE_URL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Tests wrote media into src/media (2 GB locally). pytest walked that
tree at collection, and through a Docker bind mount the suite
appeared to hang. Root test media in a temporary directory and stop
pytest recursing into generated directories.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The setup-uv action never started its jobs on this repository; the
run failed within seconds with no annotation. pipx is preinstalled on
GitHub-hosted runners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PROPS_IMAGE selects the image repository for the prod profile. The
default stays ghcr.io for self-hosters; RWTS-run deployments set it
to the internal GHES registry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GitHub Actions do not run for the organisation on github.com, so the
workflows target github.realworld.net.au: self-hosted runners labelled
ubuntu-latest (no ubuntu-latest-large), images published to
containers.github.realworld.net.au, and uv pinned to 0.12.15 on the
runner. github.com stays the public mirror.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The same workflows run on github.com (publishing to ghcr.io) and on
the internal GHES (publishing to its registry), so either host can be
the active CI home. The runner label comes from the RUNNER_LABEL
repository variable, default ubuntu-latest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pipx is not present on the GHES runner image.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GHES cannot resolve recent setup-uv tags through GitHub Connect, and
the runner image has no pipx. The installer is pinned to 0.12.15.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GHES does not support upload-artifact v4, so the image no longer
travels between jobs. The release workflow builds, tests and publishes
in a single job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pin the installer by SHA-256 and install into RUNNER_TEMP so nothing
persists on a shared runner PATH.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dockerfile and .env.example are excluded by .dockerignore, matching
the existing docker-compose.yml skip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MJML 5 (released 2026-04-16) drops the header include, and with it the
brand colour and logo template variables, from the compiled emails.
The unpinned install has produced unbranded emails in every image
built since then and broke the email template tests in CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The image carries only GIT_COMMIT. The release version reaches the app
through the APP_VERSION environment variable, which compose fills from
PROPS_VERSION, so one image per commit can be promoted from a pull
request to develop and then to a release without a rebuild. Sentry
keeps using APP_VERSION as the release.

The `test` target layers the dev dependency group on the `base` stage
for CI and the dev compose profile; the default `runtime` target is
what gets published and contains no dev tooling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A develop-to-main pull request has the develop merge commit as its
head, which promote-develop already tagged. That image was tested when
its pull request merged, so CI only records the existing image.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@andrewyager
andrewyager merged commit ce1dd85 into develop Sep 17, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant