Repository navigation
🔧 Stop dependabot rewriting manifest ranges it does not need to: versioning-strategy increase-if-necessary for the uv ecosystem - #1942
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1942 +/- ##
=======================================
Coverage 91.69% 91.70%
=======================================
Files 129 129
Lines 18155 18152 -3
=======================================
- Hits 16648 16647 -1
+ Misses 1507 1505 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
chrisjsewell
force-pushed
the
ci/dependabot-lockfile-only
branch
from
September 15, 2026 21:48
8486667 to
c444029
Compare
…ioning-strategy increase-if-necessary for the uv ecosystem Dependabot's default strategy for a project with a lockfile is `increase`: it rewrites the manifest requirement to match the new release even when the requirement already allows it. Every specifier in this workspace's manifests is a decision -- floors are series, caps carry their reason as a comment, intra-workspace floors are moved by `propagate_floors.py` at release time -- so that is the wrong strategy here, and the 2026-09 batch (#1940) showed it: it raised sphinx-needs' `matplotlib>=3.3.0`, the floor of a user-facing extra that already allowed 3.11.2, to `>=3.11.2`. An earlier batch rewrote `~=0.20.0` as `>=0.20,<0.24`. `increase-if-necessary` leaves a requirement alone when it already allows the new release, so only the lock moves, and widens it only when a release falls outside the range. That widening pull request is kept on purpose: 44 of the 97 external specifiers here carry a cap or a series, and the pull request is the one signal that a new major of one of them exists; its review is where a stale reason comment or a widened series is re-expressed by hand. `lockfile-only` was considered and rejected for that reason -- it never edits a manifest, but it also never proposes such a release. The `ignore` rules are unaffected. The option is supported for the uv ecosystem (dependabot options reference, "Supported by: bundler, cargo, composer, helm, mix, npm, pip, pub, and uv").
chrisjsewell
force-pushed
the
ci/dependabot-lockfile-only
branch
from
September 15, 2026 22:06
c444029 to
0eb1c2e
Compare
chrisjsewell
added a commit
that referenced
this pull request
Sep 16, 2026
…ten down (#1944) Reverts #1942's `versioning-strategy: increase-if-necessary` and records, in two places, what was measured and what a reviewer does instead. ### What went wrong with #1942 After the merge, every dependabot job for this repository errored with "Dependabot can't resolve your Python dependency files": ``` No solution found when resolving dependencies for split (… included: sphinx-needs-workspace[group:typing] …): Because sphinx>=7.4.0,<=7.4.7 depends on docutils>=0.20,<0.22 and sphinx-needs-workspace:typing depends on docutils==0.23, … ``` The mechanism, from dependabot-core's python update checker (which uv reuses): every strategy except `lockfile-only` *unlocks* a requirement before asking uv for the newest resolvable version. For the `typing` group's `docutils~=0.20.0` that asks uv to resolve docutils 0.23 beside `sphinx~=7.4`, which forbids anything above 0.21. Under the default strategy that probe degrades to "no update for docutils" (which is why the 2026-09 batch, #1940, came through with three updates and no docutils); under `increase-if-necessary` it is fatal to the whole job. Both refreshes of #1940 after the merge failed that way, and the run triggered by the config change proposed nothing. Net effect: no dependabot updates of any kind. `lockfile-only` was the other candidate. It never unlocks, so it cannot hit this, and it never edits a manifest, but it also never proposes a release outside a range. 44 of the 97 external specifiers in these manifests carry a cap or a series, and the widening pull request is the one signal that a new major of one of them exists. Not worth the silence. ### What this does instead - `.github/dependabot.yml`: the option is removed, and the comment beside the uv block records both measurements and why the default stays. - `AGENTS.md`: one paragraph under *Pull request requirements* telling a reviewer of a dependabot uv pull request to read the `pyproject.toml` hunks, not only the lock; what the default strategy does to floors, caps and `~=` series, with #1940's three cases as the examples; and the procedure: keep the lock update, restore the specifier lines by hand on the dependabot branch, relock, and give a cap or series that really should move its own pull request (#1943 is the shape). ### After the merge Dependabot's run triggered by this config change should complete green on the Insights page, which is the proof the jobs are back. #1940 then gets its specifier lines restored by hand and is merged as the lock update it should have been; #1943 drops the click cap on its own reasoning. ### Checked `uv run poe lint` 8/8 (yamlfmt, taplo, check-readme included). No lock change.
chrisjsewell
added a commit
that referenced
this pull request
Sep 16, 2026
… fails every uv job (#1946) Adds `docutils` to the uv ecosystem's ignore list for major and minor updates, beside `types-docutils`, and corrects a wrong claim #1944 made. ### The error, and what it is not Every rebase or recreate of #1940 errored with "Dependabot can't resolve your Python dependency files", and so did the rebase job that ran after #1944 reverted `versioning-strategy`, under the default: ``` Because sphinx>=7.4.0,<=7.4.7 depends on docutils>=0.20,<0.22 and sphinx-needs-workspace:typing depends on docutils==0.23, … ``` #1944 attributed the error to `increase-if-necessary`. That was wrong: the same job fails under the default strategy. The comment in `dependabot.yml` and the paragraph in `AGENTS.md` now say so where the claim was made. ### What it is docutils 0.23 has been out since May. What changed is #1871: sphinx-codelinks arrived with a bare `docutils` dependency, so docutils became a *direct, unpinned* dependency in dependabot's eyes, and dependabot now tries to lock 0.23. It does that by writing `docutils==0.23` on every requirement line it finds, including the `typing` group's `docutils~=0.20.0`, and uv correctly refuses that beside `sphinx~=7.4`, which needs docutils below 0.22. The lock already carries docutils at 0.20.1 in the typing split and 0.21.2 everywhere else, which is exactly what `[tool.uv] conflicts` provides; dependabot has no way to express "0.20.x here, newest elsewhere", and the uv updater treats uv's refusal as fatal to the whole job. Group refreshes before #1871 (the 5 September batch, #1851) rebased fine. ### The fix docutils joins `types-docutils` under the same ignore: major and minor updates ignored, patches inside the series still flow. The earlier rationale for leaving docutils unlisted, keeping new series flowing to the main environment, never operated: dependabot only moves a lock entry for a direct dependency, and docutils was not one until #1871, so the default environment sat on 0.21.2 with 0.23 out. It moves by hand now, with `uv lock --upgrade-package docutils`, the way the typing series itself does. ### Consequences for the strategy question `increase-if-necessary` (#1942) was reverted for a reason that turns out not to hold, so it is untested rather than unusable. The comment records it as a candidate to retry once this rule has been seen to keep the jobs green; that is a separate decision and a separate pull request. ### Proof after the merge The dependabot run this config change triggers should complete green on the Insights page, where the red "Rebase update" entries are. There is no open dependabot PR to refresh right now, so that run is the evidence. ### Checked `uv run poe lint` 8/8. No lock change.
chrisjsewell
added a commit
that referenced
this pull request
Sep 16, 2026
…#1943) Removes one line from `packages/sphinx-codelinks/pyproject.toml`, `"click < 8.2"`, with a changelog bullet, and relocks. click leaves the lock entirely. ### Why the cap existed, and why it is dead The line's comment said: *click 8.2.* produces empty errors if no args are given*. That was click 8.2.0 against the typer of the time, and the cap kept `codelinks` printing its usage panel when run bare. Two facts make it dead weight now, both measured on this branch: - this package never imports click itself (`git grep` over `src/`: nothing); - the typer it requires, 0.27.2 in the lock, depends on annotated-doc, colorama, rich and shellingham, and not on click. So the cap was the *only* reason click was installed at all, and `uv lock` drops click from the lock once the line goes. ### Checked In the synced environment, with click no longer importable: - `codelinks` with no arguments prints its usage panel and exits 2, exactly as before; - `codelinks nonsense` prints the "No such command" error panel, exactly as before; - `uv run poe test-codelinks`: 360 passed, 58 snapshots; - `uv run poe lint`: 8/8. ### Context Dependabot's 2026-09 batch (#1940) proposed lifting the cap to `<8.6` past the comment explaining it, which is what prompted re-checking whether the cap still had a reason. #1942 stops dependabot rewriting manifest specifiers like this in future; the deliberate lift is this pull request. Precedent for the shape: #1896, the typer cap.
chrisjsewell
added a commit
that referenced
this pull request
Sep 17, 2026
…pgrade` job (#1951) ### Why Dependabot's uv updater cannot update this lock, for two measured reasons. **It pins one candidate version on every requirement line before it relocks.** This workspace legitimately locks some packages at two versions across its `[tool.uv] conflicts` splits — `docutils` 0.20.1 in the `typing` group beside the newest elsewhere, `myst-parser` 4.x in the `sphinx-7` cell beside 5.x elsewhere. uv refuses the universal pin, and the updater treats the refusal as fatal to the whole job: every group refresh died with "Dependabot can't resolve your Python dependency files". **Its default `versioning-strategy` rewrites manifest specifiers it has no reason to touch.** #1940 raised `matplotlib>=3.3.0` to `>=3.11.2` on a user-facing extra whose floor already allowed the release, and widened `click < 8.2` past the comment explaining the cap. Three merged pull requests (#1942, #1944, #1946) and a fourth (#1948, six more ignore rules, closed unmerged in favour of this) went into working around that. Meanwhile `uv lock --upgrade` does the job in one command: on master today it resolves cleanly across every split and moves 13 packages, including `docutils v0.20.1, v0.21.2 -> v0.20.1, v0.22.4` — the exact thing dependabot cannot express — and it never touches a manifest. ### What this does Adds `.github/workflows/uv-update.yaml` — `UV update`, monthly on `0 7 1 * *` (an hour after `Prek update`, so both land on the same morning and neither depends on the other) plus `workflow_dispatch` — modelled step for step on `prek-update.yaml`: the bot-app token, `actions/checkout@v7`, `astral-sh/setup-uv@v10.1.0` on the root `.python-version`, and one pull request from the fixed branch `uv-update` titled `⬆️ Update the uv lock (monthly)`. The steps: `uv lock --upgrade`, capturing uv's lock-diff lines (stderr, so `2>&1` into `$RUNNER_TEMP`); an assertion that **no manifest changed**, which fails the job loudly if one did; `uv sync --frozen` and `uv run --frozen prek run --all-files`, recording whether the hooks came out clean; the held-back list; and the composed body. The body reports **all three of uv's lock-diff verbs**, not just `Updated`. `Added` and `Removed` get bullets of their own — a package entering the dependency graph is the most review-worthy line a lock update can carry, and a body that drops it silently is worse than one that says nothing — and any other `<Verb> <name> v<version>` line uv may invent is printed as `unrecognised uv line:` rather than discarded, so the body announces a new verb instead of quietly shrinking. Measured on a real 18-line upgrade log: 18 lines in, 18 bullets out. Then `.github/dependabot.yml` loses its uv section — ignore rules, `ty` group and all — and gains ten lines saying why. `AGENTS.md`'s dependabot-review paragraph is replaced by one paragraph on the job, the same length as the one it replaces, with the history left in `dependabot.yml` where it belongs. `tools/tests/test_dependabot_config.py` fences the configuration so the uv entry cannot come back, and fences the replacement *as a command* in the `update` step — `uv lock --upgrade` also appears in prose in the same file, so a substring check would stay green while the job was gutted to a no-op relock. ### What stays with dependabot The **`github-actions`** ecosystem, untouched: that half has never had a problem here. And **security alerts and security-update pull requests**, which are a repository feature reading `uv.lock` independently of `dependabot.yml`'s version-update config — removing the uv section does not stop them. ### Two decisions, with their measurements **No cooldown**, unlike `prek update --cooldown-days 7`. `uv lock --upgrade --exclude-newer` is a *global index cutoff*, not a per-package delay, and it failed both halves of the test: at seven days it made the lock unresolvable (`Because only ty<=0.0.79 is available and sphinx-needs-workspace:typing depends on ty>=0.0.80,<0.1.dev0 …` — the `typing` group's own floor is younger than the window, and so would be any floor a release bump had just raised); and with a cutoff recent enough to resolve, it wrote `[options] exclude-newer` into `uv.lock`, after which a plain `uv lock --check` reported "The lockfile at `uv.lock` needs to be updated" and the `uv-lock` prek hook rewrote the lock and failed — on the job's own pull request. The workflow carries a comment saying so. Dependabot had no cooldown here either, so nothing is lost. **The held-back list comes from `uv tree --outdated --frozen --depth 1`**, run after the upgrade. `uv pip list --outdated` was tried first and cannot do it: it reads one installed environment, so `pydata-sphinx-theme` (behind the `theme-pds` extra) is invisible to it — as is every conflicting group. `uv tree` reads the lock, `--depth 1` is each member's own requirements, and running it after the upgrade leaves only what is held back rather than what was merely pending. Scored against the known positives: it flags `jsonschema-rs` (0.53.0, capped `<0.54.0`) and `pydata-sphinx-theme` (0.17.1, capped `<0.20.0`), and correctly does *not* flag `comment-parser` (already newest wherever installable) or `myst-parser` (already 5.1.0 by default; 4.0.1 survives only in the `sphinx-7` and `typing` cells, held by `sphinx~=7.4`). A grep over the manifests puts the specifier beside the versions — and the body says plainly what that column is: every specifier this workspace writes for that name, while what actually blocks the newer release may be one of those, a cap elsewhere in the graph, or a `[tool.uv] conflicts` split, with `uv lock --upgrade-package <name>` as the way to find out. The list is the **default resolution's** view; a package held back only in a matrix split is not in it, and the body says that too. ### Proving it without merging it A `workflow_dispatch` needs the file on the default branch, so the five `run:` blocks were extracted from the YAML *verbatim* (parsed with pyyaml, the way `tools/tests/test_ci_plantuml_steps.py` reads workflow YAML) and run locally under `bash -e` with the runner's environment variables. Three runs: * **on today's lock** — `changed=true`, the 13 `Updated` lines captured, manifests untouched, all eight hooks Passed against the upgraded lock (ty included), the held-back list carrying both known positives, and the body composed in full; * **with the lock already current** — `changed=false`, "no lock updates available; nothing to open a pull request for", and every later step skipped by its `if:`; * **with a floor edited by hand** (dependabot's own `matplotlib>=3.3.0` → `>=3.11.2`) — the assert step fails with `::error::uv lock --upgrade edited a manifest; this job only ever moves uv.lock` and `exit 1`. Plus, for the body's new verbs: a constructed log covering `Updated`/`Added`/`Removed`/an unknown verb, and a real 18-line upgrade log rebuilt from a two-week-old `uv.lock`. And for the fence: weakening the update step's `uv lock --upgrade` to `uv lock` turns the test red. `uv.lock` is deliberately **not** part of this pull request; the tree was returned to clean before anything was committed. ### After the merge Dispatch `UV update` once by hand as the rehearsal; it opens the first real pull request. Then delete the `ci/dependabot-ignore-unlockable` branch, kept on origin as the fallback until this landed. ### Review Built by one agent against a binding brief, reviewed by a second that re-ran every proof from its own extraction of the YAML and added 15 mutations of its own; its three findings (the dropped `Added`/`Removed` lines, the fence matching prose, the misleading "no upper bound" sentence) are the fix round folded in above, and it re-verified the two mechanisms afterwards (the invented verbs `Downgraded`/`Yanked` come out loud, five uv summary shapes stay ignored; the fence is red for a weakened command and green for weakened prose). Two constructed, not observed, residuals remain and are accepted: a version *pair* on an `Added`/`Removed` line would malform that bullet's PyPI link (the package is still named), and a hyphenated verb would slip the catch-all. Gates re-run by hand on the final head: `uv run poe lint` 8/8, `pytest tools/tests` 316 passed.
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.
One line in
.github/dependabot.yml:versioning-strategy: increase-if-necessaryfor the uv ecosystem, with the reason written next to it. (First opened aslockfile-only; changed after weighing the cost below.)Why
Dependabot's default strategy for a project with a lockfile is
increase: it rewrites the manifest requirement to match the new release even when the requirement already allows it. Every specifier in this workspace's manifests is a decision, so that is the wrong strategy here. Floors are series (ty~=0.0.80,types-docutils~=0.20.0), caps carry their reason as a comment, and the intra-workspace floors are moved bypropagate_floors.pyat release time.The 2026-09 batch, #1940, is the measurement. As proposed it raised sphinx-needs'
matplotlib>=3.3.0, the floor of the user-facingplottingextra, to>=3.11.2, the newest release, although the floor already allowed it. An earlier batch rewrote~=0.20.0as>=0.20,<0.24, which is why thetypes-docutilsignore rule exists.What changes
With
increase-if-necessary, a requirement that already allows the new release is left alone and only the lock moves. A release outside a range still produces a pull request that widens the range.That second half is kept on purpose. 44 of the 97 external specifiers in these manifests carry a cap or a series, many of them runtime caps on sphinx-needs' user-facing dependencies (
jsonschema-rs<0.54,requests-file<4,pydata-sphinx-theme<0.20,sphinx-immaterial<0.14,pytest<10, ...). The widening pull request is the one signal that a new major of one of them exists, and its review is where a stale reason comment or a widened~=series gets re-expressed by hand.lockfile-onlywas considered and rejected for exactly this: it never edits a manifest, but it also never proposes such a release, so the workspace would sit on an old series until a user noticed.The
ignorerules stay and are unaffected: they stop lock-only updates that the manifests would allow but the test matrix and the typing floor do not want (a sphinx minor inside>=7.4,<10, atypes-docutilsminor inside its series). The member-name ignores stay for their own reason (dependabot-core#14004).Applied to last night's batch
Under this strategy #1940 would have been: matplotlib and uv as lock-only updates, and the click bump as a widening of
click < 8.2for review. That cap turned out to pin a package nothing uses, and #1943 drops it deliberately.Caveat
The options reference lists
uvas supportingversioning-strategyand lists the values for the option as a whole; it notes an ecosystem may not support every value. If uv does not accept this one, dependabot rejects the configuration visibly on its settings page rather than doing anything silently, andlockfile-onlyis the fallback.Checked
uv run poe lint8/8 (yamlfmt included). No lock change: the file is configuration only.