Skip to content

chore(deps): refresh the lockfile, drop python-jose, move to Python 3.13 - #67

Merged
mariuspruvot merged 1 commit into
mainfrom
chore/dependency-refresh
Aug 1, 2026
Merged

chore(deps): refresh the lockfile, drop python-jose, move to Python 3.13#67
mariuspruvot merged 1 commit into
mainfrom
chore/dependency-refresh

Conversation

@mariuspruvot

Copy link
Copy Markdown
Owner

The code was already 2026-idiomatic — the audit's idiom sweep came back empty (no class Config, no .dict(), no @app.on_event, no bare Depends(), no # type: ignore). The lockfile was the problem.

12 vulnerable packages, now zero

pip-audit against the exported lockfile:

starlette 1.0.0          6 advisories   -> 1.3.1
aiohttp 3.13.5           5             -> 3.14.1
python-multipart 0.0.24  5             -> 0.0.31
pyasn1 0.6.3             4             -> 0.6.4    (via python-jose)
urllib3 2.6.3            3             -> 2.7.0
idna 3.11                2             -> 3.15
cryptography 46.0.7      GHSA-537c-gmf6-5ccf -> 48.0.1
ecdsa 0.19.2             PYSEC-2026-1325, NO FIX  (via python-jose)
click / mako / pydantic-settings / sqladmin   1 each

The pyproject floors already permitted every fix, so this is uv lock --upgrade. After it: No known vulnerabilities found.

python-jose out, PyJWT in

Unmaintained since 2021, and FastAPI's own security tutorial moved to PyJWT. It was also the source of the one advisory no upgrade could close — ecdsa PYSEC-2026-1325 has no fixed version. Dropping it removes ecdsa, pyasn1, rsa and six from the tree entirely.

Four call sites; encode/decode are drop-in, JWTError becomes PyJWTError. algorithms=["HS256"] was already pinned, so there was no algorithm-confusion exposure to begin with.

Python 3.13

3.12 has been security-only since April 2025. The move is a drop-in — the full locked set installs with no source builds and the suite passes unchanged. Stopping at 3.13 rather than 3.14 because aiodocker declares support no further.

The mypy overrides were hiding four things

Five per-module disable_error_code blocks covered four actual errors between them, and one block (installation.service) was entirely dead. All five are removed and the four fixed rather than suppressed:

  • verify_github_webhook_signature declared signature: str while its caller passes str | None. Runtime was always safe — the function's first line rejects a falsy signature — so this was an annotation stating something untrue, which is exactly what a suppression should not hide.
  • Two exception handlers declared their narrow exception type; Starlette types every handler as Callable[[Request, Exception], ...]. They now take Exception and narrow, with a thin adapter for slowapi's handler since that one is third-party.
  • AsyncSession.execute is typed as returning Result, but an UPDATE returns a CursorResult, which is where rowcount lives. A cast — an assertion about a known runtime type, not a suppression.

mypy src/ is clean with no overrides at all.

Build and CI hygiene

  • uv pinned to 0.9.16 instead of :latest, and uv sync --frozen in both stages. CI validates with --frozen; the images did not, so an image could resolve a different dependency set than the one CI tested.
  • permissions: contents: read — no job here writes to the repo.
  • A concurrency group. push: ['**'] and pull_request: [main] both matched a push to a PR branch, so every job ran twice and superseded runs were never cancelled.
  • enable-cache: true on setup-uv; the frontend jobs already cached npm.
  • alembic upgrade head runs in CI. Tests build the schema with create_all, so nothing exercised the revision chain — a broken revision could only surface as a crash loop on deploy, since production runs it in the container CMD.
  • Test and CI secrets lengthened past the RFC 7518 HMAC minimum. PyJWT warns below 32 bytes, and validate_production_secrets already enforces the same floor in production, so the short fixtures were both noisy and unrepresentative.

Verification

  • 389 tests pass on Python 3.13.11 with the upgraded set; ruff, ruff format and mypy clean
  • pip-audit on the new lockfile: no known vulnerabilities
  • alembic upgrade head applied against a pristine Postgres, full chain, exit 0

Not done here

nginx:alpine and @anthropic-ai/claude-code are still unpinned. The CLI one matters most — the whole SSE pipeline parses its stream-json output, so an upstream format change would break production at image-rebuild time with no code change and no CI signal. Pinning it needs a deliberate choice of version and a bump process, which belongs in its own change rather than being buried here.

pip-audit reported 12 vulnerable packages in the lockfile, which was about
six months stale. The pyproject floors already allowed every fix, so this
is `uv lock --upgrade` plus the one dependency that needed replacing.
pip-audit now reports no known vulnerabilities.

python-jose has been unmaintained since 2021 and FastAPI's own security
tutorial moved to PyJWT. It also dragged in ecdsa, whose PYSEC-2026-1325
has no fix available at all -- the only advisory the upgrade could not
close. Swapping it out removes ecdsa, pyasn1, rsa and six along with it.
encode/decode are drop-in; JWTError becomes PyJWTError.

Python 3.12 has been security-only since April 2025. 3.13 is a drop-in:
the whole locked dependency set installs with no source builds and the
suite passes unchanged. Stopping at 3.13 because aiodocker declares
support no further.

The five per-module mypy overrides were hiding four errors between them,
so they are gone and the four are fixed rather than suppressed:
- verify_github_webhook_signature declared `signature: str` while the
  caller passes `str | None`; the function's own first line handles None,
  so the annotation was simply wrong
- two exception handlers declared their narrow exception type, while
  Starlette types every handler as taking Exception. They take Exception
  and narrow, with a thin adapter for slowapi's handler
- AsyncSession.execute is typed as returning Result, but an UPDATE returns
  CursorResult, which is where rowcount lives -- a cast, not a suppression

Build and CI hygiene, from the same audit:
- uv pinned instead of :latest, and `uv sync --frozen` so an image cannot
  resolve a different dependency set than the one CI tested
- CI declares `permissions: contents: read` and a concurrency group; a
  push to a PR branch matched both triggers and ran every job twice
- uv cache enabled; the frontend jobs already cached npm
- `alembic upgrade head` runs in CI. Tests build the schema with
  create_all, so a broken revision chain could only surface as a crash
  loop on deploy
- test and CI secrets lengthened past the RFC 7518 HMAC floor, which PyJWT
  warns about and production already enforces
@helprs-prod

helprs-prod Bot commented Aug 1, 2026

Copy link
Copy Markdown

helPRs session created for this PR.

Skill: challenge-me | Open session

@mariuspruvot
mariuspruvot merged commit ff52364 into main Aug 1, 2026
12 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