Skip to content

🔧 Dependabot: back to the default strategy, with the review rule written down - #1944

Merged
chrisjsewell merged 1 commit into
masterfrom
ci/dependabot-auto-and-review-note
Sep 16, 2026
Merged

chrisjsewell merged 1 commit into
masterfrom
ci/dependabot-auto-and-review-note

Conversation

@chrisjsewell

Copy link
Copy Markdown
Member

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

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.

…ten down

#1942 set `versioning-strategy: increase-if-necessary` for the uv ecosystem to
stop dependabot rewriting manifest specifiers it does not need to. Measured
after the merge: under that strategy EVERY dependabot job for this repository
errors -- "Dependabot can't resolve your Python dependency files" -- because
every strategy except lockfile-only unlocks a requirement before asking uv for
the newest resolvable version, and for the `typing` group's `docutils~=0.20.0`
that asks uv to resolve docutils 0.23 beside `sphinx~=7.4`, which forbids it.
Under the default the same probe degrades to "no update"; 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. `lockfile-only` would avoid the error but never proposes a
release outside a range, and 44 of the 97 external specifiers here carry a
cap or a series.

So the option goes, and what stays is the review: the comment in
`.github/dependabot.yml` records both measurements, and the root `AGENTS.md`
gains one paragraph telling a reviewer of a dependabot uv pull request to read
the `pyproject.toml` hunks, restore the specifier lines by hand, relock, and
give a cap or series that really should move its own pull request.
@github-actions github-actions Bot added the pkg: workspace The repository as a whole: workflows, CI, release, docker, tooling, the workspace root label Sep 16, 2026
@codecov

codecov Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.70%. Comparing base (d68d10d) to head (6f02988).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1944   +/-   ##
=======================================
  Coverage   91.69%   91.70%           
=======================================
  Files         129      129           
  Lines       18155    18152    -3     
=======================================
- Hits        16648    16647    -1     
+ Misses       1507     1505    -2     
Flag Coverage Δ
codelinks 93.42% <ø> (ø)
mounts 94.03% <ø> (+0.01%) ⬆️
pytests 91.42% <ø> (+0.01%) ⬆️
reports 88.75% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chrisjsewell
chrisjsewell merged commit cb96177 into master Sep 16, 2026
36 checks passed
@chrisjsewell
chrisjsewell deleted the ci/dependabot-auto-and-review-note branch September 16, 2026 09:09
@chrisjsewell

Copy link
Copy Markdown
Member Author

Correction, retracting one claim in this pull request's description and in the comment it added to .github/dependabot.yml: the dependabot job that ran after this merged, under the default strategy, errored exactly the way the increase-if-necessary runs did. The strategy was never the cause. The failing probe is docutils, made a direct dependency by sphinx-codelinks' bare docutils line in #1871, which dependabot tries to pin to 0.23 on every requirement line including the typing group's series pin. #1946 fences it with an ignore rule and corrects the wording here and in AGENTS.md.

chrisjsewell added a commit that referenced this pull request Sep 16, 2026
… fails every uv job

Every rebase or recreate of #1940, and the run after #1944 reverted the
versioning strategy, errored with "Dependabot can't resolve your Python
dependency files". #1944 blamed the strategy; that was wrong, and this commit
says so where the claim was made. The failing probe is docutils: since
sphinx-codelinks arrived (#1871) with a bare `docutils` dependency, docutils is
a direct, unpinned dependency, so dependabot tries to lock 0.23 -- by writing
`docutils==0.23` on every requirement line it finds, the `typing` group's
`docutils~=0.20.0` included -- and uv refuses that beside `sphinx~=7.4`. The
lock already carries docutils at 0.20.1 in the typing split and 0.21.2
elsewhere, which is what `[tool.uv] conflicts` is for; dependabot has no way to
express that, and the uv updater treats the refusal as fatal to the whole job.

So docutils joins types-docutils under the same ignore: major and minor
updates ignored, patches inside the series still flow, and the default
environment's docutils moves by hand with `uv lock --upgrade-package docutils`.
The earlier rationale for not listing it -- keeping new series flowing to the
main environment -- never operated, because docutils was not a direct
dependency until #1871; the default environment sat on 0.21.2 with 0.23 out.

`increase-if-necessary` is therefore untested rather than unusable, and the
comment records it as a candidate to retry once this rule is seen to keep the
jobs green; the AGENTS.md paragraph is corrected to match.
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 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: workspace The repository as a whole: workflows, CI, release, docker, tooling, the workspace root

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant