Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/python-bindings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Python bindings

on:
pull_request:
paths:
- "Cargo.lock"
- "Cargo.toml"
- "crates/alien-bindings/**"
- "crates/alien-ai-gateway/**"
- "crates/alien-core/src/bindings/**"
- "crates/alien-bindings-python/**"
- "examples/python-bindings/**"
- ".github/workflows/python-bindings.yml"
push:
branches: [main]
paths:
- "Cargo.lock"
- "Cargo.toml"
- "crates/alien-bindings/**"
- "crates/alien-ai-gateway/**"
- "crates/alien-core/src/bindings/**"
- "crates/alien-bindings-python/**"
- "examples/python-bindings/**"
- ".github/workflows/python-bindings.yml"

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-14]
python: ["3.10", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
- name: Install test tools
run: python -m pip install 'maturin>=1.9,<2' 'mypy>=1.18,<2' 'pytest>=8,<10' 'pytest-asyncio>=1,<2' 'ruff>=0.13,<1'
- name: Build and install local-provider wheel
run: |
maturin build --manifest-path crates/alien-bindings-python/Cargo.toml --no-default-features --features local --out dist
python -m pip install --force-reinstall dist/*.whl
- name: Check Python sources
run: |
ruff check crates/alien-bindings-python/python crates/alien-bindings-python/tests examples/python-bindings
mypy crates/alien-bindings-python/python examples/python-bindings
cargo fmt -p alien-bindings-python --check
cargo clippy -p alien-bindings-python --no-default-features --features local --no-deps -- -D warnings
- name: Test Python bindings
run: pytest -q crates/alien-bindings-python/tests

wheels:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64
manylinux: "2014"
- os: ubuntu-24.04-arm
target: aarch64
manylinux: "2_28"
- os: macos-14
target: aarch64
manylinux: "off"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: PyO3/maturin-action@v1
with:
command: build
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
args: --manifest-path crates/alien-bindings-python/Cargo.toml --release --out dist --no-default-features --features all-platforms
- uses: actions/upload-artifact@v4
with:
name: alienplatform-${{ matrix.os }}-${{ matrix.target }}
path: dist/*.whl
if-no-files-found: error
203 changes: 203 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,208 @@ jobs:
publish_tarball "qualified/client-sdks/alienplatform-platform-api-${VERSION}.tgz" "@alienplatform/platform-api"
publish_tarball "qualified/client-sdks/alienplatform-manager-api-${VERSION}.tgz" "@alienplatform/manager-api"

# Python distributions are built during qualification and those exact wheels
# are promoted during publication. abi3-py310 covers supported CPython 3.10+.
build-python-wheels:
needs: prepare
if: inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'qualification'
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- runner: depot-ubuntu-24.04-16
target: x86_64
manylinux: "2014"
artifact: linux-x86_64
smoke: true
- runner: depot-ubuntu-24.04-arm-16
target: aarch64
manylinux: "2_28"
artifact: linux-aarch64
smoke: true
- runner: depot-macos-15
target: aarch64
manylinux: "off"
artifact: macos-aarch64
smoke: true
- runner: depot-macos-15
target: x86_64
manylinux: "off"
artifact: macos-x86_64
smoke: false
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.source_ref }}

- uses: actions/setup-python@v5
with:
python-version: "3.10"

- uses: dtolnay/rust-toolchain@stable

- name: Build Python wheel (${{ matrix.artifact }})
uses: PyO3/maturin-action@v1
with:
command: build
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
args: --manifest-path crates/alien-bindings-python/Cargo.toml --release --out dist --no-default-features --features all-platforms

- name: Validate wheel identity
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
python - <<'PY'
import email
import glob
import os
import zipfile

wheels = glob.glob("dist/*.whl")
if len(wheels) != 1:
raise SystemExit(f"expected one wheel, found {wheels}")
wheel = wheels[0]
with zipfile.ZipFile(wheel) as archive:
paths = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")]
if len(paths) != 1:
raise SystemExit(f"expected one METADATA file, found {paths}")
metadata = email.message_from_bytes(archive.read(paths[0]))
if metadata["Name"] != "alienplatform":
raise SystemExit(f"unexpected package name: {metadata['Name']}")
if metadata["Version"] != os.environ["VERSION"]:
raise SystemExit(
f"wheel version {metadata['Version']} does not match release {os.environ['VERSION']}"
)
if "-abi3-" not in os.path.basename(wheel):
raise SystemExit(f"wheel is not abi3: {wheel}")
print(os.path.basename(wheel))
PY

- name: Smoke-test installed wheel
if: matrix.smoke
run: |
python -m pip install --force-reinstall dist/*.whl
python -c 'import alienplatform; import alienplatform._native'

- name: Upload qualified Python wheel
uses: actions/upload-artifact@v7
with:
name: qualified-python-${{ matrix.artifact }}
retention-days: 30
if-no-files-found: error
path: dist/*.whl

publish-python:
needs: prepare
if: ${{ inputs.mode == 'stable' && needs.prepare.outputs.ready == 'true' && inputs.stable_action == 'publish' && !inputs.dry_run }}
runs-on: depot-ubuntu-24.04-arm
timeout-minutes: 15
environment:
name: pypi
url: https://pypi.org/p/alienplatform
permissions:
contents: read
id-token: write
steps:
- name: Download qualified Python wheels
uses: actions/download-artifact@v8
with:
run-id: ${{ needs.prepare.outputs.qualification_run_id }}
github-token: ${{ secrets.REPO_ACCESS_TOKEN }}
pattern: qualified-python-*
path: dist
merge-multiple: true

- name: Check qualified wheels against PyPI
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
python - <<'PY'
import glob
import hashlib
import json
import os
import urllib.error
import urllib.request

wheels = sorted(glob.glob("dist/*.whl"))
if len(wheels) != 4:
raise SystemExit(f"expected four qualified wheels, found {wheels}")
expected = {
os.path.basename(path): hashlib.sha256(open(path, "rb").read()).hexdigest()
for path in wheels
}
url = f"https://pypi.org/pypi/alienplatform/{os.environ['VERSION']}/json"
try:
with urllib.request.urlopen(url) as response:
published = json.load(response)
except urllib.error.HTTPError as error:
if error.code != 404:
raise
published = {"urls": []}
existing = {
item["filename"]: item["digests"]["sha256"]
for item in published["urls"]
if item["filename"] in expected
}
mismatched = {
name: (expected[name], digest)
for name, digest in existing.items()
if expected[name] != digest
}
if mismatched:
raise SystemExit(f"PyPI already contains different wheel contents: {mismatched}")
print(f"qualified wheels: {sorted(expected)}")
print(f"already published with matching hashes: {sorted(existing)}")
PY

# Resuming a partially completed upload is safe because the preceding
# checksum check rejects an existing filename with different contents.
- name: Publish qualified Python wheels
uses: pypa/gh-action-pypi-publish@106e0b0b7c337fa67ed433972f777c6357f78598
with:
packages-dir: dist
skip-existing: true

- name: Verify published wheel hashes
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
for attempt in $(seq 1 30); do
if python - <<'PY'
import glob
import hashlib
import json
import os
import urllib.request

expected = {
os.path.basename(path): hashlib.sha256(open(path, "rb").read()).hexdigest()
for path in glob.glob("dist/*.whl")
}
with urllib.request.urlopen(
f"https://pypi.org/pypi/alienplatform/{os.environ['VERSION']}/json"
) as response:
published = json.load(response)
actual = {item["filename"]: item["digests"]["sha256"] for item in published["urls"]}
if any(actual.get(name) != digest for name, digest in expected.items()):
raise SystemExit(1)
print(f"verified {len(expected)} wheels on PyPI")
PY
then
exit 0
fi
echo "Waiting for PyPI metadata propagation (${attempt}/30)..."
sleep 10
done
echo "::error::Published Python wheels did not appear with their qualified hashes"
exit 1

# ─── Native addon prebuilds (@alienplatform/bindings) ──────────────
#
# The bindings addon publishes as a thin wrapper plus per-platform prebuild
Expand Down Expand Up @@ -2160,6 +2362,7 @@ jobs:
- publish-crates
- publish-npm
- publish-client-sdks
- publish-python
- publish-bindings
- publish-ai-gateway
- upload-binaries
Expand Down
Loading
Loading