Merge pull request #202 from mwyau/chore/v0.7-api-release-polish #825
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
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - 'v*.*.*' | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.ref_type != 'tag' }} | |
| permissions: | |
| contents: read | |
| env: | |
| UV_VERSION: '0.12.1' | |
| PYTHON_VERSION: '3.14' | |
| jobs: | |
| select-test-matrix: | |
| name: Select test matrix | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 5 | |
| outputs: | |
| unit_matrix: ${{ steps.select.outputs.unit_matrix }} | |
| integration_matrix: ${{ steps.select.outputs.integration_matrix }} | |
| parity_matrix: ${{ steps.select.outputs.parity_matrix }} | |
| uv_version: ${{ env.UV_VERSION }} | |
| steps: | |
| - name: Select representative or full matrix | |
| id: select | |
| run: | | |
| set -euo pipefail | |
| if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then | |
| UNIT_MATRIX='{ | |
| "include": [ | |
| {"os": "ubuntu-26.04", "python-version": "3.12", "mode": "min-deps"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "mode": "coverage", "coverage_flag": "unit"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "mode": "jit-coverage", "coverage_flag": "unit-jit-coverage"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "mode": "disabled-jit-coverage", "coverage_flag": "unit-jit-disabled"}, | |
| {"os": "windows-2025", "python-version": "3.13", "mode": "normal"}, | |
| {"os": "macos-26", "python-version": "3.14", "mode": "normal"} | |
| ] | |
| }' | |
| INTEGRATION_MATRIX='{ | |
| "include": [ | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "mpi": "mpich", "coverage": true} | |
| ] | |
| }' | |
| PARITY_MATRIX='{ | |
| "include": [ | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "coverage": true} | |
| ] | |
| }' | |
| else | |
| UNIT_MATRIX='{ | |
| "include": [ | |
| {"os": "ubuntu-26.04", "python-version": "3.12", "mode": "normal"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.12", "mode": "min-deps"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.13", "mode": "normal"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "mode": "coverage", "coverage_flag": "unit"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "mode": "jit-coverage", "coverage_flag": "unit-jit-coverage"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "mode": "disabled-jit-coverage", "coverage_flag": "unit-jit-disabled"}, | |
| {"os": "ubuntu-26.04", "python-version": "3.14t", "mode": "free-threaded"}, | |
| {"os": "windows-2025", "python-version": "3.13", "mode": "normal"}, | |
| {"os": "macos-26", "python-version": "3.14", "mode": "normal"} | |
| ] | |
| }' | |
| INTEGRATION_MATRIX='{ | |
| "include": [ | |
| {"os": "ubuntu-26.04-arm", "python-version": "3.14", "mpi": "mpich", "coverage": false}, | |
| {"os": "ubuntu-24.04", "python-version": "3.14", "mpi": "mpich", "coverage": true}, | |
| {"os": "windows-2025", "python-version": "3.13", "mpi": "msmpi", "coverage": false}, | |
| {"os": "macos-26", "python-version": "3.14", "mpi": "mpich", "coverage": false} | |
| ] | |
| }' | |
| PARITY_MATRIX='{ | |
| "include": [ | |
| {"os": "ubuntu-26.04", "python-version": "3.14", "coverage": true} | |
| ] | |
| }' | |
| fi | |
| { | |
| echo "unit_matrix<<EOF" | |
| echo "$UNIT_MATRIX" | |
| echo "EOF" | |
| echo "integration_matrix<<EOF" | |
| echo "$INTEGRATION_MATRIX" | |
| echo "EOF" | |
| echo "parity_matrix<<EOF" | |
| echo "$PARITY_MATRIX" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| code-quality: | |
| name: Code quality checks | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Ruff lint | |
| uses: astral-sh/ruff-action@v4.1.0 | |
| with: | |
| args: check | |
| version-file: uv.lock | |
| - name: Ruff format | |
| if: ${{ !cancelled() }} | |
| uses: astral-sh/ruff-action@v4.1.0 | |
| with: | |
| args: format --check | |
| version-file: uv.lock | |
| - name: Set up uv | |
| if: ${{ !cancelled() }} | |
| uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| version: ${{ env.UV_VERSION }} | |
| enable-cache: true | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install type-checking environment | |
| if: ${{ !cancelled() }} | |
| run: uv sync --frozen --all-extras | |
| - name: Check TrackJSON schema | |
| if: ${{ !cancelled() }} | |
| run: uv run --no-sync python scripts/generate_trackjson_schema.py --check | |
| - name: Ty | |
| if: ${{ !cancelled() }} | |
| run: uv run --no-sync ty check | |
| - name: Mypy | |
| if: ${{ !cancelled() }} | |
| run: uv run --no-sync mypy | |
| unit-tests: | |
| name: Unit tests (${{ matrix.os }}, Python ${{ matrix.python-version }}${{ matrix.mode != 'normal' && format(', {0}', matrix.mode) || '' }}) | |
| needs: [select-test-matrix, code-quality] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 40 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.select-test-matrix.outputs.unit_matrix) }} | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| version: ${{ env.UV_VERSION }} | |
| enable-cache: true | |
| python-version: ${{ matrix.python-version }} | |
| - name: Set up Microsoft MPI on Windows | |
| if: runner.os == 'Windows' | |
| uses: mpi4py/setup-mpi@v1.4.4 | |
| with: | |
| mpi: msmpi | |
| - name: Install dependencies | |
| if: matrix.mode == 'normal' || matrix.mode == 'coverage' || matrix.mode == 'jit-coverage' || matrix.mode == 'disabled-jit-coverage' | |
| run: uv sync --frozen --all-extras | |
| - name: Install minimum direct dependencies | |
| if: matrix.mode == 'min-deps' | |
| run: uv sync --all-extras --resolution lowest-direct | |
| - name: Install free-threaded dependencies (without EOF, GRIB, or Zarr) | |
| if: matrix.mode == 'free-threaded' | |
| run: uv sync --frozen --extra mpi --extra netcdf4 | |
| - name: Smoke-check GRIB and Zarr dependencies | |
| if: matrix.mode != 'free-threaded' | |
| run: | | |
| uv run --no-sync python -m cfgrib selfcheck | |
| uv run --no-sync python -c 'import zarr; print("zarr import success")' | |
| - name: Run unit tests | |
| if: matrix.mode == 'normal' || matrix.mode == 'min-deps' | |
| run: uv run --no-sync pytest -vv | |
| - name: Run unit tests with coverage | |
| if: matrix.mode == 'coverage' | |
| run: uv run --no-sync pytest -vv --cov=pystormtracker --cov-report=term-missing --cov-report=xml --junitxml=junit.xml -o junit_family=legacy | |
| - name: Run unit tests with JIT coverage | |
| if: matrix.mode == 'jit-coverage' | |
| env: | |
| NUMBA_JIT_COVERAGE: '1' | |
| NUMBA_CACHE_DIR: ${{ runner.temp }}/numba-coverage-cache | |
| run: uv run --no-sync pytest -vv --cov=pystormtracker --cov-report=term-missing --cov-report=xml | |
| - name: Run unit tests with JIT disabled coverage | |
| if: matrix.mode == 'disabled-jit-coverage' | |
| env: | |
| NUMBA_DISABLE_JIT: '1' | |
| run: uv run --no-sync pytest -vv --cov=pystormtracker --cov-report=term-missing --cov-report=xml | |
| - name: Verify free-threaded Python | |
| if: matrix.mode == 'free-threaded' | |
| env: | |
| PYTHON_GIL: '0' | |
| run: uv run --no-sync python -c 'import sys; assert not sys._is_gil_enabled()' | |
| - name: Run free-threaded unit tests | |
| if: matrix.mode == 'free-threaded' | |
| env: | |
| PYTHON_GIL: '0' | |
| run: uv run --no-sync pytest -vv | |
| - name: Upload coverage | |
| if: matrix.mode == 'coverage' || matrix.mode == 'jit-coverage' || matrix.mode == 'disabled-jit-coverage' | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: ./coverage.xml | |
| disable_search: true | |
| flags: ${{ matrix.coverage_flag }} | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload test results | |
| if: ${{ !cancelled() && matrix.mode == 'coverage' }} | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: ./junit.xml | |
| disable_search: true | |
| report_type: test_results | |
| flags: unit | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| integration-tests: | |
| name: Integration tests (${{ matrix.os }}, Python ${{ matrix.python-version }}) | |
| needs: [select-test-matrix, code-quality] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 70 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.select-test-matrix.outputs.integration_matrix) }} | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| version: ${{ env.UV_VERSION }} | |
| enable-cache: true | |
| python-version: ${{ matrix.python-version }} | |
| - name: Set up MPI | |
| uses: mpi4py/setup-mpi@v1.4.4 | |
| with: | |
| mpi: ${{ matrix.mpi }} | |
| - name: Install dependencies | |
| run: uv sync --frozen --all-extras | |
| - name: Run integration tests with coverage | |
| if: matrix.coverage == true | |
| env: | |
| NUMBA_JIT_COVERAGE: '1' | |
| NUMBA_CACHE_DIR: ${{ runner.temp }}/numba-coverage-cache | |
| run: uv run --no-sync pytest -vv --cov=pystormtracker --cov-report=term-missing --cov-report=xml --junitxml=junit.xml -o junit_family=legacy tests/integration/ -m "not slow and not data" | |
| - name: Run integration tests | |
| if: matrix.coverage == false | |
| run: uv run --no-sync pytest -vv tests/integration/ -m "not slow and not data" | |
| - name: Upload coverage | |
| if: matrix.coverage == true | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: ./coverage.xml | |
| disable_search: true | |
| flags: integration | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload test results | |
| if: ${{ !cancelled() && matrix.coverage == true }} | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: ./junit.xml | |
| disable_search: true | |
| report_type: test_results | |
| flags: integration | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| parity-tests: | |
| name: Parity tests (${{ matrix.os }}, Python ${{ matrix.python-version }}) | |
| needs: [select-test-matrix, code-quality] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.select-test-matrix.outputs.parity_matrix) }} | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| version: ${{ env.UV_VERSION }} | |
| enable-cache: true | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install dependencies | |
| run: uv sync --frozen --all-extras | |
| - name: Run parity tests with coverage | |
| if: matrix.coverage == true | |
| env: | |
| NUMBA_JIT_COVERAGE: '1' | |
| NUMBA_CACHE_DIR: ${{ runner.temp }}/numba-coverage-cache | |
| run: uv run --no-sync pytest -vv --cov=pystormtracker --cov-report=term-missing --cov-report=xml --junitxml=junit.xml -o junit_family=legacy tests/parity/ -m "not slow and not data" | |
| - name: Run parity tests | |
| if: matrix.coverage == false | |
| run: uv run --no-sync pytest -vv tests/parity/ -m "not slow and not data" | |
| - name: Upload coverage | |
| if: matrix.coverage == true | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: ./coverage.xml | |
| disable_search: true | |
| flags: parity | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload test results | |
| if: ${{ !cancelled() && matrix.coverage == true }} | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: ./junit.xml | |
| disable_search: true | |
| report_type: test_results | |
| flags: parity | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| package-tests: | |
| name: Package tests | |
| needs: code-quality | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| version: ${{ env.UV_VERSION }} | |
| enable-cache: true | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Build wheel and source distribution | |
| run: uv build --wheel --sdist | |
| - name: Create wheel test environment | |
| run: uv venv --seed .wheel-venv | |
| - name: Install wheel using pip | |
| run: .wheel-venv/bin/python -m pip install --no-cache-dir dist/*.whl | |
| - name: Test wheel import | |
| run: .wheel-venv/bin/python -c 'import pystormtracker; print("wheel import success")' | |
| - name: Test packaged TrackJSON schema | |
| run: >- | |
| .wheel-venv/bin/python -c | |
| 'import json; from importlib.resources import files; | |
| schema_bytes = files("pystormtracker.schemas").joinpath("trackjson.schema.json").read_bytes(); | |
| json.loads(schema_bytes); print("packaged TrackJSON schema is valid")' | |
| - name: Test wheel CLI | |
| run: .wheel-venv/bin/stormtracker --help | |
| - name: Create source-distribution test environment | |
| if: github.event_name != 'pull_request' | |
| run: uv venv --seed .sdist-venv | |
| - name: Install source distribution using pip | |
| if: github.event_name != 'pull_request' | |
| run: .sdist-venv/bin/python -m pip install --no-cache-dir dist/*.tar.gz | |
| - name: Test source-distribution import | |
| if: github.event_name != 'pull_request' | |
| run: .sdist-venv/bin/python -c 'import pystormtracker; print("sdist import success")' | |
| - name: Upload release distributions | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-dists | |
| path: dist/ | |
| if-no-files-found: error | |
| docs: | |
| name: Build documentation | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@v10.0.1 | |
| with: | |
| version: ${{ env.UV_VERSION }} | |
| enable-cache: true | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Build documentation | |
| run: uv run --no-dev --group docs sphinx-build -W --keep-going -b html docs docs/_build/html | |
| docker-build: | |
| name: Build and test Docker images | |
| if: github.event_name != 'workflow_dispatch' | |
| needs: | |
| [select-test-matrix, code-quality, unit-tests, integration-tests, parity-tests, package-tests, docs] | |
| uses: ./.github/workflows/docker-build.yml | |
| with: | |
| push_images: ${{ github.event_name != 'pull_request' }} | |
| ref: ${{ github.sha }} | |
| uv_version: ${{ needs.select-test-matrix.outputs.uv_version }} | |
| secrets: | |
| DOCKER_HUB_ACCESS_TOKEN: ${{ github.event_name != 'pull_request' && secrets.DOCKER_HUB_ACCESS_TOKEN || '' }} | |
| release_tag: | |
| name: Validate release tag | |
| if: github.ref_type == 'tag' | |
| needs: [code-quality, unit-tests, integration-tests, parity-tests, package-tests, docs] | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 10 | |
| outputs: | |
| python_repository: ${{ steps.release.outputs.python_repository }} | |
| version: ${{ steps.release.outputs.version }} | |
| release_tag: ${{ steps.release.outputs.release_tag }} | |
| docker_target: ${{ steps.release.outputs.docker_target }} | |
| steps: | |
| - uses: actions/checkout@v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify tag is reachable from main | |
| run: | | |
| git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main | |
| git merge-base --is-ancestor "$GITHUB_SHA" origin/main | |
| - name: Validate release tag and package version | |
| id: release | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| VERSION="${TAG#v}" | |
| PACKAGE_VERSION="$(sed -n 's/^version = "\([^"]*\)"/\1/p' pyproject.toml)" | |
| test "$PACKAGE_VERSION" = "$VERSION" | |
| if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "python_repository=pypi" >> "$GITHUB_OUTPUT" | |
| echo "docker_target=release" >> "$GITHUB_OUTPUT" | |
| elif [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$ ]]; then | |
| echo "python_repository=testpypi" >> "$GITHUB_OUTPUT" | |
| echo "docker_target=" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Unsupported version tag: $TAG" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "release_tag=$TAG" >> "$GITHUB_OUTPUT" | |
| publish-docker-edge: | |
| name: Publish Docker edge image | |
| if: github.ref == 'refs/heads/main' | |
| needs: docker-build | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| uses: ./.github/workflows/docker-publish.yml | |
| with: | |
| source_tag: ${{ needs.docker-build.outputs.source_tag }} | |
| target: edge | |
| secrets: | |
| DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | |
| GHCR_PAT: ${{ secrets.GHCR_PAT }} | |
| publish-docker-release: | |
| name: Publish Docker release image | |
| if: github.ref_type == 'tag' && needs.release_tag.outputs.docker_target == 'release' | |
| needs: [docker-build, release_tag] | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| uses: ./.github/workflows/docker-publish.yml | |
| with: | |
| source_tag: ${{ needs.docker-build.outputs.source_tag }} | |
| target: release | |
| version: ${{ needs.release_tag.outputs.release_tag }} | |
| secrets: | |
| DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | |
| GHCR_PAT: ${{ secrets.GHCR_PAT }} |