-
Notifications
You must be signed in to change notification settings - Fork 17
224 lines (196 loc) · 8.76 KB
/
Copy pathci.yml
File metadata and controls
224 lines (196 loc) · 8.76 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
name: CI
on:
# Only run the (expensive) test matrix when files that can affect the tests or
# package change. Docs, license, images and other non-code changes are skipped.
# This is an allowlist: anything test-relevant added later (e.g. a new data dir
# or a root conftest.py) must be added here or CI will silently skip it.
push:
branches: ["main"]
paths:
- "src/**" # birdnet, birdnet_tests (incl. TEST_FILES), birdnet_benchmark
- "example/**" # test data: test_issue29/55 load example/soundscape.wav
- "pyproject.toml" # deps + tox + pytest/ruff/mypy config
- "MANIFEST.in" # packaging -> build/twine check step
- ".github/workflows/ci.yml" # the workflow itself
pull_request:
branches: ["main"]
paths:
- "src/**"
- "example/**"
- "pyproject.toml"
- "MANIFEST.in"
- ".github/workflows/ci.yml"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Cancel a running PR check when new commits are pushed, so a wedged run does not
# keep burning runner hours after it has been superseded. Pushes to main still run
# to completion.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
run-tests:
runs-on: ${{ matrix.os }}
continue-on-error: false
# Backstop for a wedged job. The library defaults to the "spawn" start method
# (see birdnet.core.start_method), which removes the fork-after-TensorFlow
# deadlock from the general suite; the isolated fork lanes can still burn up to
# the 600 s per-test timeout each when that inherent deadlock fires. Budget is
# set by the fattest cell, ubuntu 3.12, which runs three tox envs (py312 +
# py312-repro + py312-coverage, ~45 min healthy): 40 was proven too tight when
# a repro fork-lane deadlock pushed it over.
timeout-minutes: 50
env:
# Fixed, cacheable location for birdnet's model/label downloads so the ~11 min
# "load_model" download step is skipped on a cache hit. Placed under the
# workspace because the `runner` context is not available in job-level env;
# this is safe since checkout runs once, before the cache is restored, and is
# never re-run. tox forwards this var to the pytest subprocess via `passenv`.
BIRDNET_APP_DATA: ${{ github.workspace }}/.birdnet-app-data
# Enables the GIL-immune hang watchdog (see birdnet_tests/conftest.py): a
# wedged test that survives pytest-timeout gets its thread stacks dumped
# here and the process hard-killed shortly after its timeout, instead of
# silently burning the job budget. Uploaded as an artifact on failure.
BIRDNET_TEST_WATCHDOG_DIR: ${{ github.workspace }}/.watchdog-dumps
strategy:
fail-fast: false
matrix:
os:
[
ubuntu-24.04,
ubuntu-24.04-arm,
macos-15,
macos-15-intel,
windows-2025,
]
python-version: ["3.11", "3.12", "3.13", "3.14"]
exclude:
- os: macos-15-intel
python-version: "3.13" # no TF support
- os: macos-15-intel
python-version: "3.14" # no TF / torch x86 macOS wheels
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 2
- name: Install audio system libs (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libsndfile1
- name: Install audio system libs (macOS)
if: startsWith(matrix.os, 'macos')
run: |
brew update
brew install libsndfile
- name: Install audio system libs (Windows)
if: startsWith(matrix.os, 'windows')
run: |
choco install libsndfile
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
# Provides the `uv` binary and persists uv's wheel cache across runs, so the
# large TensorFlow/torch/cuda downloads are not repeated every run.
- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
# Cache the downloaded models/labels. The files are identical across OS and
# Python, so we key only on the download *profile* (full vs the TF-free py3.14
# surface) plus the model definitions (their URLs/sizes). This keeps the cache
# to ~2 entries (~3.6 GB) instead of one ~3 GB copy per (os, python), which
# overran the 10 GB repo cache limit and thrashed. A changed definition busts
# the key; restore-keys then falls back to the last cache of the same profile
# and only the stale models are re-downloaded.
- name: Cache downloaded models
uses: actions/cache@v4
with:
path: ${{ env.BIRDNET_APP_DATA }}
key: birdnet-models-${{ matrix.python-version == '3.14' && 'notf' || 'full' }}-${{ hashFiles('src/birdnet/**/models/**/*.py') }}
restore-keys: |
birdnet-models-${{ matrix.python-version == '3.14' && 'notf' || 'full' }}-
- name: Install dependencies
run: uv pip install --system '.[tests]'
- name: Run tests py311
if: matrix.python-version == '3.11'
run: |
python -m tox -e py311
- name: Run tests py312
if: matrix.python-version == '3.12'
run: |
python -m tox -e py312
- name: Run tests py313
if: matrix.python-version == '3.13'
run: |
python -m tox -e py313
# TensorFlow has no Python 3.14 wheels yet (issue #55), so 3.14 installs
# without it and only the TensorFlow-free surface (acoustic 3.0 via onnx)
# is exercised here.
- name: Run tests py314
if: matrix.python-version == '3.14'
run: |
python -m tox -e py314
- name: Run repro tests
if: matrix.python-version == '3.12' && matrix.os != 'macos-15-intel'
run: |
python -m tox -e py312-repro
- name: Run coverage tests
if: matrix.os == 'ubuntu-24.04' && matrix.python-version == '3.12'
run: |
python -m tox -e py312-coverage
- name: Upload results to Codecov
if: matrix.os == 'ubuntu-24.04' && matrix.python-version == '3.12'
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: birdnet-team/birdnet
- name: Extract log path
id: extract-log-path
shell: pwsh
run: |
$logPaths = @()
if (Test-Path output.log) {
$output = Get-Content output.log -Raw
$pattern = '(?<path>(?:[A-Za-z]:\\|/)[^\r\n]*?\.log)'
$logPaths = [regex]::Matches($output, $pattern) |
ForEach-Object { $_.Groups['path'].Value } |
Sort-Object -Unique |
Where-Object { Test-Path $_ -PathType Leaf }
}
if ($logPaths.Count -gt 0) {
"log_paths<<EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
$logPaths | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
}
else {
"log_paths=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
}
- name: Upload log file
if: failure() && steps.extract-log-path.outputs.log_paths != ''
uses: actions/upload-artifact@v7
with:
name: birdnet-log
path: ${{ steps.extract-log-path.outputs.log_paths }}
# Diagnostic for silent test-process deaths: rules the kernel OOM killer
# in or out directly in the job log. (Checked for the fork-lane wedges:
# no OOM kills logged — those are the inherent fork-after-TF child
# deadlock, ended by pytest-timeout at the per-test timeout.)
- name: Check for OOM kills (Linux)
if: failure() && startsWith(matrix.os, 'ubuntu')
run: sudo dmesg | grep -iE "out of memory|oom-kill|killed process" || echo "no OOM kills logged"
# Stack dumps written by the hang watchdog (see BIRDNET_TEST_WATCHDOG_DIR
# above); on a wedge they show every thread's stack at kill time.
# include-hidden-files is required: upload-artifact skips dotfiles by
# default, and the dump dir is hidden (.watchdog-dumps).
- name: Upload hang watchdog dumps
if: failure()
uses: actions/upload-artifact@v7
with:
name: watchdog-dumps-${{ matrix.os }}-py${{ matrix.python-version }}
path: .watchdog-dumps/
if-no-files-found: ignore
include-hidden-files: true