-
Notifications
You must be signed in to change notification settings - Fork 3
288 lines (280 loc) · 13.3 KB
/
Copy pathci.yml
File metadata and controls
288 lines (280 loc) · 13.3 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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
name: CI
# Python jobs (lint/test/build) skip when a change touches only prose or
# metadata -- Markdown, the docs site, the JOSS paper (paper.md/paper.bib), and
# citation/archive metadata (CITATION.cff, .zenodo.json, LICENSE) -- none of
# which affect the package. Spell-checking (typos.yml) and the paper PDF build
# (draft-pdf.yml) have their own triggers, so prose is still checked.
#
# push runs on both `main` (release branch) and `dev` (integration branch, the
# default branch): post-merge pushes to dev are what catch drift that only
# shows up outside a PR's exact base -- e.g. issue #241, where a ~1e-6
# trajectory shift moved a stopping iteration and only failed once the PR's
# changes landed on dev.
#
# `concurrency` below keys on github.sha for push events but on github.ref for
# pull_request events. For a PR, that keeps the original behavior: rapid pushes
# to the same PR branch share one group, so an update cancels the older,
# now-superseded run. For a push, every commit gets its OWN group instead --
# pushes never cancel each other -- because each commit landing on main/dev
# needs its own CI result (that is the entire point of adding the push trigger
# above); a same-ref grouping there would let a later push cancel an earlier
# commit's still-running, still-meaningful CI.
#
# Interaction with auto-bump-dev.yml/auto-tag.yml (PR #290 review): both bump
# the version and push straight back to dev/main with a PAT (deliberately, so
# the push re-triggers workflows -- see their own headers). Per-SHA grouping
# means that bump push can never land in the same concurrency group as the
# merge commit's run, so it cannot cancel it -- the failure mode this section
# used to describe (a rapid follow-up push cancelling the real commit's run
# and leaving the trivial bump commit as the one that gets tested) cannot
# happen at all now. `lint`/`typecheck` below still skip when the push's head
# commit is a bot bump (same author-email + message-prefix guard
# auto-bump-dev.yml/auto-tag.yml use on themselves to avoid looping), and every
# other job `needs` one of those two so the skip cascades to the whole run --
# now purely to avoid spending compute on a run that would only re-verify
# state the merge commit's own run already covered, not to prevent a
# cancellation.
#
# The weekly full-suite run (slow tests + Fortran-parity, macOS) lives in its
# own schedule-only workflow, .github/workflows/weekly-macos-slow.yml, so it
# never gates a PR or a push here (issue #246).
on:
push:
branches: [main, dev]
paths-ignore:
- "**.md"
- "**.bib"
- "docs/**"
- "mkdocs.yml"
- "CITATION.cff"
- ".zenodo.json"
- "LICENSE"
pull_request:
paths-ignore:
- "**.md"
- "**.bib"
- "docs/**"
- "mkdocs.yml"
- "CITATION.cff"
- ".zenodo.json"
- "LICENSE"
concurrency:
# push: per-SHA group (each push to main/dev gets its own run, never
# cancelled by a later push -- see the header comment above).
# pull_request: per-ref group, unchanged (a PR's older, superseded runs are
# still cancelled by a newer push to the same branch).
group: ci-${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint (ruff)
runs-on: ubuntu-latest
# Skip the workflow (see the header comment above) when the push's head
# commit is auto-bump-dev.yml's or auto-tag.yml's own bump-and-push --
# matches the guard those workflows already use on themselves.
if: |
github.event.head_commit.author.email != 'pamica-bot@users.noreply.github.com'
&& !startsWith(github.event.head_commit.message, 'Bump version to')
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
- run: uv sync
- name: ruff check
run: uv run ruff check .
- name: ruff format --check
run: uv run ruff format --check .
typecheck:
name: Type check (ty)
runs-on: ubuntu-latest
# Same guard as lint above; every other job needs one of these two, so
# GitHub cascades the skip to the whole run for a bot bump push.
if: |
github.event.head_commit.author.email != 'pamica-bot@users.noreply.github.com'
&& !startsWith(github.event.head_commit.message, 'Bump version to')
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
# ty is in the dev dependency group and version-locked via uv.lock, so
# `uv sync` installs the same version used locally and by the pre-commit
# hook. Optional-extra imports absent from the base env (mlx, mne) carry
# scoped `# ty: ignore` comments, so this runs on the plain base env like
# the other jobs (issue #119).
- run: uv sync
- name: ty check
run: uv run ty check .
test:
name: Test (Python 3.12)
needs: [lint, typecheck]
runs-on: ubuntu-latest
env:
PYTORCH_ENABLE_MPS_FALLBACK: "1"
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
- run: uv sync
# Build the dependency-free native binary (epic #165) so the AMICANative
# engine's end-to-end tests run for real on Linux (they skip without
# PAMICA_NATIVE_BINARY). Static reference LAPACK/BLAS, same as the release.
- name: Build native AMICA binary (for engine E2E tests)
run: |
sudo apt-get update
sudo apt-get install -y gfortran liblapack-dev libblas-dev
LAPACK_LIBS="-l:liblapack.a -l:libblas.a" EXTRA_LDFLAGS="-static-libquadmath" \
bash native/build.sh
echo "PAMICA_NATIVE_BINARY=$PWD/native/amica15_shim" >> "$GITHUB_ENV"
# Exclude the "slow" parity tests: they invoke the macOS-only Fortran
# reference binary (pamica/sample_data/amica15mac), which cannot run on
# the Linux runner. MPS-specific tests self-skip when MPS is absent.
# -n auto parallelizes the (independent) torch fits across runner cores;
# pytest-cov combines per-worker branch coverage automatically. Coverage
# config + the fail-under gate live in pyproject.toml / the flag below.
- name: pytest (excluding slow / Fortran-binary parity tests)
run: uv run pytest -m "not slow" -n auto --cov-fail-under=80
- name: Upload coverage HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: htmlcov/
retention-days: 14
# Codecov is informational (badge + PR coverage-diff comments); the hard
# gate is --cov-fail-under in the pytest step above, so a flaky upload
# (fail_ci_if_error: false) never breaks the build.
- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
fail_ci_if_error: false
# Apple Silicon coverage (issue #246). The Linux jobs above cannot exercise
# three things the project depends on:
# * pamica/tests/mlx_tests/ -- MLX is Apple-GPU only and is the recommended
# Apple backend (AGENTS.md), so it had never run in CI at all.
# * the Accelerate BLAS numerical path. Summation order differs from
# OpenBLAS enough to move a stopping iteration, which is how a broken
# assertion in test_ng_convergence.py stayed green on Linux through #241
# and failed immediately on macOS.
# * pamica/tests/mne_tests/ (the AMICAICA/MNE wrapper, issue #139) --
# previously exercised only by test-mne below, which is Linux/OpenBLAS
# only. Installing --extra mne here runs the same suite against
# Accelerate; per #241's precedent a real Accelerate-vs-OpenBLAS
# divergence there is a genuine finding, not CI flakiness -- report it,
# do not loosen the assertion to mask it.
# macos-26 is the current arm64 image (macos-14/15 are also arm64; 14 is
# deprecated). Standard runners are free and unmetered for public repos --
# do not switch to a -large/-xlarge variant, which is billed even here.
test-macos:
name: Test (macOS 26, Apple Silicon)
needs: [lint, typecheck]
runs-on: macos-26
env:
# The only job on hardware where torch auto-selects MPS (torch_impl/
# utils.py picks it whenever no device is pinned and MPS is available),
# so it is the only job where the fallback actually does anything.
PYTORCH_ENABLE_MPS_FALLBACK: "1"
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
# --extra mlx is the whole point of this job: without it mlx_tests/
# self-skips and the run adds nothing the Linux job did not already do.
# --extra mne (issue #246) makes pamica/tests/mne_tests/ (self-skipped
# via importorskip("mne") otherwise) run against Accelerate instead of
# only ever against Linux/OpenBLAS in test-mne below.
- run: uv sync --extra mlx --extra mne
- name: Assert MLX sees an Apple GPU
# mlx_tests/ is gated on BOTH an mlx import and
# mx.default_device().type == gpu (test_mlx_backend.py:28-33). A hosted
# runner that imported mlx but fell back to CPU would skip all 11 tests
# and still report success -- green while testing nothing, the exact
# failure this job exists to prevent. Assert the device, not just the
# import, so that regression fails here instead of passing silently.
run: |
uv run python -c "
import mlx.core as mx
dev = mx.default_device()
print('mlx', mx.__version__, 'device', dev)
assert dev.type == mx.DeviceType.gpu, f'no Apple GPU: {dev}'
"
- name: Build native AMICA binary (for engine E2E tests)
# Same recipe as the macos-arm64 target in release-binaries.yml:
# gfortran from brew's gcc, LAPACK/BLAS from Accelerate, and the static
# libquadmath the release build uses.
run: |
brew install gcc
LAPACK_LIBS="-framework Accelerate" EXTRA_LDFLAGS="-static-libquadmath" \
bash native/build.sh
echo "PAMICA_NATIVE_BINARY=$PWD/native/amica15_shim" >> "$GITHUB_ENV"
# "not slow" matches the Linux job, but NOT for the Linux job's reason.
# There it is because the slow tests need the macOS-only reference binary;
# here they are excluded purely for wall-clock (2000-iteration fits) --
# this job still runs on every push/PR and stays fast. The tests that do
# shell out (e.g. test_ng_backend.py:1096,:1179) pass binary_path=None,
# which resolves via PAMICA_NATIVE_BINARY (native/resolver.py:112) --
# set above -- so they use the arm64 binary this job just built, never
# Rosetta; they just aren't run here because they are @pytest.mark.slow.
# The weekly schedule-only job (weekly-macos-slow.yml, issue #246) runs
# the full suite without this filter, giving CI its first Fortran
# parity + slow-test coverage, without slowing every PR down.
# pamica/tests/mne_tests/ is picked up here too (pytest.ini's testpaths
# covers all of pamica/tests) now that --extra mne is installed above.
# --no-cov: the Linux job owns the coverage gate and the Codecov upload;
# a second partial run would report a different total for the same commit.
- name: pytest (excluding slow tests)
run: uv run pytest -m "not slow" -n auto --no-cov
test-mne:
name: Test MNE wrapper (optional [mne] extra)
needs: [lint, typecheck]
runs-on: ubuntu-latest
env:
PYTORCH_ENABLE_MPS_FALLBACK: "1"
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
# mne IS pip-installable on Linux (unlike the Apple-only mlx extra), so the
# AMICAICA wrapper (issue #139) is exercised for real in CI here. Its tests
# live in pamica/tests/mne_tests and self-skip in the base `test` job
# (importorskip on mne); this job installs the [mne] extra so they run on
# the real sample EEG. pamica/mne_compat is omitted from coverage in
# pyproject (the base env has no mne), so no fail-under gate applies here.
- run: uv sync --extra mne
- name: pytest (mne wrapper, real sample EEG)
run: uv run pytest pamica/tests/mne_tests
build:
name: Build + import (Python ${{ matrix.python-version }})
needs: [lint, typecheck]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Build sdist + wheel
run: uv build
# Verify requires-python>=3.12 compatibility: install the built wheel and
# its declared dependencies into a clean environment and import it.
- name: Install wheel into a clean env and import
run: |
uv venv --python ${{ matrix.python-version }} .import-venv
uv pip install --python .import-venv dist/*.whl
.import-venv/bin/python -c "import pamica; print('pamica', pamica.__version__)"