-
-
Notifications
You must be signed in to change notification settings - Fork 21
579 lines (555 loc) · 22.7 KB
/
Copy pathpython-app.yml
File metadata and controls
579 lines (555 loc) · 22.7 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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
name: Python CI
on:
push:
branches: [ "master", "main" ]
pull_request:
branches: [ "master", "main" ]
workflow_dispatch:
permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
# A conflict resolved carelessly during a rebase can leave markers behind in
# a file no other gate reads. CHANGELOG.md is the usual victim: it collides
# on every parallel branch and nothing lints it, so the markers ride a fully
# green pull request all the way to main. Only the opening and closing
# markers are matched, never a bare row of "=", because that is legitimate
# Markdown (a setext heading underline).
- name: Check for merge-conflict markers
run: |
if git grep -nE '^(<{7} |>{7} )' -- . ; then
echo "::error::Unresolved merge-conflict markers found in the files listed above."
exit 1
fi
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install -e .
- name: Lint with Ruff
run: ruff check .
- name: Type check with Mypy
run: mypy src scripts
- name: Security check with Bandit
run: bandit -r src
# Fast gate (~12 s harness, runtime deps only, no pytest): the committed
# docs/CONFORMANCE.md, and every count quoted from it, must equal a fresh
# `make conformance` run.
conformance:
name: Conformance report up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Regenerate docs/CONFORMANCE.md and the counts quoted from it
run: make conformance
# `make conformance` now writes as well as reads: it regenerates the report
# and rewrites the counts quoted in .zenodo.json, the docs/ mirror and the
# site frontmatter. So the staleness gate has to cover the whole tree, not
# just the report. Diffing only docs/CONFORMANCE.md would let the rewrite
# fix the prose in the runner's checkout and pass, while the branch keeps
# the stale numbers.
- name: Fail if the committed report or any quoted count is stale
run: |
if ! git diff --exit-code; then
echo "::error::The conformance report or a count quoted from it is out of date - run 'make conformance' and commit the result."
exit 1
fi
# Belt and braces, and the command to run locally: the read-only check on
# the committed state. The report can be current while the prose quoting it
# is not; the counts were typed out in three places and two of them fell a
# release behind.
- name: Fail if any page quotes a stale conformance count
run: python scripts/check_conformance_claims.py
# Drift gate for the generated Starlight API reference: the committed pages
# under site/src/content/docs/reference/api and the sidebar fragment must
# equal a fresh `make api-docs` run (deterministic, stdlib-only generator).
api-docs:
name: Generated API reference up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Regenerate the API reference
run: make api-docs
- name: Fail if the committed API reference is stale
run: |
if ! git diff --exit-code -- site/src/content/docs/reference/api site/src/generated; then
echo "::error::The generated API reference is out of date - run 'make api-docs' and commit the result."
exit 1
fi
untracked=$(git ls-files --others --exclude-standard -- site/src/content/docs/reference/api site/src/generated)
if [ -n "$untracked" ]; then
echo "::error::Generated API pages are not committed:"
echo "$untracked"
exit 1
fi
# Drift gate for the llms artifacts: the committed llms.txt, llms-full.txt,
# the shards under site/public/llms and the generated overview mirrors under
# docs/ must equal a fresh `make llms` run. Every other generated artifact
# had this gate; these were regenerated by the docs workflow and never
# compared, so a stale commit could not be told from a fresh one.
llms:
name: llms artifacts up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Regenerate the llms artifacts
run: make llms
- name: Fail if the committed artifacts are stale
run: |
if ! git diff --exit-code -- llms.txt llms-full.txt site/public/llms docs; then
echo "::error::The llms artifacts are out of date - run 'make llms' and commit the result."
exit 1
fi
untracked=$(git ls-files --others --exclude-standard -- site/public/llms docs)
if [ -n "$untracked" ]; then
echo "::error::Generated llms files are not committed:"
echo "$untracked"
exit 1
fi
# Drift gate for the evidence pages the site renders from docs/: the bodies
# of docs/CONFORMANCE.md and docs/ERRATA.md are transplanted into the four
# reference/{conformance,errata} pages (EN + ES), so the site can never show
# a hand-copied or stale version of either document.
site-reports:
name: Generated evidence pages up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
# Stdlib-only generator: no project install needed, it only reshapes text.
- name: Check the transplanted conformance and errata pages
run: python scripts/generate_site_reports.py --check
# Evidence gate for docs/ERRATA.md. Each entry there is a permanent public
# statement that a named body or author printed something wrong, so it must
# cite the page it quotes rather than an extraction: PDF text layers delete
# radicals and minus signs silently, and one entry was drafted on an
# extraction that had eaten a square root, and caught in review. The script
# flags any multiplicative claim whose ratio sits on sqrt(2), sqrt(3), pi,
# 2 pi, 1/sqrt(2), ln 2 or a small integer, which is the signature of a lost
# glyph. See scripts/check_errata_evidence.py and CONTRIBUTING.md.
errata-evidence:
name: Errata entries cite their page
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
# Stdlib-only checker: it reads docs/ERRATA.md and nothing else.
- name: Check the errata evidence
run: python scripts/check_errata_evidence.py
# Coverage gate for the curated quick table in docs/reference/api/index.md: every
# phonometry.__all__ name must have a table row (extra rows such as methods
# or namespace entries are fine). See scripts/check_api_reference.py.
api-reference-table:
name: Curated API quick table covers __all__
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install runtime dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Check docs/reference/api/index.md coverage
run: python scripts/check_api_reference.py
# The committed documentation figures (.github/images) must match a fresh
# `make graphs` run. The rendering + compute stack is pinned in
# requirements-figures.txt so SVG *structure* (elements, text, colours) and
# WebP dimensions/encoding are stable -- matplotlib/fonttools/pillow fix the
# layout, numpy/scipy fix the computed data. The comparison itself
# (scripts/check_figures.py) is tolerance-aware rather than a byte diff:
# GitHub's heterogeneous runner CPUs shift a few path coordinates ~1 ULP,
# which is visually irrelevant but breaks a byte compare. Bump the pinned
# stack together with a fresh `make graphs` regeneration.
figures:
name: Documentation figures up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install the locked figure-rendering stack
run: |
python -m pip install --upgrade pip
pip install -e .
# Pin the render+compute stack last so it wins over looser deps.
pip install -r requirements-figures.txt
- name: Regenerate figures and diagrams
run: make graphs
- name: Fail if any committed figure is stale
# Tolerance-aware compare instead of a byte diff: GitHub's runner fleet is
# hardware-heterogeneous, so the pinned stack computes a few path
# coordinates ~1 ULP apart depending on the CPU the run lands on. That
# sub-pixel drift is irrelevant but breaks a byte diff; the script checks
# SVG structure + numeric tolerance and raster RMS instead, so real figure
# changes still fail while cross-CPU noise passes. See scripts/check_figures.py.
run: python scripts/check_figures.py
- name: Fail if any shaded region is illegible
# Staleness says the figures match the code; it says nothing about
# whether a shaded region can be seen. This measures every filled area
# against the page it is drawn on and fails below the CIEDE2000
# threshold, which is what catches a fill tuned on one theme and left
# invisible on the other. It parses the committed SVGs (a few seconds
# for the whole set) and needs no rendering stack of its own.
run: python scripts/check_figure_contrast.py
# The Python snippets printed in the guides must run. They are the first
# thing a reader copies, and nothing executed them until this job existed:
# the metrology split shipped seven blocks where `from phonometry import
# signals` sat next to `from scipy import signal` and silently rebound the
# name, which Python does not warn about and no other gate can see. The
# script also holds the English and Spanish pages to the same API and skips,
# with a written reason, the pages whose blocks are excerpts of a workflow
# rather than a script.
doc-snippets:
name: Documentation snippets run
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install dependencies
# The guides plot and print fiches, so the snippets need the figure and
# report stacks as well as the package itself.
run: |
python -m pip install --upgrade pip
pip install -e .
pip install -r requirements-figures.txt -r requirements-reports.txt
- name: Run every snippet the guides print
run: python scripts/check_doc_snippets.py
# The guides are hard-wrapped, so a sentence can wrap onto a "-" or a ">" and
# stop being a sentence: CommonMark ends the paragraph at the marker. One
# variant takes the site build down (unclosed inline maths, which MDX then
# evaluates as JavaScript); the other is silent and ships a quoted block in
# the middle of a paragraph. Needs no dependencies, so it runs on its own
# rather than waiting behind an install.
markdown-hazards:
name: Markdown renders the way it reads
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Check the hand-written markdown
run: python scripts/check_markdown_hazards.py
# The committed example .report() fiches (.github/reports) must match a fresh
# `make reports` run. Same drift gate as the figures, one layer further down
# the pipeline: the fiches are what the documentation links to as worked
# examples of what the library prints, so a stale one publishes a wrong
# answer. Nothing checked them until this job existed, and two of them sat a
# plot-styling release behind the code for weeks. The rendering stack is
# pinned across both requirements files (matplotlib/numpy in -figures,
# svglib/reportlab/pypdfium2 in -reports) because every link in the chain
# fixes the rendered page the check compares.
reports:
name: Example report fiches up to date
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install the locked fiche-rendering stack
run: |
python -m pip install --upgrade pip
pip install -e .
# Pin the render+compute stack last so it wins over looser deps.
pip install -r requirements-figures.txt -r requirements-reports.txt
- name: Regenerate the example fiches
run: make reports
- name: Fail if any committed fiche is stale
# Tolerance-aware compare instead of a byte diff: GitHub's heterogeneous
# runner CPUs shift a few of the embedded plot's path coordinates ~1 ULP,
# which rewrites the PDF page stream for no visible reason. The script
# compares the extracted text exactly and the rendered page within a
# calibrated pixel tolerance, so a real fiche change fails while
# cross-CPU coordinate noise passes. See scripts/check_reports.py.
run: python scripts/check_reports.py
tests:
runs-on: ${{ matrix.os }}
permissions:
contents: read
# Required for the OIDC-based Codecov upload (no token secret needed).
id-token: write
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install -e .
# Heavy third-party oracle sets (the EBU loudness test set, the stipa.info
# certified STIPA bench, the NORAH2 public release) are never fetched here.
# The suites that use them fall back to the committed derived series and
# extracts under tests/data/ - see tests/data/README.md - which is the path
# CI takes, and whose assertions never skip. A handful of cases that exist
# only to exercise a full set do skip here and report as such (the EBU
# programme-audio cases and the full-bench inventory guard); pytest prints
# the resolution in its run header.
- name: Run tests
# -n auto fans the suite out across the runner's cores via pytest-xdist;
# pytest-cov combines the per-worker coverage into a single coverage.xml.
# Pin the numerical thread pools to one thread each so the per-core xdist
# workers do not oversubscribe the CPU with nested BLAS/OpenMP pools.
env:
OMP_NUM_THREADS: "1"
MKL_NUM_THREADS: "1"
OPENBLAS_NUM_THREADS: "1"
NUMEXPR_NUM_THREADS: "1"
VECLIB_MAXIMUM_THREADS: "1"
run: |
pytest -n auto --junitxml=test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=src --cov-report=xml
- name: Upload Test Results
uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.os }}-${{ matrix.python-version }}
path: |
test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml
coverage.xml
if: always()
- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v7
with:
files: coverage.xml
use_oidc: true
fail_ci_if_error: false
# The main matrix runs against the latest numpy with the pure-Python
# impulse kernel; this job is the only one exercising the numba-jitted
# path (numba pins numpy, so it lives in its own environment).
tests-perf:
name: tests (numba-jitted kernel)
runs-on: ubuntu-latest
permissions:
contents: read
# Declared once for the whole job so the guard below runs under exactly the
# environment it vouches for. Split across the two steps, the guard could
# stay green on its own NUMBA_DISABLE_JIT while the suite lost its copy and
# ran interpreted from end to end.
env:
# conftest.py disables JIT by default (coverage); override it here, this
# is the only job exercising the numba-compiled kernel.
NUMBA_DISABLE_JIT: "0"
# Pin the numerical thread pools to one thread each so the per-core xdist
# workers do not oversubscribe the CPU with nested BLAS/OpenMP pools,
# exactly as the main test job does.
OMP_NUM_THREADS: "1"
MKL_NUM_THREADS: "1"
OPENBLAS_NUM_THREADS: "1"
NUMEXPR_NUM_THREADS: "1"
VECLIB_MAXIMUM_THREADS: "1"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: 'pip'
- name: Install dependencies (with numba)
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -e ".[perf,plot]"
# Fails the job if the kernel falls back to the interpreted path, which
# would leave this job green while exercising nothing it exists for.
# Compiling here also fills numba's on-disk cache next to the module, so
# the test workers below load the kernel instead of each compiling a copy.
- name: Check the jitted kernel compiles
run: python scripts/check_jit_kernel.py
- name: Run tests (jitted kernel)
# Same fan-out as the main matrix. The compiled kernel is not a reason
# to stay serial: the step above compiles it once and the workers that
# reach it read the cache in a fraction of a second, against a suite
# that takes about twenty minutes to run one test at a time.
run: pytest -q -n auto
sonar:
needs: tests
runs-on: ubuntu-latest
# Dependabot runs with a restricted token and no access to repository secrets, so
# SONAR_TOKEN is empty there and the scan can only ever fail. Skip it for those runs.
if: >-
github.actor != 'dependabot[bot]' &&
(github.event_name == 'pull_request' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Download coverage report
uses: actions/download-artifact@v8
with:
name: test-results-ubuntu-latest-3.13
- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@v8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
pr-comment:
needs: [quality, tests]
runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.13
uses: actions/setup-python@v7
with:
python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
- name: Generate numerical conformance report
# Never block the comment: if a check regresses the harness exits
# non-zero, but we still want the report (with the failing rows) posted.
run: python scripts/conformance_report.py || true
- name: Download Test Results
uses: actions/download-artifact@v8
with:
pattern: test-results-*
path: test-results
continue-on-error: true
- name: Generate Comment Body
run: python .github/scripts/comment_pr.py
env:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
- name: Post PR Comment
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('pr_comment_body.md', 'utf8');
const marker = '<!-- phonometry-ci-conformance -->';
// Update the single sticky conformance comment instead of posting a
// new one every run: find the existing marked comment and edit it.
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const existing = comments.find(
(c) => c.body && c.body.includes(marker)
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
}