chore(deps): refresh the lockfile, drop python-jose, move to Python 3.13 - #67
Merged
Conversation
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 session created for this PR. Skill: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The code was already 2026-idiomatic — the audit's idiom sweep came back empty (no
class Config, no.dict(), no@app.on_event, no bareDepends(), no# type: ignore). The lockfile was the problem.12 vulnerable packages, now zero
pip-auditagainst the exported lockfile: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 —
ecdsaPYSEC-2026-1325 has no fixed version. Dropping it removesecdsa,pyasn1,rsaandsixfrom the tree entirely.Four call sites;
encode/decodeare drop-in,JWTErrorbecomesPyJWTError.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
aiodockerdeclares support no further.The mypy overrides were hiding four things
Five per-module
disable_error_codeblocks 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_signaturedeclaredsignature: strwhile its caller passesstr | 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.Callable[[Request, Exception], ...]. They now takeExceptionand narrow, with a thin adapter for slowapi's handler since that one is third-party.AsyncSession.executeis typed as returningResult, but anUPDATEreturns aCursorResult, which is whererowcountlives. Acast— an assertion about a known runtime type, not a suppression.mypy src/is clean with no overrides at all.Build and CI hygiene
uvpinned to0.9.16instead of:latest, anduv sync --frozenin 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.concurrencygroup.push: ['**']andpull_request: [main]both matched a push to a PR branch, so every job ran twice and superseded runs were never cancelled.enable-cache: trueonsetup-uv; the frontend jobs already cached npm.alembic upgrade headruns in CI. Tests build the schema withcreate_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.validate_production_secretsalready enforces the same floor in production, so the short fixtures were both noisy and unrepresentative.Verification
ruff,ruff formatandmypycleanpip-auditon the new lockfile: no known vulnerabilitiesalembic upgrade headapplied against a pristine Postgres, full chain, exit 0Not done here
nginx:alpineand@anthropic-ai/claude-codeare 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.