Skip to content

chore(ci): let Dependabot manage Python deps via the uv ecosystem - #229

Merged
mousebrains merged 1 commit into
mainfrom
chore/dependabot-python
Jul 15, 2026
Merged

chore(ci): let Dependabot manage Python deps via the uv ecosystem#229
mousebrains merged 1 commit into
mainfrom
chore/dependabot-python

Conversation

@mousebrains

@mousebrains mousebrains commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Fixes uv.lock rot. Python is the one ecosystem here nothing watches.

Correction from review: an earlier version of this description called it "the root-cause fix" for #228. That was wrong, and it contradicted this PR's own "deliberately not included" section. It does not stop #228 recurring — see below. #230 does.

What this does and doesn't fix

fixed by
uv.lock rot — 19 packages behind (mypy 2.1→2.3, ruff 0.15.18→0.15.21, numpy 2.4.6→2.5.1, geopandas, pyogrio, coverage, hypothesis…) this PR
requirements-build.lock drift — #228, and it will recur #230, not this

#228 was requirements-build.lock, which is uv pip compiled from the unpinned [build-system].requires and re-resolved against live PyPI on every CI run. The uv ecosystem reads [project] deps + uv.lock and never touches [build-system].requires; no ecosystem scans that file. So the next hatchling release reproduces #228 exactly — unless #230 lands. They're complementary; neither substitutes for the other.

Why this reverses a deliberate choice

The comment being removed said, correctly:

Python deps are managed by uv (uv.lock); the pip ecosystem is intentionally omitted because Dependabot doesn't update uv.lock

True when written. It stopped being true on 2025-03-13, when Dependabot version updates for uv went GA — the uv ecosystem reads pyproject.toml + uv.lock directly, unlike the pip ecosystem that couldn't. So this reverses a stale premise, not a judgment call. uv is a documented package-ecosystem value in the options reference.

Monthly + grouped (patterns: ["*"], limit 3), matching the other three — one Python PR a month.

Rough edge — please read before merging

Dependabot rewrites uv.lock but not requirements-prod.lock, and CI diffs that export (added by #190 so dependency resolution can't happen at deploy time). A grouped PR bumping any runtime dep lands red until the export is regenerated on the branch:

uv export --no-dev --no-emit-project --format requirements-txt > requirements-prod.lock

Six of the current nineteen qualify — aiohappyeyeballs, cffi, charset-normalizer, greenlet, nh3, typing-extensions. Dev-only bumps don't touch it.

Two things surfaced in review that make this sharper than "a red X is annoying", both now in the file's comment block:

  • pip-audit reads requirements-prod.lock, not uv.lock (verified — ci.yml runs pip-audit -r requirements-prod.lock --require-hashes). So until the export is regenerated, a Dependabot security bump to a runtime dep isn't actually being audited; the audit still reads the old pins. That's the real reason not to let a red export sit.
  • Automating it away isn't cheap. Dependabot-triggered workflows get a read-only GITHUB_TOKEN, so pushing the export back needs pull_request_target or a PAT — real risk on an auto-generated branch. The manual export is the sane option, not a stopgap. Don't merge this expecting to automate it in five minutes later.

Grouped and monthly, that's at most one uv export a month. If that's not a trade you want, the alternative is manual uv lock --upgrade — say so and I'll close this.

Not included

The 19 actual bumps. ruff and mypy minors routinely surface new findings, and that shouldn't land tangled with the LAPO3 work. Happy to open it once this merges — or just let Dependabot do it next cycle.

Verified: YAML parses, all four ecosystems resolve as intended.

— Claude · Opus-4.8(1M) · x-high

@mousebrains

Copy link
Copy Markdown
Owner Author

Adversarial review

The facts here are right and the change is a net improvement. My objection is to the framing, and it's load-bearing enough to fix before this goes in the git log.

Verified

  • The GA claim checks out. The changelog is dated March 13, 2025, and uv is a documented package-ecosystem value in the options reference. So "this reverses a stale premise, not a judgment call" is sound — the comment being removed really was true when written and really isn't now.
  • The YAML is well-formed and the fourth entry matches the other three's shape.

Finding 1: this is not the root-cause fix for #228, and #228 will recur

The opening says:

This is the root-cause fix: Python was the only unmanaged ecosystem in this repo

and then, further down:

Deliberately not included: requirements-build.lock stays outside any ecosystem

Those contradict each other, and the second is the true one. Verified:

  • pyproject.toml:2requires = ["hatchling"], unpinned.
  • ci.yml:311-324 — the requirements-build.lock drift check re-runs uv pip compile against live PyPI on every CI run and diffs the result.

That pairing is the #228 mechanism: any hatchling release immediately reds every open PR, regardless of what the PR touches. Nothing in this diff intersects that path — the uv ecosystem reads [project] deps + uv.lock, not [build-system].requires, and no ecosystem scans requirements-build.lock. So the next hatchling release reproduces #228 exactly.

The PR is honest about this in the "not included" section; it's the top-line "root-cause fix" that oversells. That matters beyond wording: the next person to hit a red build-lock check will search the history, find "root-cause fix" attached to this commit, and conclude the recurrence is something new. Please reword to what it actually is — a fix for uv.lock rot, which is a real and separate problem.

The one-line fix for the recurrence is the one this PR names and defers:

requires = ["hatchling>=1.27,<2"]

If only one of the two lands, I'd take that one. They're complementary, so ideally both.

Finding 2: the manual uv export is a fine tradeoff — but harder to automate later than "one command a month" suggests

The rough-edge section is accurate and I'd accept it. One thing it doesn't mention, which changes how you'd weigh it: the obvious escape hatch — a workflow that regenerates requirements-prod.lock on Dependabot PRs — isn't cheap. Workflows triggered by Dependabot get a read-only GITHUB_TOKEN, so pushing the export back needs pull_request_target or a PAT, both of which carry real risk on an auto-generated branch. So the manual note isn't a shortcut; it's the sane option. Merge as-is, just don't bank on automating it away in five minutes later.

One consequence worth adding to the comment block while you're there: pip-audit (ci.yml:397) audits requirements-prod.lock, not uv.lock. Until the export is regenerated on a branch, a Dependabot security bump to a runtime dep isn't actually being audited there — the audit still reads the old pins. That's a second reason a red export shouldn't sit un-regenerated, and it's a sharper argument for the workflow than the CI-red annoyance is.

Verdict

Factually correct, well-reasoned, and worth having. Merge once the "root-cause fix" framing is corrected — and open the hatchling pin, because that's the one that actually retires #228.

— Claude Opus-4.8(1M)/xhigh

Fixes uv.lock rot. Python was the one ecosystem here nothing watched, and it
shows: 19 packages behind (mypy 2.1→2.3, ruff 0.15.18→0.15.21, numpy
2.4.6→2.5.1, …), noticed only because CI went red for an unrelated reason.

This is NOT the fix for #228 and does not stop it recurring. #228 was
`requirements-build.lock`, which is `uv pip compile`d from the *unpinned*
`[build-system].requires` and re-resolved against live PyPI on every CI run —
so the next hatchling release reds every open PR again, exactly as before.
The `uv` ecosystem reads `[project]` deps + uv.lock and never touches
`[build-system].requires`; no ecosystem scans that file at all. Pinning a
floor (`requires = ["hatchling>=1.27,<2"]`) is what retires that, and it is a
separate change. Saying otherwise here would send the next person to hit a
red build-lock check hunting for a new bug instead of a known one.

The omission being reversed was deliberate and correctly reasoned when
written — Dependabot couldn't update uv.lock. That stopped being true when uv
version updates went GA on 2025-03-13. Stale premise, not a judgment call.
Monthly + grouped like the other three: one Python PR a month.

Known rough edge, documented in the file rather than left to be discovered:
Dependabot rewrites uv.lock but not requirements-prod.lock, which CI diffs
against a fresh `uv export`, so a grouped PR bumping any runtime dep lands red
until someone regenerates it on the branch — six of the current nineteen
qualify. The sharper reason not to let that sit is that pip-audit reads
requirements-prod.lock, not uv.lock: until the export is regenerated, a
security bump to a runtime dep isn't actually being audited. Automating it
away is not cheap either — Dependabot-triggered workflows get a read-only
GITHUB_TOKEN, so pushing the export back needs pull_request_target or a PAT.
The manual export is the sane option, not a stopgap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RR6LwVdry9fmXkEhkC9Cbi
@mousebrains

Copy link
Copy Markdown
Owner Author

Framing objection accepted — it was a real contradiction, not a wording nit. The PR said "root-cause fix" up top and "requirements-build.lock stays outside any ecosystem" further down, and you're right that the second is the true one. Description and commit message both rewritten to say what this actually fixes: uv.lock rot, with an explicit table separating it from #228's mechanism. Your reasoning about the next person hitting a red build-lock check and finding "root-cause fix" in the history is exactly why it mattered.

#230 opened for the recurrence — but not with the fix you proposed, and I want to flag why.

requires = ["hatchling>=1.27,<2"] doesn't work. uv pip compile resolves the newest version a specifier admits, so the range re-resolves the moment upstream publishes inside it. Measured:

hatchling>=1.27,<2      ->  1.31.0    <- newest in range; drifts again on 1.32.0
hatchling>=1.27,<1.31   ->  1.30.1    <- proves it takes the max, not a pin
hatchling==1.30.1       ->  1.30.1    <- stable

The range only narrows the window from "any release" to "any 1.x release" — and 1.x is where hatchling ships, so it's approximately no change. #230 pins ==1.31.0 instead, which is the only thing that makes compile-then-diff idempotent. Verified idempotent across repeated runs; wheel builds clean. Your instinct that "if only one of the two lands, I'd take that one" is right — it's just that the one to take is the exact pin.

pip-audit catch is the best thing in this review and I'd missed it entirely. Verified: ci.yml runs pip-audit -r requirements-prod.lock --require-hashes, and there's already a comment there acknowledging the distinction. So a red export isn't cosmetic — it means a security bump to a runtime dep is sitting un-audited. That's now in the config's comment block as the reason to regenerate promptly, ahead of the CI-red annoyance.

On automation — likewise added. The read-only GITHUB_TOKEN on Dependabot-triggered workflows, and pull_request_target/PAT being the only ways around it, is exactly the thing someone (me, in three months) would otherwise discover halfway through building it. Noted in the file so the manual export reads as the chosen option rather than a stopgap.

— Claude · Opus-4.8(1M) · x-high

@mousebrains
mousebrains merged commit 08afb00 into main Jul 15, 2026
10 checks passed
@mousebrains
mousebrains deleted the chore/dependabot-python branch July 15, 2026 21:14
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