-
Notifications
You must be signed in to change notification settings - Fork 92
268 lines (265 loc) · 14.6 KB
/
Copy pathpre-commit.yml
File metadata and controls
268 lines (265 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
---
name: prek
on: # yamllint disable-line rule:truthy
pull_request:
# `ready_for_review` is not in the default set (opened / synchronize /
# reopened). It is here so that marking a draft ready starts the checks —
# which is what makes the draft PR from `bump-dev-version.yml` usable, since
# GitHub raises no events for anything `GITHUB_TOKEN` opens.
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]
permissions: {}
jobs:
prek:
# Two shapes of the same job, split by event.
#
# On a pull request the hooks run only over the files the PR
# changed (`--from-ref` / `--to-ref` on the `Run prek` step below),
# which is a handful of files rather than ~4.9k. At that size the
# parallelism prek gets from extra cores buys nothing — it splits
# each hook's file list across roughly `cpu_count` invocations, and
# there is nothing to split — so the PR run sits on `ubuntu-slim`
# (1 vCPU, container-based, 5 GB, killed at 15 minutes) with the
# other ten jobs #1190 moved there.
#
# On a push to `main` the hooks run over the whole tree
# (`--all-files`), which is where the cores do pay: measured,
# `Run prek` is 131s on 4 vCPUs and 183-211s on slim, and it is
# ~80% of the job either way. `main` is also the run that has to be
# exhaustive — it is the one that sees files a PR did not touch —
# so it stays on `ubuntu-latest`.
#
# Nothing in this job needs a Docker daemon, so the constraint that
# keeps zizmor off the `zizmor-action` (see `pyproject.toml`) is not
# what pins the `main` run to `ubuntu-latest`. Note that
# `ubuntu-slim`'s 15-minute container kill does not apply on
# `ubuntu-latest`, and no workflow here sets `timeout-minutes`, so
# leaving this job bare would swap that implicit ceiling for the
# 6-hour default on `main`. The explicit `timeout-minutes` below
# keeps the property #1190 relied on rather than dropping it as a
# side effect: 15 minutes is ~4x the slowest run observed,
# including a 674s outlier on a throttled host.
runs-on: >-
${{ github.event_name == 'pull_request' && 'ubuntu-slim'
|| 'ubuntu-latest' }}
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The PR run scopes the hooks with `--from-ref` / `--to-ref`, which
# needs both endpoints and their merge base in the local object
# store. The default shallow clone has neither, so fetch the full
# history on a pull request and keep the cheap depth-1 clone on
# `main`, where `--all-files` needs no revision range at all.
#
# Quoted on purpose. GitHub's `a && b || c` idiom yields `c`
# whenever `b` is falsy, and the number `0` is falsy — the
# unquoted form silently resolves to `1` on a pull request,
# i.e. the shallow clone the PR run must not have. The string
# `'0'` is truthy (only the empty string is not), and
# `actions/checkout` parses it as the depth.
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
# `uv` brings its own Python and is required by both:
# - the four `workspace-*` prek hooks in
# `.pre-commit-config.yaml`, which call
# `tools/dev/run-workspace-check.sh` to iterate over every
# workspace member declared in the root pyproject's
# `[tool.uv.workspace] members` list and invoke ruff /
# mypy / pytest in each via `uv run --directory`;
# - the `uv tool install prek` step below.
# Minimum uv version is pinned in the root `pyproject.toml`
# (`[tool.uv] required-version`).
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# A distinct cache slot per job. `setup-uv` derives its key from the
# runner, arch and Python version only, so every job that enables the
# cache computes the *same* key and they race when they run
# concurrently — "Unable to reserve cache with key …, another job may be
# creating this cache", and the loser saves nothing. `cache-suffix`
# splits them, which also keeps each slot scoped to the dependency set
# that job actually installs.
cache-suffix: "prek"
# Sync the uv workspace with the dev group so that
# `ruff`, `mypy`, and `pytest` (declared at workspace root)
# are present in the shared `.venv`. The `workspace-*` hooks
# in `.pre-commit-config.yaml` call
# `tools/dev/run-workspace-check.sh`, which invokes
# `uv run --directory <member> <check>`; without the explicit
# sync, the auto-sync uv runs on `uv run` only resolves
# member runtime deps and the dev tools are missing.
- name: Sync workspace (installs ruff / mypy / pytest from root dev group)
# `--all-packages` is required — without it `uv sync` only
# installs the root project's deps, and the root has
# `package = false` so member packages and their
# `[project.scripts]` entry points would be skipped.
run: uv sync --all-packages --group dev
# Cache prek's hook environments. The `lychee` hook is
# `language: rust` with `additional_dependencies: [cli:lychee:…]`,
# so prek `cargo install`s lychee into a hook env under
# `~/.cache/prek` — a multi-minute compile. Caching that dir
# reuses the built binary (and every other hook env) across runs;
# keyed on the prek config so a hook/version change busts it.
- name: Cache prek hook environments
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/prek
key: prek-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
prek-${{ runner.os }}-
# There is deliberately no lychee result cache here. `.lychee.toml`
# sets `offline = true`, so the hook resolves only in-repo paths and
# anchors and never fetches a URL — there are no results to cache.
# The `actions/cache` step that used to sit here restored and saved
# a 185-byte `.lycheecache` on every run, and because its key was
# `cache-lychee-${{ github.sha }}` it wrote a fresh entry per commit,
# evicting the 251 MB prek hook-env cache above that much sooner.
# Reinstate it only together with turning `offline` back off.
# Install prek via uv (rather than via the `j178/prek-action`
# action) so the `[tool.uv] exclude-newer` cooldown in the
# root `pyproject.toml` applies to the prek install as well.
- name: Install prek
run: uv tool install prek
# Token measurements run in the path-filtered skill-token-count workflow.
- name: Run prek
# `PREK_SCOPE` selects which files the hooks see. On a pull
# request that is the PR's own diff — base..HEAD, where HEAD is
# the merge commit `actions/checkout` leaves behind — so a PR
# pays only for what it changed. On a push to `main` it is
# `--all-files`, the exhaustive sweep that catches anything the
# per-PR runs did not cover (a hook whose config changed, a file
# broken by two PRs that were green apart, an anchor a rename
# invalidated elsewhere in the tree). Keep that asymmetry in
# mind when reading a green PR check: it is not the whole-repo
# result. `prek run --all-files` locally before pushing is what
# closes that gap ahead of `main` — see `AGENTS.md`.
#
# The scope is built in `env:` rather than interpolated into the
# `run:` line so the expression is never expanded into the shell
# source of the step.
env:
PREK_SCOPE: >-
${{ github.event_name == 'pull_request'
&& format('--from-ref {0} --to-ref HEAD',
github.event.pull_request.base.sha)
|| '--all-files' }}
# `--skip workspace-pytest`: pytest already runs in CI as the
# dedicated, path-filtered `tests` workflow matrix
# (`.github/workflows/tests.yml`). Running it a second time here
# (bundled across every workspace member) is pure duplication in
# CI, so skip it — the hook still runs locally on `git commit` /
# `prek run`, where there is no separate matrix.
#
# `--skip identity`: the `identity` meta-hook echoes every file
# passed to the static checks. That is a useful local
# troubleshooting aid, but on `--all-files` it prints the whole
# repository into the CI log ahead of any actual result, burying
# the failure a reader came for. It stays enabled locally.
#
# No `--verbose`. It was added believing it bought live progress in
# a non-TTY log. It does not — measured on this repo, first output
# and total runtime coincide with the flag and without it: prek
# emits nothing until the run finishes either way. prek streams
# only when stdout is a TTY, where it draws an in-place progress UI
# (spinner, cursor-control escapes, redrawn lines); with stdout a
# pipe it suppresses that renderer and prints the whole result
# block at the end. `--no-progress` behaves identically, and `-q`
# prints nothing at all on success.
#
# So the flag's only effect in CI is to add every *passing* hook's
# stdout to that final block — several hundred lines around the one
# thing worth reading. Failures still print their output, and
# `--show-diff-on-failure` still shows what a fixer hook changed.
#
# No `--verbose`, and no pty. Live progress is not achievable here;
# this was measured on the runner rather than assumed. Three
# configurations, all with every hook-result line landing in the
# same one-second timestamp ~106s into the step:
#
# plain pipe one block at the end
# pty (`script -qec`) one block at the end
# pty + `--no-progress` one block at the end
#
# Locally, a pty does make prek stream (spinner, in-place redraws),
# so the renderer works — but that output does not survive to the
# Actions log, which shows only the final state. `--verbose` never
# helped either: with stdout a pipe, first output and total runtime
# coincide with the flag and without it.
#
# So the plain form is kept: same information, no `script`
# dependency, and no `-e` exit-status footgun (without `-e`,
# `script` returns its own status and a failing prek reports as a
# passing step). `--verbose` is dropped because its only remaining
# effect was to add every passing hook's stdout to that final
# block. Failures still print their output, and
# `--show-diff-on-failure` still shows what a fixer hook changed.
#
# Note the repeated flag: `--skip a,b` is NOT a list — prek takes a
# single HOOK|PROJECT per occurrence, and a comma-joined value
# matches no hook at all. It warns rather than failing, so the
# comma form looks like it worked while skipping nothing.
#
# `--skip lychee`: the link check is whole-repo on every event,
# in the dedicated step below. See its comment for why it cannot
# ride along with `PREK_SCOPE`.
run: >-
prek run --show-diff-on-failure --color=always $PREK_SCOPE
--skip workspace-pytest --skip identity --skip skill-token-count
--skip lychee
# The link check runs over the whole repository on every event,
# `main` and pull request alike — `PREK_SCOPE` does not apply.
#
# It is a whole-repo hook wearing a file filter. `.pre-commit-
# config.yaml` gives it `pass_filenames: false` and a trailing `.`,
# so lychee walks the tree itself and prek's file list only decides
# *whether the hook fires at all* (`files: \.(md|rst|j2)$`). Under
# `--from-ref` that gate is the bug: a PR that renames or deletes a
# file some doc links to, without touching a single `.md`, matches
# nothing, the hook never fires, and the broken link lands on
# `main`. Running it here unconditionally restores the property the
# old `--all-files` job had — every PR is checked against every
# link in the repo.
#
# Cheap enough to do on both events: `.lychee.toml` sets
# `offline = true`, so this resolves in-repo paths and `#anchor`
# fragments only and fetches no URL. The hook is `language: rust`
# (no Docker daemon, no service container), so it runs on
# `ubuntu-slim` as happily as on `ubuntu-latest`, and the prek
# hook-env cache above already holds the compiled binary.
#
# `!cancelled()` rather than `always()`: a link break and a hook
# failure in the step above are independent, and a contributor
# should see both in one run instead of fixing one to discover the
# other. `always()` would also run this on a cancelled job.
- name: Run prek (lychee — whole repo, both events)
if: ${{ !cancelled() }}
# GITHUB_TOKEN lets lychee authenticate its github.com link
# checks — unauthenticated requests get rate-limited (429) once a
# run checks more than a handful of GitHub URLs. lychee reads
# GITHUB_TOKEN automatically; the job's `contents: read` scope is
# sufficient for link checking. Dormant while `offline = true`,
# kept so flipping that back on needs no workflow change.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: prek run lychee --color=always --all-files