chore(sparrow-engine-python): bump 0.1.7 -> 0.1.8 — Windows CLI fix r… #15
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | |
| # -------- CPU build matrix -------- | |
| build-cpu-linux: | |
| name: Build CPU wheel (Linux manylinux_2_28 x86_64) | |
| runs-on: ubuntu-latest | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| # -------- 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') | |
| 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: | |
| fail_on_unmatched_files: true | |
| 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 |