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: name: Build wheels (${{ matrix.os }}) runs-on: ${{ matrix.os }} if: github.event_name == 'release' && github.event.action == 'published' strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v6 - name: Build wheels 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' || '' }} - name: Upload wheels as artifact uses: actions/upload-artifact@v7 with: name: wheels-${{ matrix.os }} path: dist # ------------------------------------------------------------------------- # Publish to PyPI # ------------------------------------------------------------------------- publish: name: Publish to PyPI runs-on: ubuntu-latest needs: build-wheels 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: 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: build-wheels 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