Skip to content

docs: Update projects #24

docs: Update projects

docs: Update projects #24

# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on upstream's own Python build/publish tooling at this tag:
# https://github.com/Unleash/yggdrasil-bindings/blob/python-engine-v1.3.1/python-engine/{build.sh,tox.ini,scripts/repair_wheel.py}
# The PyPI package `yggdrasil-engine` is Unleash's ctypes binding to their
# Rust `yggdrasilffi` cdylib, not Google's Yggdrasil Decision Forests.
name: Build yggdrasil-engine wheels (riscv64)
on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/yggdrasil-engine.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-yggdrasil-engine.yml'
- 'docs/packages/yggdrasil-engine.yaml'
push:
branches: [main]
paths:
- '.github/workflows/build-yggdrasil-engine.yml'
- 'docs/packages/yggdrasil-engine.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: yggdrasil-engine
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 yggdrasil-engine ${{ matrix.version }} cp312-abi3-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 60
env:
YGGDRASIL_ENGINE_VERSION: ${{ matrix.version }}
steps:
- name: Checkout yggdrasil-bindings python-engine-v${{ env.YGGDRASIL_ENGINE_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Unleash/yggdrasil-bindings
ref: python-engine-v${{ env.YGGDRASIL_ENGINE_VERSION }}
persist-credentials: false
# build-python.yml's own "Get client spec" step.
- name: Get client spec
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Unleash/client-specification
ref: v6.1.0
path: client-specification
persist-credentials: false
- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: python-wheels
persist-credentials: false
# compat_glibc.rs defines SYS_GETTID/SYS_STATX for x86_64 and aarch64
# only but compiles unconditionally, so the crate fails to build on
# riscv64. See patches/yggdrasil-engine/1.3.1 for the full rationale.
- name: Patch yggdrasilffi source
run: git apply python-wheels/patches/yggdrasil-engine/${{ env.YGGDRASIL_ENGINE_VERSION }}/00*.patch
# LICENSE.md lives at the repo root; poetry-core only auto-bundles a
# licence file that sits beside pyproject.toml, and upstream's own
# published wheel carries none either (same gap build-valkey-glide.yml
# hit).
- name: Stage the project licence beside pyproject.toml
run: cp LICENSE.md python-engine/LICENSE.md
# Mirrors build-python.yml (build the ffi, `poetry run pytest`) and
# python-engine/build.sh + tox.ini (vendor the binary, `poetry build`,
# repair_wheel.py retags the resulting py3-none-any wheel): upstream
# downloads a prebuilt binary here instead of building it, but ships
# none for riscv64, so this builds it natively in-container. musllinux
# is skipped: rustup ships no riscv64gc-unknown-linux-musl host
# toolchain (gotcha 10).
- name: Write in-container build script
run: |
cat > "$RUNNER_TEMP/build.sh" <<'SCRIPT'
set -euxo pipefail
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
cargo build --release -p yggdrasilffi
pybin=/opt/python/cp312-cp312/bin
"$pybin/pip" install -q poetry-core setuptools build pytest
mkdir -p python-engine/yggdrasil_engine/lib python-engine/staging
cp target/release/libyggdrasilffi.so python-engine/yggdrasil_engine/lib/
cd python-engine
"$pybin/python3" -m pytest
"$pybin/python3" -m build --wheel --no-isolation
PLATFORM_TAG=manylinux_2_39_riscv64 "$pybin/python3" scripts/repair_wheel.py
SCRIPT
- name: Build wheel
run: |
docker run --rm \
-v "$(pwd)":/workspace \
-v "$RUNNER_TEMP/build.sh":/build.sh \
-w /workspace \
"${MANYLINUX_RISCV64_IMAGE}" \
bash /build.sh
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: yggdrasil-engine-${{ env.YGGDRASIL_ENGINE_VERSION }}-cp312-abi3-manylinux_riscv64
path: python-engine/staging/*.whl
if-no-files-found: error
test_wheel:
name: Test yggdrasil-engine ${{ 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
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# abi3 wheels are not compatible with free-threaded interpreters
# (packaging.tags._abi3_applies), matching upstream, which ships no
# cp3XXt wheel either; 3.12/3.13/3.14 exercise the same cp312-abi3
# wheel via abi3 forward compatibility.
python-version: ['3.12', '3.13', '3.14']
env:
YGGDRASIL_ENGINE_VERSION: ${{ matrix.version }}
steps:
- name: Download wheel
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: yggdrasil-engine-${{ env.YGGDRASIL_ENGINE_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 . yggdrasil-engine==${{ env.YGGDRASIL_ENGINE_VERSION }}
# 2.0.0 changed is_enabled from returning Optional[bool] (None for an
# unknown toggle) to returning a FeatureToggle, whose unknown-toggle
# answer is is_enabled=False, is_found=False and whose __bool__ raises
# TypeError (upstream python-engine/tests/test_engine.py,
# test_is_enabled_reports_not_found_when_toggle_missing_without_fallback).
- name: Test wheel
run: |
python -c "
from importlib.metadata import version
from yggdrasil_engine.engine import UnleashEngine
engine = UnleashEngine()
engine.take_state('{\"version\": 2, \"features\": [{\"name\": \"my-toggle\", \"enabled\": true, \"strategies\": [{\"name\": \"default\"}]}]}')
hit = engine.is_enabled('my-toggle', {})
miss = engine.is_enabled('missing-toggle', {})
if int(version('yggdrasil-engine').split('.')[0]) >= 2:
assert (hit.name, hit.is_enabled, hit.is_found) == ('my-toggle', True, True), hit
assert (miss.name, miss.is_enabled, miss.is_found) == ('missing-toggle', False, False), miss
else:
assert hit is True, hit
assert miss is None, miss
print('Smoke test passed')
"
publish:
name: Publish yggdrasil-engine ${{ 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: yggdrasil-engine-${{ matrix.version }}-cp312-abi3-manylinux_riscv64