Skip to content

Merge pull request #205 from mwyau/docs/api-wording-cleanup #829

Merge pull request #205 from mwyau/docs/api-wording-cleanup

Merge pull request #205 from mwyau/docs/api-wording-cleanup #829

Workflow file for this run

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 }}