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
225 changes: 225 additions & 0 deletions .github/workflows/build-marin-iris-native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on the Linux leg of marin-community/marin's own release
# pipeline for lib/iris/rust (maturin build of pyext/, module name iris_native).
name: Build marin-iris-native wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/marin-iris-native.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-marin-iris-native.yml'
- 'docs/packages/marin-iris-native.yaml'
push:
branches: [main]
paths:
- '.github/workflows/build-marin-iris-native.yml'
- 'docs/packages/marin-iris-native.yaml'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
# marin-community/marin pushes no marin-iris-native-v0.1.3 tag (continuous
# internal releases, no tags at all in the repo); this is the commit whose
# lib/iris/rust tree is byte-identical to the 0.1.3 sdist on PyPI (gotcha 103).
# Move it together with the pending version in docs/packages/marin-iris-native.yaml.
IRIS_NATIVE_REF: 0a33b5b2ce3bb5a3ed36e217c38f431bf2ba401c

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: marin-iris-native
version: ${{ inputs.version }}

build_wheel:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Build marin-iris-native ${{ matrix.version }} cp312-abi3-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 360

env:
IRIS_NATIVE_VERSION: ${{ matrix.version }}

steps:
- name: Checkout marin @ ${{ env.IRIS_NATIVE_REF }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: marin-community/marin
ref: ${{ env.IRIS_NATIVE_REF }}
persist-credentials: false

- name: Check package version
run: grep -qx 'version = "'"${IRIS_NATIVE_VERSION}"'"' lib/iris/rust/pyproject.toml

# [tool.maturin] has no `license-files`, so maturin's default LICEN[CS]E*
# glob only looks next to lib/iris/rust/pyproject.toml -- the repo's
# actual LICENSE lives at the monorepo root (gotcha 146). Upstream's own
# published wheel has the same gap (no LICENSE in its dist-info); copy
# it in so ours doesn't.
- name: Copy LICENSE next to the crate's pyproject.toml
run: cp LICENSE lib/iris/rust/LICENSE

# pyo3's "abi3-py312" feature (pyext/Cargo.toml) makes one cp312-abi3
# wheel serve every interpreter >=3.12, matching upstream's own
# cp312-abi3-manylinux_2_34_{x86_64,aarch64} shape.
- name: Build wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
command: build
target: riscv64gc-unknown-linux-gnu
args: --release --locked --out dist
manylinux: '2_39'
working-directory: lib/iris/rust
before-script-linux: git config --global --add safe.directory "*"

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: marin-iris-native-${{ env.IRIS_NATIVE_VERSION }}-cp312-abi3-manylinux_riscv64
path: lib/iris/rust/dist/*.whl
if-no-files-found: error

test_wheel:
name: Test marin-iris-native ${{ matrix.version }} on Python ${{ matrix.python-version }}
needs: [setup, build_wheel]
if: needs.setup.outputs.versions != '[]'
runs-on: ubuntu-24.04-riscv
timeout-minutes: 30
env:
IRIS_NATIVE_VERSION: ${{ matrix.version }}
UV_PYTHON_PREFERENCE: only-managed
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# Floor is 3.12 (pyo3 abi3-py312); no 3.14t -- pyo3's abi3 feature
# targets the pre-3.13 limited API, which free-threaded CPython does
# not implement, so the wheel is not loadable there.
python-version: ['3.12', '3.13', '3.14']

steps:
- name: Download wheel
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: marin-iris-native-${{ env.IRIS_NATIVE_VERSION }}-cp312-abi3-manylinux_riscv64

- name: Install Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
enable-cache: false

- name: Install wheel
run: uv pip install --reinstall --no-index --find-links . marin-iris-native

# No wheel-level test suite ships in the sdist (gotcha 187): iris_native
# is a thin pyo3 binding with no Python-side tests upstream. Drive the
# real extension instead of a bare import -- bind a real TCP listener
# through the Rust tokio runtime, read back the JSON stats properties,
# and send one real HTTP request through axum's router to prove request
# handling, not just module load, works on riscv64.
- name: Test wheel
run: |
set -euo pipefail
python3 - <<'PY'
import json
import urllib.error
import urllib.request

import iris_native as native

auth_config = json.dumps(
{
"mode": "permissive",
"issuers": [],
"jwks": {"keys": []},
"leeway_seconds": 0,
"cache_capacity": 10,
"cache_ttl_seconds": 60,
"control_audience": "test",
"proxy_audience": "test",
"proxy_scope": "test",
"federation_audience": "test",
"session_cookie": "session",
"iap_public_keys_url": "https://example.invalid/keys",
"iap_issuer": "https://example.invalid",
"default_user_role": "user",
}
)

with native.NativeProxy(
"127.0.0.1",
0,
"http://127.0.0.1:1",
"test-secret",
auth_config,
1,
) as proxy:
assert proxy.port > 0, proxy.port
# The `address` getter returns `format!("http://{}", self.address)`
# (pyext/src/lib.rs), i.e. a full URL, not a bare "host:port".
assert proxy.address == f"http://127.0.0.1:{proxy.port}", proxy.address

stats = json.loads(proxy.stats_json)
rpc_metrics = json.loads(proxy.rpc_metrics_json)
print("stats:", stats)
print("rpc_metrics:", rpc_metrics)

# A bare "/" is not a dedicated 404 route -- ingress_inner (src/lib.rs)
# only short-circuits to a 404 "route not found" for a request whose path
# is exactly DECISION_PATH ("/_iris/internal/proxy-decision", an endpoint
# meant to be unreachable from outside); everything else, "/" included,
# falls through to the real proxy/controller forwarding, which correctly
# surfaces a 502 against the deliberately-unreachable upstream
# ("http://127.0.0.1:1" above). Hit the real DECISION_PATH to exercise the
# 404 fallback this test actually means to check.
try:
urllib.request.urlopen(
f"http://127.0.0.1:{proxy.port}/_iris/internal/proxy-decision", timeout=5
)
except urllib.error.HTTPError as error:
assert error.code == 404, error.code
body = json.loads(error.read())
assert body == {"error": "route not found"}, body
else:
raise AssertionError("expected the native listener to 404 on the decision path")

proxy.pause_registry()

print("ASSERT OK: NativeProxy binds, serves, and reports stats on riscv64")
PY

publish:
name: Publish marin-iris-native ${{ matrix.version }}
needs: [setup, build_wheel, test_wheel]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
secrets:
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}
with:
artifact-pattern: marin-iris-native-${{ matrix.version }}-*-manylinux_riscv64
5 changes: 5 additions & 0 deletions docs/packages/marin-iris-native.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: marin-iris-native
source-code: https://github.com/marin-community/marin
license: Apache-2.0
versions:
- version: 0.1.3
Loading