Skip to content

🔧 Add a scheduled prek update workflow - #1806

Merged
chrisjsewell merged 1 commit into
masterfrom
ci/prek-update
Sep 2, 2026
Merged

chrisjsewell merged 1 commit into
masterfrom
ci/prek-update

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Adds .github/workflows/prek-update.yaml, a Prek update workflow that keeps the hook
revisions in .pre-commit-config.yaml current and opens a pull request with the result.
It replaces the weekly autoupdate PR that pre-commit.ci has been raising.

What it does

Runs monthly — cron: "0 6 1 * *", 06:00 UTC on the 1st, the same cadence dependabot
uses in this repo — and on workflow_dispatch for manual runs. Each run:

  • uv run prek update --cooldown-days 7, on the same
    astral-sh/setup-uv@v10.0.1 (enable-cache: true) toolchain the Lint job uses. The
    seven-day cooldown keeps day-zero hook releases out: as of today it selects ruff
    v0.16.4 and holds back v0.16.5, which is younger than a week.
  • If nothing moved, the job logs no hook updates eligible and ends green having created
    nothing. No empty PRs.
  • If something moved, it runs prek run --all-files over the updated tree, so formatter
    and --fix churn arrives already applied rather than landing on whoever merges. Hooks
    that still report findings do not fail the workflow — the PR is opened anyway, red,
    with hooks clean after autofix: no and a link to the run. A rev bump that needs human
    attention should be visible, not silently withheld.
  • Opens the PR on the fixed branch prek-update, so a re-run updates the existing PR
    instead of stacking a new one. The body lists each repository with its old and new rev.

Why a GitHub App token

A pull request created or pushed with the default GITHUB_TOKEN does not trigger
pull_request workflows — GitHub's anti-recursion rule. Lint is a required check on
master under strict mode, so such a PR would never become mergeable. The workflow
therefore mints a token from the org bot app (BOT_APP_ID / BOT_PRIVATE_KEY, both
repository secrets) and uses it for every write. Top-level permissions stay
contents: read; the default token only ever performs the checkout.

One setup item to confirm: the app needs Contents and Pull requests read/write, and
ideally Workflows: Read & write as well. yamlfmt is one of the hooks being bumped and
it formats .github/workflows/*.yaml, so a future yamlfmt release that changes its
formatting would put a workflow file in the PR, and GitHub rejects app pushes that touch
workflows without that permission. Runs fail loudly if it is missing, so it is safe to
add later.

Why not keep pre-commit.ci

prek update is workspace-aware: one invocation updates every .pre-commit-config.yaml
in the tree. pre-commit.ci reads only the root config with vanilla pre-commit, so it
cannot follow the per-package configs the uv-workspace monorepo layout will introduce.
This workflow is monorepo-ready; pre-commit.ci is not.

Merge ordering

Merge this after #1805. #1805 carries the same ruff and mypy rev bumps together with
the select pin that makes them tolerable. If this lands first, its next run re-proposes
those bumps without that pin — a rehearsal on current master produced 151 remaining
ruff findings and a mypy unused-ignore, across 23 rewritten files.

After merge

  1. Smoke-test it by hand: gh workflow run prek-update.yaml. With 🔧 Unify the ruff and mypy pins, and pin the rule set that goes with them #1805 in, expect the
    no-op path — no hook updates eligible, green, no PR. That single run also proves the
    app token and its install, which cannot be checked any other way. The first real PR
    appears once a hook release clears the seven-day cooldown.
  2. Uninstall the pre-commit.ci app. Its autofix pushes and its weekly autoupdate PR are
    both superseded — autofix by the Lint gate, autoupdate by this workflow. Nothing in
    this PR touches .pre-commit-config.yaml, so the ci: block question does not arise.

@codecov

codecov Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.26%. Comparing base (4e10030) to head (f4843ef).
⚠️ Report is 343 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1806      +/-   ##
==========================================
+ Coverage   86.87%   91.26%   +4.38%     
==========================================
  Files          56       77      +21     
  Lines        6532    11761    +5229     
==========================================
+ Hits         5675    10734    +5059     
- Misses        857     1027     +170     
Flag Coverage Δ
pytests 91.26% <ø> (+4.38%) ⬆️

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.

Replaces the weekly pre-commit.ci autoupdate pull request. pre-commit.ci only
reads the root `.pre-commit-config.yaml` with vanilla pre-commit, whereas
`prek update` is workspace-aware and updates every config in the tree.

Runs monthly (and on demand), applies hook autofixes so formatter churn arrives
in the same pull request, and opens it on a fixed branch through the bot app —
a pull request pushed with the default GITHUB_TOKEN would not trigger CI, and
`Lint` is a required check.

@ubmarco ubmarco left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool new change

@chrisjsewell
chrisjsewell merged commit 8ac8c25 into master Sep 2, 2026
24 checks passed
@chrisjsewell
chrisjsewell deleted the ci/prek-update branch September 2, 2026 07:48
chrisjsewell added a commit that referenced this pull request Sep 2, 2026
`uv.lock` has been in `.gitignore` since uv was adopted. This commits
it, re-enables the
`uv-lock` hook that keeps it honest, and points CI and dependabot at it.

## Why commit the lock

A uv lock file is **universal**: one file records the resolution for
every operating
system, architecture and Python version the project supports, so there
is no per-platform
lock to maintain and no reason to regenerate it per CI job.

It is also **not package metadata**. It is not in the sdist or the
wheel, and pip and uv
both ignore it when installing `sphinx-needs` as a dependency. **Nothing
changes for anyone
who installs this package** — the version ranges in `pyproject.toml`
remain the contract.
What the lock pins is *contributors and CI*, which is exactly what has
been unpinned.

The practical effect is on the failure mode. Today, when any transitive
dependency of the
lint environment ships a breaking release, every open pull request goes
red at once and
somebody has to work out which upstream did it. With the lock committed,
the environment
stays fixed until it is deliberately changed, so that same release turns
one dependabot
pull request red instead, in isolation, with the offending bump named in
the diff.

Scope, to be clear: the lock governs the `Lint` and `Prek update` jobs,
which are the only
uv-based jobs here. The pytest matrix still installs with pip across
`sphinx~=7.4/8.2/9.1`
on purpose — testing the *range* is the point there, and that is
unchanged.

## What changed

**The lock.** `uv.lock` removed from `.gitignore` and committed. It was
generated with uv
0.12.9 — the same version the hook below pins, and the same version
`astral-sh/setup-uv@v10`
installs today — and re-running `uv lock` leaves it byte-identical.
Nothing needed a
`tool.uv.conflicts` table; it resolves clean at 113 packages.

**The `uv-lock` hook, re-enabled.** `.pre-commit-config.yaml` has
carried a commented-out
`astral-sh/uv-pre-commit` block at `rev: 0.5.5` with `# TODO this does
not work on
pre-commit.ci`. That blocker is gone — pre-commit.ci is no longer in use
and the `Lint` job
runs prek — so the block is now live at `rev: 0.12.9`, and the stale
comment is deleted.
The hook uses its default `files:
^(uv\.lock|pyproject\.toml|uv\.toml)$`, so it only fires
when the dependencies actually change; the rest of the time it is a
no-op. Change a
dependency without re-locking and it fails with the refreshed lock
already in the diff.

**`--frozen` on every CI uv command** — the `Lint` job's `uv run prek
run`, and both
`uv run` calls in the `Prek update` workflow. `--frozen` means *use the
committed lock
verbatim, never re-resolve, and error out if it is missing*. Without it,
uv silently
re-resolves and writes a new lock when one is absent — no warning, no
non-zero exit — which
is precisely the drift the lock is meant to remove.

Not `--locked`, which additionally errors when the lock is *stale*: that
would be a
redundant second check, because the `uv-lock` hook already runs in the
same `Lint` job via
prek and fails on a stale lock, with a more useful message and the fix
in the diff.

**Dependabot: `pip` → `uv`.** The `pip` ecosystem reads `pyproject.toml`
and proposes bumps
to the `~=` pins in the extras. Nobody merges those — #1431, #1415 and
#1377 have been open
for the best part of a year. The `uv` ecosystem instead refreshes
`uv.lock`, which is what
now needs refreshing. Same `directory`, same monthly schedule, same `⬆️`
prefix, plus one
group: **minor and patch updates are batched into a single monthly pull
request, while
major updates fall outside the group and arrive one per dependency.**
Breaking changes
concentrate in majors, so the bumps most likely to go red are the ones
that come isolated,
and the routine churn does not become one PR per transitive dependency
(the `uv` ecosystem
tracks indirect dependencies too). If a batch ever goes red on a 0.x
"minor" that is really
a major, the escape hatch is `exclude-patterns` on the group —
dependabot then rebuilds the
batch without it and that dependency gets its own pull request. The
`github-actions`
stanza is untouched.

> Please close #1431, #1415 and #1377 after this merges — they are
pip-ecosystem pull
> requests that nothing will update any more. (#1360 and #1197 are
`github-actions` pull
> requests and are *not* affected by this change; #1360 in particular is
worth merging on
> its own, since `codecov/codecov-action@v3` is old enough that
actionlint flags it.)

**Docs.** `docs/contributing.rst` gains a short paragraph next to `uv
sync`: the lock is
committed so `uv sync` installs exactly those versions, edit
`pyproject.toml` and let the
`uv-lock` hook (or `uv lock`) update it, and dependabot refreshes it
monthly. No minimum uv
version is called out because none is needed — uv 0.9.24 reads this lock
and agrees it is
up to date, so no one has to upgrade uv for this.

## What the lock resolves to

`sphinx` **7.4.7** and `docutils` **0.20** — one version each, no
per-marker split. That is
not uv being conservative: the `mypy` dependency-group pins
`sphinx==7.4.7` and
`docutils==0.20` exactly, and a universal resolution has to satisfy
every group and extra
at once, so those two `==` pins fix sphinx and docutils across the whole
lock. This is
existing behaviour rather than anything new — `uv sync` on `master`
resolves to the same
thing today — but committing the lock makes it visible, and it is worth
knowing before
anyone is surprised by a `uv sync` dev environment on sphinx 7.4.

## Out of scope

`requires-python` and the CI matrix are **untouched**; the lock is
generated for the
current `>=3.10,<4`. The move of the floor to 3.11 is user-visible
(matrix, classifiers,
changelog) and belongs in its own pull request. The tox configuration is
likewise left
alone. No changelog entry, matching #1804, #1805 and #1806.
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.

2 participants