9011250f99
Enhance the documentation in PACKAGING.md and PLATFORMS.md to clarify the supported Python versions and platforms for wheel and source distribution releases. Update the CI workflow in CI.yml to include separate jobs for building wheels on Linux, macOS, and Windows, as well as a job for building the source distribution. Ensure that the publish job verifies the presence of expected distributions before publishing to PyPI.
384 lines
12 KiB
YAML
384 lines
12 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: ["main"]
|
|
pull_request:
|
|
branches: ["main"]
|
|
release:
|
|
types: [published]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# -------------------------------------------------------------------------
|
|
# Version consistency — Cargo.toml and pyproject.toml must have same version
|
|
# -------------------------------------------------------------------------
|
|
changelog-check:
|
|
name: CHANGELOG has [Unreleased] entry
|
|
runs-on: ubuntu-latest
|
|
if: github.event_name == 'pull_request'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- name: Check CHANGELOG.md
|
|
run: python3 scripts/check_changelog.py
|
|
|
|
version-check:
|
|
name: Version consistency (Cargo.toml == pyproject.toml)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- name: Check version parity
|
|
run: |
|
|
CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
|
PYPROJECT_VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
|
echo "Cargo.toml version: $CARGO_VERSION"
|
|
echo "pyproject.toml version: $PYPROJECT_VERSION"
|
|
if [ "$CARGO_VERSION" != "$PYPROJECT_VERSION" ]; then
|
|
echo "ERROR: Version mismatch! Update both files before releasing."
|
|
exit 1
|
|
fi
|
|
echo "OK: versions match ($CARGO_VERSION)"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Modularized CI suites.
|
|
# Keep release publishing jobs in this file so trusted-publisher bindings
|
|
# remain anchored to CI.yml for PyPI and crates.io.
|
|
# -------------------------------------------------------------------------
|
|
rust-suite:
|
|
name: Rust and audit
|
|
uses: ./.github/workflows/ci-rust.yml
|
|
|
|
python-suite:
|
|
name: Python quality and tests
|
|
uses: ./.github/workflows/ci-python.yml
|
|
|
|
wasm:
|
|
name: WASM binding
|
|
uses: ./.github/workflows/ci-wasm.yml
|
|
|
|
docs:
|
|
name: Documentation
|
|
uses: ./.github/workflows/ci-docs.yml
|
|
with:
|
|
upload-pages-artifact: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Deploy docs to GitHub Pages (on push to main only)
|
|
# -------------------------------------------------------------------------
|
|
deploy-docs:
|
|
name: Deploy docs to GitHub Pages
|
|
runs-on: ubuntu-latest
|
|
needs: docs
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
environment:
|
|
name: github-pages
|
|
url: ${{ steps.deployment.outputs.page_url }}
|
|
permissions:
|
|
pages: write
|
|
id-token: write
|
|
steps:
|
|
- name: Deploy to GitHub Pages
|
|
id: deployment
|
|
uses: actions/deploy-pages@v4
|
|
|
|
# -------------------------------------------------------------------------
|
|
# CI gate — all required jobs must pass before this job succeeds.
|
|
# Set "ci-complete" as a required status check in branch protection rules
|
|
# to block merging of PRs that fail any required job.
|
|
# -------------------------------------------------------------------------
|
|
ci-complete:
|
|
name: CI complete (required gate)
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- changelog-check
|
|
- version-check
|
|
- rust-suite
|
|
- python-suite
|
|
- wasm
|
|
- docs
|
|
if: always()
|
|
steps:
|
|
- name: Check all required jobs passed
|
|
run: |
|
|
results='${{ toJSON(needs) }}'
|
|
echo "Job results: $results"
|
|
failed="$(
|
|
RESULTS_JSON="$results" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
needs = json.loads(os.environ["RESULTS_JSON"])
|
|
failed = [
|
|
name
|
|
for name, data in needs.items()
|
|
if data["result"] not in ("success", "skipped")
|
|
]
|
|
if failed:
|
|
print(" ".join(failed))
|
|
PY
|
|
)"
|
|
if [ -n "$failed" ]; then
|
|
echo "FAILED jobs: $failed"
|
|
exit 1
|
|
fi
|
|
echo "All required CI jobs passed."
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Build wheels for all platforms and publish to PyPI on release
|
|
# Keep release jobs here because trusted-publisher configuration points to
|
|
# CI.yml specifically.
|
|
# -------------------------------------------------------------------------
|
|
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:
|
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Build wheel
|
|
uses: PyO3/maturin-action@v1
|
|
with:
|
|
command: build
|
|
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-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
|
|
# -------------------------------------------------------------------------
|
|
publish:
|
|
name: Publish to PyPI
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- build-wheels-linux
|
|
- build-wheels-macos
|
|
- build-wheels-windows
|
|
- build-sdist
|
|
if: github.event_name == 'release' && github.event.action == 'published'
|
|
environment:
|
|
name: pypi
|
|
url: https://pypi.org/p/ferro-ta
|
|
permissions:
|
|
id-token: write
|
|
steps:
|
|
- name: Download all wheels
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: wheels-*
|
|
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
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Publish ferro_ta_core to crates.io (requires CARGO_REGISTRY_TOKEN secret)
|
|
# -------------------------------------------------------------------------
|
|
publish-cratesio:
|
|
name: Publish to crates.io
|
|
runs-on: ubuntu-latest
|
|
if: github.event_name == 'release' && github.event.action == 'published'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@v1
|
|
with:
|
|
toolchain: stable
|
|
|
|
- name: Publish ferro_ta_core to crates.io
|
|
run: cargo publish -p ferro_ta_core --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
|
|
# -------------------------------------------------------------------------
|
|
# SBOM generation — Software Bill of Materials for supply-chain transparency
|
|
# Generates SBOMs for both Python (syft/CycloneDX) and Rust (cargo-cyclonedx)
|
|
# and uploads them as GitHub Release assets.
|
|
# -------------------------------------------------------------------------
|
|
sbom:
|
|
name: Generate SBOM (Python + Rust)
|
|
runs-on: ubuntu-latest
|
|
needs: publish
|
|
if: github.event_name == 'release' && github.event.action == 'published'
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Set up Python 3.12
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install maturin
|
|
run: pip install maturin numpy
|
|
|
|
- name: Build and install ferro_ta wheel
|
|
run: |
|
|
maturin build --release --out dist
|
|
pip install dist/*.whl
|
|
|
|
- name: Generate Python SBOM (CycloneDX via anchore/sbom-action)
|
|
uses: anchore/sbom-action@v0.24.0
|
|
with:
|
|
artifact-name: ferro-ta-python-sbom.spdx.json
|
|
output-file: ferro-ta-python-sbom.spdx.json
|
|
format: spdx-json
|
|
|
|
- name: Install Rust stable
|
|
uses: dtolnay/rust-toolchain@v1
|
|
with:
|
|
toolchain: stable
|
|
|
|
- name: Install cargo-cyclonedx
|
|
run: cargo install cargo-cyclonedx --locked
|
|
|
|
- name: Generate Rust SBOM (CycloneDX)
|
|
run: cargo cyclonedx --format json --output-cdx ferro-ta-rust-sbom.cdx.json
|
|
|
|
- name: Upload Python SBOM to release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
files: ferro-ta-python-sbom.spdx.json
|
|
|
|
- name: Upload Rust SBOM to release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
files: ferro-ta-rust-sbom.cdx.json
|