From 3507219bfbfecb1b10bc579cbf8418ce56fe468f Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 1 Mar 2026 12:15:26 +0400 Subject: [PATCH 1/9] Create .python-version --- .python-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 From 26495f3e1e39f52111097449894395b563600583 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 1 Mar 2026 17:16:15 +0400 Subject: [PATCH 2/9] retire the setup-project action --- .github/actions/setup-project/action.yml | 81 ------ .github/workflows/main.yml | 325 ++++++++++++++--------- .github/workflows/release.yml | 231 ++++++++++++---- 3 files changed, 369 insertions(+), 268 deletions(-) delete mode 100644 .github/actions/setup-project/action.yml diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml deleted file mode 100644 index 21b22333..00000000 --- a/.github/actions/setup-project/action.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Action: Setup Project (composite action) -# -# Purpose: Bootstrap a Python project within GitHub Actions by: -# - Installing uv and uvx into a local ./bin directory and adding it to PATH -# - Detecting the presence of pyproject.toml and exposing that as an output -# - Creating a virtual environment with uv and (optionally) syncing dependencies -# -# Inputs: -# - python-version: Python version for the uv-managed virtual environment (default: 3.12) -# -# Outputs: -# - pyproject_exists: "true" if pyproject.toml exists, otherwise "false" -# -# Notes: -# - Safe to run in repositories without pyproject.toml; dependency sync will be skipped. -# - Purely a CI helper — it does not modify repository files. - -name: 'Setup Project' -description: 'Setup the project' - -inputs: - python-version: - description: 'Python version to use' - required: false - default: '3.12' - -outputs: - pyproject_exists: - description: 'Flag indicating whether pyproject.toml exists' - value: ${{ steps.check_pyproject.outputs.exists }} - -runs: - using: 'composite' - steps: - - name: Set up uv, uvx and the venv - shell: bash - run: | - mkdir -p bin - - # Add ./bin to the PATH - echo "Adding ./bin to PATH" - echo "$(pwd)/bin" >> $GITHUB_PATH - - # Install uv and uvx - curl -fsSL https://astral.sh/uv/install.sh | UV_INSTALL_DIR="./bin" sh - - - name: Check version for uv - shell: bash - run: | - uv --version - - - name: Check for pyproject.toml - id: check_pyproject - shell: bash - run: | - if [ -f "pyproject.toml" ]; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Build the virtual environment - shell: bash - run: uv venv --python ${{ inputs.python-version }} - - - name: "Sync the virtual environment for ${{ github.repository }} if pyproject.toml exists" - shell: bash - run: | - if [ -f "pyproject.toml" ]; then - uv sync --all-extras - else - echo "No pyproject.toml found, skipping package installation" - fi - - - name: Show dependencies - shell: bash - run: uv pip list - - - name: Show Python version - shell: bash - run: uv run python -c "import sys; print(sys.version)" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6cc45e03..883e5079 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,26 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python +# ===================================================================== +# 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 @@ -9,222 +30,268 @@ on: 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@v6 - - - uses: actions/setup-python@v6 + - uses: actions/checkout@v4 with: - python-version: '3.14' - - - name: install pre-commit - run: python3 -m pip install pre-commit + fetch-depth: 0 - - name: Checkout code - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 with: - fetch-depth: 0 + enable-cache: true + + - name: Install pre-commit + run: uv pip install pre-commit - - name: Get changed files + - name: Determine changed files (PR only) id: changed-files run: | - CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | tr '\n' ' ') - echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV + 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 - - name: Print changed files - run: | - echo "Changed files: $CHANGED_FILES" + echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV - - name: Run pre-commit on changed files + - name: Run pre-commit run: | if [ -n "$CHANGED_FILES" ]; then - pre-commit run --color always --files $CHANGED_FILES --show-diff-on-failure + uv run 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 # https://github.com/orgs/community/discussions/26434 + 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"] - fail-fast: false # to not fail all combinations if just one fails steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@v7 with: enable-cache: true - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} + - 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: Display Python version - run: python -c "import sys; print(sys.version)" + - name: Install base + dev dependencies + run: | + uv pip install ".[dev]" - - name: Install dependencies - shell: bash - run: uv pip install ".[dev]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 + - name: Verify dependency graph + run: uv pip check - - name: Show dependencies - run: uv pip list + - name: Run pytest (core only) + run: uv run pytest ./tests - - name: Test with pytest - 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: (${{ matrix.python-version }}, ${{ matrix.os }}) + name: full (${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} + env: - MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 + 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"] - fail-fast: false # to not fail all combinations if just one fails steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@v7 with: enable-cache: true - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} + - 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: Display Python version - run: python -c "import sys; print(sys.version)" + - name: Install full dependency set + run: | + uv pip install ".[dev,all_extras]" - - name: Install dependencies - shell: bash - run: uv pip install ".[dev,all_extras]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 + - name: Verify dependency graph + run: uv pip check - - name: Show dependencies - run: uv pip list + - name: Run full pytest suite + run: uv run pytest ./tests - - name: Test with pytest - 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 (${{ matrix.python-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + name: coverage (3.12 on ubuntu) + runs-on: ubuntu-latest needs: code-quality + env: - MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.12"] + MPLBACKEND: Agg steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@v7 with: enable-cache: true - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Display Python version - run: python -c "import sys; print(sys.version)" + # 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 - shell: bash - run: uv pip install ".[dev,all_extras]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 - - - name: Show dependencies - run: uv pip list + run: | + uv pip install ".[dev,all_extras]" + uv pip install pytest-cov - name: Generate coverage report run: | - pip install pytest pytest-cov - pytest --cov=./ --cov-report=xml + uv run pytest \ + --cov=./ \ + --cov-report=xml + # Upload currently disabled - name: Upload coverage to Codecov - # if false in order to skip for now 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: - matrix: - python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ] fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v6 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - - name: Install dependencies - shell: bash - run: uv pip install ".[dev,all_extras,notebook_test]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 - - - name: Show dependencies - run: uv pip list - - # Discover all notebooks - - name: Collect notebooks - id: notebooks - shell: bash - run: | - NOTEBOOKS=$(find cookbook -name '*.ipynb' -print0 | xargs -0 echo) - echo "notebooks=$NOTEBOOKS" >> $GITHUB_OUTPUT - - # Run all discovered notebooks with nbmake - - name: Test notebooks - shell: bash - run: | - uv run pytest --reruns 3 --nbmake --nbmake-timeout=3600 -vv ${{ steps.notebooks.outputs.notebooks }} + - 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 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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6b50c32..fd8b4b10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,114 +1,229 @@ +# ================================================================ +# PyPI Release Workflow (Fully uv-native, no setup-python) +# +# Design principles: +# - Use uv for EVERYTHING (Python provisioning, build, install, run) +# - Never rely on system Python or PATH resolution +# - Never test source installs — always test the built wheel +# - Enforce tag == pyproject.toml version +# - Use OIDC for secure PyPI publishing +# +# Interpreter resolution hierarchy: +# - Local development: .python-version +# - Build job: .python-version (implicitly) +# - Test matrix: uv python pin (overrides) +# +# Result: +# Deterministic, explicit, infrastructure-grade release pipeline. +# ================================================================ + name: PyPI Release +# ------------------------------------------------ +# Trigger only when a GitHub Release is published. +# This prevents accidental uploads from branch pushes. +# ------------------------------------------------ on: release: types: [published] jobs: + + # ================================================================ + # 1️⃣ CHECK TAG CONSISTENCY + # + # Ensures that: + # git tag vX.Y.Z == project.version in pyproject.toml + # + # This prevents publishing a version that does not match + # the declared package metadata. + # + # We run inside the official uv container to: + # - guarantee Python 3.11+ (for tomllib) + # - avoid system dependency drift + # ================================================================ check_tag: - name: Check tag runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 + container: + image: ghcr.io/astral-sh/uv:0.9.30-bookworm - - uses: actions/setup-python@v6 - with: - python-version: '3.11' + steps: + - uses: actions/checkout@v4 - - shell: bash + - name: Validate tag matches pyproject version run: | + # Extract tag name from GitHub event TAG="${{ github.event.release.tag_name }}" + + # Remove leading "v" (common semantic versioning style) GH_TAG_NAME="${TAG#v}" + + # Read version from pyproject.toml using Python + # We rely on tomllib (Python 3.11+) for robust parsing. PY_VERSION=$(python - <<'PY' import pathlib, tomllib - data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) - print(data.get("project").get("version")) + data = tomllib.loads(pathlib.Path("pyproject.toml").read_text()) + print(data["project"]["version"]) PY ) - if [ "${GH_TAG_NAME}" != "${PY_VERSION}" ]; then - echo "::error::Tag (${GH_TAG_NAME}) does not match pyproject.toml version (${PY_VERSION})." - exit 2 + + # Fail fast if mismatch + if [ "$GH_TAG_NAME" != "$PY_VERSION" ]; then + echo "::error::Tag ($GH_TAG_NAME) does not match pyproject version ($PY_VERSION)" + exit 1 fi - build_wheels: - name: Build wheels + # ================================================================ + # 2️⃣ BUILD DISTRIBUTIONS + # + # Build wheel (.whl) and source distribution (.tar.gz) + # using uv's native build backend. + # + # IMPORTANT: + # The Python version used here is determined by: + # - .python-version + # + # Since we are inside a uv container, + # uv resolves Python deterministically. + # + # Artifacts are uploaded and used by the test job. + # We NEVER rebuild during testing. + # ================================================================ + build: runs-on: ubuntu-latest - needs: [check_tag] + needs: check_tag - steps: - - uses: actions/checkout@v6 + container: + image: ghcr.io/astral-sh/uv:0.9.30-bookworm - - uses: actions/setup-python@v6 - with: - python-version: '3.11' + steps: + - uses: actions/checkout@v4 - - name: Build wheel + - name: Build wheel and sdist run: | - python -m pip install build - python -m build --wheel --sdist --outdir wheelhouse + # uv build uses pyproject.toml backend + # and respects .python-version + uv build --out-dir wheelhouse + + - name: Show built artifacts (sanity check) + run: ls -lah wheelhouse - - name: Store wheels - uses: actions/upload-artifact@v7 + - name: Upload build artifacts + uses: actions/upload-artifact@v4 with: - name: wheels + name: dist path: wheelhouse/* - pytest-nosoftdeps: - name: no-softdeps - runs-on: ${{ matrix.os }} - needs: [build_wheels] + # ================================================================ + # 3️⃣ TEST BUILT WHEEL (Matrix) + # + # This is the most important job. + # + # We test the BUILT WHEEL, not the source tree. + # This guarantees: + # - package data is correct + # - MANIFEST configuration is correct + # - metadata is correct + # - no accidental source leakage + # + # We do NOT use setup-python. + # Instead: + # - uv installs the requested interpreter + # - uv pins that interpreter for the project + # + # Matrix overrides .python-version deterministically. + # + # Execution uses: + # uv run pytest + # to ensure uv-managed execution environment. + # ================================================================ + test: + needs: build + runs-on: ubuntu-latest + + container: + image: ghcr.io/astral-sh/uv:0.9.30-bookworm + strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + # Define supported Python versions. + # These should align with: + # - pyproject.toml requires-python + # - your long-term support policy + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - uses: actions/download-artifact@v4 with: - python-version: ${{ matrix.python-version }} + name: dist + path: wheelhouse - - name: Setup macOS - if: runner.os == 'macOS' + - name: Install and pin Python ${{ matrix.python-version }} run: | - brew install libomp # https://github.com/pytorch/pytorch/issues/20030 + # Install requested interpreter version + uv python install ${{ matrix.python-version }} - - name: Get full Python version - id: full-python-version - shell: bash - run: echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT + # Pin interpreter for this project context. + # This overrides .python-version for the job. + uv python pin ${{ matrix.python-version }} - - name: Install dependencies - shell: bash - run: | - pip install ".[dev]" + # Show interpreter for auditability + uv run python --version - - name: Show dependencies - run: python -m pip list + - name: Install built wheel with dev dependencies + run: | + # Install the wheel artifact (NOT source install) + # [dev] assumes pytest is declared as optional dependency. + uv pip install wheelhouse/*.whl[dev] - - name: Run pytest - shell: bash - run: python -m pytest tests + - name: Verify dependency graph integrity + run: | + # Equivalent to pip check + # Ensures no broken or conflicting dependencies + uv pip check - upload_wheels: - name: Upload wheels to PyPI + - name: Run test suite inside uv-managed environment + run: | + # Explicit execution through uv ensures: + # - correct interpreter + # - correct environment + # - no PATH ambiguity + uv run pytest tests + + # ================================================================ + # 4️⃣ PUBLISH TO PYPI + # + # Only runs if: + # - Tag matches version + # - Build succeeded + # - All matrix tests succeeded + # + # Uses OIDC authentication: + # - No API token stored + # - No secrets leakage + # + # Uploads EXACTLY the artifacts tested above. + # No rebuild occurs here. + # ================================================================ + publish: + needs: test runs-on: ubuntu-latest - needs: [pytest-nosoftdeps] permissions: + # Required for trusted publishing via OIDC id-token: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v4 with: - name: wheels + name: dist path: wheelhouse - - name: Publish package to PyPI + - name: Publish distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: wheelhouse/ From 1b52b6d826c251cc261308e9f31e012d0b45cf94 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 1 Mar 2026 17:21:34 +0400 Subject: [PATCH 3/9] use uvx for pre-commit --- .github/workflows/main.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 883e5079..8c354241 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,9 +62,6 @@ jobs: with: enable-cache: true - - name: Install pre-commit - run: uv pip install pre-commit - - name: Determine changed files (PR only) id: changed-files run: | @@ -82,7 +79,7 @@ jobs: - name: Run pre-commit run: | if [ -n "$CHANGED_FILES" ]; then - uv run pre-commit run \ + uvx pre-commit run \ --color always \ --files $CHANGED_FILES \ --show-diff-on-failure From 93c25d449f3eb72145534227ca02ecd5801cf3df Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 1 Mar 2026 17:25:58 +0400 Subject: [PATCH 4/9] fix CI: create venv before uv pip install uv pip install requires a virtual environment; add `uv venv` before each install step across all jobs. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c354241..b96d4ff7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -132,6 +132,7 @@ jobs: - name: Install base + dev dependencies run: | + uv venv uv pip install ".[dev]" - name: Verify dependency graph @@ -179,6 +180,7 @@ jobs: - name: Install full dependency set run: | + uv venv uv pip install ".[dev,all_extras]" - name: Verify dependency graph @@ -219,6 +221,7 @@ jobs: - name: Install dependencies run: | + uv venv uv pip install ".[dev,all_extras]" uv pip install pytest-cov @@ -276,6 +279,7 @@ jobs: - name: Install notebook dependencies run: | + uv venv uv pip install ".[dev,all_extras,notebook_test]" - name: Collect notebooks From 00c3d3621c55f8399bb9b35348e88286b37d1bc1 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 1 Mar 2026 17:30:26 +0400 Subject: [PATCH 5/9] fix CI: use uv venv --clear to handle pre-existing venvs Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b96d4ff7..6bdda960 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -132,7 +132,7 @@ jobs: - name: Install base + dev dependencies run: | - uv venv + uv venv --clear uv pip install ".[dev]" - name: Verify dependency graph @@ -180,7 +180,7 @@ jobs: - name: Install full dependency set run: | - uv venv + uv venv --clear uv pip install ".[dev,all_extras]" - name: Verify dependency graph @@ -221,7 +221,7 @@ jobs: - name: Install dependencies run: | - uv venv + uv venv --clear uv pip install ".[dev,all_extras]" uv pip install pytest-cov @@ -279,7 +279,7 @@ jobs: - name: Install notebook dependencies run: | - uv venv + uv venv --clear uv pip install ".[dev,all_extras,notebook_test]" - name: Collect notebooks From 4184e4701eeee183e238832839e3616080fab21b Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 1 Mar 2026 17:37:17 +0400 Subject: [PATCH 6/9] fix CI: update Python versions and dependency installation steps - Add Python 3.14 to test matrix. - Remove obsolete container specification and redundant pinning steps. - Improve dependency installation with dynamic version handling and additional extras. --- .github/workflows/main.yml | 6 ------ .github/workflows/release.yml | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6bdda960..32fa64ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -213,12 +213,6 @@ jobs: 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd8b4b10..201cd51e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,9 +45,6 @@ jobs: check_tag: runs-on: ubuntu-latest - container: - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - steps: - uses: actions/checkout@v4 @@ -152,7 +149,7 @@ jobs: # These should align with: # - pyproject.toml requires-python # - your long-term support policy - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 @@ -164,6 +161,15 @@ jobs: - name: Install and pin Python ${{ matrix.python-version }} run: | + # Read version from pyproject.toml + PY_VERSION=$(python - <<'PY' + import pathlib, tomllib + data = tomllib.loads(pathlib.Path("pyproject.toml").read_text()) + print(data["project"]["version"]) + PY + ) + echo "PY_VERSION=${PY_VERSION}" >> $GITHUB_ENV + # Install requested interpreter version uv python install ${{ matrix.python-version }} @@ -177,8 +183,8 @@ jobs: - name: Install built wheel with dev dependencies run: | # Install the wheel artifact (NOT source install) - # [dev] assumes pytest is declared as optional dependency. - uv pip install wheelhouse/*.whl[dev] + # [dev,all_extras] assumes these are declared as optional dependencies. + uv pip install "wheelhouse/pyportfolioopt-${PY_VERSION}-py3-none-any.whl[dev,all_extras]" - name: Verify dependency graph integrity run: | From f830f0bedb191dabe94596d5df7cb1e49e9b1f55 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Mon, 2 Mar 2026 07:51:17 +0400 Subject: [PATCH 7/9] fix CI: add notebook change detection, expand release matrix, clean up dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.yml: add detect-notebooks-change job; gate notebooks on it to skip expensive runs on unrelated PRs (always fires on push to main) - main.yml: remove dead if: false codecov upload step - release.yml: drop Linux-only container from test job; add macOS and Windows to release test matrix (5 → 15 jobs) - release.yml: rename PY_VERSION → PKG_VERSION to avoid confusion with the Python interpreter version Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yml | 48 +++++++++++++++++++++++++++++------ .github/workflows/release.yml | 27 ++++++++++++-------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 32fa64ea..a092cdf7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -225,19 +225,50 @@ jobs: --cov=./ \ --cov-report=xml - # Upload currently disabled - - name: Upload coverage to Codecov - if: false - uses: codecov/codecov-action@v5 + + # ================================================================ + # 5️⃣ NOTEBOOK CHANGE DETECTION + # + # Checks whether any notebook-relevant paths changed. + # Skips expensive notebook execution on unrelated PRs. + # + # On push to main, always runs notebooks (something was merged). + # On PRs, diffs against origin/main to detect changes in: + # - cookbook/ (notebooks themselves) + # - pypfopt/ (library code notebooks depend on) + # - pyproject.toml (dependency changes) + # ================================================================ + detect-notebooks-change: + needs: code-quality + runs-on: ubuntu-latest + outputs: + notebooks: ${{ steps.check.outputs.notebooks }} + + steps: + - uses: actions/checkout@v4 with: - files: ./coverage.xml - fail_ci_if_error: true + fetch-depth: 0 + + - name: Fetch main branch + run: git fetch origin main + + - name: Check if notebook-related files changed + id: check + run: | + if [ "${{ github.event_name }}" = "push" ]; then + echo "notebooks=true" >> $GITHUB_OUTPUT + elif git diff --quiet origin/main -- cookbook/ pypfopt/ pyproject.toml; then + echo "notebooks=false" >> $GITHUB_OUTPUT + else + echo "notebooks=true" >> $GITHUB_OUTPUT + fi # ================================================================ - # 5️⃣ NOTEBOOK TESTING + # 6️⃣ NOTEBOOK TESTING # # Executes Jupyter notebooks via nbmake. + # Only runs when notebook-relevant files changed (see above). # # For each Python version: # - Install interpreter via uv @@ -250,7 +281,8 @@ jobs: # - No silent drift between code and notebooks # ================================================================ notebooks: - needs: code-quality + needs: detect-notebooks-change + if: ${{ needs.detect-notebooks-change.outputs.notebooks == 'true' }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 201cd51e..6b6bff19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: # Read version from pyproject.toml using Python # We rely on tomllib (Python 3.11+) for robust parsing. - PY_VERSION=$(python - <<'PY' + PKG_VERSION=$(python - <<'PY' import pathlib, tomllib data = tomllib.loads(pathlib.Path("pyproject.toml").read_text()) print(data["project"]["version"]) @@ -66,8 +66,8 @@ jobs: ) # Fail fast if mismatch - if [ "$GH_TAG_NAME" != "$PY_VERSION" ]; then - echo "::error::Tag ($GH_TAG_NAME) does not match pyproject version ($PY_VERSION)" + if [ "$GH_TAG_NAME" != "$PKG_VERSION" ]; then + echo "::error::Tag ($GH_TAG_NAME) does not match pyproject version ($PKG_VERSION)" exit 1 fi @@ -137,10 +137,7 @@ jobs: # ================================================================ test: needs: build - runs-on: ubuntu-latest - - container: - image: ghcr.io/astral-sh/uv:0.9.30-bookworm + runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -150,25 +147,31 @@ jobs: # - pyproject.toml requires-python # - your long-term support policy python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + - uses: actions/download-artifact@v4 with: name: dist path: wheelhouse - name: Install and pin Python ${{ matrix.python-version }} + shell: bash run: | - # Read version from pyproject.toml - PY_VERSION=$(python - <<'PY' + # Read package version from pyproject.toml + PKG_VERSION=$(python - <<'PY' import pathlib, tomllib data = tomllib.loads(pathlib.Path("pyproject.toml").read_text()) print(data["project"]["version"]) PY ) - echo "PY_VERSION=${PY_VERSION}" >> $GITHUB_ENV + echo "PKG_VERSION=${PKG_VERSION}" >> $GITHUB_ENV # Install requested interpreter version uv python install ${{ matrix.python-version }} @@ -181,10 +184,12 @@ jobs: uv run python --version - name: Install built wheel with dev dependencies + shell: bash run: | # Install the wheel artifact (NOT source install) # [dev,all_extras] assumes these are declared as optional dependencies. - uv pip install "wheelhouse/pyportfolioopt-${PY_VERSION}-py3-none-any.whl[dev,all_extras]" + uv venv --clear + uv pip install "wheelhouse/pyportfolioopt-${PKG_VERSION}-py3-none-any.whl[dev,all_extras]" - name: Verify dependency graph integrity run: | From ce6f273d49d7284eac30812b7f3df2efa7b8c7e4 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Mon, 2 Mar 2026 07:57:11 +0400 Subject: [PATCH 8/9] fix CI: use uv in check_tag, unpin build container image - check_tag: provision Python via uv run instead of system Python, consistent with the fully uv-native philosophy - build: switch container from pinned uv:0.9.30-bookworm to uv:bookworm so it tracks the latest uv release without manual bumps Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/release.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b6bff19..62a574a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,9 +38,8 @@ jobs: # This prevents publishing a version that does not match # the declared package metadata. # - # We run inside the official uv container to: - # - guarantee Python 3.11+ (for tomllib) - # - avoid system dependency drift + # Python is provisioned via uv (consistent with all other jobs). + # tomllib is stdlib in Python 3.11+. # ================================================================ check_tag: runs-on: ubuntu-latest @@ -48,6 +47,10 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + - name: Validate tag matches pyproject version run: | # Extract tag name from GitHub event @@ -56,9 +59,9 @@ jobs: # Remove leading "v" (common semantic versioning style) GH_TAG_NAME="${TAG#v}" - # Read version from pyproject.toml using Python - # We rely on tomllib (Python 3.11+) for robust parsing. - PKG_VERSION=$(python - <<'PY' + # Read version from pyproject.toml via uv-managed Python. + # tomllib is stdlib in Python 3.11+. + PKG_VERSION=$(uv run python - <<'PY' import pathlib, tomllib data = tomllib.loads(pathlib.Path("pyproject.toml").read_text()) print(data["project"]["version"]) @@ -92,7 +95,7 @@ jobs: needs: check_tag container: - image: ghcr.io/astral-sh/uv:0.9.30-bookworm + image: ghcr.io/astral-sh/uv:bookworm steps: - uses: actions/checkout@v4 From ccc425e0a2e2ea1b9d8154b02825426a8c1a8390 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Mon, 2 Mar 2026 08:08:49 +0400 Subject: [PATCH 9/9] CI: upload coverage report as artifact Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a092cdf7..a1f25827 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -225,6 +225,12 @@ jobs: --cov=./ \ --cov-report=xml + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage.xml + # ================================================================ # 5️⃣ NOTEBOOK CHANGE DETECTION