Skip to content

Release sparrow-engine wheels #23

Release sparrow-engine wheels

Release sparrow-engine wheels #23

Workflow file for this run

name: Release sparrow-engine wheels
# RP-11 (Phase C, 2026-05-24): build + publish sparrow-engine / sparrow-engine-gpu
# Python wheels via maturin.
#
# Trigger matrix:
# - Tag push `vX.Y.Z` (no hyphen) -> build + publish to PyPI (production), GPU build only.
# - Tag push `vX.Y.Z-<prerelease>` (any -tag) -> build only (no publish to either index).
# - workflow_dispatch (target: testpypi) -> build + publish to TestPyPI.
# - workflow_dispatch (target: build-only) -> build only, no publish.
#
# CPU wheels: 3 platforms (Linux manylinux_2_28 x86_64, macOS arm64, Windows x86_64).
# macOS x86_64 (Intel Mac) is NOT in the matrix — no ORT 1.25.1 wheel exists for that
# platform AND the macos-13 GitHub-hosted runner pool has chronic 25+ min queue latency
# that blocks every release. Intel-Mac users build from source per `docs/install.md`.
# GPU wheel: Linux x86_64 inside Rocky 8 / glibc 2.28 container (Phase F switch from
# Ubuntu 24.04 to satisfy manylinux_2_28 policy).
#
# Phase H (2026-05-25): GPU prod-PyPI publish ENABLED. Phase E (nvjpeg dlopen) +
# Phase F (Rocky 8 container + auditwheel hard gate + CUDA runtime preload)
# made the GPU wheel manylinux_2_28-compliant and runtime-self-contained; the
# v0.1.3 TestPyPI publish + dev-box E.7-E.10 manual test verified end-to-end
# install + inference. Tag-version validation step (mirrored from publish-pypi-cpu)
# guards the prod-PyPI immutability invariant.
#
# OIDC trusted-publisher prerequisites (USER action, not automatable):
# - prod PyPI: claim `sparrow-engine` + `sparrow-engine-gpu` names; configure
# publisher: repo `microsoft/Pytorch-Wildlife`, workflow `release.yml`,
# env `pypi`.
# - TestPyPI: same names; env `testpypi`.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
target:
description: 'Publish target'
required: true
type: choice
options:
- build-only
- testpypi
default: build-only
concurrency:
group: release-${{ github.ref }}
# Tag-push runs (prod release) MUST NOT cancel each other; manual workflow_dispatch
# runs (build-only / TestPyPI) MAY cancel-in-progress so a re-trigger supersedes
# a stale run instead of queueing behind it.
cancel-in-progress: ${{ github.event_name == 'workflow_dispatch' }}
permissions:
contents: read
jobs:
# ---------------------------------------------------------------------------
# Preflight: enforce UTF-8 BOM on installer/*.ps1 files (PW#11 regression
# guard). Windows PowerShell 5.1 mis-decodes BOM-less UTF-8 as Windows-1252
# and produces parser errors on any multi-byte character. Failing this gate
# early prevents shipping a broken installer to end users.
# Refs: OQ-2026-05-27-7.
# ---------------------------------------------------------------------------
check-installer-ps1-bom:
name: Preflight — installer .ps1 BOM guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify installer/*.ps1 files start with UTF-8 BOM if non-ASCII
run: python3 installer/check_ps1_bom.py
# -------- CPU build matrix --------
build-cpu-linux:
name: Build CPU wheel (Linux manylinux_2_28 x86_64)
runs-on: ubuntu-latest
needs: check-installer-ps1-bom
steps:
- uses: actions/checkout@v4
- name: Build CPU wheel
uses: PyO3/maturin-action@v1
with:
manylinux: 2_28
working-directory: sparrow-engine/sparrow-engine-python
command: build
args: >-
--release
--auditwheel skip
--no-default-features
--features extension-module
--features cpu
- name: Inspect built wheel
run: |
ls -la sparrow-engine/target/wheels/
# Stable ABI tag must be present (cp311-abi3).
for w in sparrow-engine/target/wheels/sparrow_engine-*.whl; do
echo "Wheel: $w"
case "$w" in
*cp311-abi3*manylinux_2_28_x86_64*) echo " OK: abi3 + manylinux_2_28";;
*) echo " FAIL: expected cp311-abi3-manylinux_2_28_x86_64 tag in filename"; exit 1;;
esac
done
- name: Audit wheel (manylinux policy — HARD GATE)
run: |
python3 -m pip install --user auditwheel
for w in sparrow-engine/target/wheels/sparrow_engine-*.whl; do
python3 -m auditwheel show "$w"
# Hard gate: any external DT_NEEDED beyond the manylinux_2_28 allow-list
# (e.g. a future regression that re-introduces libonnxruntime / libnvjpeg /
# libpython linkage) must fail this job, not the PyPI upload step.
python3 -m auditwheel show "$w" | grep -q 'manylinux_2_28_x86_64' \
|| { echo "FAIL: wheel $w is not manylinux_2_28_x86_64 compatible"; exit 1; }
done
- uses: actions/upload-artifact@v4
with:
name: wheel-cpu-linux
path: sparrow-engine/target/wheels/sparrow_engine-*.whl
if-no-files-found: error
# -------- CPU Linux abi3 import smoke test (3.11, 3.12, 3.13) --------
#
# Validates the abi3-py311 promise: one wheel imports on three CPython minors.
# Also validates the RP-3 ORT shim path: with onnxruntime present, no manual
# symlink should be needed.
smoke-cpu-linux:
name: Smoke test CPU Linux wheel (Python ${{ matrix.python }})
needs: build-cpu-linux
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ['3.11', '3.12', '3.13']
steps:
- uses: actions/download-artifact@v4
with:
name: wheel-cpu-linux
path: dist
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install wheel + onnxruntime, run import + init()
run: |
python -m pip install --upgrade pip
# The wheel's runtime dep on onnxruntime>=1.25.1,<1.26 is resolved by pip.
python -m pip install dist/sparrow_engine-*.whl
python -c "import sparrow_engine; sparrow_engine.init(); print('Smoke OK on', __import__('sys').version)"
build-cpu-macos-arm64:
name: Build CPU wheel (macOS arm64)
runs-on: macos-14
needs: check-installer-ps1-bom
env:
MACOSX_DEPLOYMENT_TARGET: '11.0'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build CPU wheel
uses: PyO3/maturin-action@v1
with:
working-directory: sparrow-engine/sparrow-engine-python
command: build
target: aarch64-apple-darwin
args: >-
--release
--auditwheel skip
--no-default-features
--features extension-module
--features cpu
- name: Inspect built wheel
run: |
ls -la sparrow-engine/target/wheels/
for w in sparrow-engine/target/wheels/sparrow_engine-*.whl; do
echo "Wheel: $w"
case "$w" in
*cp311-abi3-macosx_11_0_arm64*) echo " OK: abi3 + macosx_11_0_arm64";;
*) echo " FAIL: expected cp311-abi3-macosx_11_0_arm64 tag (MACOSX_DEPLOYMENT_TARGET=11.0)"; exit 1;;
esac
done
- uses: actions/upload-artifact@v4
with:
name: wheel-cpu-macos-arm64
path: sparrow-engine/target/wheels/sparrow_engine-*.whl
if-no-files-found: error
build-cpu-windows:
name: Build CPU wheel (Windows x86_64)
runs-on: windows-latest
needs: check-installer-ps1-bom
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build CPU wheel
uses: PyO3/maturin-action@v1
with:
working-directory: sparrow-engine/sparrow-engine-python
command: build
target: x86_64-pc-windows-msvc
args: >-
--release
--auditwheel skip
--no-default-features
--features extension-module
--features cpu
- name: Inspect built wheel
shell: bash
run: |
ls -la sparrow-engine/target/wheels/
for w in sparrow-engine/target/wheels/sparrow_engine-*.whl; do
echo "Wheel: $w"
case "$w" in
*cp311-abi3*win_amd64*) echo " OK: abi3 + win_amd64";;
*) echo " FAIL: expected cp311-abi3-win_amd64 tag"; exit 1;;
esac
done
- uses: actions/upload-artifact@v4
with:
name: wheel-cpu-windows
path: sparrow-engine/target/wheels/sparrow_engine-*.whl
if-no-files-found: error
# -------- GPU build (Phase C: build-only, no publish) --------
build-gpu-linux:
name: Build GPU wheel (Linux x86_64, CUDA 12.6 + cuDNN, Rocky 8 / glibc 2.28)
runs-on: ubuntu-latest
needs: check-installer-ps1-bom
container:
# Rocky 8 base = RHEL 8 clone = glibc 2.28 (the manylinux_2_28 floor).
# Phase F (2026-05-25): swapped from ubuntu24.04 (glibc 2.39) so the
# `auditwheel repair --plat manylinux_2_28_x86_64` step in build.sh
# can succeed — that step hard-fails on glibc > 2.28.
image: nvidia/cuda:12.6.3-cudnn-devel-rockylinux8
steps:
- name: Install build prerequisites in container
run: |
# Rocky 8 / RHEL 8: dnf instead of apt; python3.11 is the newest
# cpython available via AppStream and matches the wheel's abi3-cp311
# target + the project's requires-python>=3.11 floor.
dnf install -y --setopt=install_weak_deps=False \
ca-certificates curl git gcc gcc-c++ make pkgconfig \
python3.11 python3.11-devel python3.11-pip
ln -sf /usr/bin/python3.11 /usr/local/bin/python3
ln -sf /usr/bin/python3.11 /usr/local/bin/python
# auditwheel >=6.0.0 needed for manylinux_2_28 policy support.
# patchelf MUST come from PyPI (not Rocky 8 EPEL, which ships
# v0.12 — auditwheel repair requires >=0.14). The PyPI patchelf
# package wraps the upstream binary release (currently 0.18+)
# and puts it in $HOME/.local/bin, which is prepended to PATH
# via $GITHUB_PATH so it takes precedence over any system
# patchelf. build.sh calls `auditwheel repair` at line 152 —
# must be on PATH before the `Build GPU wheel via build.sh` step.
python3 -m pip install --user --upgrade \
"auditwheel>=6.0.0" \
"patchelf>=0.14"
echo "$HOME/.local/bin" >> $GITHUB_PATH
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install maturin
run: |
uv tool install maturin
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Verify libnvjpeg present in container
run: |
ldconfig -p | grep -i nvjpeg || true
find /usr -name 'libnvjpeg*' 2>/dev/null || true
- name: Build GPU wheel via build.sh
run: |
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
cd sparrow-engine/sparrow-engine-python
SPARROW_ENGINE_FLAVOR=gpu ./build.sh
- name: Inspect built wheel
run: |
ls -la sparrow-engine/target/wheels/
for w in sparrow-engine/target/wheels/sparrow_engine_gpu-*.whl; do
echo "Wheel: $w"
case "$w" in
*cp311-abi3*manylinux_2_28_x86_64*) echo " OK: abi3 + manylinux_2_28_x86_64";;
*) echo " FAIL: expected cp311-abi3-manylinux_2_28_x86_64 tag"; exit 1;;
esac
done
- name: Audit wheel (manylinux policy — HARD GATE)
run: |
# Mirror the CPU build's hard gate (release.yml § build-cpu-linux).
# Any DT_NEEDED beyond the manylinux_2_28 allow-list — e.g. a
# regression that re-introduces libnvjpeg.so or libcuda.so linkage,
# bypassing the Phase E dlopen design — must fail this job, not the
# PyPI upload step. libonnxruntime is excluded by build.sh because
# the runtime install pulls onnxruntime-gpu separately.
python3 -m pip install --user auditwheel
for w in sparrow-engine/target/wheels/sparrow_engine_gpu-*.whl; do
python3 -m auditwheel show "$w"
python3 -m auditwheel show "$w" | grep -q 'manylinux_2_28_x86_64' \
|| { echo "FAIL: wheel $w is not manylinux_2_28_x86_64 compatible"; exit 1; }
done
- uses: actions/upload-artifact@v4
with:
name: wheel-gpu-linux
path: sparrow-engine/target/wheels/sparrow_engine_gpu-*.whl
if-no-files-found: error
build-gpu-windows:
name: Build GPU wheel (Windows x86_64)
runs-on: windows-latest
needs: check-installer-ps1-bom
# No CUDA Toolkit on the runner. cudarc's `fallback-dynamic-loading`
# feature (vendor/cudarc/build.rs:70-78) activates the `dynamic-loading`
# cfg from the feature flag alone, with no nvcc / driver probing.
# nvjpeg-sys (vendor/nvjpeg-sys/build.rs) is a 2-line stub that ships
# pre-generated bindings — no bindgen at build time. `ort` uses
# `load-dynamic`, so libonnxruntime is dlopen'd at runtime. None of
# these crates needs the CUDA SDK at compile time.
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: astral-sh/setup-uv@v3
- name: Install maturin
shell: bash
run: |
uv tool install maturin
# uv tool dir --bin prints the platform-correct bin directory
# (~/.local/bin on Linux, %USERPROFILE%\.local\bin on Windows
# in recent uv versions) — avoids hard-coding either path.
uv tool dir --bin >> "$GITHUB_PATH"
- name: Build GPU wheel via build.sh
shell: bash
run: |
# Git Bash on windows-latest reports OSTYPE=msys; build.sh's
# IS_WINDOWS detection catches that and skips the Linux-only
# `--compatibility linux` flag and `auditwheel repair` step.
cd sparrow-engine/sparrow-engine-python
SPARROW_ENGINE_FLAVOR=gpu ./build.sh
- name: Inspect built wheel
shell: bash
run: |
ls -la sparrow-engine/target/wheels/
for w in sparrow-engine/target/wheels/sparrow_engine_gpu-*.whl; do
echo "Wheel: $w"
case "$w" in
*cp311-abi3*win_amd64*) echo " OK: abi3 + win_amd64";;
*) echo " FAIL: expected cp311-abi3-win_amd64 tag"; exit 1;;
esac
done
- uses: actions/upload-artifact@v4
with:
name: wheel-gpu-windows
path: sparrow-engine/target/wheels/sparrow_engine_gpu-*.whl
if-no-files-found: error
smoke-gpu-windows:
name: Smoke test GPU Windows wheel (Python ${{ matrix.python }})
needs: build-gpu-windows
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python: ['3.11', '3.12', '3.13']
steps:
- uses: actions/download-artifact@v4
with:
name: wheel-gpu-windows
path: dist
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install wheel + onnxruntime-gpu, run import
shell: bash
run: |
python -m pip install --upgrade pip
# PEP 508 markers in METADATA skip nvidia-cudnn-cu12 / cublas /
# curand / cufft on sys_platform != 'linux'; onnxruntime-gpu has
# native Windows wheels and resolves normally.
python -m pip install dist/sparrow_engine_gpu-*.whl
# GitHub-hosted windows-latest has no NVIDIA GPU, so we cannot
# call sparrow_engine.init() — Device::Auto/Cpu coerce to
# Cuda(0) on the GPU flavor (flavor-strict post-MT-4.1-2),
# which would fail at CUDA context creation. Import-only test
# still validates: pip resolution, cdylib load, PyO3 binding,
# METADATA Provides-Dist mutex with sparrow-engine.
python -c "import sparrow_engine; print('Smoke OK on', __import__('sys').version, '— version:', sparrow_engine.__version__)"
# -------- TestPyPI publish (workflow_dispatch) --------
publish-testpypi-cpu:
name: Publish CPU wheels to TestPyPI
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
needs:
- build-cpu-linux
- build-cpu-macos-arm64
- build-cpu-windows
- smoke-cpu-linux
runs-on: ubuntu-latest
environment:
name: testpypi-cpu
url: https://test.pypi.org/p/sparrow-engine
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheel-cpu-*
path: dist
merge-multiple: true
- name: Show collected dist/
run: ls -la dist/
- uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
# TestPyPI is a sandbox; treat duplicate version uploads as no-ops
# so workflow_dispatch retries (e.g. after a downstream job fails)
# don't error out at the CPU publish step. Prod-PyPI publishes
# below MUST NOT set this flag — there a duplicate is a real error.
skip-existing: true
# -------- Prod PyPI publish (tag push, non-RC only) --------
publish-pypi-cpu:
name: Publish CPU wheels to PyPI
# Only on actual version tags. ANY hyphen in the tag name (`v0.1.0-rc1`,
# `v0.1.0-beta1`, `v1.0.0-alpha`, etc.) marks the tag as a prerelease and
# skips prod publish. workflow_dispatch also skips this job.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
needs:
- build-cpu-linux
- build-cpu-macos-arm64
- build-cpu-windows
- smoke-cpu-linux
runs-on: ubuntu-latest
environment:
name: pypi-cpu
url: https://pypi.org/p/sparrow-engine
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheel-cpu-*
path: dist
merge-multiple: true
- name: Show collected dist/
run: ls -la dist/
- name: Validate tag matches wheel version (PyPI immutability guard)
run: |
# Strip leading 'v' from tag: refs/tags/v0.1.0 -> 0.1.0
tag_version="${GITHUB_REF_NAME#v}"
# Extract version from any wheel filename; abi3 wheels share one version.
# Wheel filename shape: sparrow_engine-<version>-cp311-abi3-<platform>.whl
wheel_version="$(ls dist/sparrow_engine-*.whl | head -1 \
| sed -E 's|.*/sparrow_engine-([^-]+)-cp311-abi3-.*|\1|')"
echo "Tag version: $tag_version"
echo "Wheel version: $wheel_version"
if [ "$tag_version" != "$wheel_version" ]; then
echo "FAIL: tag ($tag_version) and wheel ($wheel_version) versions disagree."
echo "Bump pyproject.toml [project].version before tagging."
exit 1
fi
- uses: pypa/gh-action-pypi-publish@release/v1
# -------- GPU prod PyPI publish (still gated until Phase H) --------
publish-pypi-gpu:
name: Publish GPU wheel to PyPI
# Phase H (2026-05-25): gate flipped from `if: false` to mirror
# publish-pypi-cpu (only on actual non-prerelease version tags).
# Phase E (nvjpeg dlopen) + Phase F (Rocky 8 build container + auditwheel
# hard gate + CUDA runtime preload) made the GPU wheel manylinux_2_28-
# compliant and runtime-self-contained; v0.1.3 TestPyPI publish + dev-box
# E.7-E.10 manual test verified end-to-end install + inference.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
needs:
- build-gpu-linux
- build-gpu-windows
- smoke-gpu-windows
runs-on: ubuntu-latest
environment:
name: pypi-gpu
url: https://pypi.org/p/sparrow-engine-gpu
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheel-gpu-*
path: dist
merge-multiple: true
- name: Show collected dist/
run: ls -la dist/
- name: Validate tag matches wheel version (PyPI immutability guard)
run: |
# Strip leading 'v' from tag: refs/tags/v0.1.4 -> 0.1.4
tag_version="${GITHUB_REF_NAME#v}"
# Extract version from any wheel filename; abi3 wheels share one version.
# Wheel filename shape: sparrow_engine_gpu-<version>-cp311-abi3-<platform>.whl
wheel_version="$(ls dist/sparrow_engine_gpu-*.whl | head -1 \
| sed -E 's|.*/sparrow_engine_gpu-([^-]+)-cp311-abi3-.*|\1|')"
echo "Tag version: $tag_version"
echo "Wheel version: $wheel_version"
if [ "$tag_version" != "$wheel_version" ]; then
echo "FAIL: tag ($tag_version) and wheel ($wheel_version) versions disagree."
echo "Bump pyproject.toml [project].version (and Cargo.toml) before tagging."
exit 1
fi
- uses: pypa/gh-action-pypi-publish@release/v1
publish-testpypi-gpu:
name: Publish GPU wheel to TestPyPI
# Phase F (2026-05-25): GPU TestPyPI publish enabled. Phase E's nvjpeg
# dlopen rewrite removed libnvjpeg from DT_NEEDED; the Rocky 8 build
# container above now satisfies the manylinux_2_28 glibc floor; and the
# `auditwheel show` hard gate in build-gpu-linux confirms the wheel.
# Prod PyPI gate (publish-pypi-gpu) stays `if: false` until Phase H
# adds the tag-version validation step.
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
needs:
- build-gpu-linux
- build-gpu-windows
- smoke-gpu-windows
runs-on: ubuntu-latest
environment:
name: testpypi-gpu
url: https://test.pypi.org/p/sparrow-engine-gpu
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheel-gpu-*
path: dist
merge-multiple: true
- name: Show collected dist/
run: ls -la dist/
- uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
# See publish-testpypi-cpu comment above — TestPyPI sandbox,
# duplicate-version uploads treated as no-ops on retry.
skip-existing: true
# ---------------------------------------------------------------------------
# RP-4 (2026-05-26) — CLI tarball matrix.
#
# Build per-platform tarballs of the `spe` / `spe-gpu` CLI with bundled
# libonnxruntime, layout matching `installer/sparrow-engine-install.sh:531`.
# Output naming: sparrow-engine-{cpu,gpu}-{ver}-{platform}.tar.gz (+ .sha256).
#
# Build jobs run on every tag push AND workflow_dispatch (target=build-only
# or testpypi); the publish-cli-release-assets job below only attaches to
# a GH Release on actual prod tags (no hyphen).
# ---------------------------------------------------------------------------
build-cli-linux-cpu:
name: Build CLI tarball (sparrow-engine-cpu, Linux x86_64)
runs-on: ubuntu-latest
needs: check-installer-ps1-bom
container:
# manylinux_2_28 = glibc 2.28 floor, matches build-cpu-linux's wheel target.
image: quay.io/pypa/manylinux_2_28_x86_64
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Stage ORT runtime (pip onnxruntime, capi/libonnxruntime.so.X.Y.Z)
run: |
ORT_VENV="$RUNNER_TEMP/ort-venv"
rm -rf "$ORT_VENV"
/opt/python/cp311-cp311/bin/python -m venv "$ORT_VENV"
"$ORT_VENV/bin/pip" install --quiet "onnxruntime>=1.25.1,<1.26"
ORT_CAPI=$("$ORT_VENV/bin/python" -c 'import onnxruntime, pathlib; print(pathlib.Path(onnxruntime.__file__).parent / "capi")')
echo "ORT_STAGE_DIR=$ORT_CAPI" >> "$GITHUB_ENV"
ls -la "$ORT_CAPI"/libonnxruntime.so.*
- name: Build spe (release, CPU flavor, load-dynamic via ort_resolver)
working-directory: sparrow-engine
run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo build --release -p sparrow-engine-cli --bin spe \
--no-default-features --features cpu
- name: Hard gate — no DT_NEEDED libonnxruntime (load-dynamic invariant)
working-directory: sparrow-engine
run: |
set -euo pipefail
needed="$(readelf -d target/release/spe)"
printf '%s\n' "$needed" | grep -E 'NEEDED' || true
if printf '%s\n' "$needed" | grep -q 'libonnxruntime'; then
echo "FAIL: spe has DT_NEEDED libonnxruntime — load-dynamic contract violated (RP-3/RP-4)"
exit 1
fi
- name: Package tarball
working-directory: sparrow-engine
env:
FLAVOR: cpu
TARBALL_PLATFORM: linux-x86_64
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$GITHUB_REF_TYPE" != "tag" ]]; then
VERSION="0.0.0-ci-${GITHUB_SHA::8}"
fi
VERSION="$VERSION" ./scripts/package_cli_tarball.sh
- uses: actions/upload-artifact@v4
with:
name: cli-cpu-linux
path: |
sparrow-engine/dist/sparrow-engine-cpu-*-linux-x86_64.tar.gz
sparrow-engine/dist/sparrow-engine-cpu-*-linux-x86_64.tar.gz.sha256
if-no-files-found: error
build-cli-linux-gpu:
name: Build CLI tarball (sparrow-engine-gpu, Linux x86_64)
runs-on: ubuntu-latest
needs: check-installer-ps1-bom
container:
# Same Rocky 8 / glibc 2.28 image as build-gpu-linux.
image: nvidia/cuda:12.6.3-cudnn-devel-rockylinux8
steps:
- name: Install build prerequisites
run: |
dnf install -y --setopt=install_weak_deps=False \
ca-certificates git gcc gcc-c++ make pkgconfig \
python3.11 python3.11-devel python3.11-pip binutils
ln -sf /usr/bin/python3.11 /usr/local/bin/python3
ln -sf /usr/bin/python3.11 /usr/local/bin/python
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Stage ORT GPU runtime (onnxruntime-gpu + CUDA provider sidecars)
run: |
ORT_VENV="$RUNNER_TEMP/ort-venv"
rm -rf "$ORT_VENV"
python3 -m venv "$ORT_VENV"
"$ORT_VENV/bin/pip" install --quiet "onnxruntime-gpu>=1.25.1,<1.26"
ORT_CAPI=$("$ORT_VENV/bin/python" -c 'import onnxruntime, pathlib; print(pathlib.Path(onnxruntime.__file__).parent / "capi")')
echo "ORT_STAGE_DIR=$ORT_CAPI" >> "$GITHUB_ENV"
ls -la "$ORT_CAPI"/libonnxruntime*.so* | head -20
- name: Patch GPU ORT RUNPATH for provider sidecars
run: |
set -euo pipefail
python3 -m pip install --user --quiet "patchelf>=0.14"
export PATH="$HOME/.local/bin:$PATH"
patched=0
for so in "$ORT_STAGE_DIR"/libonnxruntime.so.* "$ORT_STAGE_DIR"/libonnxruntime_providers_*.so; do
[[ -e "$so" ]] || continue
patchelf --set-rpath '$ORIGIN' "$so"
readelf -d "$so" | grep -E 'RUNPATH|RPATH'
readelf -d "$so" | grep -q '\$ORIGIN' || { echo "FAIL: $so lacks \$ORIGIN RUNPATH"; exit 1; }
patched=$((patched + 1))
done
if [[ "$patched" -eq 0 ]]; then
echo "FAIL: no ORT shared libraries were patched"
exit 1
fi
- name: Build spe-gpu (release, GPU flavor, load-dynamic via ort_resolver)
working-directory: sparrow-engine
run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo build --release -p sparrow-engine-cli --bin spe-gpu \
--no-default-features --features gpu
- name: Hard gate — no DT_NEEDED libonnxruntime (load-dynamic invariant)
working-directory: sparrow-engine
run: |
set -euo pipefail
needed="$(readelf -d target/release/spe-gpu)"
printf '%s\n' "$needed" | grep -E 'NEEDED' || true
if printf '%s\n' "$needed" | grep -q 'libonnxruntime'; then
echo "FAIL: spe-gpu has DT_NEEDED libonnxruntime — load-dynamic contract violated (RP-3/RP-4)"
exit 1
fi
- name: Package tarball
working-directory: sparrow-engine
env:
FLAVOR: gpu
TARBALL_PLATFORM: linux-x86_64
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$GITHUB_REF_TYPE" != "tag" ]]; then
VERSION="0.0.0-ci-${GITHUB_SHA::8}"
fi
VERSION="$VERSION" ./scripts/package_cli_tarball.sh
- uses: actions/upload-artifact@v4
with:
name: cli-gpu-linux
path: |
sparrow-engine/dist/sparrow-engine-gpu-*-linux-x86_64.tar.gz
sparrow-engine/dist/sparrow-engine-gpu-*-linux-x86_64.tar.gz.sha256
if-no-files-found: error
build-cli-macos-arm64:
name: Build CLI tarball (sparrow-engine-cpu, macOS arm64)
runs-on: macos-14
needs: check-installer-ps1-bom
env:
MACOSX_DEPLOYMENT_TARGET: '11.0'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install GNU coreutils for packaging
run: |
brew install coreutils
echo "$(brew --prefix coreutils)/libexec/gnubin" >> "$GITHUB_PATH"
- name: Stage ORT runtime
run: |
ORT_VENV="$RUNNER_TEMP/ort-venv"
rm -rf "$ORT_VENV"
python3 -m venv "$ORT_VENV"
"$ORT_VENV/bin/pip" install --quiet "onnxruntime>=1.25.1,<1.26"
ORT_CAPI=$("$ORT_VENV/bin/python" -c 'import onnxruntime, pathlib; print(pathlib.Path(onnxruntime.__file__).parent / "capi")')
echo "ORT_STAGE_DIR=$ORT_CAPI" >> "$GITHUB_ENV"
- name: Build spe (release, CPU flavor, macOS arm64)
working-directory: sparrow-engine
run: |
cargo build --release -p sparrow-engine-cli --bin spe \
--no-default-features --features cpu
- name: Hard gate — no LC_LOAD_DYLIB libonnxruntime (load-dynamic invariant)
working-directory: sparrow-engine
run: |
set -euo pipefail
loaded="$(otool -L target/release/spe)"
printf '%s\n' "$loaded"
if printf '%s\n' "$loaded" | grep -q 'libonnxruntime'; then
echo "FAIL: spe has LC_LOAD_DYLIB libonnxruntime — load-dynamic contract violated"
exit 1
fi
- name: Package tarball
working-directory: sparrow-engine
env:
FLAVOR: cpu
TARBALL_PLATFORM: macos-aarch64
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$GITHUB_REF_TYPE" != "tag" ]]; then
VERSION="0.0.0-ci-${GITHUB_SHA::8}"
fi
VERSION="$VERSION" ./scripts/package_cli_tarball.sh
- uses: actions/upload-artifact@v4
with:
name: cli-cpu-macos-arm64
path: |
sparrow-engine/dist/sparrow-engine-cpu-*-macos-aarch64.tar.gz
sparrow-engine/dist/sparrow-engine-cpu-*-macos-aarch64.tar.gz.sha256
if-no-files-found: error
build-cli-windows:
name: Build CLI tarball (sparrow-engine-cpu, Windows x86_64)
runs-on: windows-latest
needs: check-installer-ps1-bom
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Stage ORT runtime
shell: bash
run: |
ORT_VENV="$(cygpath -u "$RUNNER_TEMP")/ort-venv"
rm -rf "$ORT_VENV"
python3 -m venv "$ORT_VENV"
"$ORT_VENV/Scripts/pip" install --quiet "onnxruntime>=1.25.1,<1.26"
# `.as_posix()` so the path uses forward slashes even on Windows
# (default pathlib repr is `C:\foo\bar`). The packaging script's
# bash path tests (`[[ -d $ort_capi ]]`) and command substitutions
# are more reliable with forward slashes under Git Bash + MSYS.
ORT_CAPI=$("$ORT_VENV/Scripts/python" -c "import onnxruntime, pathlib; print((pathlib.Path(onnxruntime.__file__).parent / 'capi').as_posix())")
echo "ORT_STAGE_DIR=$ORT_CAPI" >> "$GITHUB_ENV"
- name: Install zip (MSYS Git Bash does not ship it by default)
shell: pwsh
run: choco install zip -y --no-progress --no-color
- name: Build spe.exe (release, CPU flavor, Windows x86_64)
working-directory: sparrow-engine
shell: bash
run: |
cargo build --release -p sparrow-engine-cli --bin spe \
--no-default-features --features cpu
- name: Configure MSVC developer command prompt
uses: ilammy/msvc-dev-cmd@v1
# Windows DLL link verification: dumpbin (MSVC) lists imports.
- name: Hard gate — no onnxruntime.dll in import table (load-dynamic invariant)
working-directory: sparrow-engine
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$imports = & dumpbin /dependents target\release\spe.exe
if ($LASTEXITCODE -ne 0) {
throw "dumpbin failed with exit code $LASTEXITCODE"
}
if ($imports -match 'onnxruntime\.dll') {
Write-Error "FAIL: spe.exe imports onnxruntime.dll - load-dynamic contract violated"
exit 1
}
- name: Package zip
working-directory: sparrow-engine
shell: bash
env:
FLAVOR: cpu
TARBALL_PLATFORM: windows-x86_64
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$GITHUB_REF_TYPE" != "tag" ]]; then
VERSION="0.0.0-ci-${GITHUB_SHA::8}"
fi
VERSION="$VERSION" ./scripts/package_cli_tarball.sh
- uses: actions/upload-artifact@v4
with:
name: cli-cpu-windows
path: |
sparrow-engine/dist/sparrow-engine-cpu-*-windows-x86_64.zip
sparrow-engine/dist/sparrow-engine-cpu-*-windows-x86_64.zip.sha256
if-no-files-found: error
# ---------------------------------------------------------------------------
# Attach CLI tarballs as GitHub Release assets on prod tag push.
# Hyphenated tags (RC / pre) skip publish; build jobs still ran above.
# ---------------------------------------------------------------------------
publish-cli-release-assets:
name: Attach CLI tarballs to GitHub Release
# Tag-push only; skips on hyphenated tags (RC / pre-release).
# Fires when the 3 reliable CLI builds succeed; build-cli-windows is
# allowed to fail in the meantime (Windows .zip packaging is a known
# follow-up — see RP-4 Windows tracking ticket). When Windows succeeds
# the .zip is attached automatically via the files glob below; when it
# fails, the release ships with 6 of 8 assets (Linux + macOS + GPU
# tarballs and their sha256s).
if: |
!cancelled()
&& github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
&& !contains(github.ref_name, '-')
&& needs['build-cli-linux-cpu'].result == 'success'
&& needs['build-cli-linux-gpu'].result == 'success'
&& needs['build-cli-macos-arm64'].result == 'success'
needs:
- build-cli-linux-cpu
- build-cli-linux-gpu
- build-cli-macos-arm64
- build-cli-windows
runs-on: ubuntu-latest
permissions:
# softprops/action-gh-release needs write access to create / append the
# GH Release. The wheel publish jobs above use OIDC for PyPI; this job
# uses standard GITHUB_TOKEN scoped to contents:write.
contents: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: cli-*
path: dist
merge-multiple: true
- name: Show collected dist/
run: ls -la dist/
- name: Validate tag matches tarball version
run: |
tag_version="${GITHUB_REF_NAME#v}"
got=$(ls dist/sparrow-engine-cpu-*-linux-x86_64.tar.gz | head -1 \
| sed -E 's|.*/sparrow-engine-cpu-([^-]+)-linux-x86_64\.tar\.gz|\1|')
echo "Tag version: $tag_version"
echo "Tarball version: $got"
if [ "$tag_version" != "$got" ]; then
echo "FAIL: tag ($tag_version) and tarball ($got) versions disagree."
exit 1
fi
- uses: softprops/action-gh-release@v2
with:
# Soft-fail when Windows tarball is missing — see job-level if
# gate above. When build-cli-windows starts succeeding, the
# glob picks up the .zip automatically; for now the 6 reliable
# assets ship.
fail_on_unmatched_files: false
files: |
dist/sparrow-engine-cpu-*-linux-x86_64.tar.gz
dist/sparrow-engine-cpu-*-linux-x86_64.tar.gz.sha256
dist/sparrow-engine-gpu-*-linux-x86_64.tar.gz
dist/sparrow-engine-gpu-*-linux-x86_64.tar.gz.sha256
dist/sparrow-engine-cpu-*-macos-aarch64.tar.gz
dist/sparrow-engine-cpu-*-macos-aarch64.tar.gz.sha256
dist/sparrow-engine-cpu-*-windows-x86_64.zip
dist/sparrow-engine-cpu-*-windows-x86_64.zip.sha256