Merge pull request #221 from BioVisionCenter/fix/flaky-win-tests3 #1114
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # CI — primary test suite | |
| # | |
| # Scope: Runs on every PR and push to main/tags. Uses pixi (locked environments) | |
| # for fast, reproducible tests across all supported Python versions and platforms. | |
| # Jobs: lint (pre-commit), check-manifest, test matrix (pixi), deploy (pip, tag-only) | |
| # Matrix: PRs → ubuntu × py3.11/3.13 | main/tags → ubuntu+macos+windows × py3.11–3.14 | |
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - "v*" | |
| pull_request: | |
| workflow_dispatch: | |
| schedule: | |
| # run every week to catch upstream breakage early | |
| - cron: "0 0 * * 0" | |
| # cancel in-progress runs that use the same workflow and branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: {} | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.13" | |
| - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 | |
| check-manifest: | |
| # check-manifest is a tool that checks that all files in version control are | |
| # included in the sdist (unless explicitly excluded) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - run: pipx run --spec check-manifest==0.51 check-manifest | |
| typecheck: | |
| # `src/` must stay free of type errors. `tests/` is deliberately not gated | |
| # yet — see the 1.0 checklist. | |
| name: type check (src) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🔧 Set up pixi | |
| uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 | |
| with: | |
| environments: dev | |
| - name: 🔍 ty | |
| run: pixi run -e dev ty check src/ | |
| docs: | |
| # Build the docs and run every snippet on PRs. Kept here rather than in | |
| # docs.yml so the deploy workflow's `contents: write` is not extended to | |
| # pull requests. | |
| name: docs build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🔧 Set up pixi | |
| uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 | |
| with: | |
| environments: docs | |
| - name: 📄 Run docs snippets | |
| run: pixi run -e docs test_snippets | |
| - name: 📚 Build docs | |
| run: pixi run -e docs build_docs | |
| test-min-deps: | |
| # Installs the exact lower bounds declared in pyproject so the floors are | |
| # tested rather than asserted. Lowering a floor without updating this job | |
| # will not be caught anywhere else. | |
| name: minimum dependencies | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: 📦 Install at the declared floors | |
| # The `test` extra deliberately is NOT installed here, and cannot be: | |
| # test-only dependencies demand versions that contradict ngio's runtime | |
| # contract — `aiomoto[pandas]` needs pyarrow>=22 against our pyarrow>=16, | |
| # and s3fs pins fsspec to its own date release against our fsspec>=2025.3. | |
| # Only the tooling the reduced suite below needs is installed. | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -c .github/min-constraints.txt . \ | |
| pytest pytest-xdist scikit-image | |
| - name: 🔒 Verify the floors actually took effect | |
| run: python .github/check_min_deps.py | |
| - name: Restore shared data cache (read-only) | |
| # Read-only, so this job adds no cache-poisoning surface; on a miss the | |
| # fixtures are downloaded instead. | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: data/ | |
| key: "${{ runner.os }}-data-${{ hashFiles('src/ngio/utils/_datasets.py') }}" | |
| restore-keys: | | |
| ${{ runner.os }}-data- | |
| - name: 🧪 Run Tests | |
| # Scoped to `tests/unit` (overriding `testpaths`) because | |
| # `tests/stores/conftest.py` imports aiomoto/boto3 at module level, and | |
| # `test_s3fs_config.py` imports s3fs — none of which can be installed | |
| # alongside the floors. That excludes 21 of 441 test functions, all of | |
| # them exercising s3fs/aiomoto rather than a declared runtime floor. | |
| run: | | |
| pytest tests/unit -n 4 --durations=10 \ | |
| --ignore=tests/unit/utils/test_s3fs_config.py | |
| setup: | |
| # Outputs a reduced matrix for PRs, full matrix for main/tags/schedule | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.set-matrix.outputs.matrix }} | |
| steps: | |
| - id: set-matrix | |
| run: | | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| echo 'matrix=[{"environment":"test11","platform":"ubuntu-latest"},{"environment":"test13","platform":"ubuntu-latest"}]' >> $GITHUB_OUTPUT | |
| else | |
| echo 'matrix=[{"environment":"test11","platform":"ubuntu-latest"},{"environment":"test12","platform":"ubuntu-latest"},{"environment":"test13","platform":"ubuntu-latest"},{"environment":"test14","platform":"ubuntu-latest"},{"environment":"test11","platform":"macos-latest"},{"environment":"test12","platform":"macos-latest"},{"environment":"test13","platform":"macos-latest"},{"environment":"test14","platform":"macos-latest"},{"environment":"test11","platform":"windows-latest"},{"environment":"test12","platform":"windows-latest"},{"environment":"test13","platform":"windows-latest"},{"environment":"test14","platform":"windows-latest"}]' >> $GITHUB_OUTPUT | |
| fi | |
| test: | |
| name: ${{ matrix.platform }} (${{ matrix.environment }}) | |
| needs: setup | |
| runs-on: ${{ matrix.platform }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.setup.outputs.matrix) }} | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🔧 Set up pixi | |
| uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 | |
| with: | |
| environments: ${{ matrix.environment }} | |
| - name: Restore shared data cache (PR — read-only) | |
| if: github.event_name == 'pull_request' | |
| id: cache-data-restore | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: data/ | |
| key: "${{ runner.os }}-data-${{ hashFiles('src/ngio/utils/_datasets.py') }}" | |
| restore-keys: | | |
| ${{ runner.os }}-data- | |
| - name: Restore/save shared data cache (trusted) | |
| if: github.event_name != 'pull_request' | |
| id: cache-data | |
| # zizmor: ignore[cache-poisoning] -- restricted to trusted (non pull_request) | |
| # triggers above; PR runs use the read-only restore step instead, so this | |
| # cache can't be poisoned by untrusted code. | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: data/ | |
| key: "${{ runner.os }}-data-${{ hashFiles('src/ngio/utils/_datasets.py') }}" | |
| restore-keys: | | |
| ${{ runner.os }}-data- | |
| - name: 🧪 Run Tests | |
| run: pixi run -e ${{ matrix.environment }} pytest -n 4 --durations=10 ${{ matrix.platform == 'ubuntu-latest' && matrix.environment == 'test11' && '--cov=ngio --cov-report=xml' || '' }} | |
| # If something goes wrong with scheduled tests, open an issue in the repo | |
| - name: 📝 Report Failures | |
| if: failure() && github.event_name == 'schedule' | |
| uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PLATFORM: ${{ matrix.platform }} | |
| PYTHON: ${{ matrix.environment }} | |
| RUN_ID: ${{ github.run_id }} | |
| TITLE: "[test-bot] Scheduled tests are failing" | |
| with: | |
| filename: .github/TEST_FAIL_TEMPLATE.md | |
| update_existing: true | |
| - name: Coverage | |
| if: success() && matrix.platform == 'ubuntu-latest' && matrix.environment == 'test11' | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: /home/runner/work/ngio/ngio/coverage.xml | |
| deploy: | |
| name: deploy | |
| # Every gate must pass before anything reaches PyPI, not just the test matrix. | |
| needs: [test, lint, check-manifest, typecheck, docs, test-min-deps] | |
| if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| environment: pypi | |
| permissions: | |
| # IMPORTANT: this permission is mandatory for trusted publishing on PyPi | |
| # see https://docs.pypi.org/trusted-publishers/ | |
| id-token: write | |
| # This permission allows writing releases | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: 🐍 Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.x" | |
| - name: 👷 Build | |
| run: | | |
| python -m pip install build twine | |
| python -m build | |
| - name: 🔍 Check the built distributions | |
| run: twine check dist/* | |
| - name: 🏷️ Assert the tag matches the built version | |
| # hatch-vcs derives the version from the tag, so a mismatch means the | |
| # checkout is not what the tag points at. | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| python - <<'EOF' | |
| import os, pathlib, sys | |
| tag = os.environ["TAG"].removeprefix("v") | |
| wheels = list(pathlib.Path("dist").glob("*.whl")) | |
| if len(wheels) != 1: | |
| sys.exit(f"expected exactly one wheel, found {wheels}") | |
| built = wheels[0].name.split("-")[1] | |
| if built != tag: | |
| sys.exit(f"tag {tag!r} does not match built version {built!r}") | |
| print(f"tag and built version agree: {built}") | |
| EOF | |
| - name: 🚢 Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 | |
| - name: 🚢 Create GitHub Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh release create "${GITHUB_REF_NAME}" ./dist/* --generate-notes |