Files
ferro-ta/.github/workflows/CI.yml
T
Pratik Bhadane 9011250f99 chore: update packaging and CI workflow for PyPI releases
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.
2026-03-24 01:00:17 +05:30

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