Skip to content

fix: wake workers on producer completion instead of polling for it (~… #207

fix: wake workers on producer completion instead of polling for it (~…

fix: wake workers on producer completion instead of polling for it (~… #207

Workflow file for this run

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