Repository navigation
159 lines (151 loc) · 7.67 KB
/
Copy pathci.yml
File metadata and controls
159 lines (151 loc) · 7.67 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
name: CI
on:
push:
branches: [main, development]
pull_request:
branches: [main, development]
jobs:
check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv python install ${{ matrix.python-version }}
- run: uv sync --python ${{ matrix.python-version }}
- run: uv run ruff check .
- run: uv run mypy src/
- run: uv run pytest --cov --cov-report=term-missing --cov-fail-under=100
# The declared numpy floor (1.26.4) behaves differently from the version uv
# resolves by default (2.x): stricter generic stubs (register C-19) AND ~1-ulp
# float differences in histogram binning (register C-24). The boundary a
# consumer actually pins to must be both type- AND behaviour-checked, so this
# job runs mypy *and* pytest at the floor.
floor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv python install 3.10
- run: uv run --python 3.10 --with numpy==1.26.4 mypy --strict src/
- run: uv run --python 3.10 --with numpy==1.26.4 pytest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv build
# Documentation consistency, including the check that the README's version
# banner still matches pyproject.toml. That banner check was added after the
# README fell a whole release cycle behind reality (register C-70) — but the
# script was never wired into CI, so it only ran when someone typed it
# (register C-74). This job is what makes it a gate.
#
# Its own job on purpose: the script is bash and grep, with no Python
# involved, so running it inside the four-version `check` matrix would repeat
# it four times and tie a documentation check to the list of Python versions
# we support — an unrelated thing to change.
#
# The architecture-tree check runs here too. It was written in S2 to stop the standard's
# §2 tree going stale again, and then ran nowhere — its own docstring said "not wired
# into CI", pointing at an issue that closed having done something else. That is C-74
# exactly: a check written to prevent drift, left to be typed by hand. It now also covers
# README's tree (register C-86), which is the PyPI long-description, so the cost of it
# running nowhere had gone up. It is stdlib-only and needs no `uv`, so the runner's own
# `python3` keeps this job free of a Python install.
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: bash docs/validate_docs.sh
- run: python3 scripts/check_arch_tree.py
# Cross-reference resolution: ~3,300 ADR/concern/disagreement/path citations across the
# governance corpus, none of which was checked before 2026-09-02. Offline by design —
# issue references need the network and are left to a manual `--check-issues` sweep.
- run: python3 scripts/check_doc_refs.py
# Formatting, kept out of the four-version matrix for the same reason as the
# docs job: `ruff format` does not depend on the Python version, so running it
# four times would be waste. Its own job rather than a step in `docs` because
# it needs `uv`, which that job deliberately does not install.
#
# This could only be armed once the tree was already formatted (register C-74,
# S10) — enabling it against 18 drifted files would have failed every pull
# request. It exists so the drift cannot silently return.
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv run ruff format --check .
# The two scripts README §quickstart tells readers to run. They were advertised as
# runnable and executed by nothing (register C-83), while notebooks/ has had a drift
# check since #151 — an asymmetry that looked like oversight rather than decision.
#
# This job BLOCKS on a broken example — it is not `continue-on-error` like the notebooks
# job, which stays green regardless. Blocks literally: since 2026-08-18 the `Required
# checks` ruleset requires this job on both `main` and `development`, so a red run holds
# the merge (register C-92). Until then nothing in this repo was required and every such
# claim meant "a red X beside a working merge button" — do not write "gate" here again
# without checking `gh api repos/.../rules/branches/<branch>`.
#
# The difference from notebooks.yml is deliberate. That job is `continue-on-error` because
# "a slow or flaky notebook never blocks a merge — the notebooks are un-gated dev
# artifacts". Neither reason applies here: these scripts run in 0.24s each, import only
# numpy, the stdlib and this package, and produce byte-identical output across runs. And
# unlike a notebook, README tells a new consumer to run them — a quickstart that does not
# run is a broken promise, not a flaky artifact.
#
# One job, NOT a step in the four-version matrix — but not for the reason `docs`, `format`
# and `imports` give. Those are genuinely version-independent; this one executes the
# package, and `cross_level.py` calls `hdi()`, exactly the code the `floor` job exists for
# (numpy ulp differences, register C-24). The reason is different: these scripts smoke-test
# the **documented on-ramp**, not behaviour. Behaviour across versions is what the matrix
# and the floor job cover.
#
# Pinned to 3.10 — the declared floor (`requires-python = ">=3.10"`). The failure this job
# exists to catch is on-ramp-shaped, and the on-ramp is promised to a floor consumer: a
# 3.11+-only construct in a future example would otherwise pass here while breaking the
# quickstart for exactly the reader most likely to pin conservatively.
#
# `timeout-minutes` makes the "these are fast" claim enforced rather than asserted. The
# 0.24s measurement is of today's two scripts; the glob applies this gate to every future
# one, and a grid-scale demo (register C-71) would otherwise hold every PR in the repo.
#
# The loop is not decoration: hard-coding filenames would silently cover two of three the
# day someone adds a third, and it keeps going after a failure so one broken script does
# not hide another's status. `_`-prefixed files are skipped as helpers — `notebooks/` has
# exactly that shape in `_synthetic.py`. An empty set fails rather than passing vacuously,
# the lesson from the completeness checks in `docs/validate_docs.sh`.
examples:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv python install 3.10
- name: Run every example script
run: |
shopt -s nullglob
scripts=(examples/[!_]*.py)
if [ ${#scripts[@]} -eq 0 ]; then
echo "::error::no runnable scripts under examples/ — this job would pass vacuously"
exit 1
fi
failed=0
for f in "${scripts[@]}"; do
echo "--- $f"
uv run --python 3.10 "$f" || { echo "::error file=$f::$f exited non-zero"; failed=1; }
done
exit "$failed"
# The import contracts in pyproject.toml's [tool.importlinter]. Its own job for the
# same reason as `docs` and `format`: import-linter reads the source with grimp and
# never imports the package, so its result cannot depend on the Python version, and
# running it inside the four-version matrix would repeat identical work four times.
imports:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv run lint-imports