diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 15dbf01..682e716 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -130,32 +130,108 @@ jobs: # Keep release jobs here because trusted-publisher configuration points to # CI.yml specifically. # ------------------------------------------------------------------------- - build-wheels: - name: Build wheels (${{ matrix.os }}) - runs-on: ${{ matrix.os }} + build-wheels-linux: + name: Build wheels (linux / py${{ matrix.python-version }}) + runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v6 - - name: Build wheels + - name: Build wheel uses: PyO3/maturin-action@v1 with: command: build - args: --release --out dist - manylinux: auto - # Build for both Intel and Apple Silicon on macOS - target: ${{ matrix.os == 'macos-latest' && 'universal2-apple-darwin' || '' }} + args: --release --out dist --compatibility pypi -i python${{ matrix.python-version }} + manylinux: "2_17" - name: Upload wheels as artifact uses: actions/upload-artifact@v7 with: - name: wheels-${{ matrix.os }} - path: dist + name: wheels-linux-py${{ matrix.python-version }} + path: dist/*.whl + + build-wheels-macos: + name: Build wheels (macos / py${{ matrix.python-version }}) + runs-on: macos-latest + if: github.event_name == 'release' && github.event.action == 'published' + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Build universal2 wheel + uses: PyO3/maturin-action@v1 + with: + command: build + args: --release --out dist --compatibility pypi -i python + target: universal2-apple-darwin + + - name: Upload wheels as artifact + uses: actions/upload-artifact@v7 + with: + name: wheels-macos-py${{ matrix.python-version }} + path: dist/*.whl + + build-wheels-windows: + name: Build wheels (windows / py${{ matrix.python-version }}) + runs-on: windows-latest + if: github.event_name == 'release' && github.event.action == 'published' + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Build wheel + uses: PyO3/maturin-action@v1 + with: + command: build + args: --release --out dist --compatibility pypi -i python + + - name: Upload wheels as artifact + uses: actions/upload-artifact@v7 + with: + name: wheels-windows-py${{ matrix.python-version }} + path: dist/*.whl + + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/checkout@v6 + + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + + - name: Upload sdist as artifact + uses: actions/upload-artifact@v7 + with: + name: sdist + path: dist/*.tar.gz # ------------------------------------------------------------------------- # Publish to PyPI @@ -163,7 +239,11 @@ jobs: publish: name: Publish to PyPI runs-on: ubuntu-latest - needs: build-wheels + needs: + - build-wheels-linux + - build-wheels-macos + - build-wheels-windows + - build-sdist if: github.event_name == 'release' && github.event.action == 'published' environment: name: pypi @@ -178,6 +258,52 @@ jobs: merge-multiple: true path: dist + - name: Download source distribution + uses: actions/download-artifact@v8 + with: + name: sdist + path: dist + + - name: Verify distribution coverage + run: | + python3 - <<'PY' + import fnmatch + import sys + from pathlib import Path + + dist = Path("dist") + files = sorted(p.name for p in dist.iterdir() if p.is_file()) + print("Distributions:") + for name in files: + print(f" - {name}") + + expected = [ + "ferro_ta-*-cp310-cp310-manylinux*_x86_64.whl", + "ferro_ta-*-cp311-cp311-manylinux*_x86_64.whl", + "ferro_ta-*-cp312-cp312-manylinux*_x86_64.whl", + "ferro_ta-*-cp313-cp313-manylinux*_x86_64.whl", + "ferro_ta-*-cp310-cp310-win_amd64.whl", + "ferro_ta-*-cp311-cp311-win_amd64.whl", + "ferro_ta-*-cp312-cp312-win_amd64.whl", + "ferro_ta-*-cp313-cp313-win_amd64.whl", + "ferro_ta-*-cp310-cp310-macosx*_universal2.whl", + "ferro_ta-*-cp311-cp311-macosx*_universal2.whl", + "ferro_ta-*-cp312-cp312-macosx*_universal2.whl", + "ferro_ta-*-cp313-cp313-macosx*_universal2.whl", + "ferro_ta-*.tar.gz", + ] + + missing = [ + pattern for pattern in expected + if not any(fnmatch.fnmatch(name, pattern) for name in files) + ] + if missing: + print("Missing expected distributions:") + for pattern in missing: + print(f" - {pattern}") + sys.exit(1) + PY + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 @@ -207,7 +333,7 @@ jobs: sbom: name: Generate SBOM (Python + Rust) runs-on: ubuntu-latest - needs: build-wheels + needs: publish if: github.event_name == 'release' && github.event.action == 'published' permissions: contents: write diff --git a/PACKAGING.md b/PACKAGING.md index d8385e3..1d2cd4e 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -5,6 +5,15 @@ This document describes how ferro-ta is packaged and published. ## PyPI (pip) Wheels are built by CI on release (see [RELEASE.md](RELEASE.md)). +Release publishing currently targets CPython 3.10, 3.11, 3.12, and 3.13 on: + +- Linux x86_64 (`manylinux_2_17` / `manylinux2014`) +- macOS universal2 (covers Intel and Apple Silicon) +- Windows x86_64 + +Each release also publishes a source distribution (`sdist`) so compatible +environments outside the wheel matrix can still build from source. + Publishing uses PyPI Trusted Publishing via GitHub OIDC; no long-lived PyPI API token is required. diff --git a/PLATFORMS.md b/PLATFORMS.md index a760e58..25b8055 100644 --- a/PLATFORMS.md +++ b/PLATFORMS.md @@ -20,13 +20,14 @@ Pre-compiled wheels are published to PyPI for the following targets: | OS | Architecture | Notes | |---------|-----------------|-------| -| Linux | x86_64 (manylinux2014 / `manylinux_2_17`) | Default CI runner | -| Linux | aarch64 | Built via maturin cross-compilation | -| macOS | x86_64 | Intel | -| macOS | arm64 | Apple Silicon | -| macOS | universal2 | Intel + Apple Silicon fat binary | +| Linux | x86_64 (manylinux2014 / `manylinux_2_17`) | Pre-compiled wheel | +| macOS | universal2 | One wheel covers Intel + Apple Silicon | | Windows | x86_64 | | +Wheel releases target CPython 3.10, 3.11, 3.12, and 3.13. A source +distribution is also published so other compatible environments can build from +source. + > **Note:** Python 3.14+ is not yet tested. Set > `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` to attempt a build on a newer > interpreter and report any issues. @@ -39,8 +40,7 @@ Pre-compiled wheels are published to PyPI for the following targets: pip install ferro-ta ``` -No C-compiler required — pre-compiled wheels are available for all platforms -listed above. +No C-compiler required on the wheel targets listed above. ### conda / conda-forge diff --git a/RELEASE.md b/RELEASE.md index dd92b2e..ee551ab 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -15,6 +15,14 @@ For the packaging and release overview, see [PACKAGING.md](PACKAGING.md). | **npm (WASM)** | Workflow `wasm-publish`| | **crates.io** | CI job `publish-cratesio` | +PyPI releases are expected to include: + +- Wheels for CPython 3.10, 3.11, 3.12, and 3.13 +- Linux x86_64 (`manylinux_2_17`) +- macOS universal2 +- Windows x86_64 +- One source distribution (`sdist`) + --- ## Pre-release checklist @@ -128,7 +136,7 @@ git push origin v0.2.0 4. Paste the changelog section for `v0.2.0` into the release notes. 5. Click **Publish release**. -Publishing the release triggers the CI `build-wheels` and `publish` jobs +Publishing the release triggers the CI wheel build jobs, `build-sdist`, and `publish` automatically (the workflow responds to `release: published`). The PyPI upload uses Trusted Publishing via GitHub OIDC, so no `PYPI_API_TOKEN` secret is used. @@ -136,7 +144,7 @@ uses Trusted Publishing via GitHub OIDC, so no `PYPI_API_TOKEN` secret is used. ## Step 7 — Monitor CI and verify PyPI -1. Watch the **Actions** tab: `build-wheels` → `publish` (PyPI), `publish-cratesio` (crates.io), and the **wasm-publish** workflow (npm). +1. Watch the **Actions** tab: the release wheel jobs, `build-sdist`, `publish` (PyPI), `publish-cratesio` (crates.io), and the **wasm-publish** workflow (npm). 2. After the `publish` job succeeds, verify the package is live: ```bash @@ -144,6 +152,16 @@ pip install ferro-ta==0.2.0 python -c "import ferro_ta; print(ferro_ta.__version__ if hasattr(ferro_ta,'__version__') else 'ok')" ``` +For version-specific verification, also check at least one install on each +supported Python line, for example: + +```bash +uv venv --python 3.13 .venv-313 +. .venv-313/bin/activate +uv pip install ferro-ta==0.2.0 +python -c "import ferro_ta; print(ferro_ta.SMA([1.0, 2.0, 3.0], 2))" +``` + 3. If anything fails: fix the issue, bump to a patch version (`0.2.1`), and repeat. ---