Skip to content

CI/CD Modernisation: Fully uv-native Workflows #551

CI/CD Modernisation: Fully uv-native Workflows

CI/CD Modernisation: Fully uv-native Workflows #551

Workflow file for this run

# =====================================================================
# Main CI Workflow (Fully uv-native)
#
# Goals:
# - No setup-python
# - No system Python usage
# - All interpreters provisioned via uv
# - All execution via uv run
# - Deterministic environments
# - Explicit Python pinning per matrix job
#
# This workflow validates:
# - Code quality via pre-commit
# - Core functionality without soft dependencies
# - Full functionality with extras
# - Coverage generation
# - Notebook execution
#
# Interpreter resolution:
# - Each job installs and pins its own Python version
# - .python-version is ignored in matrix jobs (overridden explicitly)
#
# =====================================================================
name: pytest
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
# Prevent overlapping CI runs on same branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ================================================================
# 1️⃣ CODE QUALITY
#
# Runs pre-commit only on changed files (for PRs).
# Uses uv-managed Python.
#
# We explicitly:
# - install Python via uv
# - pin interpreter
# - run pre-commit through uv
#
# No setup-python.
# ================================================================
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Determine changed files (PR only)
id: changed-files
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
CHANGED_FILES=$(git diff --name-only \
${{ github.event.pull_request.base.sha }} \
${{ github.sha }} | tr '\n' ' ')
else
# On push to main, check entire repository
CHANGED_FILES=$(git ls-files | tr '\n' ' ')
fi
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
- name: Run pre-commit
run: |
if [ -n "$CHANGED_FILES" ]; then
uvx pre-commit run \
--color always \
--files $CHANGED_FILES \
--show-diff-on-failure
else
echo "No changed files to check."
fi
# ================================================================
# 2️⃣ CORE TESTS (No Soft Dependencies)
#
# Tests package with only base + dev dependencies.
#
# For each Python version + OS:
# - Install Python via uv
# - Pin interpreter
# - Install dependencies via uv
# - Execute via uv run
#
# Ensures:
# - minimal dependency footprint works
# - no accidental hard dependency on extras
# ================================================================
pytest-nosoftdeps:
needs: code-quality
name: nosoftdeps (${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
env:
MPLBACKEND: Agg
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install and pin Python ${{ matrix.python-version }}
run: |
uv python install ${{ matrix.python-version }}
uv python pin ${{ matrix.python-version }}
uv run python --version
- name: Install base + dev dependencies
run: |
uv venv --clear
uv pip install ".[dev]"
- name: Verify dependency graph
run: uv pip check
- name: Run pytest (core only)
run: uv run pytest ./tests
# ================================================================
# 3️⃣ FULL TEST MATRIX (All Extras)
#
# Same as above but includes optional extras.
#
# Validates:
# - optional dependency combinations
# - platform compatibility
# ================================================================
pytest:
needs: pytest-nosoftdeps
name: full (${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
env:
MPLBACKEND: Agg
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install and pin Python ${{ matrix.python-version }}
run: |
uv python install ${{ matrix.python-version }}
uv python pin ${{ matrix.python-version }}
uv run python --version
- name: Install full dependency set
run: |
uv venv --clear
uv pip install ".[dev,all_extras]"
- name: Verify dependency graph
run: uv pip check
- name: Run full pytest suite
run: uv run pytest ./tests
# ================================================================
# 4️⃣ COVERAGE (Single Reference Interpreter)
#
# We run coverage only once (e.g., Python 3.12)
# to reduce CI cost.
#
# Coverage executed via uv-managed environment.
# ================================================================
codecov:
name: coverage (3.12 on ubuntu)
runs-on: ubuntu-latest
needs: code-quality
env:
MPLBACKEND: Agg
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
# this is somewhat obsolete as we have .python-version...
- name: Install and pin Python 3.12
run: |
uv python install 3.12
uv python pin 3.12
- name: Install dependencies
run: |
uv venv --clear
uv pip install ".[dev,all_extras]"
uv pip install pytest-cov
- name: Generate coverage report
run: |
uv run pytest \
--cov=./ \
--cov-report=xml
# Upload currently disabled
- name: Upload coverage to Codecov
if: false
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
fail_ci_if_error: true
# ================================================================
# 5️⃣ NOTEBOOK TESTING
#
# Executes Jupyter notebooks via nbmake.
#
# For each Python version:
# - Install interpreter via uv
# - Install notebook test dependencies
# - Discover notebooks dynamically
# - Execute via uv run
#
# Ensures:
# - Documentation stays executable
# - No silent drift between code and notebooks
# ================================================================
notebooks:
needs: code-quality
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install and pin Python ${{ matrix.python-version }}
run: |
uv python install ${{ matrix.python-version }}
uv python pin ${{ matrix.python-version }}
uv run python --version
- name: Install notebook dependencies
run: |
uv venv --clear
uv pip install ".[dev,all_extras,notebook_test]"
- name: Collect notebooks
id: notebooks
run: |
NOTEBOOKS=$(find cookbook -name '*.ipynb' -print0 | xargs -0 echo)
echo "notebooks=$NOTEBOOKS" >> $GITHUB_OUTPUT
- name: Execute notebooks
run: |
uv run pytest \
--reruns 3 \
--nbmake \
--nbmake-timeout=3600 \
-vv \
${{ steps.notebooks.outputs.notebooks }}